Переполнение кучи потоков Haskell, несмотря на то, что общая память памяти составляет всего 22 Мб?

Я пытаюсь распараллелить луч-трассировщик. Это означает, что у меня очень длинный список небольших вычислений. Программа Vanilla работает на определенной сцене в 67,98 секунды и 13 МБ общей памяти и 99,2% производительности.

В моей первой попытке я использовал параллельную стратегию parBuffer с размером буфера 50. Я выбрал parBuffer, потому что он просматривает список только так быстро, как искры потребляются, и не заставляет позвоночник списка например parList, который будет использовать много памяти, поскольку список очень длинный. С -N2 он работал за 100,46 секунды и 14 МБ общей памяти и 97,8% производительности. Искровная информация: SPARKS: 480000 (476469 converted, 0 overflowed, 0 dud, 161 GC'd, 3370 fizzled)

Большая доля искривленных искр указывает на то, что зернистость искр была слишком мала, поэтому я попытался использовать стратегию parListChunk, которая разбивает список на куски и создает искру для каждого куска. Я получил лучшие результаты с размером куска 0.25 * imageWidth. Программа работала в 93,43 секунды и 236 МБ общей памяти и 97,3% производительности. Информация о искре: SPARKS: 2400 (2400 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled). Я считаю, что гораздо больший объем использования памяти связан с тем, что parListChunk заставляет позвоночник списка.

Затем я попытался написать свою собственную стратегию, которая лениво разделила список на куски, а затем передала куски на parBuffer и объединила результаты.

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

Это заработало 95,99 секунд и 22 МБ общего объема памяти и 98,8% производительности. Это было успешным в том смысле, что все искры преобразуются, а использование памяти намного ниже, однако скорость не улучшается. Вот изображение части профиля eventlog. Профиль журнала событий

Как вы видите, потоки останавливаются из-за переполнения кучи. Я попытался добавить +RTS -M1G, который увеличивает размер кучи по умолчанию до 1 Гб. Результаты не изменились. Я прочитал, что основной поток Haskell будет использовать память из кучи, если ее стек переполняется, поэтому я также попытался увеличить размер стека по умолчанию с помощью +RTS -M1G -K1G, но это также не повлияло.

Есть ли что-нибудь еще, что я могу попробовать? Я могу опубликовать более подробную информацию профилирования для использования памяти или журнала событий, если это необходимо, я не включил все это, потому что это много информации, и я не думал, что все это необходимо включить.

EDIT: я читал о многоядерной поддержке Haskell RTS, и в нем говорится о наличии HEC (контекст исполнения Haskell) для каждого ядра, Каждая HEC содержит, помимо прочего, область распределения (которая является частью единой общей кучи). Всякий раз, когда какая-либо область распределения HEC исчерпана, необходимо выполнить сбор мусора. По-видимому, это параметр Вот ссылка на репозиторий github, посвященный этому вопросу. Я включил результаты профилирования в папку профилирования.

EDIT3: Вот соответствующий бит кода:

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

Сетки представляют собой случайные поплавки, которые предварительно вычисляются и используются colorPixel. Тип colorPixel:

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

Ответ 1

Не решение вашей проблемы, но намек на причину:

Haskell кажется очень консервативным в повторном использовании памяти, и когда интерпретатор видит потенциал для восстановления блока памяти, он идет на это. Описание проблемы соответствует описанному ниже незначительному поведению GC (внизу) https://wiki.haskell.org/GHC/Memory_Management.

Новые данные выделяются в 512kb "питомнике". Как только он исчерпал себя, "незначительные GC" - он сканирует питомник и освобождает неиспользуемые значения.

Итак, если вы нарезаете данные на более мелкие куски, вы даете возможность движку выполнять очистку раньше - GC запускается.