2013-05-18 19 views
7

Ho un problema nel comprendere le prestazioni di una funzione Common Lisp (sono ancora alle prime armi). Ho due versioni di questa funzione, che calcola semplicemente la somma di tutti gli interi fino a un dato n.Common Lisp: Perché la mia funzione ricorsiva di coda causa un overflow dello stack?

versione non-tail-ricorsiva:

(defun addup3 (n) 
    (if (= n 0) 
    0 
    (+ n (addup (- n 1))))) 

Tail-recursive versione:

(defun addup2 (n) 
    (labels ((f (acc k) 
       (if (= k 0) 
        acc 
        (f (+ acc k) (- k 1))))) 
    (f 0 n))) 

Sto cercando di eseguire queste funzioni in CLISP con ingresso n = 1000000. Ecco il risultato

[2]> (addup3 1000000) 
500000500000 
[3]> (addup2 1000000) 

*** - Program stack overflow. RESET 

posso funzionare sia con successo in SBCL, ma quello non-tail-ricorsiva è più veloce (solo da un po ', ma che sembra strano per me). Ho analizzato le domande StackOverflow per le risposte ma non ho trovato nulla di simile. Perché ottengo uno stack overflow sebbene la funzione ricorsiva di coda sia progettata per NON mettere tutte le chiamate di funzioni ricorsive nello stack? Devo dire all'interprete/compilatore di ottimizzare le chiamate di coda? (Ho letto qualcosa come (proclaim '(optimize (debug 1)) per impostare il livello di debug e ottimizzare al costo delle abilità di tracciamento, ma non so cosa faccia). Forse la risposta è ovvia e il codice è una cazzata, ma non riesco proprio a capirlo. L'aiuto è apprezzato.

Modifica: danlei ha sottolineato l'errore di battitura, dovrebbe essere una chiamata a addup3 nella prima funzione, quindi è ricorsivo. Se corretto, entrambe le versioni troppo pieno, ma non il suo unico

(defun addup (n) 
     "Adds up the first N integers" 
     (do ((i 0 (+ i 1)) 
       (sum 0 (+ sum i))) 
      ((> i n) sum))) 

Mentre può essere un modo più tipico per farlo, trovo strano che la ricorsione in coda non è sempre ottimizzato, considerando miei istruttori piacerebbe dirmi che è molto più efficiente e roba del genere.

risposta

7

Non è necessario che un'implementazione Common Lisp abbia ottimizzazione delle chiamate tail. La maggior parte lo fa, tuttavia (penso che ABCL non lo fa, a causa delle limitazioni della Java virtual machine).

La documentazione dell'implementazione dovrebbe indicare quali impostazioni di ottimizzazione devono essere scelte per avere il TCO (se disponibile).

E 'più idiomatico per il codice Common Lisp di utilizzare uno dei costrutti di ciclo:

(loop :for i :upto n 
     :sum i) 

(let ((sum 0)) 
    (dotimes (i n) 
    (incf sum (1+ i)))) 

(do ((i 0 (1+ i)) 
    (sum 0 (+ sum i))) 
    ((> i n) sum)) 

In questo caso, naturalmente, è meglio usare la "piccola Gauss":

(/ (* n (1+ n)) 2) 
+1

Come sottolineato da danlei, la prima funzione ha un errore di battitura, deve chiamarsi e non 'addup', che è essenzialmente la funzione che hai descritto. Se correggo l'errore di battitura, anche questo trabocca. Tuttavia, sono perplesso sul fatto che il costrutto 'do' sia più capace di quello ricorsivo. Non riesco a trovare nulla riguardo TCO per CLISP quando googlando e navigando le specifiche su clisp.org. Non sarebbe strano se la ricorsione della coda non fosse più potente della normale ricorsione? – oarfish

+0

Non dovresti essere sorpreso. L'ottimizzazione delle chiamate tail fa semplicemente eseguire una definizione ricorsiva in uno spazio costante (stack), in modo che possa essere veloce come una definizione iterativa. Non c'è magia coinvolta che potrebbe renderlo più veloce di così. – Svante

+0

In Barski's Land of Lisp, ho appena letto che in effetti CLISP ottimizza solo le chiamate di coda, quando si compila la funzione. In effetti, la coda ricorsiva è un po 'più veloce, quindi non c'è nessun mistero qui. – oarfish

4

Bene, il tuo addup3 non è ricorsivo per tutto.

(defun addup3 (n) 
    (if (= n 0) 
    0 
    (+ n (addup (- n 1))))) ; <-- 

Chiama ciò che è addup. Provare una versione corretta in SBCL:

CL-USER> (defun addup3 (n) 
      (if (= n 0) 
       0 
       (+ n (addup3 (- n 1))))) 
ADDUP3 
CL-USER> (addup3 100000) 
Control stack guard page temporarily disabled: proceed with caution 
; .. 
; Evaluation aborted on #<SB-SYS:MEMORY-FAULT-ERROR {C2F19B1}>. 

Come ci si aspetterebbe.

+0

Tutto il resto è corretto correttamente da Svante. – danlei

+0

Oh dio, che sciocco. Sono davvero pessimo nel rilevare questo tipo di errori di battitura. Grazie. La funzione 'addup' che non ho incluso sopra, fa lo stesso, ma con un costrutto' do'. Non è stato pensato per essere chiamato però. – oarfish

+1

Non preoccuparti, tutti noi commettiamo errori come questi di tanto in tanto e * sono * difficili da individuare. – danlei

1

Utilizzo di GNU CommonLisp, GCL 2.6.12, la compilazione di addup2 ottimizzerà le chiamate di coda, qui è quello che ho ottenuto:

>(compile 'addup2)                  

Compiling /tmp/gazonk_3012_0.lsp. 
End of Pass 1. 

;; Note: Tail-recursive call of F was replaced by iteration. 
End of Pass 2. 
OPTIMIZE levels: Safety=0 (No runtime error checking), Space=0, Speed=3 
Finished compiling /tmp/gazonk_3012_0.lsp. 
Loading /tmp/gazonk_3012_0.o 
start address -T 0x9556e8 Finished loading /tmp/gazonk_3012_0.o 
#<compiled-function ADDUP2> 
NIL 
NIL 

>>(addup2 1000000)                                    

500000500000 
>(addup3 1000000) 

Error: ERROR "Invocation history stack overflow." 
Fast links are on: do (si::use-fast-links nil) for debugging 
Signalled by IF. 
ERROR "Invocation history stack overflow." 

Broken at +. Type :H for Help. 
    1 Return to top level. 

>>(compile 'addup3)                                   

Compiling /tmp/gazonk_3012_0.lsp. 
End of Pass 1. 
End of Pass 2. 
OPTIMIZE levels: Safety=0 (No runtime error checking), Space=0, Speed=3 
Finished compiling /tmp/gazonk_3012_0.lsp. 
Loading /tmp/gazonk_3012_0.o 
start address -T 0x955a00 Finished loading /tmp/gazonk_3012_0.o 
#<compiled-function ADDUP3> 
NIL 
NIL 
>>(addup3 1000000)                                    

Error: ERROR "Value stack overflow." 

Speranza che aiuta.