Mam problem ze zrozumieniem działania funkcji Common Lisp (nadal jestem nowicjuszem). Mam dwie wersje tej funkcji, która po prostu wylicza sumę wszystkich liczb całkowitych do danego n
.Common Lisp: Dlaczego funkcja rekurencji ogona powoduje przepełnienie stosu?
Non-tail-rekurencyjna wersja:
(defun addup3 (n)
(if (= n 0)
0
(+ n (addup (- n 1)))))
Tail-rekurencyjna wersja:
(defun addup2 (n)
(labels ((f (acc k)
(if (= k 0)
acc
(f (+ acc k) (- k 1)))))
(f 0 n)))
Próbuję uruchomić te funkcje w clisp z wejściem n = 1000000
. Oto wynik
[2]> (addup3 1000000)
500000500000
[3]> (addup2 1000000)
*** - Program stack overflow. RESET
mogę uruchomić zarówno z powodzeniem w SBCL, ale jeden non-tail-rekurencyjne jest szybsza (tylko trochę, ale to wydaje się dziwne dla mnie). Przeszukałem pytania Stackoverflow w poszukiwaniu odpowiedzi, ale nie mogłem znaleźć czegoś podobnego. Dlaczego dostaję przepełnienie stosu, mimo że funkcja rekurencji ogona nie jest zaprojektowana tak, aby umieścić na stosie wszystkie wywołania funkcji rekurencyjnych? Czy muszę powiedzieć interpreterowi/kompilatorowi, aby zoptymalizować wywołania ogona? (Czytam coś w rodzaju (proclaim '(optimize (debug 1))
, aby ustawić poziom debugowania i optymalizować koszt śledzenia umiejętności, ale nie wiem, co to robi). Może odpowiedź jest oczywista, a kod to bzdury, ale po prostu nie mogę tego rozgryźć. Pomoc jest doceniana.
Edytuj: danlei wskazał literówkę, powinno być to połączenie z addup3
w pierwszej funkcji, więc jest rekurencyjne. Jeżeli poprawione, zarówno wersje przepełnienie, ale nie jego jednego
(defun addup (n)
"Adds up the first N integers"
(do ((i 0 (+ i 1))
(sum 0 (+ sum i)))
((> i n) sum)))
Chociaż może to być bardziej typowy sposób to zrobić, uważam, że to dziwne, że ogon rekurencja nie zawsze jest zoptymalizowana, biorąc pod uwagę moje instruktorów podoba mi powiedzieć, że to o wiele bardziej wydajne i takie tam.
Jak zauważyła danlei, pierwsza funkcja ma literówkę, powinna wywoływać samą siebie, a nie 'addup', która jest w istocie funkcją, którą opisałeś. Jeśli poprawię literówkę, to również się przepełni. Mimo to jestem zaskoczony, że konstrukcja 'do' jest bardziej zdolna niż rekursywna. Nie wydaje mi się, żebym znalazł coś odnośnie TCO dla CLISP podczas wyszukiwania i przeglądania specyfikacji na clisp.org. Czy nie byłoby dziwne, gdyby rekurencja ogona nie była silniejsza od zwykłej rekursji? – oarfish
Nie powinieneś być zaskoczony. Optymalizacja wywołania ogona sprawia, że definicja rekursywna działa w przestrzeni stałej (stos), dzięki czemu może być tak szybka, jak definicja iteracyjna. Nie ma magii, która mogłaby uczynić ją szybszą. – Svante
W Barski's Land of Lisp, właśnie przeczytałem, że rzeczywiście CLISP optymalizuje tylko wywołania ogona, gdy kompiluje się funkcję. W rzeczywistości, rekurencyjny ogon jest trochę szybszy, więc nie ma tu tajemnicy. – oarfish