Ленивый ввод-вывод параллелизм: преобразование изображения в оттенки серого

#haskell #parallel-processing

#haskell #параллельная обработка

Вопрос:

Я пытаюсь добавить параллелизм к программе, которая преобразует формат .bmp в формат .bmp в оттенках серого. Обычно я наблюдаю снижение производительности параллельного кода в 2-4 раза. Я настраиваю размеры parBuffer / chunking и, похоже, до сих пор не могу разобраться в этом. Ищу рекомендации.

Здесь использован весь исходный файл: http://lpaste.net/106832

Мы используем Codec.BMP для чтения в потоке пикселей, представленных type RGBA = (Word8, Word8, Word8, Word8) . Чтобы преобразовать в оттенки серого, просто сопоставьте преобразование «яркости» по всем пикселям.

Последовательная реализация буквально:

 toGray :: [RGBA] -> [RGBA]
toGray x = map luma x
  

Тестовый ввод в формате bmp составляет 5184 x 3456 (71,7 МБ).

Последовательная реализация выполняется за ~ 10 секунд, ~ 550 нс / пиксель. Threadscope выглядит чистым:

последовательный

Почему это происходит так быстро? Я полагаю, что в нем есть что-то с lazy ByteString (хотя Codec.BMP использует строгую байтовую строку — происходит ли здесь неявное преобразование?) и слияние.

Добавление параллелизма

Первая попытка добавления параллелизма была через parList . О боже. Программа использовала ~ 4-5 ГБ памяти, и система начала перекачиваться.

Затем я прочитал раздел «Распараллеливание ленивых потоков с помощью parBuffer» в книге О’Рейли Саймона Марлоу и попробовал parBuffer использовать большой размер. Это по-прежнему не обеспечивало желаемой производительности. Размеры spark были невероятно малы.

Затем я попытался увеличить размер spark, разбив отложенный список на фрагменты, а затем придерживаясь parBuffer для обеспечения параллелизма:

 toGrayPar :: [RGBA] -> [RGBA]
toGrayPar x = concat $ (withStrategy (parBuffer 500 rpar) . map (map luma))
                       (chunk 8000 x)

chunk :: Int -> [a] -> [[a]]
chunk n [] = []
chunk n xs = as : chunk n bs where
  (as,bs) = splitAt (fromIntegral n) xs
  

Но это все еще не дает желаемой производительности:

   18,934,235,760 bytes allocated in the heap
  15,274,565,976 bytes copied during GC
     639,588,840 bytes maximum residency (27 sample(s))
     238,163,792 bytes maximum slop
            1910 MB total memory in use (0 MB lost due to fragmentation)

                                    Tot time (elapsed)  Avg pause  Max pause
  Gen  0     35277 colls, 35277 par   19.62s   14.75s     0.0004s    0.0234s
  Gen  1        27 colls,    26 par   13.47s    7.40s     0.2741s    0.5764s

  Parallel GC work balance: 30.76% (serial 0%, perfect 100%)

  TASKS: 6 (1 bound, 5 peak workers (5 total), using -N2)

  SPARKS: 4480 (2240 converted, 0 overflowed, 0 dud, 2 GC'd, 2238 fizzled)

  INIT    time    0.00s  (  0.01s elapsed)
  MUT     time   14.31s  ( 14.75s elapsed)
  GC      time   33.09s  ( 22.15s elapsed)
  EXIT    time    0.01s  (  0.12s elapsed)
  Total   time   47.41s  ( 37.02s elapsed)

  Alloc rate    1,323,504,434 bytes per MUT second

  Productivity  30.2% of total user, 38.7% of total elapsed

gc_alloc_block_sync: 7433188
whitehole_spin: 0
gen[0].sync: 0
gen[1].sync: 1017408
  

par1

Как я могу лучше рассуждать о том, что здесь происходит?

Комментарии:

1. Установили ли вы разумное базовое время? Сколько времени требуется, чтобы просто вычислить длину [RGBA] ? Поскольку другие ваши комментарии указывают, что это значение передается с помощью ленивого ввода-вывода, вполне возможно, что время ввода-вывода всегда будет доминировать над любой обработкой, которую вы выполняете, параллельной или нет. Итак, сколько времени выполнения занимает только ввод-вывод и синтаксический анализ?

2. Я могу попытаться посмотреть, сколько времени занимает ввод-вывод и синтаксический анализ Codec.BMP. Я использую последовательную реализацию, которая занимает ~ 10 секунд. Я думаю, что это достаточно полезно для сравнения с 30-40 секундами, которые занимает параллельная реализация.

Ответ №1:

У вас есть большой список пикселей RGBA. Почему вы не используете parListChunk с разумным размером фрагмента?

Комментарии:

1. Это, похоже, скорее комментарий, чем ответ, это не решает проблему OP, а просто предлагает что-то попробовать.

2. parListChunk обрабатывает корешок изображения [5184 x 3456], которое занимает много ГБ памяти. Я пытаюсь избежать этого и по-прежнему использую ленивый ввод-вывод.