Ottimizzazione delle chiamate tail è 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 di modificare una chiamata di funzione con un nuovo stack frame in un salto.
Renditi conto che il classico metodo fattoriale:
int factorial(n) {
if(n == 0) return 1;
if(n == 1) return 1;
return n * factorial(n - 1);
}
è non chiamata di coda ottimizzabile a causa dell'ispezione necessaria al reso. ( Esempio di codice sorgente e output compilato )
Per rendere questa chiamata di coda ottimizzata,
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);
}
Compilare questo codice con gcc -O2 -S fact.c
(il -O2 è necessario per abilitare l'ottimizzazione nel compilatore, ma con più ottimizzazioni di -O3 diventa difficile per un essere umano leggere ...)
_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
, il jne
piuttosto che un call
(che fa una chiamata di subroutine con un nuovo frame dello stack).
Si noti che questo è stato fatto con l'ottimizzazione delle chiamate di C. Tail in Java è difficile e dipende dall'implementazione della JVM (detto ciò, non ho visto nessuno che lo faccia, perché è difficile e implica il modello di sicurezza Java richiesto richiede stack frame - che è ciò che evita TCO) - tail-recursion + java e tail-recursion + optimization sono buoni set di tag da sfogliare. Potresti trovare altre lingue JVM in grado di ottimizzare meglio la ricorsione della coda (prova clojure (che richiede ricorrere per ottimizzare le chiamate tail), o scala).
Detto questo,
C'èunacertagioianelsaperechehaiscrittoqualcosagiusto-nelmodoidealeincuipuòesserefatto.
Eora vado a prendere dello scotch e metto un po 'di elettronica tedesca ...
Alla domanda generale di "metodi per evitare uno stack overflow in un algoritmo ricorsivo" ...
Un altro approccio è includere un contatore di ricorsione. Questo è più utile per rilevare loop infiniti 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, sbagli (qui, solo un ritorno di -1, anche se in altre lingue potresti preferire lanciare un'eccezione). L'idea è di evitare che accadano cose peggiori (errori di memoria insufficienti) quando si fa una ricorsione molto più profonda del previsto e probabilmente un ciclo infinito.
In teoria, non dovresti averne bisogno. In pratica, ho visto un codice scritto male che ha colpito questo a causa di una moltitudine di piccoli errori e cattive pratiche di codifica (problemi di concorrenza multithread in cui qualcosa cambia qualcosa al di fuori del metodo che fa entrare 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, appare che stai cercando di risolverlo nel modo xkcd :
Staiiniziandoconunnumeroefacendounattraversamentodialberi.Questoportarapidamenteaunospaziodiricercamoltoampio.Unacorsarapidapercalcolareilnumerodiiterazioniperirisultatidellarispostacorrettaincirca500passaggi.Questonondovrebbeessereunproblemaperlaricorsioneconunpiccolostackframe.
Pursapendochelasoluzionericorsivanonèunabruttacosa,bisognaancherendersicontochemoltevoltelasoluzioneiterativa è migliore . Un certo numero di modi per avvicinarsi alla conversione di un algoritmo ricorsivo in uno iterativo può essere visto su Stack Overflow all'indirizzo Modo per passare dalla ricorsione all'iterazione .