Durch welchen Mechanismus wird diese Fibonacci-Funktion gespeichert?
fib = (map fib' [0..] !!)
where fib' 1 = 1
fib' 2 = 1
fib' n = fib (n-2) + fib (n-1)
Und in einem ähnlichen Zusammenhang, warum ist diese Version nicht?
fib n = (map fib' [0..] !! n)
where fib' 1 = 1
fib' 2 = 1
fib' n = fib (n-2) + fib (n-1)
fib 0
nicht beendet: Sie wahrscheinlich die Basis Fällen wollenfib'
seinfib' 0 = 0
undfib' 1 = 1
.fibs = 1:1:zipWith (+) fibs (tail fibs)
undfib = (fibs !!)
.Antworten:
Der Bewertungsmechanismus in Haskell ist bedarfsgerecht : Wenn ein Wert benötigt wird, wird er berechnet und für den Fall, dass er erneut angefordert wird, bereitgehalten. Wenn wir eine Liste definieren
xs=[0..]
und später nach ihrem 100. Element fragen,xs!!99
wird der 100. Platz in der Liste "konkretisiert", wobei die Nummer99
jetzt für den nächsten Zugriff bereitgehalten wird.Das ist es, was dieser Trick, "eine Liste durchgehen", ausnutzt. Bei der normalen doppelt rekursiven Fibonacci-Definition wird
fib n = fib (n-1) + fib (n-2)
die Funktion selbst zweimal von oben aufgerufen, was die exponentielle Explosion verursacht. Aber mit diesem Trick erstellen wir eine Liste für die Zwischenergebnisse und gehen "durch die Liste":Der Trick besteht darin, dass diese Liste erstellt wird und diese Liste zwischen den Aufrufen von nicht (durch Speicherbereinigung) verschwindet
fib
. Der einfachste Weg, dies zu erreichen, besteht darin , diese Liste zu benennen . "Wenn du es nennst, wird es bleiben."Ihre erste Version definiert eine monomorphe Konstante und die zweite eine polymorphe Funktion. Eine polymorphe Funktion kann nicht dieselbe interne Liste für verschiedene Typen verwenden, die sie möglicherweise bereitstellen muss, also keine Freigabe , dh keine Memoisierung.
Mit der ersten Version ist der Compiler großzügig mit uns, nimmt diesen konstanten Unterausdruck (
map fib' [0..]
) heraus und macht ihn zu einer separaten gemeinsam nutzbaren Einheit, ist jedoch nicht dazu verpflichtet. und es gibt tatsächlich Fälle, in denen wir nicht möchten, dass dies automatisch für uns erledigt wird.( bearbeiten :) Betrachten Sie diese Umschreibungen:
Die eigentliche Geschichte scheint also von den verschachtelten Bereichsdefinitionen zu handeln. Bei der 1. Definition gibt es keinen äußeren Bereich, und bei der dritten Definition wird darauf geachtet, nicht den äußeren Bereich
fib3
, sondern dieselbe Ebene zu nennenf
.Jeder neue Aufruf von
fib2
scheint seine verschachtelten Definitionen neu zu erstellen, da jeder von ihnen (theoretisch) je nach Wert von unterschiedlich definiert werden könnte (danke an Vitus und Tikhon für den Hinweis). Bei der ersten Definition gibt es keine Abhängigkeit, und bei der dritten gibt es eine Abhängigkeit, aber bei jedem einzelnen Aufruf von Aufrufen wird darauf geachtet, nur Definitionen aus demselben Bereich aufzurufen, die für diesen spezifischen Aufruf von intern sind , damit dasselbe erreicht wird für diesen Aufruf von wiederverwendet (dh geteilt) .n
n
fib3
f
fib3
xs
fib3
Nichts hindert den Compiler jedoch daran zu erkennen, dass die internen Definitionen in einer der obigen Versionen tatsächlich unabhängig von der äußeren
n
Bindung sind, um schließlich das Lambda-Heben durchzuführen , was zu einer vollständigen Memoisierung führt (mit Ausnahme der polymorphen Definitionen). Genau das passiert bei allen drei Versionen, wenn sie mit monomorphen Typen deklariert und mit dem Flag -O2 kompiliert werden. Zeigt bei polymorphen Typdeklarationenfib3
lokale Freigabe und überhauptfib2
keine Freigabe.Abhängig von einem Compiler und den verwendeten Compiler-Optimierungen und wie Sie ihn testen (Laden von Dateien in GHCI, kompiliert oder nicht, mit -O2 oder nicht oder eigenständig) und ob es sich um einen monomorphen oder einen polymorphen Typ handelt, kann das Verhalten letztendlich sein Vollständige Änderung - ob lokale (pro Anruf) gemeinsame Nutzung (dh lineare Zeit bei jedem Anruf), Memoisierung (dh lineare Zeit beim ersten Anruf und 0-mal bei nachfolgenden Anrufen mit demselben oder einem kleineren Argument) oder überhaupt keine gemeinsame Nutzung ( exponentielle Zeit).
Die kurze Antwort lautet: Es ist eine Compilersache. :) :)
quelle
fib'
für jeden
und somitfib'
infib 1
≠fib'
in neu definiert wirdfib 2
, was auch impliziert, dass die Listen unterschiedlich sind. Selbst wenn Sie den Typ als monomorph festlegen, zeigt er dieses Verhalten.where
Klauseln führen das Teilen ähnlich wielet
Ausdrücke ein, aber sie neigen dazu, Probleme wie dieses zu verbergen. Wenn Sie es etwas expliziter umschreibenInt -> Integer
) geben, werden siefib2
in exponentieller Zeit ausgeführt,fib1
undfib3
beide werden in linearer Zeit ausgeführt, aberfib1
auch gespeichert - wiederum, weil fürfib3
die lokalen Definitionen für jeden neu definiert werdenn
.pwr (x:xs) = pwr xs ++ map (x:) pwr xs ; pwr [] = [[]]
Wir möchtenpwr xs
zweimal unabhängig voneinander berechnet werden, damit der Müll im laufenden Betrieb gesammelt und verbraucht werden kann.Ich bin nicht ganz sicher, aber hier ist eine fundierte Vermutung:
Der Compiler geht davon aus, dass
fib n
dies bei einem anderen unterschiedlich sein könnte,n
und müsste daher die Liste jedes Mal neu berechnen. Die Bits in derwhere
Anweisung könnten schließlich davon abhängenn
. Das heißt, in diesem Fall ist die gesamte Liste der Zahlen im Wesentlichen eine Funktion vonn
.Die Version ohne
n
kann die Liste einmal erstellen und in eine Funktion einschließen. Die Liste kann nicht vom Wert dern
übergebenen Liste abhängen und ist leicht zu überprüfen. Die Liste ist eine Konstante, in die dann indiziert wird. Es ist natürlich eine Konstante, die träge ausgewertet wird, sodass Ihr Programm nicht versucht, die gesamte (unendliche) Liste sofort abzurufen. Da es sich um eine Konstante handelt, kann sie von allen Funktionsaufrufen gemeinsam genutzt werden.Es wird überhaupt gespeichert, weil der rekursive Aufruf nur einen Wert in einer Liste nachschlagen muss. Da die
fib
Version die Liste einmal träge erstellt, berechnet sie gerade genug, um die Antwort zu erhalten, ohne redundante Berechnungen durchzuführen. Hier bedeutet "faul", dass jeder Eintrag in der Liste ein Thunk ist (ein nicht bewerteter Ausdruck). Wenn Sie den Thunk auswerten, wird er zu einem Wert. Wenn Sie also das nächste Mal darauf zugreifen, wird die Berechnung nicht wiederholt. Da die Liste von Anrufen gemeinsam genutzt werden kann, werden alle vorherigen Einträge bereits zum Zeitpunkt berechnet, zu dem Sie den nächsten benötigen.Es ist im Wesentlichen eine clevere und kostengünstige Form der dynamischen Programmierung, die auf der faulen Semantik von GHC basiert. Ich denke, der Standard legt nur fest, dass er nicht streng sein muss , sodass ein kompatibler Compiler diesen Code möglicherweise kompilieren kann, um ihn nicht zu speichern. In der Praxis wird jedoch jeder vernünftige Compiler faul sein.
Weitere Informationen darüber, warum der zweite Fall überhaupt funktioniert, finden Sie unter Grundlegendes zu einer rekursiv definierten Liste (Fibs in Bezug auf zipWith) .
quelle
fib' n
könnte auf einem anderen anders seinn
"?fib
, einschließlichfib'
, bei jedem anders sein kannn
. Ich denke, das ursprüngliche Beispiel ist ein bisschen verwirrend, weil esfib'
auch von seinem eigenen abhängt,n
welches das andere beschattetn
.Erstens ist mit ghc-7.4.2, kompiliert mit
-O2
, die nicht gespeicherte Version nicht so schlecht, die interne Liste der Fibonacci-Nummern wird immer noch für jeden Aufruf der Funktion auf oberster Ebene gespeichert. Es ist jedoch nicht und kann nicht vernünftigerweise über verschiedene Anrufe der obersten Ebene hinweg gespeichert werden. Bei der anderen Version wird die Liste jedoch von mehreren Anrufen gemeinsam genutzt.Das liegt an der Monomorphismusbeschränkung.
Die erste ist an eine einfache Musterbindung gebunden (nur der Name, keine Argumente), daher muss sie durch die Monomorphismusbeschränkung einen monomorphen Typ erhalten. Der abgeleitete Typ ist
und eine solche Einschränkung wird standardmäßig (sofern keine Standarddeklaration etwas anderes sagt) auf festgelegt
Integer
, wobei der Typ als festgelegt wirdSomit gibt es nur eine Liste (vom Typ
[Integer]
), die gespeichert werden muss.Das zweite wird mit einem Funktionsargument definiert, bleibt also polymorph, und wenn die internen Listen über Aufrufe hinweg gespeichert würden, müsste für jeden Typ in eine Liste gespeichert werden
Num
. Das ist nicht praktisch.Kompilieren Sie beide Versionen mit deaktivierter Monomorphismus-Einschränkung oder mit identischen Typensignaturen, und beide zeigen genau das gleiche Verhalten. (Das galt nicht für ältere Compilerversionen, ich weiß nicht, welche Version es zuerst getan hat.)
quelle
fib 1000000
viele Typen aufruft, verbraucht das eine Menge Speicher. Um dies zu vermeiden, würde man eine Heuristik benötigen, die Listen enthält, um sie aus dem Cache zu werfen, wenn sie zu groß wird. Und eine solche Memoisierungsstrategie würde vermutlich auch für andere Funktionen oder Werte gelten, sodass der Compiler sich mit einer potenziell großen Anzahl von Dingen befassen müsste, die für potenziell viele Typen gespeichert werden müssen. Ich denke, es wäre möglich, eine (teilweise) polymorphe Memoisierung mit einer einigermaßen guten Heuristik zu implementieren, aber ich bezweifle, dass es sich lohnen würde.Sie benötigen keine Memoize-Funktion für Haskell. Nur empirische Programmiersprachen benötigen diese Funktionen. Haskel ist jedoch funktional lang und ...
Dies ist also ein Beispiel für einen sehr schnellen Fibonacci-Algorithmus:
zipWith ist eine Funktion aus dem Standard-Präludium:
Prüfung:
Ausgabe:
Verstrichene Zeit: 0,00018s
quelle