Débordement de tas de threads Haskell malgré une consommation totale de mémoire de seulement 22 Mo?

J’essaie de paralléliser un traceur de rayons. Cela signifie que j’ai une très longue liste de petits calculs. Le programme vanilla fonctionne sur une scène spécifique en 67,98 secondes et 13 Mo de mémoire totale et 99,2% de productivité.

Lors de ma première tentative, j’ai utilisé le parBuffer stratégie parallèle avec une taille de mémoire tampon de 50. J’ai choisi parBuffer car il ne parcourait la liste que lorsque les étincelles étaient consommées et ne forçait pas la colonne comme parList . beaucoup de mémoire puisque la liste est très longue. Avec -N2 , il a fonctionné dans un temps de 100,46 secondes et 14 Mo d’utilisation totale de la mémoire et une productivité de 97,8%. L’information d’étincelle est: SPARKS: 480000 (476469 converted, 0 overflowed, 0 dud, 161 GC'd, 3370 fizzled)

La grande proportion d’étincelles pétillantes indique que la granularité des étincelles était trop faible. J’ai donc essayé d’utiliser la stratégie parListChunk , qui divise la liste en morceaux et crée une étincelle pour chaque bloc. J’ai obtenu les meilleurs résultats avec une taille de bloc de 0.25 * imageWidth . Le programme a fonctionné en 93,43 secondes et 236 Mo de mémoire totale et 97,3% de productivité. L’information d’étincelle est la suivante: SPARKS: 2400 (2400 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled) . Je pense que la plus grande utilisation de la mémoire est parce que parListChunk force le dos de la liste.

Ensuite, j’ai essayé d’écrire ma propre stratégie qui divisait paresseusement la liste en morceaux, puis passais les morceaux à parBuffer et concaténais les résultats.

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

Cela a fonctionné dans 95,99 secondes et 22 Mo de mémoire totale et 98,8% de productivité. Cela a été un succès dans la mesure où toutes les étincelles ont été converties et l’utilisation de la mémoire est beaucoup plus faible, mais la vitesse n’est pas améliorée. Voici une image d’une partie du profil du journal des événements. Profil du journal des événements

Comme vous pouvez le voir, les threads sont arrêtés en raison de débordements de tas. J’ai essayé d’append +RTS -M1G ce qui augmente la taille de +RTS -M1G par défaut jusqu’à 1 Go. Les résultats n’ont pas changé. J’ai lu que le thread principal Haskell utilisera la mémoire du tas si sa stack déborde, aussi j’ai aussi essayé d’augmenter la taille de stack par défaut avec +RTS -M1G -K1G mais cela n’a pas non plus d’impact.

Y a-t-il autre chose que je peux essayer? Je peux publier des informations de profilage plus détaillées pour l’utilisation de la mémoire ou le journal des événements si nécessaire, je ne les ai pas tous inclus car il y a beaucoup d’informations et je ne pensais pas que toutes les informations étaient nécessaires.

EDIT: Je lisais à propos du support multicœur Haskell RTS , et il est question d’un HEC (Haskell Execution Context) pour chaque cœur. Chaque HEC contient, entre autres, une zone d’allocation (qui fait partie d’un même segment de mémoire partagé). Chaque fois qu’une zone d’allocation HEC est épuisée, une collecte de données doit être effectuée. Le semble être une option RTS pour le contrôler, -A. J’ai essayé -A32M mais je n’ai vu aucune différence.

EDIT2: Voici un lien vers un repository github dédié à cette question . J’ai inclus les résultats de profilage dans le dossier de profilage.

EDIT3: Voici le bit de code pertinent:

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

Les grids sont des flottants aléatoires précalculés et utilisés par colorPixel. Le type de colorPixel est le suivant:

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

Pas la solution à votre problème, mais un indice à la cause:

Haskell semble être très conservateur dans la réutilisation de la mémoire et lorsque l’interprète voit le potentiel de récupérer un bloc de mémoire, il le fait. Votre description de problème correspond au comportement mineur du GC décrit ici (en bas) https://wiki.haskell.org/GHC/Memory_Management .

De nouvelles données sont allouées dans 512kb “nursery”. Une fois épuisé, un “GC mineur” se produit – il scanne la pépinière et libère les valeurs inutilisées.

Donc, si vous coupez les données en morceaux plus petits, vous activez le moteur pour effectuer le nettoyage plus tôt – GC se lance.