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