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