Estouro de heap de threads de Haskell apesar do uso de memória total de apenas 22 MB?


114

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 parBuffercom um tamanho de buffer de 50. Escolhi parBufferporque 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 -N2ele 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 parBuffere 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.Perfil de log de eventos

Como você pode ver, os threads estão sendo interrompidos devido a estouros de heap. Tentei adicionar, o +RTS -M1Gque 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 -K1Gmas 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

Respostas:


2

Não é a solução para o seu problema, mas uma dica para a causa:

Haskell parece ser muito conservador na reutilização de memória e quando o intérprete vê o potencial de recuperar um bloco de memória, ele vai em frente. A descrição do seu problema se encaixa no comportamento secundário do GC descrito aqui (parte inferior) https://wiki.haskell.org/GHC/Memory_Management .

Novos dados são alocados em "berçário" de 512kb. Depois de esgotado, ocorre "GC menor" - ele verifica o berçário e libera os valores não utilizados.

Portanto, se você dividir os dados em pedaços menores, habilitará o mecanismo para fazer a limpeza mais cedo - o GC entra em ação.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.