Ich versuche einen Raytracer zu parallelisieren. Dies bedeutet, dass ich eine sehr lange Liste kleiner Berechnungen habe. Das Vanille-Programm wird in 67,98 Sekunden und 13 MB Gesamtspeicherverbrauch sowie 99,2% Produktivität in einer bestimmten Szene ausgeführt.
Bei meinem ersten Versuch habe ich die parallele Strategie parBuffer
mit einer Puffergröße von 50 verwendet. Ich habe mich dafür entschieden, parBuffer
weil sie nur so schnell durch die Liste läuft, wie Funken verbraucht werden, und nicht den Rücken der Liste erzwingt parList
, was viel Speicher verbrauchen würde da ist die liste sehr lang. Mit -N2
lief es in einer Zeit von 100,46 Sekunden und 14 MB Gesamtspeicherauslastung und 97,8% Produktivität. Die Funkeninformation lautet:SPARKS: 480000 (476469 converted, 0 overflowed, 0 dud, 161 GC'd, 3370 fizzled)
Der große Anteil an funkelnden Funken weist darauf hin, dass die Granularität der Funken zu gering war. Als Nächstes habe ich versucht, die Strategie zu verwenden parListChunk
, bei der die Liste in Blöcke aufgeteilt wird und für jeden Block ein Funke erzeugt wird. Ich habe die besten Ergebnisse mit einer Blockgröße von erzielt 0.25 * imageWidth
. Das Programm lief in 93,43 Sekunden und 236 MB Gesamtspeicherauslastung und 97,3% Produktivität. Die Funkeninformation lautet : SPARKS: 2400 (2400 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
. Ich glaube, der viel größere Speicherbedarf liegt daran parListChunk
, dass er den Rücken der Liste erzwingt.
Dann versuchte ich, meine eigene Strategie zu schreiben, die die Liste träge in Blöcke aufteilte und dann die Blöcke an parBuffer
die Ergebnisse weitergab und sie verkettete.
concat $ withStrategy (parBuffer 40 rdeepseq) (chunksOf 100 (map colorPixel pixels))
Dies lief in 95,99 Sekunden und 22 MB Gesamtspeicherauslastung und 98,8% Produktivität. Dies war in dem Sinne erfolgreich, dass alle Funken konvertiert werden und die Speichernutzung viel geringer ist, die Geschwindigkeit jedoch nicht verbessert wird. Hier ist ein Bild eines Teils des Eventlog-Profils.
Wie Sie sehen, werden die Threads aufgrund von Heap-Überläufen gestoppt. Ich habe versucht hinzuzufügen, +RTS -M1G
wodurch die Standardgröße des Heapspeichers auf 1 GB erhöht wird. Die Ergebnisse haben sich nicht geändert. Ich habe gelesen, dass der Haskell-Hauptthread Speicher vom Heap verwendet, wenn sein Stapel überläuft, daher habe ich auch versucht, die Standardstapelgröße zu erhöhen, +RTS -M1G -K1G
aber dies hatte auch keine Auswirkungen.
Kann ich noch etwas ausprobieren? Ich kann bei Bedarf detailliertere Profilinformationen für die Speichernutzung oder das Ereignisprotokoll veröffentlichen. Ich habe nicht alles aufgenommen, da es sich um eine Menge Informationen handelt und ich nicht dachte, dass alles erforderlich ist, um sie aufzunehmen.
BEARBEITEN: Ich habe über die Haskell RTS-Multicore-Unterstützung gelesen und es wird davon gesprochen, dass es für jeden Kern einen HEC (Haskell Execution Context) gibt. Jeder HEC enthält unter anderem einen Zuordnungsbereich (der Teil eines einzelnen gemeinsam genutzten Heaps ist). Immer wenn der Zuordnungsbereich eines HEC erschöpft ist, muss eine Speicherbereinigung durchgeführt werden. Das scheint eine RTS-Option zu sein, um es zu steuern, -A. Ich habe -A32M ausprobiert, aber keinen Unterschied festgestellt.
EDIT2: Hier ist ein Link zu einem Github-Repo, das dieser Frage gewidmet ist . Ich habe die Profilerstellungsergebnisse in den Profilerstellungsordner aufgenommen.
EDIT3: Hier ist das relevante Codebit:
render :: [([(Float,Float)],[(Float,Float)])] -> World -> [Color]
render grids world = cs where
ps = [ (i,j) | j <- reverse [0..wImgHt world - 1] , i <- [0..wImgWd world - 1] ]
cs = map (colorPixel world) (zip ps grids)
--cs = withStrategy (parListChunk (round (wImgWd world)) rdeepseq) (map (colorPixel world) (zip ps grids))
--cs = withStrategy (parBuffer 16 rdeepseq) (map (colorPixel world) (zip ps grids))
--cs = concat $ withStrategy (parBuffer 40 rdeepseq) (chunksOf 100 (map (colorPixel world) (zip ps grids)))
Die Gitter sind zufällige Floats, die von colorPixel vorberechnet und verwendet werden. Der Typ colorPixel
ist:
colorPixel :: World -> ((Float,Float),([(Float,Float)],[(Float,Float)])) -> Color
quelle
concat $ withStrategy …
? Ich kann dieses Verhalten nicht reproduzieren, da dies6008010
der Bearbeitung am nächsten kommt.Strategy
. Hätte ein besseres Wort wählen sollen. Das Problem mit dem Heap-Überlauf tritt auch beiparListChunk
und aufparBuffer
.Antworten:
Nicht die Lösung für Ihr Problem, sondern ein Hinweis auf die Ursache:
Haskell scheint bei der Wiederverwendung von Speicher sehr konservativ zu sein, und wenn der Interpreter das Potenzial sieht, einen Speicherblock zurückzugewinnen, ist dies der Fall. Ihre Problembeschreibung passt zum hier beschriebenen geringfügigen GC-Verhalten (unten) https://wiki.haskell.org/GHC/Memory_Management .
Wenn Sie also die Daten in kleinere Blöcke zerlegen, aktivieren Sie die Engine, um die Bereinigung früher durchzuführen - GC wird aktiviert.
quelle