Eine große Liste von Zahlen zu summieren ist zu langsam

8

Aufgabe: "Summiere die ersten 15.000.000 geraden Zahlen."

Haskell:

%Vor%

... aber MySum braucht Ewigkeiten. Genauer gesagt, etwa 10-20 mal langsamer als C / C ++.

Viele Male habe ich herausgefunden, dass eine Haskell-Lösung, die auf natürliche Weise codiert ist, ungefähr zehnmal langsamer ist als C. Ich habe erwartet, dass GHC ein sehr sauberer Compiler und eine solche Aufgabe ist, die so schwierig scheint.

Man würde also etwas 1,5 bis 2 mal langsamer als C erwarten. Wo ist das Problem?

Kann das besser gelöst werden?

Dies ist der C-Code, mit dem ich es vergleiche:

%Vor%

Edit 1 : Ich weiß wirklich, dass es in O (1) berechnet werden kann. Bitte, widerstehen.

Edit 2 : Ich weiß wirklich, dass even [2,4..] ist, aber die Funktion even könnte etwas anderes O(1) sein und muss als Funktion implementiert werden.

    
Cartesius00 15.11.2012, 14:00
quelle

7 Antworten

24

Listen sind keine Schleifen

Seien Sie also nicht überrascht, wenn Sie Listen als Schleifenersatz verwenden, erhalten Sie langsameren Code, wenn der Schleifenkörper klein ist.

%Vor%

sum ist kein "guter Konsument", daher ist GHC (noch) nicht in der Lage, die Zwischenlisten vollständig zu eliminieren.

Wenn Sie mit Optimierungen kompilieren (und nat nicht exportieren), ist GHC intelligent genug, um filter mit der Enumeration zu fusionieren,

%Vor%

Aber das ist so weit, wie die Fusion von GHC es braucht. Sie bleiben mit dem Boxen Int s und dem Konstruieren von Listenzellen. Wenn Sie es eine Schleife geben, wie Sie es dem C-Compiler geben,

%Vor%

Sie erhalten die ungefähre Relation der Laufzeiten zwischen der C- und der Haskell-Version, die Sie erwartet haben.

Diese Art von Algorithmus ist nicht das, was GHC gelernt hat, um es gut zu optimieren, es gibt größere Fische, die anderswo frittiert werden müssen, bevor die begrenzten Manpower in diese Optimierungen gesteckt wird.

    
Daniel Fischer 15.11.2012, 14:34
quelle
12

Das Problem, warum Listenfusion hier nicht funktionieren kann, ist eigentlich eher subtil. Angenommen, wir definieren das richtige RULE , um die Liste zu verschmelzen:

%Vor%

(Die kurze Erklärung ist, dass wir sum2 als Alias ​​von sum definieren, was verbietet, dass GHC früh inline wird, also hat RULE eine Chance zu feuern, bevor sum2 eliminiert wird. Dann schauen wir für sum2 direkt neben dem Listengenerator build (siehe Definition ) und durch direkte Arithmetik ersetzen.)

Dies hat gemischten Erfolg, da es den folgenden Kern ergibt:

%Vor%

Was ist ein schöner, vollständig fusionierter Code - mit dem einzigen Problem, dass wir in einer Nicht-Tail-Call-Position einen Aufruf an $wgo haben. Dies bedeutet, dass wir nicht auf eine Schleife schauen, sondern tatsächlich auf eine tief rekursive Funktion mit vorhersagbaren Programm-Ergebnissen:

%Vor%

Das Hauptproblem besteht darin, dass die Listenfusion des Preludes nur rechte Falten fusionieren kann und die Berechnung der Summe als eine direkte Falte direkt den übermäßigen Stapelverbrauch verursacht. Die naheliegende Lösung wäre, ein Fusions-Framework zu verwenden, das tatsächlich mit linken Falten umgehen kann, wie Duncans stream-fusion-Paket , die tatsächlich sum fusion implementiert.

Eine andere Lösung wäre, sie zu hacken - und die linke Falte mit einer rechten Falte zu implementieren:

%Vor%

Dies erzeugt tatsächlich nahezu perfekten Code mit aktuellen Versionen von GHC. Auf der anderen Seite ist dies im Allgemeinen eine gute Idee, da es darauf angewiesen ist, dass GHC intelligent genug ist, die teilweise angewandten Funktionen zu eliminieren. Bereits das Hinzufügen eines filter in die Kette wird diese spezielle Optimierung durchbrechen.

    
Peter Wortmann 15.11.2012 18:03
quelle
5

Summe der ersten 15.000.000 geraden Zahlen:

%Vor%

sollte der schnellste sein.

    
Will Ness 15.11.2012 14:28
quelle
4

Wenn Sie sicher sein wollen, dass Sie die Liste nur einmal durchqueren, können Sie den Durchlauf explizit schreiben:

%Vor%     
amindfv 15.11.2012 14:31
quelle
3

Erstens kannst du schlau sein, als der junge Gauss und die Summe in O (1 ) .

Spaß beiseite, Ihre Haskell-Lösung verwendet Listen. Ich bin mir ziemlich sicher, dass Ihre C / C ++ Lösung dies nicht tut. (Haskell-Listen sind sehr einfach zu verwenden, daher ist man versucht, sie auch in Fällen zu verwenden, in denen dies nicht angebracht ist.) Versuchen Sie, dies zu vergleichen:

%Vor%

Kompilieren Sie es mithilfe von GHC mit -O2 -Argument . Diese Funktion ist tailrecursive , sodass der Compiler sie sehr effizient implementieren kann.

Update: Wenn Sie even function verwenden möchten, ist es möglich:

%Vor%

Sie können die Filterfunktion auch einfach zu einem Parameter machen:

%Vor%     
Petr Pudlák 15.11.2012 14:32
quelle
2

Strict Version funktioniert viel schneller:

%Vor%     
Cfr 15.11.2012 14:09
quelle
1

Es ist noch eine Sache zu beachten, dass nats und evens sogenannte Constant Applicative Forms oder kurz CAFs sind. Im Wesentlichen entsprechen diese Top-Level-Definitionen ohne Argumente. CAFs sind ein bisschen eine seltsame Ente, zum Beispiel der Grund für die gefürchtete Monomorphismus-Beschränkung; Ich bin mir nicht sicher, ob die Sprachdefinition sogar erlaubt, dass CAFs inline sind.

In meinem mentalen Modell, wie Haskell ausführt, wird dumbSum zu dem Zeitpunkt, zu dem evens einen Wert zurückgibt, so ausgewertet, dass es etwa wie 2:4: ... : 30000000 : <thunk> und nats bis 1:2: ... : 30000000 : <thunk> aussieht, wobei <thunk> s repräsentieren etwas, das noch nicht angeschaut wurde. Wenn mein Verständnis stimmt, müssen diese Zuordnungen von : passieren und können nicht weg optimiert werden.

Eine Möglichkeit, Dinge zu beschleunigen, ohne den Code zu sehr zu verändern, wäre einfach zu schreiben:

%Vor%

oder

%Vor%

Auf meinem Rechner, der mit -O2 kompiliert wurde, scheint das allein zu einer Beschleunigung von etwa 30% zu führen.

Ich bin kein GHC-Kenner (ich habe noch nie ein Haskell-Programm profiliert!), also könnte ich sehr daneben sein.

    
yatima2975 15.11.2012 20:54
quelle

Tags und Links