Es gibt 3 Funktionen, die das vorletzte Element in einer Liste finden. Der mitlast . init
scheint viel schneller als der Rest. Ich kann nicht herausfinden warum.
Zum Testen habe ich eine Eingabeliste von [1..100000000]
(100 Millionen) verwendet. Der letzte läuft fast sofort, während die anderen einige Sekunden dauern.
-- slow
myButLast :: [a] -> a
myButLast [x, y] = x
myButLast (x : xs) = myButLast xs
myButLast _ = error "List too short"
-- decent
myButLast' :: [a] -> a
myButLast' = (!! 1) . reverse
-- fast
myButLast'' :: [a] -> a
myButLast'' = last . init
init
wurde optimiert, um ein mehrmaliges "Auspacken" der Liste zu vermeiden.myButLast
viel langsamer?. Es scheint, dass keine Listeinit
[x, y]
für(x:(y:[]))
, also packt es die äußeren Nachteile, eine zweite Nachteile, aus und prüft, ob der Schwanz der Sekundecons
ist[]
. Außerdem entpackt die zweite Klausel die Liste erneut in(x:xs)
. Ja, das Auspacken ist einigermaßen effizient, aber wenn es sehr oft vorkommt, verlangsamt dies natürlich den Prozess.init
nicht wiederholt überprüft wird, ob das Argument eine Singleton-Liste oder eine leere Liste ist. Sobald die Rekursion beginnt, wird lediglich davon ausgegangen, dass das erste Element an das Ergebnis des rekursiven Aufrufs angeheftet wird.myButLast
automatisch geben sollte. Ich denke, es ist wahrscheinlicher, dass die Listenfusion für die Beschleunigung verantwortlich ist.Antworten:
Wenn man Geschwindigkeit und Optimierung studiert, ist das sehr einfach , völlig falsche Ergebnisse erzielen . Insbesondere können Sie nicht wirklich sagen, dass eine Variante schneller als eine andere ist, ohne die Compilerversion und den Optimierungsmodus Ihres Benchmarking-Setups zu erwähnen. Selbst dann sind moderne Prozessoren so ausgefeilt, dass sie Verzweigungsprädiktoren auf der Basis neuronaler Netzwerke sowie alle Arten von Caches enthalten. Selbst bei sorgfältiger Einrichtung sind die Benchmarking-Ergebnisse verschwommen.
Davon abgesehen ...
Benchmarking ist unser Freund.
criterion
ist ein Paket, das erweiterte Benchmarking-Tools bietet. Ich habe schnell einen Benchmark wie diesen entworfen:Wie Sie sehen, habe ich die Variante hinzugefügt, die explizit mit zwei Elementen gleichzeitig übereinstimmt, ansonsten handelt es sich jedoch wörtlich um denselben Code. Ich führe die Benchmarks auch in umgekehrter Reihenfolge aus, um mir der Verzerrung durch Caching bewusst zu werden. Also, lass uns rennen und sehen!
Sieht so aus, als ob unsere "langsame" Version überhaupt nicht langsam ist! Und die Feinheiten des Mustervergleichs tragen nichts dazu bei. (Eine leichte Beschleunigung, die wir zwischen zwei aufeinanderfolgenden Läufen von sehen,
match2
schreibe ich den Effekten des Caching zu.)Es gibt eine Möglichkeit, mehr "wissenschaftliche" Daten zu erhalten: Wir können
-ddump-simpl
und sehen, wie der Compiler unseren Code sieht.Die Inspektion von Zwischenstrukturen ist unser Freund.
"Core" ist eine interne Sprache von GHC. Jede Haskell-Quelldatei wird zu Core vereinfacht, bevor sie in das endgültige Funktionsdiagramm für die Ausführung durch das Laufzeitsystem umgewandelt wird. Wenn wir uns diese Zwischenstufe ansehen, wird sie uns das sagen
myButLast
undbutLast2
sind gleichwertig. Es braucht einen Blick, da beim Umbenennen alle unsere netten Kennungen zufällig entstellt werden.Es scheint das
A1
undA4
sind am ähnlichsten. Eine gründliche Inspektion wird zeigen , dass in der Tat die CodestrukturenA1
undA4
sind identisch. DasA2
und dasA3
Gleiche sind auch sinnvoll, da beide als Zusammensetzung zweier Funktionen definiert sind.Wenn Sie die
core
Ausgabe ausführlich untersuchen möchten, ist es sinnvoll, auch Flags wie-dsuppress-module-prefixes
und anzugeben-dsuppress-uniques
. Sie machen es so viel einfacher zu lesen.Eine kurze Liste unserer Feinde.
Was kann also beim Benchmarking und der Optimierung schief gehen?
ghci
Da die Haskell-Quelle für interaktives Spielen und schnelle Iteration konzipiert ist, kompiliert sie die Haskell-Quelle auf eine bestimmte Art von Bytecode anstatt auf die endgültige ausführbare Datei und verzichtet auf teure Optimierungen zugunsten eines schnelleren Neuladens.Das mag traurig aussehen. Aber es ist wirklich nicht das, was einen Haskell-Programmierer die meiste Zeit betreffen sollte. Echte Geschichte: Ich habe einen Freund, der erst kürzlich angefangen hat, Haskell zu lernen. Sie hatten ein Programm zur numerischen Integration geschrieben, und es war schildkrötenlang. Also setzten wir uns zusammen und schrieben eine kategoriale Beschreibung des Algorithmus mit Diagrammen und Dingen. Als sie den Code neu schrieben, um ihn an die abstrakte Beschreibung anzupassen, wurde er auf magische Weise schnell wie ein Gepard und schlank im Gedächtnis. Wir haben π in kürzester Zeit berechnet. Moral der Geschichte? Perfekte abstrakte Struktur, und Ihr Code wird sich selbst optimieren.
quelle
ghci
scheint es andere Ergebnisse (in Bezug auf die Geschwindigkeit) zu geben, als zuerst eine Exe zu machen, wie Sie sagten.