Prawie rozumiem, jak działa rekurencja ogona i jaka jest różnica między nią a normalną rekurencją. Nie rozumiem tylko , dlaczego nie wymaga stosu do zapamiętania adresu zwrotnego.
// tail recursion
int fac_times (int n, int acc) {
if (n == 0) return acc;
else return fac_times(n - 1, acc * n);
}
int factorial (int n) {
return fac_times (n, 1);
}
// normal recursion
int factorial (int n) {
if (n == 0) return 1;
else return n * factorial(n - 1);
}
Nie ma nic do zrobienia po wywołaniu samej funkcji w funkcji rekurencyjnej ogona, ale nie ma to dla mnie sensu.
c
algorithm
recursion
tail-recursion
Alan Coromano
źródło
źródło
-O3
. Odnośnik służy do wcześniejszej dyskusji, która obejmuje bardzo podobny temat i omawia, co jest konieczne do wdrożenia tej optymalizacji.Odpowiedzi:
Kompilator jest w stanie to po prostu przekształcić
na coś takiego:
źródło
Pytasz, dlaczego „nie wymaga stosu do zapamiętania swojego adresu zwrotnego”.
Chciałbym to zmienić. To nie używać stosu do zapamiętania adres zwrotny. Sztuczka polega na tym, że funkcja, w której występuje rekurencja ogona, ma swój własny adres zwrotny na stosie, a kiedy skacze do wywoływanej funkcji, potraktuje to jako swój własny adres zwrotny.
Konkretnie, bez optymalizacji wywołań końcowych:
W tym przypadku, gdy
g
zostanie wywołany, stos będzie wyglądał następująco:Z drugiej strony, z optymalizacją połączeń ogonowych:
W tym przypadku, gdy
g
zostanie wywołany, stos będzie wyglądał następująco:Oczywiście, po
g
powrocie, wróci do lokalizacji, z którejf
został wywołany.EDYCJA : W powyższym przykładzie wykorzystano przypadek, w którym jedna funkcja wywołuje inną funkcję. Mechanizm jest identyczny, gdy funkcja wywołuje samą siebie.
źródło
Rekurencję ogonową można zwykle przekształcić w pętlę przez kompilator, zwłaszcza gdy używane są akumulatory.
skompilowałoby się do czegoś takiego jak
źródło
W funkcji rekurencyjnej muszą występować dwa elementy:
„Zwykła” funkcja rekurencyjna utrzymuje (2) w ramce stosu.
Zwracane wartości w zwykłej funkcji rekurencyjnej składają się z dwóch typów wartości:
Spójrzmy na twój przykład:
Ramka f (5) „przechowuje”, na przykład, wynik własnego obliczenia (5) i wartość f (4). Jeśli wywołam silnię (5), tuż przed tym, jak wywołania stosu zaczną się zwijać, mam:
Zauważ, że każdy stos przechowuje, oprócz wartości, o których wspomniałem, cały zakres funkcji. Tak więc użycie pamięci dla funkcji rekurencyjnej f wynosi O (x), gdzie x jest liczbą wywołań rekurencyjnych, które muszę wykonać. Tak więc, jeśli potrzebuję 1kb pamięci RAM do obliczenia silni (1) lub silni (2), potrzebuję ~ 100k do obliczenia silni (100) i tak dalej.
Funkcja rekurencyjna ogona umieszcza (2) w swoich argumentach.
W rekursji ogonowej przekazuję wynik obliczeń częściowych w każdej klatce rekurencyjnej do następnej za pomocą parametrów. Zobaczmy nasz przykład silni, Tail Recursive:
int factorial (int n) {int helper (int num, intumulated) {if num == 0 returnumulated else return helper (num - 1 ,umulated * num)} return helper (n, 1)
}
Spójrzmy na jego ramki w silni (4):
Widzisz różnice? W „zwykłych” wywołaniach rekurencyjnych funkcje zwracające rekursywnie tworzą wartość końcową. W Tail Recursion odwołują się tylko do przypadku podstawowego (ostatni oceniany) . Nazywamy akumulator argumentem, który śledzi starsze wartości.
Szablony rekursji
Zwykła funkcja rekurencyjna wygląda następująco:
Aby przekształcić go w rekurencję ogonową:
Popatrz:
Zobacz różnicę?
Optymalizacja Tail Call
Ponieważ żaden stan nie jest przechowywany na stosach Non-Border-Cases of the Tail Call, nie są one tak ważne. Niektóre języki / tłumacze zastępują stary stos nowym. Tak więc, bez ramek stosu ograniczających liczbę wywołań, wywołania ogonowe zachowują się w takich przypadkach jak pętla for .
Optymalizacja zależy od kompilatora lub nie.
źródło
Oto prosty przykład, który pokazuje, jak działają funkcje rekurencyjne:
Rekursja ogona jest prostą funkcją rekurencyjną, w której rekurencja jest wykonywana na końcu funkcji, a zatem żaden kod nie jest wykonywany we wznoszeniu, co pomaga większości kompilatorów języków programowania wysokiego poziomu w wykonywaniu tego, co jest znane jako Optymalizacja rekurencji ogona , ma również bardziej złożona optymalizacja znana jako modulo rekurencji ogona
źródło
Funkcja rekurencyjna to funkcja, która sama się wywołuje
Umożliwia programistom pisanie wydajnych programów przy użyciu minimalnej ilości kodu .
Wadą jest to, że mogą powodować nieskończone pętle i inne nieoczekiwane wyniki, jeśli nie zostaną poprawnie napisane .
Wyjaśnię zarówno prostą funkcję rekurencyjną, jak i funkcję rekurencyjną ogona
Aby napisać prostą funkcję rekurencyjną
Z podanego przykładu:
Z powyższego przykładu
Decyduje o tym, kiedy wyjść z pętli
Czy faktyczne przetwarzanie ma zostać wykonane
Pozwólcie, że podzielę zadanie po kolei, aby ułatwić zrozumienie.
Zobaczmy, co się dzieje wewnętrznie, gdy biegnę
fact(4)
If
pętla kończy się niepowodzeniem, więc przechodzi doelse
pętli i zwraca4 * fact(3)
W pamięci stosu mamy
4 * fact(3)
Podstawiając n = 3
If
pętla zawodzi, więc przechodzi welse
pętlęwięc wraca
3 * fact(2)
Pamiętaj, że nazwaliśmy `` 4 * fakt (3) ''
Dane wyjściowe dla
fact(3) = 3 * fact(2)
Jak dotąd stos ma
4 * fact(3) = 4 * 3 * fact(2)
W pamięci stosu mamy
4 * 3 * fact(2)
Podstawiając n = 2
If
pętla zawodzi, więc przechodzi welse
pętlęwięc wraca
2 * fact(1)
Pamiętaj, że dzwoniliśmy
4 * 3 * fact(2)
Dane wyjściowe dla
fact(2) = 2 * fact(1)
Jak dotąd stos ma
4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
W pamięci stosu mamy
4 * 3 * 2 * fact(1)
Podstawiając n = 1
If
pętla jest prawdziwawięc wraca
1
Pamiętaj, że dzwoniliśmy
4 * 3 * 2 * fact(1)
Dane wyjściowe dla
fact(1) = 1
Jak dotąd stos ma
4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
Ostatecznie wynik faktu (4) = 4 * 3 * 2 * 1 = 24
Recursion Tail byłoby
If
pętla kończy się niepowodzeniem, więc przechodzi doelse
pętli i zwracafact(3, 4)
W pamięci stosu mamy
fact(3, 4)
Podstawiając n = 3
If
pętla zawodzi, więc przechodzi welse
pętlęwięc wraca
fact(2, 12)
W pamięci stosu mamy
fact(2, 12)
Podstawiając n = 2
If
pętla zawodzi, więc przechodzi welse
pętlęwięc wraca
fact(1, 24)
W pamięci stosu mamy
fact(1, 24)
Podstawiając n = 1
If
pętla jest prawdziwawięc wraca
running_total
Dane wyjściowe dla
running_total = 24
Ostatecznie wynik faktu (4,1) = 24
źródło
Moja odpowiedź jest raczej domysłem, ponieważ rekurencja jest czymś związanym z wewnętrzną implementacją.
W rekurencji ogonowej funkcja rekurencyjna jest wywoływana na końcu tej samej funkcji. Prawdopodobnie kompilator może zoptymalizować w następujący sposób:
Jak widać, zamykamy oryginalną funkcję przed następną iteracją tej samej funkcji, więc w rzeczywistości nie „używamy” stosu.
Ale uważam, że jeśli wewnątrz funkcji są wywoływane destruktory, to ta optymalizacja może nie mieć zastosowania.
źródło
Kompilator jest wystarczająco inteligentny, aby zrozumieć rekurencję ogonową, w przypadku, gdy podczas powrotu z wywołania rekurencyjnego nie ma operacji oczekującej, a wywołanie rekurencyjne jest ostatnią instrukcją, należy do kategorii rekurencji ogonowej. Kompilator zasadniczo przeprowadza optymalizację rekurencji ogona, usuwając implementację stosu. Rozważ poniższy kod.
Po przeprowadzeniu optymalizacji powyższy kod jest konwertowany na poniższy.
W ten sposób kompilator wykonuje optymalizację rekurencji ogona.
źródło