GHC speichert keine Funktionen.
Es berechnet jedoch einen bestimmten Ausdruck im Code höchstens einmal pro Mal, wenn sein umgebender Lambda-Ausdruck eingegeben wird, oder höchstens einmal, wenn er sich auf der obersten Ebene befindet. Das Bestimmen, wo sich die Lambda-Ausdrücke befinden, kann etwas schwierig sein, wenn Sie syntaktischen Zucker wie in Ihrem Beispiel verwenden. Konvertieren Sie diese also in eine äquivalente desugarierte Syntax:
m1' = (!!) (filter odd [1..]) -- NB: See below!
m2' = \n -> (!!) (filter odd [1..]) n
(Hinweis: Der Haskell 98-Bericht beschreibt tatsächlich einen linken Bedienerabschnitt (a %)
als äquivalent zu \b -> (%) a b
, aber GHC empfiehlt ihn (%) a
. Diese sind technisch unterschiedlich, da sie durch unterschieden werden können seq
. Ich glaube, ich habe möglicherweise ein GHC Trac-Ticket dazu eingereicht.)
In Anbetracht dieser, können Sie diese in sehen m1'
, der Ausdruck filter odd [1..]
ist nicht in jedem Lambda-Ausdruck enthalten ist , so wird es nur einmal pro Durchlauf des Programms berechnet werden, während in m2'
, filter odd [1..]
wird jedes Mal , wenn der Lambda-Ausdruck berechnet werden eingegeben wird , das heißt, bei jedem Anruf von m2'
. Das erklärt den Unterschied im Timing, den Sie sehen.
Tatsächlich teilen einige Versionen von GHC mit bestimmten Optimierungsoptionen mehr Werte als in der obigen Beschreibung angegeben. Dies kann in einigen Situationen problematisch sein. Betrachten Sie zum Beispiel die Funktion
f = \x -> let y = [1..30000000] in foldl' (+) 0 (y ++ [x])
GHC stellt möglicherweise fest, dass y
dies nicht von x
der Funktion abhängt, und schreibt sie neu in
f = let y = [1..30000000] in \x -> foldl' (+) 0 (y ++ [x])
In diesem Fall ist die neue Version viel weniger effizient, da sie etwa 1 GB aus dem Speicher lesen muss, in dem sie y
gespeichert ist, während die ursprüngliche Version auf konstantem Speicherplatz ausgeführt wird und in den Cache des Prozessors passt. Unter GHC 6.12.1 ist die Funktion f
beim Kompilieren ohne Optimierungen fast doppelt so schnell wie beim Kompilieren -O2
.
seq
m1 10000000). Es gibt jedoch einen Unterschied, wenn kein Optimierungsflag angegeben ist. Und beide Varianten Ihres "f" haben übrigens unabhängig von der Optimierung eine maximale Residenz von 5356 Bytes (mit weniger Gesamtzuordnung, wenn -O2 verwendet wird).f
:main = interact $ unlines . (show . map f . read) . lines
; kompilieren mit oder ohne-O2
; dannecho 1 | ./main
. Wenn Sie einen Test wie schreibenmain = print (f 5)
,y
kann bei der Verwendung Müll gesammelt werden, und es gibt keinen Unterschied zwischen den beidenf
s.map (show . f . read)
natürlich sein. Und jetzt, da ich GHC 6.12.3 heruntergeladen habe, sehe ich die gleichen Ergebnisse wie in GHC 6.12.1. Und ja, Sie haben Recht mit dem Originalm1
undm2
: Versionen von GHC, die diese Art des Hebens mit aktivierten Optimierungen durchführen, werden sichm2
in verwandelnm1
.m1 wird nur einmal berechnet, da es sich um eine konstante Antragsform handelt, während m2 keine CAF ist, und wird daher für jede Bewertung berechnet.
Weitere Informationen finden Sie im GHC-Wiki zu CAFs: http://www.haskell.org/haskellwiki/Constant_applicative_form
quelle
[1 ..]
während der Ausführung eines Programms nur einmal oder einmal pro Anwendung der Funktion berechnet wird, aber hängt sie mit CAF zusammen?m1
es sich um eine CAF handelt, gilt die zweite und wirdfilter odd [1..]
(nicht nur[1..]
!) Nur einmal berechnet. GHC könnte auch feststellen, dassm2
auffilter odd [1..]
denselben Thunk verwiesen wird, und einen Link zu demselben Thunk setzen, der in verwendet wird. Diesm1
wäre jedoch eine schlechte Idee: In einigen Situationen kann dies zu großen Speicherlecks führen.[1..]
undfilter odd [1..]
. Im Übrigen bin ich immer noch nicht überzeugt. Wenn ich mich nicht irre, ist CAF nur relevant, wenn wir argumentieren wollen, dass ein Compiler das In durch ein globales Thunk ersetzen könnte (das sogar das gleiche Thunk sein kann wie das in ). Aber in der Situation des Fragestellers hat, hat der Compiler nicht tun „Optimierung“ , und ich kann nicht seine Bedeutung für die Frage sehen.filter odd [1..]
m2
m1
m1
, und es tut.Es gibt einen entscheidenden Unterschied zwischen den beiden Formen: Die Monomorphismusbeschränkung gilt für m1, aber nicht für m2, da m2 explizit Argumente angegeben hat. Der Typ von m2 ist also allgemein, aber der von m1 ist spezifisch. Die ihnen zugewiesenen Typen sind:
Die meisten Haskell-Compiler und -Interpreter (alle, die ich tatsächlich kenne) merken sich keine polymorphen Strukturen, daher wird die interne Liste von m2 jedes Mal neu erstellt, wenn sie aufgerufen wird, wohingegen m1 nicht.
quelle
Ich bin mir nicht sicher, weil ich selbst für Haskell noch ziemlich neu bin, aber es scheint, dass die zweite Funktion parametrisiert ist und die erste nicht. Die Art der Funktion ist, dass ihr Ergebnis vom Eingabewert abhängt und insbesondere im Funktionsparadigma NUR von der Eingabe abhängt. Offensichtliche Implikation ist, dass eine Funktion ohne Parameter immer den gleichen Wert zurückgibt, egal was passiert.
Anscheinend gibt es im GHC-Compiler einen Optimierungsmechanismus, der diese Tatsache ausnutzt, um den Wert einer solchen Funktion nur einmal für die gesamte Programmlaufzeit zu berechnen. Es tut es zwar träge, aber es tut es trotzdem. Ich habe es selbst bemerkt, als ich die folgende Funktion geschrieben habe:
Dann, um es zu testen, gab ich GHCI ein und schrieb :
primes !! 1000
. Es dauerte ein paar Sekunden, aber schließlich bekam ich die Antwort :7927
. Dann rief ich anprimes !! 1001
und bekam sofort die Antwort. In ähnlicher Weise erhielt ich sofort das Ergebnis fürtake 1000 primes
, da Haskell die gesamte Liste mit tausend Elementen berechnen musste, um zuvor das 1001. Element zurückzugeben.Wenn Sie also Ihre Funktion so schreiben können, dass sie keine Parameter akzeptiert, möchten Sie sie wahrscheinlich. ;)
quelle