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 parBuffer
con un tamaño de búfer de 50. Elegí parBuffer
porque 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 parListChunk
fuerza el lomo de la lista.
Luego intenté escribir mi propia estrategia que dividía perezosamente la lista en partes y luego pasaba las partes parBuffer
y 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.
Como puede ver, los subprocesos se detienen debido a desbordamientos del montón. Intenté agregar lo +RTS -M1G
que 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 -K1G
pero 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 colorPixel
es:
colorPixel :: World -> ((Float,Float),([(Float,Float)],[(Float,Float)])) -> Color
fuente
concat $ withStrategy …
? No puedo reproducir este comportamiento en6008010
, que es el compromiso más cercano a su edición.Strategy
. Debería haber elegido una palabra mejor. Además, el problema del desbordamiento del montón ocurre conparListChunk
yparBuffer
también.Respuestas:
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 .
Entonces, si corta los datos en trozos más pequeños, habilita el motor para que realice la limpieza antes: GC se activa.
fuente