Performance von Floyd-Warshall in Haskell - Behebung eines Weltraumlecks

8

Ich wollte eine effiziente Implementierung des Floyd-Warshall-Algorithmus für alle Paare in Haskell mit Vector s schreiben, um eine gute Leistung zu erzielen.

Die Implementierung ist recht einfach, aber anstatt ein 3-dimensionales | V | × | V | × | V | zu verwenden Matrix wird ein 2-dimensionaler Vektor verwendet, da wir immer nur den vorherigen k -Wert gelesen haben.

Somit ist der Algorithmus wirklich nur eine Reihe von Schritten, bei denen ein 2D-Vektor übergeben wird und ein neuer 2D-Vektor erzeugt wird. Der letzte 2D-Vektor enthält die kürzesten Wege zwischen allen Knoten (i, j).

Meine Intuition sagte mir, dass es wichtig wäre, sicherzustellen, dass der vorherige 2D-Vektor vor jedem Schritt evaluiert wurde, also benutzte ich BangPatterns für das prev Argument für die fw Funktion und das strikte foldl' :

%Vor%

Wenn Sie dieses Programm jedoch mit einem 1000-Node-Graphen mit 47978 Kanten ausführen, sieht die Sache gar nicht gut aus. Die Speicherauslastung ist sehr hoch und das Programm dauert viel zu lange. Das Programm wurde mit ghc -O2 kompiliert.

Ich habe das Programm für das Profiling neu erstellt und die Anzahl der Iterationen auf 50 begrenzt:

%Vor%

Ich habe dann das Programm mit +RTS -p -hc und +RTS -p -hd :

ausgeführt

Das ist ... interessant, aber ich denke, es zeigt, dass es Tonnen von Thunks sammelt. Nicht gut.

Ok, also habe ich nach einigen Aufnahmen im Dunkeln ein deepseq in fw hinzugefügt, um sicherzustellen, dass prev wirklich ausgewertet wird:

%Vor%

Jetzt sieht die Sache besser aus, und ich kann das Programm tatsächlich mit konstanter Speicherauslastung ausführen. Es ist offensichtlich, dass der Knall auf dem Argument prev nicht genug war.

Zum Vergleich mit den vorherigen Graphen ist hier die Speicherbelegung für 50 Iterationen nach dem Hinzufügen von deepseq :

Ok, also sind die Dinge besser, aber ich habe noch ein paar Fragen:

  1. Ist das die richtige Lösung für dieses Weltraumleck? Ich habe das Gefühl, dass das Einfügen von deepseq ein bisschen hässlich ist?
  2. Ist meine Verwendung von Vector s hier idiomatisch / korrekt? Ich baue für jede Iteration einen komplett neuen Vektor und hoffe, dass der Müllsammler das alte Vector s löschen wird.
  3. Gibt es noch andere Dinge, die ich tun könnte, um das mit diesem Ansatz schneller zu machen?

Für Referenzen, ist hier graph.txt : Ссылка

Hier ist main :

%Vor%     
beta 07.10.2013, 08:45
quelle

1 Antwort

5

Zuerst einige allgemeine Codebereinigung:

In Ihrer Funktion fw ordnen Sie explizit veränderbare Vektoren zu und füllen diese. Für diesen genauen Zweck gibt es jedoch eine vorgefertigte Funktion, nämlich generate . fw kann daher als

umgeschrieben werden %Vor%

Ebenso kann der Graph-Generierungscode durch replicate und accum ersetzt werden:

%Vor%

Beachten Sie, dass dadurch jegliche Notwendigkeit für eine Mutation vollständig beseitigt wird, ohne dass eine Leistung verloren geht.

Nun zu den eigentlichen Fragen:

  1. Meiner Erfahrung nach ist deepseq sehr nützlich, aber nur als schnelle Lösung für Platzlecks wie diese. Das grundlegende Problem besteht nicht darin, dass Sie die Ergebnisse erzwingen müssen, nachdem Sie sie erstellt haben. Stattdessen bedeutet die Verwendung von deepseq , dass Sie die Struktur von Anfang an strikter aufgebaut haben sollten. In der Tat, wenn Sie ein Knallmuster in Ihrem Vektorerstellungscode wie folgt hinzufügen:

    %Vor%

    Dann ist das Problem behoben ohne deepseq . Beachten Sie, dass dies nicht mit dem generate -Code funktioniert, da vector aus irgendeinem Grund (ich könnte eine Feature-Anfrage dafür erstellen) keine strikten Funktionen für umrahmte Vektoren bietet. Wenn ich jedoch in der Antwort auf Frage 3 zu ungekoppelten Vektoren komme, die streng sind, arbeiten beide Ansätze ohne Striktheitsannotationen.

  2. Soweit ich weiß, ist das Muster der wiederholten Erzeugung neuer Vektoren idiomatisch. Das einzige, was nicht idiomatisch ist, ist die Verwendung von Mutabilität - außer wenn sie unbedingt notwendig sind, werden mutable Vektoren im Allgemeinen davon abgeraten.

  3. Es gibt ein paar Dinge zu tun:

    • Am einfachsten können Sie Map Int durch IntMap ersetzen. Da dies nicht wirklich der langsame Punkt der Funktion ist, spielt dies keine Rolle, aber IntMap kann bei hohen Arbeitslasten viel schneller sein.

    • Sie können zur Verwendung von nicht eingebetteten Vektoren wechseln. Obwohl der äußere Vektor eingerahmt bleiben muss, kann der innere Vektor sein, da Vektoren von Vektoren nicht entkoppelt werden können. Dies löst auch Ihr Striktheitsproblem - weil ungeschachtelte Vektoren in ihren Elementen streng sind, erhalten Sie kein Platzleck. Beachten Sie, dass dies auf meinem Computer die Leistung von 4,1 Sekunden auf 1,3 Sekunden verbessert, so dass das Unboxing sehr hilfreich ist.

    • Sie können den Vektor in einen einzigen reduzieren und Multiplikation und Division verwenden, um zwischen zweidimensionalen Indizes und einzelnen Indizes umzuschalten. Ich empfehle das nicht, da es ein bisschen involviert, ziemlich hässlich ist und aufgrund der Aufteilung den Code auf meinem Rechner verlangsamt.

    • Sie können repa verwenden. Dies hat den großen Vorteil, dass Sie Ihren Code automatisch parallelisieren. Beachten Sie, dass repa seine Arrays abflacht und die Divisionen, die zum Ausfüllen benötigt werden, anscheinend nicht korrekt entfernt (es ist möglich, verschachtelte Loops zu verwenden, aber ich denke, dass es eine einzelne Schleife und eine Division verwendet) gleiche Leistungseinbußen wie oben erwähnt, die Laufzeit von 1,3 Sekunden auf 1,8 bringen. Wenn Sie jedoch Parallelität aktivieren und einen Multicore-Computer verwenden, sehen Sie einige Vorteile. Unglücklicherweise ist dein aktueller Testfall zu klein, um viel Nutzen zu sehen. Also sehe ich ihn auf meiner 6-Kern-Maschine auf 1,2 Sekunden zurückfallen. Wenn ich die Größe wieder auf [1..v] anstelle von [1..50] zurückstelle, wird sie durch die Parallelität von 32 Sekunden auf 13 erhöht. Vermutlich, wenn Sie diesem Programm eine größere Eingabe geben, sehen Sie möglicherweise mehr Nutzen.

      Wenn Sie interessiert sind, habe ich meine repa -ifizierte Version hier gepostet.

    • BEARBEITEN: Verwende -fllvm . Testen auf meinem Computer, mit repa , bekomme ich 14,7 Sekunden ohne Parallelität, was fast so gut ist wie ohne -fllvm und mit Parallelität. Im Allgemeinen kann LLVM arraybasierten Code wie diesen sehr gut verarbeiten.

gereeter 28.01.2014, 21:34
quelle