Чи можуть чи компілятори перетворювати рекурсивну логіку в еквівалентну нерекурсивну логіку?


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);
}

це НЕ хвіст виклик optimizatable з інспекції , необхідної на повернення.

Щоб зробити цей заклик оптимізованим,

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а не a call(який виконує виклик підпрограми з новим фреймом стека).

Зверніть увагу, що це було зроблено з C. Оптимізація виклику хвостів у Java є важкою і залежить від впровадження JVM - хвоста-рекурсія + java та хвоста-рекурсія + оптимізація - це гарні набори тегів для перегляду. Ви можете знайти інші мови JVM здатні оптимізувати хвостову рекурсію краще (спроба Clojure (який вимагає повторювався для оптимізації хвостового виклику), або Скелі).


1
Я не впевнений, що це запитує ОП. Тільки тому, що час виконання або не споживає місця у стеці певним чином, не означає, що функція не є рекурсивною.

1
@MattFenwick Як ти це маєш на увазі? "Це змушує мене запитати, чи могли компілятори автоматично перетворювати рекурсивні функції в еквівалентну нерекурсивну форму" - відповідь "так" за певних умов ". Умови продемонстровані, і є деякі gotcha в деяких інших популярних мовах з оптимізаціями хвостових дзвінків, які я згадав.

9

Стежте обережно.

Відповідь - так, але не завжди, і не всі вони. Це техніка, яка має декілька різних назв, але ви можете знайти досить визначену інформацію тут і на wikipedia .

Я віддаю перевагу назві "Оптимізація виклику хвостів", але є інші, і деякі люди будуть плутати термін.

Це означає, що необхідно усвідомити кілька важливих речей:

  • Для оптимізації хвостового дзвінка хвостовому виклику потрібні параметри, відомі в момент здійснення дзвінка. Це означає, що якщо один з параметрів є викликом до самої функції, то її неможливо перетворити в цикл, оскільки це вимагатиме довільного введення зазначеного циклу, яке не може бути розширене під час компіляції.

  • C # не надійно оптимізує хвостові дзвінки. IL має вказівку робити так, який компілятор F # буде випромінювати, але компілятор C # випромінює це непослідовно, і залежно від ситуації JIT, JIT може чи не може це зробити взагалі. Усі вказівки на те, що ви не повинні покладатися на те, що ваші хвостові дзвінки оптимізовані в C #, ризик переповнення при цьому значний і реальний


1
Ви впевнені, що це запитує ОП? Як я розміщував під іншою відповіддю, те, що час виконання або певний спосіб не витрачає простір стеку, не означає, що функція не є рекурсивною.

1
@MattFenwick, що насправді є чудовим моментом, в реальному розумінні це залежить, компілятор F #, що випромінює вказівки щодо виклику хвоста, повністю підтримує рекурсивну логіку, він просто доручає JIT виконувати його в моді, що замінює стек-простір, а не stack-space- зростає, однак інші компілятори можуть буквально збиратись у цикл. (Технічно JIT збирається в циклі або, можливо, навіть у безлюдному стилі, якщо петля загальна на передній частині)
Jimmy Hoffa
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.