Я написал байтовую строку анализатор с использованием библиотеки Attoparsec:Диагностирования параллельной работы монады
import qualified Data.ByteString.Char8 as B
import qualified Data.Attoparsec.ByteString.Char8 as P
parseComplex :: P.Parser Complex
Моим намерение состояло в том, чтобы использовать это разобрать большую (> 5 Гб) файлы, так что реализация используется этот анализатор лениво:
import qualified Data.ByteString.Lazy.Char8 as LB
import qualified Data.Attoparsec.ByteString.Lazy as LP
extr :: LP.Result a -> a
main = do
rawData <- liftA LB.words (LB.readFile "/mnt/hgfs/outputs/out.txt")
let formatedData = map (extr.LP.parse parseComplex) rawData
...
Выполнение этого на тестовый файл с -O2
и -s
флагов, я вижу:
3,509,019,048 bytes allocated in the heap
2,086,240 bytes copied during GC
58,256 bytes maximum residency (30 sample(s))
126,240 bytes maximum slop
2 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 6737 colls, 0 par 0.03s 0.03s 0.0000s 0.0001s
Gen 1 30 colls, 0 par 0.00s 0.00s 0.0001s 0.0002s
INIT time 0.00s ( 0.00s elapsed)
MUT time 0.83s ( 0.83s elapsed)
GC time 0.04s ( 0.04s elapsed)
EXIT time 0.00s ( 0.00s elapsed)
Total time 0.87s ( 0.86s elapsed)
%GC time 4.3% (4.3% elapsed)
Alloc rate 4,251,154,493 bytes per MUT second
Productivity 95.6% of total user, 95.8% of total elapsed
Поскольку я произвольно сопоставляю функцию над списком, я думал, что этот код, возможно, выиграет от распараллеливания. Я никогда ничего подобного не делал раньше в Haskell, но возиться с Control.Monad.Par
библиотеки, я написал простой, наивный, статическую функцию paritioning что я думал бы карту мой синтаксический параллельно:
import Control.Monad.Par
parseMap :: [LB.ByteString] -> [Complex]
parseMap x = runPar $ do
let (as, bs) = force $ splitAt (length x `div` 2) x
a <- spawnP $ map (extr.LP.parse parseComplex) as
b <- spawnP $ map (extr.LP.parse parseComplex) bs
c <- get a
d <- get b
return $ C++ d
Я не ожидал слишком много от этой функции, однако производительность с параллелью была намного хуже, чем последовательное вычисление. Вот основные функции и результаты, составленные с -O2 -threaded -rtsopts
и выполнены с +RTS -s -N2
:
main = do
rawData <- liftA LB.words (LB.readFile "/mnt/hgfs/outputs/out.txt")
let formatedData = parseMap rawData
...
3,641,068,984 bytes allocated in the heap
356,490,472 bytes copied during GC
82,325,144 bytes maximum residency (10 sample(s))
14,182,712 bytes maximum slop
253 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 4704 colls, 4704 par 0.50s 0.25s 0.0001s 0.0006s
Gen 1 10 colls, 9 par 0.57s 0.29s 0.0295s 0.1064s
Parallel GC work balance: 19.77% (serial 0%, perfect 100%)
TASKS: 4 (1 bound, 3 peak workers (3 total), using -N2)
SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
INIT time 0.00s ( 0.00s elapsed)
MUT time 1.11s ( 0.72s elapsed)
GC time 1.07s ( 0.54s elapsed)
EXIT time 0.02s ( 0.02s elapsed)
Total time 2.20s ( 1.28s elapsed)
Alloc rate 3,278,811,516 bytes per MUT second
Productivity 51.2% of total user, 88.4% of total elapsed
gc_alloc_block_sync: 149514
whitehole_spin: 0
gen[0].sync: 0
gen[1].sync: 32
Как вы можете видеть, там, кажется, много мусора коллекторской деятельности в параллельном случае и нагрузки довольно плохо сбалансированы. Я профилированный выполнение с помощью threadscope и получил следующее:
Я вижу очень ясно сборщик мусора работает на НЕС 1 прерывает вычисление на HEC 2. Кроме того, HEC 1 явно меньше работы, порученной чем ГЭЦ 2. В качестве теста я попытался настроить относительный размер двух разделенных списков для повторной балансировки нагрузок, но после этого я не увидел ощутимой разницы в поведении программы. Я также попытался запустить его на входах различного размера, с более крупными распределениями кучи, а также просто используя функцию parMap
, включенную в библиотеку Control.Monad.Par
, но эти усилия также не повлияли на результат.
Я предполагаю, что где-то есть утечка пространства, возможно, из назначения let (as,bs) = ...
, поскольку использование памяти в параллельном случае намного выше. Это проблема? Если да, то как мне его решить?
EDIT: Расщепление входные данные вручную, как предложено, я теперь вижу некоторые небольшие улучшения в таймингах. Для входного файла длиной 6 м я вручную разбиваю файл на два файла с 3-мя точками и на три файла с 2-мя точками и повторяю код с использованием 2 и 3 ядер соответственно. Грубые тайминги следующим образом:
1 Ядро: 6.5s
2 Ядро: 5.7s
3 Ядро: 4.5s
Новый профиль threadscope выглядит следующим образом:
Странное поведение к началу ушло, но теперь есть еще кого-то смотрит на меня, как есть еще некоторые очевидные проблемы балансировки нагрузки.
Следуя вашему предложению, я побежал к испытаниям, один с двумя ядрами и один с тремя. Я создал входной файл и разделил его на 2 части и 3 части. Очень грубые тайминги заключаются в следующем: 1 ядро, 1 Файл, 6000000 пункты: ~ 6,5 с 2 Ядра, 2 Файлы, 3000000 балла: ~ 5,8 с 3 Ядра, 3 Файлы, 2000000 балла: ~ 4,5 s –
Итак, вы получаете некоторые скромные выигрыши в производительности. Позвольте мне изучить некоторые ... – ErikR
Знаете ли вы о книге [Параллельное и параллельное программирование в Haskell] (http://chimera.labs.oreilly.com/books/1230000000929)? – ErikR