Ich habe heute den Befehl "time" unter Unix entdeckt und dachte, ich würde ihn verwenden, um den Unterschied in den Laufzeiten zwischen tail-rekursiven und normalen rekursiven Funktionen in Haskell zu überprüfen.
Ich habe folgende Funktionen geschrieben:
--tail recursive
fac :: (Integral a) => a -> a
fac x = fac' x 1 where
fac' 1 y = y
fac' x y = fac' (x-1) (x*y)
--normal recursive
facSlow :: (Integral a) => a -> a
facSlow 1 = 1
facSlow x = x * facSlow (x-1)
Diese sind gültig, da sie ausschließlich für dieses Projekt verwendet wurden. Daher habe ich mich nicht darum gekümmert, nach Nullen oder negativen Zahlen zu suchen.
Beim Schreiben einer Hauptmethode für jede Methode, beim Kompilieren und Ausführen mit dem Befehl "time" hatten beide ähnliche Laufzeiten mit der normalen rekursiven Funktion, die die rekursive Endfunktion herausschneidet. Dies steht im Widerspruch zu dem, was ich in Bezug auf die schwanzrekursive Optimierung in Lisp gehört hatte. Was ist der Grund dafür?
quelle
fac
ist es mehr oder weniger so, wie ghcproduct [n,n-1..1]
mit einer Hilfsfunktion berechnetprod
, aber dasproduct [1..n]
wäre natürlich einfacher. Ich kann nur davon ausgehen, dass sie es in ihrem zweiten Argument nicht streng gemacht haben, weil ghc sehr zuversichtlich ist, dass dies zu einem einfachen Akkumulator kompiliert werden kann.Antworten:
Haskell verwendet Lazy-Evaluation, um die Rekursion zu implementieren. Daher wird alles als Versprechen behandelt, bei Bedarf einen Wert bereitzustellen (dies wird als Thunk bezeichnet). Thunks werden nur so weit wie nötig reduziert, um fortzufahren, nicht mehr. Dies ähnelt der Art und Weise, wie Sie einen Ausdruck mathematisch vereinfachen. Daher ist es hilfreich, ihn so zu betrachten. Die Tatsache, dass die Auswertungsreihenfolge nicht in Ihrem Code angegeben ist, ermöglicht es dem Compiler, viele noch cleverere Optimierungen vorzunehmen, als Sie es bisher gewohnt waren. Kompilieren Sie mit,
-O2
wenn Sie optimieren möchten!Mal sehen, wie wir
facSlow 5
als Fallstudie bewerten :So wie Sie sich Sorgen, haben wir eine Ansammlung von Zahlen , bevor Berechnungen passieren, aber im Gegensatz zu Ihnen besorgt, gibt es keine Stapel von
facSlow
Funktionsaufrufen rumhängen zu beenden warten - jede Reduktion angewandt wird , und geht weg, eine Abgangsstapelrahmen in seinem wake (das liegt daran, dass(*)
es streng ist und so die Bewertung seines zweiten Arguments auslöst).Haskells rekursive Funktionen werden nicht sehr rekursiv ausgewertet! Der einzige Stapel von Anrufen, der herumhängt, sind die Multiplikationen selbst. Wenn dies
(*)
als strikter Datenkonstruktor angesehen wird, wird dies als geschützte Rekursion bezeichnet (obwohl dies normalerweise bei nicht strengen Datenkonstruktoren als solche bezeichnet wird, bei denen die Datenkonstruktoren übrig bleiben - wenn sie durch weiteren Zugriff erzwungen werden).Schauen wir uns nun die Schwanzrekursive an
fac 5
:So können Sie sehen, dass die Schwanzrekursion selbst Ihnen weder Zeit noch Raum gespart hat. Insgesamt werden nicht nur mehr Schritte ausgeführt als
facSlow 5
, sondern es wird auch ein verschachtelter Thunk (hier als dargestellt{...}
) erstellt, der zusätzlichen Platz benötigt , der die zukünftige Berechnung und die durchzuführenden verschachtelten Multiplikationen beschreibt.Dieser Thunk wird dann entwirrt, indem er nach unten bewegt wird, wodurch die Berechnung auf dem Stapel neu erstellt wird. Hier besteht auch die Gefahr eines Stapelüberlaufs mit sehr langen Berechnungen für beide Versionen.
Wenn wir dies von Hand optimieren möchten, müssen wir es nur streng machen. Sie können den strengen Anwendungsoperator
$!
zum Definieren verwendenDies zwingt
facS'
dazu, in seinem zweiten Argument streng zu sein. (Es ist bereits in seinem ersten Argument streng, da dies bewertet werden muss, um zu entscheiden, welche DefinitionfacS'
angewendet werden soll.)Manchmal kann Strenge enorm helfen, manchmal ist es ein großer Fehler, weil Faulheit effizienter ist. Hier ist es eine gute Idee:
Welches ist, was Sie erreichen wollten, denke ich.
Zusammenfassung
-O2
foldr
undfoldl
zum Beispiel, und testen sie gegeneinander an.Probieren Sie diese beiden aus:
foldl1
ist schwanzrekursiv, währendfoldr1
eine geschützte Rekursion durchgeführt wird, so dass das erste Element sofort zur weiteren Verarbeitung / zum weiteren Zugriff präsentiert wird. (Die erste "Klammer" auf der linken Seite wird sofort verwendet,(...((s+s)+s)+...)+s
wodurch die Eingabeliste vollständig auf das Ende gedrängt wird und viel früher als die vollständigen Ergebnisse erstellt werden. Die zweite Klammer wird nach und nach in Klammern gesetzt, wodurch die Eingabes+(s+(...+(s+s)...))
verbraucht wird Liste Stück für Stück auf, damit das Ganze mit Optimierungen in konstantem Raum arbeiten kann).Möglicherweise müssen Sie die Anzahl der Nullen anpassen, je nachdem, welche Hardware Sie verwenden.
quelle
Es sollte erwähnt werden, dass die
fac
Funktion kein guter Kandidat für eine vorsichtige Rekursion ist. Schwanzrekursion ist der Weg hierher. Aufgrund der Faulheit erhalten Sie in Ihrerfac'
Funktion nicht die Wirkung von TCO, da die Akkumulatorargumente immer wieder große Thunks bilden, für deren Auswertung ein großer Stapel erforderlich ist. Um dies zu verhindern und den gewünschten Effekt von TCO zu erzielen, müssen Sie diese Akkumulatorargumente streng machen.Wenn Sie mit
-O2
(oder nur-O
) GHC kompilieren, wird dies wahrscheinlich in der Phase der Strenge-Analyse selbst durchgeführt .quelle
$!
als mitBangPatterns
, aber das ist eine gute Antwort. Besonders die Erwähnung der Strenge-Analyse.Sie sollten den Wiki-Artikel über die Schwanzrekursion in Haskell lesen . Insbesondere aufgrund der Ausdrucksbewertung ist die Art der gewünschten Rekursion eine geschützte Rekursion. Wenn Sie die Details herausarbeiten, was unter der Haube vor sich geht (in der abstrakten Maschine für Haskell), erhalten Sie das Gleiche wie bei der Schwanzrekursion in strengen Sprachen. Darüber hinaus haben Sie eine einheitliche Syntax für Lazy-Funktionen (die Schwanzrekursion bindet Sie an eine strenge Bewertung, während die geschützte Rekursion natürlicher funktioniert).
(Und beim Erlernen von Haskell sind auch die restlichen Wiki-Seiten fantastisch!)
quelle
Wenn ich mich richtig erinnere, optimiert GHC einfach rekursive Funktionen automatisch in schwanzrekursiv optimierte.
quelle