Haskell-Threads Heap-Überlauf trotz nur 22 MB Gesamtspeicherauslastung?

114

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 parBuffermit einer Puffergröße von 50 verwendet. Ich habe mich dafür entschieden, parBufferweil 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 -N2lief 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 parBufferdie 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.Ereignisprotokollprofil

Wie Sie sehen, werden die Threads aufgrund von Heap-Überläufen gestoppt. Ich habe versucht hinzuzufügen, +RTS -M1Gwodurch 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 -K1Gaber 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 colorPixelist:

 colorPixel :: World -> ((Float,Float),([(Float,Float)],[(Float,Float)])) -> Color
Justin Raymond
quelle
2
Könnten Sie das genaue Commit angeben, bei dem Sie es versucht haben concat $ withStrategy …? Ich kann dieses Verhalten nicht reproduzieren, da dies 6008010der Bearbeitung am nächsten kommt.
Zeta
3
Ich habe ein spezielles Repo gemacht, damit ich nicht versehentlich darin herumspiele. Ich habe auch alle Profilinformationen aufgenommen.
Justin Raymond
@dfeuer als ich sagte meine eigene Strategie zu definieren, meinte ich nicht Strategy. Hätte ein besseres Wort wählen sollen. Das Problem mit dem Heap-Überlauf tritt auch bei parListChunkund auf parBuffer.
Justin Raymond

Antworten:

2

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 .

Neue Daten werden in 512kb "Kindergarten" zugeordnet. Sobald es erschöpft ist, tritt "Minor GC" auf - es scannt den Kindergarten und gibt nicht verwendete Werte frei.

Wenn Sie also die Daten in kleinere Blöcke zerlegen, aktivieren Sie die Engine, um die Bereinigung früher durchzuführen - GC wird aktiviert.

Thinkeye
quelle