¿Los subprocesos de Haskell se desbordan a pesar de solo 22Mb de uso total de memoria?

114

Estoy tratando de paralelizar un trazador de rayos. Esto significa que tengo una lista muy larga de pequeños cálculos. El programa básico se ejecuta en una escena específica en 67,98 segundos y 13 MB de uso total de memoria y una productividad del 99,2%.

En mi primer intento utilicé la estrategia paralela parBuffercon un tamaño de búfer de 50. Elegí parBufferporque recorre la lista solo tan rápido como se consumen las chispas y no fuerza el lomo de la lista como parList, lo que usaría mucha memoria ya que la lista es muy larga. Con -N2, funcionó en un tiempo de 100,46 segundos y 14 MB de uso total de memoria y una productividad del 97,8%. La información de la chispa es:SPARKS: 480000 (476469 converted, 0 overflowed, 0 dud, 161 GC'd, 3370 fizzled)

La gran proporción de chispas apagadas indica que la granularidad de las chispas era demasiado pequeña, así que a continuación intenté usar la estrategia parListChunk, que divide la lista en partes y crea una chispa para cada parte. Obtuve los mejores resultados con un tamaño de fragmento de 0.25 * imageWidth. El programa se ejecutó en 93,43 segundos y 236 MB de uso total de memoria y 97,3% de productividad. La información de chispa es: SPARKS: 2400 (2400 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled). Creo que el uso mucho mayor de la memoria se debe a que parListChunkfuerza el lomo de la lista.

Luego intenté escribir mi propia estrategia que dividía perezosamente la lista en partes y luego pasaba las partes parBuffery concatenó los resultados.

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

Esto se ejecutó en 95,99 segundos y 22 MB de uso total de memoria y una productividad del 98,8%. Esto fue exitoso en el sentido de que todas las chispas se están convirtiendo y el uso de la memoria es mucho menor, sin embargo, la velocidad no mejora. Aquí hay una imagen de parte del perfil del registro de eventos.Perfil de registro de eventos

Como puede ver, los subprocesos se detienen debido a desbordamientos del montón. Intenté agregar lo +RTS -M1Gque aumenta el tamaño de pila predeterminado hasta 1 Gb. Los resultados no cambiaron. Leí que el hilo principal de Haskell usará la memoria del montón si su pila se desborda, así que también intenté aumentar el tamaño de pila predeterminado, +RTS -M1G -K1Gpero esto tampoco tuvo ningún impacto.

¿Hay algo más que pueda probar? Puedo publicar información de perfiles más detallada para el uso de la memoria o el registro de eventos si es necesario, no lo incluí todo porque es mucha información y no pensé que fuera necesario incluirlo todo.

EDITAR: Estaba leyendo sobre el soporte multinúcleo Haskell RTS , y habla de que hay un HEC (Contexto de ejecución de Haskell) para cada núcleo. Cada HEC contiene, entre otras cosas, un área de asignación (que es parte de un único montón compartido). Siempre que se agote el área de asignación de cualquier HEC, se debe realizar una recolección de basura. Parece ser una opción RTS para controlarlo, -A. Probé -A32M pero no vi diferencia.

EDIT2: Aquí hay un enlace a un repositorio de github dedicado a esta pregunta . He incluido los resultados de la creación de perfiles en la carpeta de creación de perfiles.

EDIT3: Aquí está el bit de código relevante:

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

Las cuadrículas son flotadores aleatorios que son precalculados y utilizados por colorPixel. El tipo de colorPixeles:

 colorPixel :: World -> ((Float,Float),([(Float,Float)],[(Float,Float)])) -> Color
Justin Raymond
fuente
2
¿Podría proporcionar la confirmación exacta donde lo intentó concat $ withStrategy …? No puedo reproducir este comportamiento en 6008010, que es el compromiso más cercano a su edición.
Zeta
3
Hice un repositorio dedicado para no perder el tiempo accidentalmente en él. También he incluido toda la información de elaboración de perfiles.
Justin Raymond
@dfeuer cuando dije definir mi propia estrategia no quise decir Strategy. Debería haber elegido una palabra mejor. Además, el problema del desbordamiento del montón ocurre con parListChunky parBuffertambién.
Justin Raymond

Respuestas:

2

No es la solución a su problema, sino una pista de la causa:

Haskell parece ser muy conservador en la reutilización de la memoria y cuando el intérprete ve la posibilidad de recuperar un bloque de memoria, lo intenta. La descripción de su problema se ajusta al comportamiento de GC menor descrito aquí (abajo) https://wiki.haskell.org/GHC/Memory_Management .

Los nuevos datos se asignan en un "vivero" de 512 kb. Una vez que se agota, se produce un "GC menor": escanea el vivero y libera los valores no utilizados.

Entonces, si corta los datos en trozos más pequeños, habilita el motor para que realice la limpieza antes: GC se activa.

Thinkeye
fuente