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

114

Я пытаюсь распараллелить трассировщик лучей. Это означает, что у меня есть очень длинный список небольших вычислений. Ванильная программа запускается на определенной сцене за 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%. Это было успешным в том смысле, что все искры преобразуются, и использование памяти намного ниже, однако скорость не улучшается. Вот изображение части профиля журнала событий.Профиль журнала событий

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

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

РЕДАКТИРОВАТЬ: я читал о поддержке многоядерности Haskell RTS , и в нем говорится о существовании HEC (Haskell Execution Context) для каждого ядра. Каждый HEC содержит, среди прочего, область распределения (которая является частью единой общей кучи). Каждый раз, когда любая область распределения HEC исчерпывается, должна выполняться сборка мусора. Кажется, это опция RTS для управления им, -A. Я попробовал -A32M, но не увидел разницы.

EDIT2: вот ссылка на репозиторий 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
Джастин Раймонд
источник
2
Не могли бы вы указать точную фиксацию, где вы пробовали concat $ withStrategy …? Я не могу воспроизвести это поведение в 6008010, что является наиболее близким к вашему изменению.
Zeta
3
Я сделал специальное репо, чтобы случайно не повозиться с ним. Я также включил всю информацию о профилировании.
Джастин Рэймонд,
@dfeuer, когда я сказал определить мою собственную стратегию, я не имел в виду Strategy. Надо было подобрать слово получше. Кроме того, проблема переполнения кучи возникает с parListChunkи parBuffer.
Джастин Рэймонд

Ответы:

2

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

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

Новые данные размещены в 512кб «детской». Как только он исчерпан, происходит «второстепенный сборщик мусора» - он сканирует питомник и освобождает неиспользованные значения.

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

Thinkeye
источник