Функция параллельного числового интегратора медленнее, чем последовательная версия. Почему?

Я изучаю параллельные стратегии в Haskell.

я написал функцию интегратора на основе трапециевидного правила (последовательного):

integrateT :: (Fractional a, Enum a, NFData a) => (a -> a) -> (a,a) -> a -> a 
integrateT f (ini, fin) dx 
  = let lst = map f [ini,ini+dx..fin]
    in sum lst * dx - 0.5 * (f ini + f fin) * dx

и я запускаю его следующим образом:

main = do
  print $ (integrateT (x -> x^4 - x^3 + x^2 + x/13 + 1) (0.0,1000000.0) 0.01 :: Double)

вот статистика из run:

stack exec lab5 -- +RTS -ls -N2 -s
1.9999974872991426e29
  18,400,147,552 bytes allocated in the heap
      20,698,168 bytes copied during GC
          66,688 bytes maximum residency (2 sample(s))
          35,712 bytes maximum slop
               3 MB total memory in use (0 MB lost due to fragmentation)

                                 Tot time (elapsed)  Avg pause  Max pause
  Gen  0     17754 colls, 17754 par    0.123s   0.105s     0.0000s    0.0011s
  Gen  1         2 colls,     1 par    0.000s   0.000s     0.0001s    0.0002s

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

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

  SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)

  INIT    time    0.001s  (  0.001s elapsed)
  MUT     time    6.054s  (  5.947s elapsed)
  GC      time    0.123s  (  0.106s elapsed)
  EXIT    time    0.001s  (  0.008s elapsed)
  Total   time    6.178s  (  6.061s elapsed)

  Alloc rate    3,039,470,269 bytes per MUT second

  Productivity  98.0% of total user, 98.2% of total elapsed

gc_alloc_block_sync: 77
whitehole_spin: 0
gen[0].sync: 0
gen[1].sync: 0

как вы можете видеть, он работает довольно быстро. Однако, когда я пытаюсь сделать его параллельным, например:

integrateT :: (Fractional a, Enum a, NFData a) => (a -> a) -> (a,a) -> a -> a 
integrateT f (ini, fin) dx 
  = let lst = (map f [ini,ini+dx..fin]) `using` parListChunk 100 rdeepseq
    in sum lst * dx - 0.5 * (f ini + f fin) * dx

Он всегда работает гораздо дольше. Вот пример статистики от сверху:

stack exec lab5 -- +RTS -ls -N2 -s
1.9999974872991426e29
  59,103,320,488 bytes allocated in the heap
  17,214,458,128 bytes copied during GC
  2,787,092,160 bytes maximum residency (15 sample(s))
  43,219,264 bytes maximum slop
        5570 MB total memory in use (0 MB lost due to fragmentation)

                                 Tot time (elapsed)  Avg pause  Max pause
  Gen  0     44504 colls, 44504 par   16.907s  10.804s     0.0002s    0.0014s
  Gen  1        15 colls,    14 par    4.006s   2.991s     0.1994s    1.2954s

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

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

  SPARKS: 1000001 (1000001 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)

  INIT    time    0.001s  (  0.001s elapsed)
  MUT     time   14.298s  ( 12.392s elapsed)
  GC      time   20.912s  ( 13.795s elapsed)
  EXIT    time    0.000s  (  0.003s elapsed)
  Total   time   35.211s  ( 26.190s elapsed)

  Alloc rate    4,133,806,996 bytes per MUT second

  Productivity  40.6% of total user, 47.3% of total elapsed

gc_alloc_block_sync: 2304055
whitehole_spin: 0
gen[0].sync: 0
gen[1].sync: 1105370

несколько вещей, которые я вижу здесь:

  • гораздо больше памяти используется
  • больше времени
  • много времени, проведенного в GC

что еще я сделал

Я немного поэкспериментировал:

  • используется функция parMap, parList и custom parListChunk для оценки списка-каждый раз с результатом намного хуже, чем последовательная версия
  • используемые различные размеры куска - от очень небольшого как 5 К как большой, как половина длины списка-результат был намного хуже, чем последовательная версия каждый раз
  • изменены факторы в основной функции на что-то очень большое, как x^123442, например, добавлено больше делений вместо умножения и т. д. А также уменьшил домен проблемы. Все, чтобы сделать меньше искр, но дороже computionally каждый. Здесь я получил результаты, аналогичные последовательной версии (которые работают около 28 сек с этими новыми полномочиями) - параллельный запуск завершен в 31 сек
  • протестирован каждый запуск с Threadscope, чтобы убедиться, что два ядра были использованы, когда ожидалось - они были!

вопрос

  1. по мере того как параллельное представление улучшило с увеличивая ценой вычисления каждого куска (e.g X^12345) и уменьшение количества кусков - это то, что переключение между кусками убивает производительность в случае, когда факторы очень малы (e.g x^4, x^3-быстро вычислить), следовательно, последовательная версия быстрее? Есть ли способ успешно распараллелить его с лучшей производительностью?
  2. почему параллельная версия использовала так много памяти и времени GC?
  3. Как уменьшить время, проведенное в GC в параллельной версии?

1 ответов


стратегия как parListChunk имеет смысл только тогда, когда большая часть вычислительных затрат заключается в оценке отдельных элементов списка по сравнению с накладными расходами на построение позвоночника списка и т. д. во-первых. С integrateT над простым многочленом, однако, эти элементы очень дешевы для вычисления и большинство стоимость будет в списке накладных расходов. Единственная причина, по которой он все еще работает эффективно в последовательной версии, заключается в том, что GHC может встроить/предохранить большую часть этого бизнеса, но видимо, не в parallelised версия.

решение:каждый поток запустите правильно простую последовательную версию, т. е. разделите интервал вместо его дискретизированной формы списка. Как

integrateT :: (Fractional a, Enum a, NFData a) => (a -> a) -> (a,a) -> a -> a 
integrateT f (ini, fin) dx 
  = let lst = map f [ini,ini+dx..fin]
    in sum lst * dx - 0.5 * (f ini + f fin) * dx

integrateT_par :: (Fractional a, Enum a, NFData a) => (a -> a) -> (a,a) -> a -> a
integrateT_par f (l,r) dx
  = let chunks = [ integrateT f (l + i*wChunk, l + (i+1)*wChunk) dx
                 | i<-[0..nChunks-1] ]
               `using` parList rdeepseq
    in sum chunks
 where nChunks = 100
       wChunk = (r-l)/nChunks

Это не будет иметь значительно хуже памяти или производительности, чем последовательная версия.

ум, как я тестирую его сейчас, он на самом деле не работает параллельно вообще, но довольно уверен, что я просто пропустил что-то в установка...