L'ottimizzazione delle chiamate di coda è presente in molte lingue e compilatori. In questa situazione, il compilatore riconosce una funzione del modulo:
int foo(n) {
...
return bar(n);
}
Qui, la lingua è in grado di riconoscere che il risultato che viene restituito è il risultato di un'altra funzione e cambiare una chiamata di funzione con un nuovo frame dello stack in un salto.
Realizza che il classico metodo fattoriale:
int factorial(n) {
if(n == 0) return 1;
if(n == 1) return 1;
return n * factorial(n - 1);
}
non è ottimizzabile per le chiamate di coda a causa dell'ispezione necessaria al ritorno. ( Esempio di codice sorgente e output compilato )
Per rendere ottimizzabile questa chiamata in coda,
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 questo codice con gcc -O2 -S fact.c
(il -O2 è necessario per abilitare l'ottimizzazione nel compilatore, ma con più ottimizzazioni del -O3 diventa difficile da leggere per un essere umano ...)
_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
( Esempio di codice sorgente e output compilato )
Si può vedere nel segmento .L3
, jne
piuttosto che un call
(che fa una chiamata di subroutine con un nuovo frame dello stack).
Si noti che questo è stato fatto con C. L'ottimizzazione delle chiamate di coda in Java è difficile e dipende dall'implementazione di JVM (detto ciò, non ne ho visto nessuno che lo fa, perché è difficile e le implicazioni del modello di sicurezza Java richiesto che richiede stack frame - che è ciò che evita il TCO) - coda-ricorsione + java e coda-ricorsione + ottimizzazione sono buoni set di tag da sfogliare. Si possono trovare altre lingue JVM sono in grado di ottimizzare la ricorsione in coda migliore (prova clojure (che richiede il ripresentarsi alla chiamata di coda ottimizzare), o di scala).
Detto ciò,
C'è una certa gioia nel sapere che hai scritto qualcosa di giusto - nel modo ideale che può essere fatto.
E ora, vado a prendere un po 'di scotch e mettere un po' di elettronica tedesca ...
Alla domanda generale di "metodi per evitare un overflow dello stack in un algoritmo ricorsivo" ...
Un altro approccio è quello di includere un contatore di ricorsione. Questo è più per rilevare infiniti loop causati da situazioni al di fuori del proprio controllo (e scarsa codifica).
Il contatore di ricorsione assume la forma di
int foo(arg, counter) {
if(counter > RECURSION_MAX) { return -1; }
...
return foo(arg, counter + 1);
}
Ogni volta che si effettua una chiamata, si incrementa il contatore. Se il contatore diventa troppo grande, ti sbagli (qui, solo un ritorno di -1, anche se in altre lingue potresti preferire lanciare un'eccezione). L'idea è di impedire che accadano cose peggiori (errori di memoria insufficiente) quando si esegue una ricorsione che è molto più profonda del previsto e probabilmente un ciclo infinito.
In teoria, non dovresti averne bisogno. In pratica, ho visto codice scritto male che ha colpito questo a causa di una miriade di piccoli errori e cattive pratiche di codifica (problemi di concorrenza multithread in cui qualcosa cambia qualcosa al di fuori del metodo che fa passare un altro thread in un ciclo infinito di chiamate ricorsive).
Usa l'algoritmo giusto e risolvi il problema giusto. In particolare per la congettura di Collatz, sembra che tu stia cercando di risolverlo nel modo xkcd :
Stai iniziando da un numero e stai attraversando un albero. Ciò porta rapidamente a uno spazio di ricerca molto ampio. Una corsa veloce per calcolare il numero di iterazioni per la risposta corretta risulta in circa 500 passaggi. Questo non dovrebbe essere un problema per la ricorsione con un frame stack piccolo.
Pur conoscendo la soluzione ricorsiva non è una brutta cosa, si dovrebbe anche rendersi conto che molte volte la soluzione iterativa è migliore . Un certo numero di modi per avvicinarsi alla conversione di un algoritmo ricorsivo in uno iterativo può essere visto su Stack Overflow at Way per passare dalla ricorsione all'iterazione .