Haskell threads débordement de tas malgré seulement 22 Mo d'utilisation totale de la mémoire?

114

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 s'exécute sur une scène spécifique en 67,98 secondes et 13 Mo d'utilisation totale de la mémoire et 99,2% de productivité.

Dans ma première tentative, j'ai utilisé la stratégie parallèle parBufferavec une taille de tampon de 50. J'ai choisi parBufferparce qu'elle parcourt la liste seulement aussi vite que les étincelles sont consommées, et ne force pas le dos de la liste comme parList, ce qui utiliserait beaucoup de mémoire puisque la liste est très longue. Avec -N2, il a fonctionné en un temps de 100,46 secondes et 14 Mo d'utilisation totale de la mémoire et une productivité de 97,8%. Les informations sur l'étincelle sont: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 petite, alors j'ai ensuite essayé d'utiliser la stratégie parListChunk, qui divise la liste en morceaux et crée une étincelle pour chaque morceau. J'ai obtenu les meilleurs résultats avec une taille de morceau de 0.25 * imageWidth. Le programme a fonctionné en 93,43 secondes et 236 Mo d'utilisation totale de la mémoire et 97,3% de productivité. Les informations d'allumage est: SPARKS: 2400 (2400 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled). Je crois que l'utilisation beaucoup plus grande de la mémoire est due au fait que parListChunkla colonne vertébrale de la liste est forcée.

Ensuite, j'ai essayé d'écrire ma propre stratégie en divisant paresseusement la liste en morceaux, puis en les transmettant parBufferet en concaténant les résultats.

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

Cela a duré 95,99 secondes et 22 Mo d'utilisation totale de la mémoire et 98,8% de productivité. Cela a été un succès dans le sens où toutes les étincelles sont 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 Eventlog.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'ajouter +RTS -M1Gce qui augmente la taille du tas 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 pile débordait, j'ai donc également essayé d'augmenter la taille de la pile par défaut, +RTS -M1G -K1Gmais cela n'a pas non plus d'impact.

Y a-t-il autre chose que je puisse 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 n'ai pas tout inclus car il s'agit de beaucoup d'informations et je ne pensais pas que tout était nécessaire.

EDIT: Je lisais sur le support multicœur Haskell RTS , et cela parle de l'existence d'un HEC (Haskell Execution Context) pour chaque noyau. Chaque HEC contient, entre autres, une zone d'allocation (qui fait partie d'un seul tas partagé). Chaque fois qu'une zone d'allocation d'un HEC est épuisée, un ramasse-miettes doit être effectué. 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 repo github dédié à cette question . J'ai inclus les résultats du 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 grilles sont des flottants aléatoires qui sont précalculés et utilisés par colorPixel. Le type de colorPixelest:

 colorPixel :: World -> ((Float,Float),([(Float,Float)],[(Float,Float)])) -> Color
Justin Raymond
la source
2
Pourriez-vous fournir le commit exact où vous avez essayé concat $ withStrategy …? Je ne peux pas reproduire ce comportement dans 6008010, qui est le commit le plus proche de votre modification.
Zeta
3
J'ai fait un repo dédié afin de ne pas me déranger accidentellement. J'ai également inclus toutes les informations de profilage.
Justin Raymond
@dfeuer quand j'ai dit définir ma propre stratégie, je ne voulais pas dire Strategy. J'aurais dû choisir un meilleur mot. En outre, le problème de débordement de tas se produit avec parListChunket parBufferaussi.
Justin Raymond

Réponses:

2

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

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

Les nouvelles données sont allouées dans 512kb "nursery". Une fois qu'il est épuisé, un "GC mineur" se produit - il scanne la pépinière et libère les valeurs inutilisées.

Donc, si vous découpez les données en morceaux plus petits, vous permettez au moteur de faire le nettoyage plus tôt - GC démarre.

Thinkeye
la source