Могут ли компиляторы преобразовывать рекурсивную логику в эквивалентную нерекурсивную логику?

15

Я изучаю F #, и это начинает влиять на то, как я думаю, когда я программирую на C #. С этой целью я использовал рекурсию, когда чувствую, что результат улучшает читабельность, и я не могу представить, что это приведет к переполнению стека.

Это заставляет меня спросить, могут ли компиляторы автоматически преобразовывать рекурсивные функции в эквивалентную нерекурсивную форму?

Аарон Анодид
источник
оптимизация хвоста вызова является хорошей , если простым примером , но это работает только если у вас есть return recursecall(args);для рекурсии, тем более сложный материал возможно путем создания явного стека и наматывая его вниз, но я сомневаюсь , что они будут
трещотки урод
@ratchet freak: рекурсия не означает «вычисления, использующие стек».
Джорджио
1
@Giorgio Я знаю , но стек это самый простой способ , чтобы преобразовать рекурсию в цикл
храповым урод

Ответы:

21

Да, некоторые языки и компиляторы преобразуют рекурсивную логику в нерекурсивную логику. Это известно как оптимизация хвостовых вызовов - обратите внимание, что не все рекурсивные вызовы являются оптимизируемыми хвостовыми вызовами. В этой ситуации компилятор распознает функцию вида:

int foo(n) {
  ...
  return bar(n);
}

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

Поймите, что классический метод факториала:

int factorial(n) {
  if(n == 0) return 1;
  if(n == 1) return 1;
  return n * factorial(n - 1);
}

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

Чтобы сделать этот хвостовой вызов оптимизируемым,

int _fact(int n, int acc) {
    if(n == 1) return acc;
    return _fact(n - 1, acc * n);
}

int factorial(int n) {
    if(n == 0) return 1;
    return _fact(n, 1);
}

Компиляция этого кода с помощью gcc -O2 -S fact.c(-O2 необходима для включения оптимизации в компиляторе, но с большей оптимизацией -O3 человеку становится трудно читать ...)

_fact:
.LFB0:
        .cfi_startproc
        cmpl    $1, %edi
        movl    %esi, %eax
        je      .L2
        .p2align 4,,10
        .p2align 3
.L4:
        imull   %edi, %eax
        subl    $1, %edi
        cmpl    $1, %edi
        jne     .L4
.L2:
        rep
        ret
        .cfi_endproc

Можно увидеть в сегменте .L4, jneа не call(который выполняет вызов подпрограммы с новым кадром стека).

Обратите внимание, что это было сделано с C. Оптимизация вызовов Tail в java сложна и зависит от реализации JVM - tail-recursion + java и tail-recursion + оптимизация - хорошие наборы тегов для просмотра. Вы можете найти другие языки JVM способны оптимизировать хвостовую рекурсию лучше (попытка Clojure (который требует повторялся для оптимизации хвостового вызова), или Скале).

Сообщество
источник
1
Я не уверен, что именно об этом спрашивает ОП. То, что среда выполнения определенным образом использует или не использует пространство стека, не означает, что функция не является рекурсивной.
1
@MattFenwick Как ты имеешь в виду? «Это заставляет меня задаться вопросом, могут ли компиляторы автоматически преобразовывать рекурсивные функции в эквивалентную нерекурсивную форму» - ответ «да при определенных условиях». Условия демонстрируются, и есть некоторые ошибки на некоторых других популярных языках с оптимизацией хвостовых вызовов, о которых я упоминал.
9

Действуй осторожно.

Ответ да, но не всегда, и не все из них. Это техника, которая называется несколько разных имен, но вы можете найти довольно определенную информацию здесь и в Википедии .

Я предпочитаю название «Оптимизация вызовов», но есть и другие, и некоторые люди будут путать этот термин.

Тем не менее, есть пара важных вещей для реализации:

  • Чтобы оптимизировать оконечный вызов, оконечный вызов требует параметров, которые известны во время вызова. Это означает, что если один из параметров является вызовом самой функции, то он не может быть преобразован в цикл, поскольку это потребует произвольного вложения указанного цикла, который не может быть раскрыт во время компиляции.

  • C # не надежно оптимизирует хвостовые вызовы. У IL есть инструкция, чтобы сделать это, что будет генерировать компилятор F #, но компилятор C # будет генерировать его непоследовательно, и в зависимости от ситуации JIT, JIT может или не может делать это вообще. Все указывает на то, что вы не должны полагаться на свои хвостовые вызовы, оптимизированные в C #, риск переполнения при этом является значительным и реальным

Джимми Хоффа
источник
1
Вы уверены, что именно об этом спрашивает ОП? Как я писал в другом ответе, то, что среда выполнения определенным образом использует или не использует пространство стека, не означает, что функция не является рекурсивной.
1
@MattFenwick, на самом деле, это замечательный момент, в реальности это зависит от того, что компилятор F #, генерирующий команды хвостового вызова, полностью поддерживает рекурсивную логику, он просто инструктирует JIT выполнять его с заменой стекового пространства, а не стекового пространства. растет, однако другие компиляторы могут буквально компилировать в цикл. (Технически JIT компилируется в цикл или, возможно, даже в цикл без петель, если цикл является полным заранее)
Джимми Хоффа