Я пытаюсь понять производительность рассылки Haskell.
У меня длинный список (длинa > 1000), который я оцениваю параллельно, используя параллельный parMap
.
Вот полный вывод статистики с использованием +RTS -s
для одного потока (EDIT: полная статистика):
54,248,802,288 bytes allocated in the heap
324,451,424 bytes copied during GC
2,970,272 bytes maximum residency (4 sample(s))
52,064 bytes maximum slop
217 MB total memory in use (1 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 251 colls, 0 par 1.45s 1.49s 0.0059s 0.0290s
Gen 1 4 colls, 0 par 0.03s 0.05s 0.0125s 0.0319s
TASKS: 4 (1 bound, 3 peak workers (3 total), using -N1)
SPARKS: 6688 (0 converted, 0 overflowed, 0 dud, 1439 GC'd, 5249 fizzled)
INIT time 0.00s ( 0.03s elapsed)
MUT time 19.76s ( 20.20s elapsed)
GC time 1.48s ( 1.54s elapsed)
EXIT time 0.00s ( 0.00s elapsed)
Total time 21.25s ( 21.78s elapsed)
Alloc rate 2,745,509,084 bytes per MUT second
Productivity 93.0% of total user, 90.8% of total elapsed
gc_alloc_block_sync: 0
whitehole_spin: 0
gen[0].sync: 0
gen[1].sync: 0
Если я запускаю два потока, используя +RTS -N2
, я получаю:
54,336,738,680 bytes allocated in the heap
346,562,320 bytes copied during GC
5,437,368 bytes maximum residency (5 sample(s))
120,000 bytes maximum slop
432 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 127 colls, 127 par 2.07s 0.99s 0.0078s 0.0265s
Gen 1 5 colls, 4 par 0.08s 0.04s 0.0080s 0.0118s
Parallel GC work balance: 41.39% (serial 0%, perfect 100%)
TASKS: 6 (1 bound, 5 peak workers (5 total), using -N2)
SPARKS: 6688 (6628 converted, 0 overflowed, 0 dud, 0 GC'd, 60 fizzled)
INIT time 0.00s ( 0.01s elapsed)
MUT time 25.31s ( 13.35s elapsed)
GC time 2.15s ( 1.03s elapsed)
EXIT time 0.01s ( 0.01s elapsed)
Total time 27.48s ( 14.40s elapsed)
Alloc rate 2,146,509,982 bytes per MUT second
Productivity 92.2% of total user, 175.9% of total elapsed
gc_alloc_block_sync: 19922
whitehole_spin: 0
gen[0].sync: 1
gen[1].sync: 0
и по четырем потокам:
54,307,370,096 bytes allocated in the heap
367,282,056 bytes copied during GC
8,561,960 bytes maximum residency (6 sample(s))
3,885,784 bytes maximum slop
860 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 62 colls, 62 par 2.45s 0.70s 0.0113s 0.0179s
Gen 1 6 colls, 5 par 0.20s 0.07s 0.0112s 0.0146s
Parallel GC work balance: 40.57% (serial 0%, perfect 100%)
TASKS: 10 (1 bound, 9 peak workers (9 total), using -N4)
SPARKS: 6688 (6621 converted, 0 overflowed, 0 dud, 3 GC'd, 64 fizzled)
INIT time 0.01s ( 0.01s elapsed)
MUT time 37.26s ( 10.95s elapsed)
GC time 2.65s ( 0.77s elapsed)
EXIT time 0.01s ( 0.01s elapsed)
Total time 39.94s ( 11.76s elapsed)
Alloc rate 1,457,427,453 bytes per MUT second
Productivity 93.4% of total user, 317.2% of total elapsed
gc_alloc_block_sync: 23494
whitehole_spin: 0
gen[0].sync: 10527
gen[1].sync: 38
Таким образом, согласно прошедшему времени (последнее число в каждом выходе), с двумя ядрами программа занимает ~ 66% однопоточной версии, а с четырьмя ядрами она занимает 54% времени. Это ускорение не так уж плохо, но намного хуже, чем теоретически ожидаемое линейное улучшение с количеством ядер, что приведет к 25% времени выполнения с четырьмя ядрами.
Теперь, смотря на приведенные выше статистические выходы, я вижу, что фактическое рабочее время процессора для программы (строки, начинающиеся с MUT
) значительно увеличивается с использованием большего количества ядер. С 1, 2 и 4 ядрами я получаю процессорное время 19.76s, 25.31s и 37.26s, и это увеличение - это то, что, как я полагаю, - потребление моей производительности параллелизации.
Типичные причины для таких издержек времени выполнения процессора с несколькими ядрами, которые мне приходят в голову:
- слишком тонкая гранулярность распределения рабочей нагрузки. Тем не менее, я пробовал ту же программу, используя
parListChunked
из пакетаparallel
с размером блока 10, но результат очень схож, поэтому я не думаю, что накладные расходы обусловлены слишком тонкой детализацией. - Сбор мусора: это был большой убийца производительности для моего кода в прошлом, но поскольку я увеличил размер GC до 100 Мб, общее время, проведенное в GC, довольно мало, как видно из статистики выше.
Каковы другие причины столь сильных накладных расходов и как их смягчить?