gloss haskell
Haskell subprocesos montón desbordamiento a pesar de solo 22Mb de uso total de memoria (1)
Estoy tratando de paralelizar un rastreador de rayos. Esto significa que tengo una lista muy larga de pequeños cálculos. El programa vainilla se ejecuta en una escena específica en 67.98 segundos y 13 MB de uso total de memoria y 99.2% de productividad.
En mi primer intento, utilicé la estrategia paralela
parBuffer
con un tamaño de búfer de 50. Elegí
parBuffer
porque recorre la lista tan rápido como se consumen las chispas, y no fuerza el lomo de la lista como
parList
, que usaría un 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 97.8% de productividad.
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 desteñidas indica que la granularidad de las chispas era demasiado pequeña, así que luego intenté usar la estrategia
parListChunk
, que divide la lista en fragmentos y crea una chispa para cada fragmento.
0.25 * imageWidth
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 la chispa es:
SPARKS: 2400 (2400 converted, 0 overflowed, 0 dud, 0 GC''d, 0 fizzled)
.
Creo que el uso de memoria mucho mayor se debe a que
parListChunk
fuerza la columna vertebral de la lista.
Luego traté de escribir mi propia estrategia que dividía perezosamente la lista en trozos y luego pasaba los trozos a
parBuffer
y concatenaba los resultados.
concat $ withStrategy (parBuffer 40 rdeepseq) (chunksOf 100 (map colorPixel pixels))
Esto funcionó en 95.99 segundos y 22 MB de uso total de memoria y 98.8% de productividad. Esto fue exitoso en el sentido de que todas las chispas se están convirtiendo y el uso de 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 están deteniendo debido a desbordamientos del montón.
Intenté agregar
+RTS -M1G
que aumenta el tamaño de
+RTS -M1G
dinámico predeterminado hasta 1Gb.
Los resultados no cambiaron.
Leí que el subproceso principal de Haskell usará memoria del montón si su pila se desborda, así que también intenté aumentar el tamaño de pila predeterminado con
+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 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.
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). Cada vez que se agota el área de asignación de HEC, se debe realizar una recolección de basura. Parece ser una opción RTS para controlarlo, -A. Intenté -A32M pero no vi ninguna 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.
EDITAR3: 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 flotantes aleatorios precalculados y utilizados por colorPixel. El tipo de
colorPixel
es:
colorPixel :: World -> ((Float,Float),([(Float,Float)],[(Float,Float)])) -> Color
No es la solución a su problema, sino una pista de la causa:
Haskell parece ser muy conservador en la reutilización de memoria y cuando el intérprete ve el potencial para reclamar un bloqueo de memoria, lo hace. La descripción de su problema se ajusta al comportamiento menor de GC descrito aquí (abajo) https://wiki.haskell.org/GHC/Memory_Management .
Se asignan nuevos datos en 512kb "vivero". 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 realizar la limpieza antes: GC se activa.