Оптимізація виклику хвоста існує у багатьох мовах та компіляторах. У цій ситуації компілятор розпізнає функцію форми:
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(int, int):
cmpl $1, %edi
movl %esi, %eax
je .L2
.L3:
imull %edi, %eax
subl $1, %edi
cmpl $1, %edi
jne .L3
.L2:
rep ret
( Приклад вихідного коду та компільований вихід )
Ви можете бачити в сегменті .L3
, jne
а не call
(який виконує виклик підпрограми з новим фреймом стека).
Зверніть увагу: це було зроблено за допомогою C. Оптимізація виклику хвостів у Java є важкою і залежить від впровадження JVM (що говорив, я не бачив жодного, хто це зробив, тому що це важко і наслідки необхідної моделі безпеки Java, що вимагає кадри стека - чого уникає TCO) - хвіст-рекурсія + java та реверсія хвоста + оптимізація - це гарні набори тегів для перегляду. Ви можете знайти інші мови JVM здатні оптимізувати хвостову рекурсію краще (спроба Clojure (який вимагає повторювався для оптимізації хвостового виклику), або Скелі).
Це сказав:
Існує певна радість, коли ти знаєш, що ти написав щось правильно - ідеально, як це можна зробити.
А тепер я збираюсь взяти трохи скотча і надіти якусь німецьку електроніку ...
До загального питання "методи уникнути переповнення стека в рекурсивному алгоритмі" ...
Інший підхід - включити лічильник рекурсії. Це більше для виявлення нескінченних циклів, спричинених ситуаціями, що перебувають поза контролем (і поганим кодуванням).
Лічильник рекурсії має форму
int foo(arg, counter) {
if(counter > RECURSION_MAX) { return -1; }
...
return foo(arg, counter + 1);
}
Кожен раз, коли ви здійснюєте дзвінок, ви збільшуєте лічильник. Якщо лічильник стає занадто великим, ви помиляєтеся (ось тут лише повернення -1, хоча в інших мовах ви можете віддати перевагу виключенню). Ідея полягає в тому, щоб не допустити, щоб гірші речі не траплялися (помилки в пам'яті), коли проводилася рекурсія, яка набагато глибша, ніж очікувалося, і, швидше за все, нескінченний цикл.
Теоретично вам це не потрібно. На практиці я бачив погано написаний код, який вплинув на це через безліч дрібних помилок та поганих практик кодування (багатопотокові проблеми одночасності, коли щось змінюється поза методом, що змушує інший потік переходити у нескінченний цикл рекурсивних викликів).
Використовуйте правильний алгоритм і вирішіть правильну задачу. Спеціально для Collatz Conjecture, здається , ви намагаєтеся вирішити це xkcd способом:
Ви починаєте з числа і робите обхід дерева. Це швидко призводить до дуже великого простору пошуку. Швидкий пробіг для обчислення кількості повторень для правильної відповіді призводить до приблизно 500 кроків. Це не повинно бути проблемою для рекурсії з невеликим фреймом стека.
Хоча знання рекурсивного рішення не є поганою справою, слід також усвідомити, що багато разів ітераційне рішення краще . Ряд способів наближення перетворюючого рекурсивного алгоритму до ітеративного можна побачити на Stack Overflow at Way, щоб перейти від рекурсії до ітерації .