I thread Haskell hanno un overflow dell'heap nonostante solo 22 MB di memoria totale?


114

Sto cercando di parallelizzare un ray-tracer. Ciò significa che ho un elenco molto lungo di piccoli calcoli. Il programma vanilla viene eseguito su una scena specifica in 67,98 secondi e 13 MB di utilizzo totale della memoria e una produttività del 99,2%.

Nel mio primo tentativo ho usato la strategia parallela parBuffercon una dimensione del buffer di 50. Ho scelto parBufferperché scorre l'elenco solo alla velocità con cui si consumano le scintille e non forza la colonna vertebrale dell'elenco come parList, il che userebbe molta memoria poiché l'elenco è molto lungo. Con -N2, ha funzionato in un tempo di 100,46 secondi e 14 MB di utilizzo totale della memoria e una produttività del 97,8%. Le informazioni sulla scintilla sono:SPARKS: 480000 (476469 converted, 0 overflowed, 0 dud, 161 GC'd, 3370 fizzled)

L'elevata percentuale di scintille sbiadite indica che la granularità delle scintille era troppo piccola, quindi ho provato a utilizzare la strategia parListChunk, che divide l'elenco in blocchi e crea una scintilla per ogni blocco. Ho ottenuto i migliori risultati con una dimensione di blocco di 0.25 * imageWidth. Il programma è stato eseguito in 93,43 secondi e 236 MB di memoria totale e una produttività del 97,3%. Le informazioni scintilla è: SPARKS: 2400 (2400 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled). Credo che l'uso della memoria molto maggiore sia dovuto parListChunkal fatto che forza la spina dorsale della lista.

Quindi ho provato a scrivere la mia strategia che divideva pigramente l'elenco in blocchi e poi li ha passati a parBuffere concatenato i risultati.

 concat $ withStrategy (parBuffer 40 rdeepseq) (chunksOf 100 (map colorPixel pixels))

Questo è stato eseguito in 95,99 secondi e 22 MB di utilizzo totale della memoria e il 98,8% di produttività. Questo ha avuto successo nel senso che tutte le scintille vengono convertite e l'utilizzo della memoria è molto inferiore, tuttavia la velocità non è migliorata. Ecco un'immagine di una parte del profilo del registro eventi.Profilo registro eventi

Come puoi vedere, i thread vengono interrotti a causa di overflow dell'heap. Ho provato ad aggiungere +RTS -M1Gche aumenta la dimensione dell'heap predefinito fino a 1 GB. I risultati non sono cambiati. Ho letto che il thread principale di Haskell utilizzerà la memoria dall'heap se il suo stack va in overflow, quindi ho anche provato ad aumentare anche la dimensione dello stack predefinito, +RTS -M1G -K1Gma anche questo non ha avuto alcun impatto.

C'è qualcos'altro che posso provare? Posso pubblicare informazioni di profilazione più dettagliate per l'utilizzo della memoria o il registro eventi se necessario, non ho incluso tutto perché sono molte informazioni e non pensavo che fosse necessario includerle tutte.

EDIT: stavo leggendo del supporto multicore Haskell RTS e parla dell'esistenza di un HEC (Haskell Execution Context) per ogni core. Ogni HEC contiene, tra le altre cose, un'area di allocazione (che fa parte di un singolo heap condiviso). Ogni volta che un'area di allocazione di HEC è esaurita, deve essere eseguita una raccolta dei rifiuti. Sembra essere un'opzione RTS per controllarlo, -A. Ho provato -A32M ma non ho visto differenze.

EDIT2: ecco un collegamento a un repository GitHub dedicato a questa domanda . Ho incluso i risultati di profilazione nella cartella di profilatura.

EDIT3: Ecco il bit di codice rilevante:

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)))

Le griglie sono float casuali precalcolati e utilizzati da colorPixel. Il tipo di colorPixelè:

 colorPixel :: World -> ((Float,Float),([(Float,Float)],[(Float,Float)])) -> Color

2
Potresti fornire il commit esatto in cui hai provato concat $ withStrategy …? Non riesco a riprodurre questo comportamento in 6008010, che è il commit più vicino alla tua modifica.
Zeta

3
Ho creato un repository dedicato in modo da non scherzarci accidentalmente. Ho incluso anche tutte le informazioni di profilazione.
Justin Raymond

@dfeuer quando ho detto di definire la mia strategia non intendevo Strategy. Avrei dovuto scegliere una parola migliore. Inoltre, il problema di overflow dell'heap si verifica anche con parListChunke parBuffer.
Justin Raymond

Risposte:


2

Non la soluzione al tuo problema, ma un accenno alla causa:

Haskell sembra essere molto conservatore nel riutilizzo della memoria e quando l'interprete vede il potenziale per recuperare un blocco di memoria, lo fa. La descrizione del problema si adatta al comportamento GC minore descritto qui (in basso) https://wiki.haskell.org/GHC/Memory_Management .

I nuovi dati sono allocati in 512kb "nursery". Una volta esaurito, si verifica un "GC minore": esegue la scansione del vivaio e libera i valori inutilizzati.

Quindi, se dividi i dati in blocchi più piccoli, abiliti il ​​motore a eseguire la pulizia prima: GC entra in gioco.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.