Я пытаюсь распараллелить трассировщик лучей. Это означает, что у меня есть очень длинный список небольших вычислений. Ванильная программа запускается на определенной сцене за 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
источник
concat $ withStrategy …
? Я не могу воспроизвести это поведение в6008010
, что является наиболее близким к вашему изменению.Strategy
. Надо было подобрать слово получше. Кроме того, проблема переполнения кучи возникает сparListChunk
иparBuffer
.Ответы:
Не решение вашей проблемы, а намек на причину:
Haskell, кажется, очень консервативен в повторном использовании памяти, и когда интерпретатор видит возможность вернуть блок памяти, он идет на это. Описание вашей проблемы соответствует второстепенному поведению GC, описанному здесь (внизу) https://wiki.haskell.org/GHC/Memory_Management .
Так что, если вы разделите данные на более мелкие куски, вы дадите возможность движку выполнить очистку раньше - сработает сборщик мусора.
источник