2017-01-09 2 views
10

Die beiden folgenden Haskell-Funktionen scheinen sich nur dadurch zu unterscheiden, ob die Indexvariable implizit oder explizit ist, aber der Unterschied in der Leistung um zwei Größenordnungen ist.GHC-Optimierung

Diese Funktion dauert etwa 0,03 Sekunden MFI B berechnen 30:

let mfib = (map fib [0..] !!) 
    where 
    fib 0 = 0 
    fib 1 = 1 
    fib x = mfib (x-1) + mfib (x-2) 

Diese Funktion nimmt ca. 3 Sekunden für MFI B 30:

let mfib i = map fib [0..] !! i 
    where 
    fib 0 = 0 
    fib 1 = 1 
    fib x = mfib (x-1) + mfib (x-2) 

Ich vermute, es hat mit GHC inline zu tun Regeln und haben versucht, inline/noinline Pragmas hinzuzufügen, um eine übereinstimmende Leistung zu erhalten.

EDIT: Ich verstehe, wie ein Nachschlagen in die faule Liste verwendet werden kann, um die Fib-Funktion zu memotisieren und warum die traditionelle Definition von fib sehr langsam ist. Ich habe erwartet, dass die Memoisierung sowohl in der zweiten als auch in der ersten Funktion funktioniert und nicht versteht, warum dies nicht der Fall ist.

+1

Der Schlüssel ist * Memoisierung *. Siehe [hier] (http://stackoverflow.com/questions/11466284/how-is-this-fibonacci-function-memoized). –

Antwort

12

Es ist einfacher, diese Unterschiede zu verstehen, wenn man sich den entzuckerten Code anschaut, daher sind hier einige entzuckte Versionen der beiden Funktionen.

let mfib = let fib 0 = 0 
       fib 1 = 1 
       fib x = mfib (x-1) + mfib (x-2) 
      in (!!) (map fib [0..]) 

Vergleich

let mfib = \i -> 
       let fib 0 = 0 
        fib 1 = 1 
        fib x = mfib (x-1) + mfib (x-2) 
       in map fib [0..] !! i 

anzumerken, dass in dem zweiten Programm, der Ausdruck map fib [0..] innen \i -> ... angezeigt wird, so wird es (in der Regel, ohne Optimierungen) für jeden Wert von i bewertet. Siehe When is memoization automatic in GHC Haskell?

+0

Ich habe einige Tests auf dem Repl versucht, dies zu bestätigen und scheint richtig. Ich schaute auch auf den Link von @ Alexey-Radkov über Memoisierung und den Vorschlag gab es, dass es mit der ersten Funktion zu tun hatte monomorph und daher zwischen Anrufen geteilt, während die zweite ist polymorph, aber ich konnte dies nicht bestätigen, z. indem map neu definiert wird, um monomorph zu sein. – Mikkel

+0

TL; DR Weil 'map fib [0 ..]' in einem Lambda ist, wird es nicht geteilt, sondern zwischen (rekursiven) Aufrufen gesammelt. Richtig? – Mikkel

+0

@Mikkel Richtig, und der Grund ist, weil (obwohl es in diesem Fall nicht) könnte es auf "i" abhängen, und dann konnte nicht geteilt werden. (Und das ist normalerweise ein guter Weg, um zu sehen, was geteilt wird, ohne zuerst das Entzucken zu machen.) –

7

Nein, das hat nichts mit Inlining zu tun. Der Unterschied ist, dass mfib = (map fib [0..] !!) keine Argumente hat. Es ist natürlich immer noch eine Funktion, aber die Auswertung dieser Funktion erfordert im Vorfeld kein Argument. Insbesondere wird durch Auswertung dieser mfib die Liste so generiert, dass sie für alle Indizes wiederverwendet werden kann.

OTOH, mfib i = map fib [0..] !! i bedeutet, dass der gesamte where Block nur berücksichtigt wird, wenn Sie tatsächlich ein Argument übergeben i.

Die beiden sind nur dann unterschiedlich wenn man eine Funktion mehrfach mehrfach auswertet. Leider für die zweite Version, die Funktionen eigene Rekursion ruft sie immer wieder auf! So muss mfib (x-1) + mfib (x-2) dann die gesamte Arbeit von mfib (x-1), und dann wieder die gesamte Arbeit von mfib (x-2) tun. So nimmt mfib n mehr als das Doppelte der Rechenaufwand von mfib (n-1) daher mfibO (2 n).

Das ist unglaublich verschwenderisch, denn die meisten Begriffe in mfib (x-2) sind auch schon in mfib (x-1) und könnten einfach wiederverwendet werden. Nun, das ist genau das, was Ihre erste Version tut, weil sie die fib Liste ein für allemal berechnet, so dass die Bewertung mfib (x-1) bereits die meisten Arbeiten erledigt, die dann einfach von mfib (x-2) wieder gelesen werden können, was die Komplexität auf Polynom reduziert.

+2

Eine kleine zusätzliche Erklärung für _why_ der 'where' Block wird neu ausgewertet: Es liegt daran, dass' i' im 'where' Block des Blocks ist. Wenn du es als 'let mfib = \ i -> map fib [0 ..] schreiben würdest !! ich wo ... 'es wäre genauso schnell wie die eta-Vertragsversion. Das heißt, ich bin überrascht, dass GHC keine Gelegenheit gefunden hat, die Full-Faul-Transformation anzuwenden und 'fib' außerhalb des Ordners zu schweben. –

+0

@BenjaminHodgson Ich habe tatsächlich versucht, die 'i' in ein Lambda, aber es macht keinen Unterschied - Memoisierung immer noch nicht" funktioniert ". – Mikkel