A otimização da chamada de cauda está presente em vários idiomas e compiladores. Nessa situação, o compilador reconhece uma função do formulário:
int foo(n) {
...
return bar(n);
}
Aqui, o idioma é capaz de reconhecer que o resultado retornado é o resultado de outra função e alterar uma chamada de função com um novo quadro de pilha em um salto.
Perceba que o método fatorial clássico:
int factorial(n) {
if(n == 0) return 1;
if(n == 1) return 1;
return n * factorial(n - 1);
}
não é otimizado para chamada final devido à inspeção necessária no retorno. ( Exemplo de código fonte e saída compilada )
Para tornar essa chamada final otimizável,
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);
}
Compilando esse código com gcc -O2 -S fact.c
(o -O2 é necessário para ativar a otimização no compilador, mas com mais otimizações de -O3 fica difícil para um ser humano ler ...)
_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
( Exemplo de código fonte e saída compilada )
Pode-se ver no segmento .L3
, em jne
vez de um call
(que faz uma chamada de sub-rotina com um novo quadro de pilha).
Observe que isso foi feito com C. A otimização de chamada de cauda em Java é difícil e depende da implementação da JVM (isto é, eu não vi nenhum que faça isso, porque é difícil e implicações do modelo de segurança Java necessário que requer quadros de pilha - que é o que o TCO evita) - recursão de cauda + java e recursão de cauda + otimização são bons conjuntos de tags para navegar. Você pode achar que outros idiomas da JVM são capazes de otimizar melhor a recursão da cauda (tente o clojure (que requer que a repetição para otimizar a chamada da cauda) ou scala).
Dito isto,
Há uma certa alegria em saber que você escreveu algo certo - da maneira ideal que isso pode ser feito.
E agora, vou pegar um pouco de uísque e colocar alguma música eletrônica alemã ...
À questão geral de "métodos para evitar um estouro de pilha em um algoritmo recursivo" ...
Outra abordagem é incluir um contador de recursão. Isso é mais para detectar loops infinitos causados por situações fora do controle de alguém (e codificação ruim).
O contador de recursão assume a forma de
int foo(arg, counter) {
if(counter > RECURSION_MAX) { return -1; }
...
return foo(arg, counter + 1);
}
Cada vez que você faz uma chamada, você incrementa o contador. Se o contador ficar muito grande, você errará (aqui, apenas um retorno de -1, embora em outros idiomas você prefira lançar uma exceção). A idéia é impedir que coisas piores aconteçam (erros de falta de memória) ao fazer uma recursão muito mais profunda do que o esperado e provavelmente um loop infinito.
Em teoria, você não deveria precisar disso. Na prática, eu vi códigos mal escritos que atingiram isso devido a uma infinidade de pequenos erros e práticas ruins de codificação (problemas de simultaneidade multithread em que algo muda algo fora do método que faz com que outro encadeamento entre em um loop infinito de chamadas recursivas).
Use o algoritmo certo e resolva o problema certo. Especificamente para a conjectura de Collatz, parece que você está tentando resolvê-lo da maneira xkcd :
Você está começando em um número e fazendo uma travessia em árvore. Isso leva rapidamente a um espaço de pesquisa muito grande. Uma execução rápida para calcular o número de iterações para a resposta correta resulta em cerca de 500 etapas. Isso não deve ser um problema de recursão com um pequeno quadro de pilha.
Embora conhecer a solução recursiva não seja algo ruim, é preciso também perceber que muitas vezes a solução iterativa é melhor . Várias maneiras de abordar a conversão de um algoritmo recursivo para um iterativo podem ser vistas no Stack Overflow at Way para ir de recursão para iteração .