Применима ли оптимизация хвостового вызова к этой функции?

Если bar вызывает bar (i / 2), если i - четное целое число, и bar (3 * i + 1) в противном случае, рекурсивная функциональная bar будет хвостовой рекурсией.

const int bar(const int i) 
{
  if (i < 2) return i;
  return i % 2 ? bar(i/2) : bar(3*i + 1);
}

Однако что, если bar вызывает либо bar, либо foo, у которого набор локальных переменных полностью отличается от bar?

const int bar(const int i) 
{
  if (i < 2) return i;
  return i % 2 ? bar(i/2) : foo(3*i + 1);
  // where foo is very complicated recursive call that has 
  // 11 different user-defined/primitive type of
  // local variables that can't be optimized out
}

Насколько я понимаю, оптимизация хвостовой рекурсии будет использовать стек вызывающего. Вызывающий объект уже обработал свои локальные переменные перед вызовом вызываемого. Итак, вызываемый может использовать его повторно. Насколько я понимаю, звучит хорошо, когда вызывающий и вызываемый - это одна и та же функция (например, foo вызывает foo, bar вызывает bar). Однако, если размер и макет стека совершенно разные, а вызываемые могут быть одной из нескольких разных функций с разным макетом стека, что произойдет?

Во-первых, будет ли это хвостовая рекурсия? (Теперь я понимаю, что может быть применена оптимизация хвостового «вызова», но это не хвостовая «рекурсия».) Во-вторых, основные компиляторы, такие как gcc, clang и т. Д., Будут оптимизировать этот вид хвостовых вызовов? (Кажется, да.)

Что, если вызываемый объект (foo во втором примере кода) намного сложнее? Если вызываемый и вызывающий вызывают друг друга (взаимная рекурсия), будет ли вызов в моем втором примере кода оптимизирован для хвостового вызова? Если вызываемый (например, foo) представляет собой сложный рекурсивный вызов, который определенно не является хвостовой рекурсией и компилятору очень сложно сократить его до цикла или около того, будет ли он по-прежнему оптимизирован для хвостового вызова?


person Stephen    schedule 21.03.2018    source источник
comment
Все, что делает foo (), не влияет на bar (). Если компилятор может оптимизировать bar () для хвостовой рекурсии, ему не нужно (по крайней мере, в вашем сценарии) беспокоиться о foo (). Предположим, вы вызвали printf () - предотвратит ли это оптимизацию хвостовой рекурсии?   -  person    schedule 21.03.2018
comment
Вызов foo() из bar() не является рекурсией. Так что я не вижу актуальности.   -  person Disillusioned    schedule 21.03.2018
comment
Оптимизация, о которой вы говорите, - это оптимизация вызова. Хотя это наиболее полезно при (взаимной) рекурсии, это никоим образом не ограничивается этим случаем.   -  person rici    schedule 21.03.2018
comment
@CraigYoung прочитал о взаимной рекурсии   -  person Mulan    schedule 21.03.2018
comment
@naomik Вопрос не может продемонстрировать взаимную рекурсию в ее "минимальном примере". (Жалкие размытые, расплывчатые, волнистые от руки изложения о foo() не заменяют демонстрацию.) Итак: Какова ваша точка зрения? OP не демонстрирует какой-либо рекурсии с foo(). Так что я все еще не вижу актуальности.   -  person Disillusioned    schedule 21.03.2018
comment
@naomik Для ясности, оптимизацию хвостовой рекурсии можно применить к коду в вопросе независимо от того, что происходит в foo(). Если OP хочет спросить об оптимизации хвостовой рекурсии с взаимной рекурсией, тогда минимальный воспроизводимый пример (с акцентом на Complete) должен указывать на рассматриваемую взаимную рекурсию. В противном случае речь идет о безграничной игре в рулетку с множеством гипотетических реализаций foo(); делая вопрос слишком широким.   -  person Disillusioned    schedule 21.03.2018


Ответы (2)


Применима ли к этой функции оптимизация хвостовой рекурсии?

Да, оптимизация хвостовой рекурсии применима в ваших примерах. Второй пример см. На ассемблере https://godbolt.org/g/cSpUZw. Применена более прогрессивная оптимизация. Рекурсия заменяется циклом.

bar(int):
  cmp edi, 1
  jg .L12
  jmp .L6
.L15:
  sar edi
  cmp edi, 1
  je .L14
.L12:
  test dil, 1
  jne .L15
  lea edi, [rdi+1+rdi*2]
  jmp foo(int)
.L14:
  mov eax, 1
  ret
.L6:
  mov eax, edi
  ret
person S.M.    schedule 21.03.2018

Термин «хвостовая рекурсия» - это локальное свойство узла вызова. На него никак не влияют другие вызовы того же метода.

Грубо говоря, вызов является хвостовым рекурсивным, если исполняемый код не должен выполняться между его возвратом и возвратом включающего метода.

Следовательно, все вызовы bar() в вашем примере хвосторекурсивны.

Но обратите внимание, что если вы сказали

return i % 2 ? bar(i/2) : 1 + bar(3*i + 1);

тогда первый вызов является хвостовым рекурсивным, а второй - нет, потому что добавление 1 + должно выполняться после его возврата.

person Gene    schedule 21.03.2018