La optimización de llamadas de cola está presente en muchos idiomas y compiladores. En esta situación, el compilador reconoce una función de la forma:
int foo(n) {
...
return bar(n);
}
Aquí, el lenguaje puede reconocer que el resultado que se devuelve es el resultado de otra función y cambiar una llamada de función con un nuevo marco de pila en un salto.
Darse cuenta de que el método factorial clásico:
int factorial(n) {
if(n == 0) return 1;
if(n == 1) return 1;
return n * factorial(n - 1);
}
No se puede optimizar la llamada de cola debido a la inspección necesaria en la devolución. ( Ejemplo de código fuente y salida compilada )
Para hacer esta llamada de cola optimizable,
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);
}
Compilar este código con gcc -O2 -S fact.c
(el -O2 es necesario para habilitar la optimización en el compilador, pero con más optimizaciones de -O3 se hace difícil para un humano leer ...)
_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
( Ejemplo de código fuente y salida compilada )
Se puede ver en el segmento .L3
, en jne
lugar de un call
(que hace una llamada de subrutina con un nuevo marco de pila).
Tenga en cuenta que esto se hizo con C. La optimización de llamadas de cola en Java es difícil y depende de la implementación de JVM (dicho esto, no he visto ninguna que lo haga, porque es difícil e implica que el modelo de seguridad de Java requerido requiere marcos de pila - que es lo que evita el TCO) - tail-recursion + java y tail-recursion + optimization son buenos conjuntos de etiquetas para navegar. Puede encontrar que otros lenguajes JVM pueden optimizar mejor la recursividad de cola (intente clojure (que requiere la recurrencia para optimizar la llamada de cola) o scala).
Dicho eso
Hay una cierta alegría en saber que escribiste algo correctamente , de la manera ideal que se puede hacer.
Y ahora, voy a conseguir un poco de whisky y ponerme algo de electrónica alemana ...
A la pregunta general de "métodos para evitar un desbordamiento de pila en un algoritmo recursivo" ...
Otro enfoque es incluir un contador de recursividad. Esto es más para detectar bucles infinitos causados por situaciones fuera del control de uno (y una codificación deficiente).
El contador de recursión toma la forma de
int foo(arg, counter) {
if(counter > RECURSION_MAX) { return -1; }
...
return foo(arg, counter + 1);
}
Cada vez que realiza una llamada, incrementa el contador. Si el contador se vuelve demasiado grande, se produce un error (aquí, solo un retorno de -1, aunque en otros idiomas puede preferir lanzar una excepción). La idea es evitar que sucedan cosas peores (errores de falta de memoria) cuando se hace una recursión que es mucho más profunda de lo esperado y probablemente un bucle infinito.
En teoría, no deberías necesitar esto. En la práctica, he visto código mal escrito que ha afectado a esto debido a una gran cantidad de pequeños errores y malas prácticas de codificación (problemas de concurrencia multiproceso donde algo cambia algo fuera del método que hace que otro hilo entre en un bucle infinito de llamadas recursivas).
Use el algoritmo correcto y resuelva el problema correcto. Específicamente para la Conjetura de Collatz, parece que estás tratando de resolverlo de la manera xkcd :
Estás comenzando en un número y haciendo un recorrido transversal del árbol. Esto lleva rápidamente a un espacio de búsqueda muy grande. Una ejecución rápida para calcular el número de iteraciones para la respuesta correcta da como resultado unos 500 pasos. Esto no debería ser un problema para la recursividad con un marco de pila pequeño.
Si bien conocer la solución recursiva no es algo malo, uno también debe darse cuenta de que muchas veces la solución iterativa es mejor . En Stack Overflow at Way se pueden ver varias formas de abordar la conversión de un algoritmo recursivo a uno iterativo para pasar de la recursividad a la iteración .