Estou tentando paralelizar um ray-tracer. Isso significa que tenho uma lista muito longa de pequenos cálculos. O programa vanilla é executado em uma cena específica em 67,98 segundos e 13 MB de uso de memória total e produtividade de 99,2%.
Na minha primeira tentativa, usei a estratégia paralela parBuffer
com um tamanho de buffer de 50. Escolhi parBuffer
porque ela percorre a lista tão rápido quanto as faíscas são consumidas e não força a espinha da lista como parList
, o que usaria muita memória já que a lista é muito longa. Com -N2
ele rodou em um tempo de 100,46 segundos e 14 MB de uso de memória total e produtividade de 97,8%. A informação da centelha é:SPARKS: 480000 (476469 converted, 0 overflowed, 0 dud, 161 GC'd, 3370 fizzled)
A grande proporção de faíscas efervescentes indica que a granularidade das faíscas era muito pequena, então, em seguida, tentei usar a estratégia parListChunk
, que divide a lista em pedaços e cria uma faísca para cada pedaço. Obtive os melhores resultados com um tamanho de bloco de 0.25 * imageWidth
. O programa rodou em 93,43 segundos e 236 MB de uso de memória total e produtividade de 97,3%. As informações de ignição é: SPARKS: 2400 (2400 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
. Eu acredito que o uso de memória muito maior é porqueparListChunk
força a espinha dorsal da lista.
Em seguida, tentei escrever minha própria estratégia que preguiçosamente dividiu a lista em pedaços e depois os passou parBuffer
e concatenou os resultados.
concat $ withStrategy (parBuffer 40 rdeepseq) (chunksOf 100 (map colorPixel pixels))
Isso funcionou em 95,99 segundos e 22 MB de uso de memória total e produtividade de 98,8%. Isso foi bem sucedido no sentido de que todas as faíscas estão sendo convertidas e o uso de memória é muito menor, porém a velocidade não é melhorada. Aqui está uma imagem de parte do perfil do log de eventos.
Como você pode ver, os threads estão sendo interrompidos devido a estouros de heap. Tentei adicionar, o +RTS -M1G
que aumenta o tamanho de heap padrão até 1 Gb. Os resultados não mudaram. Eu li que o thread principal do Haskell usará a memória do heap se sua pilha estourar, então também tentei aumentar o tamanho da pilha padrão, +RTS -M1G -K1G
mas isso também não teve nenhum impacto.
Posso tentar mais alguma coisa? Posso postar informações de perfil mais detalhadas para uso de memória ou log de eventos, se necessário. Não incluí tudo porque é uma grande quantidade de informações e não achei que fosse necessário incluir tudo.
EDIT: Eu estava lendo sobre o suporte a multicore Haskell RTS , e ele fala sobre a existência de um HEC (Haskell Execution Context) para cada núcleo. Cada HEC contém, entre outras coisas, uma área de alocação (que faz parte de um único heap compartilhado). Sempre que qualquer Área de Alocação do HEC se esgote, uma coleta de lixo deve ser realizada. Parece ser uma opção RTS para controlá-lo, -A. Tentei -A32M, mas não vi diferença.
EDIT2: Aqui está um link para um repositório github dedicado a esta questão . Incluí os resultados de criação de perfil na pasta de criação de perfil.
EDIT3: Aqui está a parte relevante do código:
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)))
As grades são flutuadores aleatórios que são pré-computados e usados por colorPixel. O tipo de colorPixel
é:
colorPixel :: World -> ((Float,Float),([(Float,Float)],[(Float,Float)])) -> Color
fonte