2015-08-03 2 views
4

Я написал байтовую строку анализатор с использованием библиотеки 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 и получил следующее:

enter image description here

Я вижу очень ясно сборщик мусора работает на НЕС 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 выглядит следующим образом:

enter image description here

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

ответ

4

Прежде всего, я хотел бы предложить ссылку на ваш просмотр кода (link), чтобы дать людям больше информации о том, что вы пытаетесь сделать.

Основная проблема заключается в том, что вы вынуждаете Haskell читать весь файл в памяти с помощью length x. То, что вы хотите сделать, - это поток в результатах, чтобы как можно меньше файлов в памяти в любое время.

Что у вас есть типичная карта-свертка вычисления, так, чтобы разделить нагрузку на две части, моя рекомендация:

  1. Откройте входной файл дважды, создав два дескрипторов файлов.
  2. Расположите второй дескриптор в «середине» файла.
  3. Создайте два вычисления - по одному для каждого дескриптора файла.
  4. Первое вычисление будет считываться с его рукоятки, пока оно не достигнет «середины»; второй будет считываться с его рукоятки, пока не достигнет конца файла.
  5. Каждое вычисление создаст Vector Int
  6. Когда каждое вычисление заканчивается мы объединяем оба вектора вместе (сложить векторы поэлементно.)

Конечно, «средний» файла начало строки, близкой к середине файла.

Сложная часть - шаг 4, поэтому, чтобы упростить ситуацию, предположим, что входной файл уже был разделен на два отдельных файла part1 и part2. Тогда ваши вычисления могут выглядеть так:

main = do 
    content1 <- LB.readFile "part1" 
    content2 <- LB.readFile "part2" 
    let v = runPar $ do a <- spawnP $ computeVector content1 
         b <- spawnP $ computeVector content2 
         vec1 <- get a 
         vec2 <- get b 
         -- combine vec1 and vec2 
         let vec3 = ...vec1 + vec2... 
         return vec3 
    ... 

Вы должны попробовать этот подход и определить, что такое ускорение. Если он выглядит хорошо, мы можем понять, как практически разделить файл на несколько частей, не имея на самом деле копии данных.

Примечание. Я на самом деле не запускаю это, поэтому не знаю, есть ли причуды w.r.t. lazy-IO и Par monad, но эта идея в той или иной форме должна работать.

+0

Следуя вашему предложению, я побежал к испытаниям, один с двумя ядрами и один с тремя. Я создал входной файл и разделил его на 2 части и 3 части. Очень грубые тайминги заключаются в следующем: 1 ядро, 1 Файл, 6000000 пункты: ~ 6,5 с 2 Ядра, 2 Файлы, 3000000 балла: ~ 5,8 с 3 Ядра, 3 Файлы, 2000000 балла: ~ 4,5 s –

+0

Итак, вы получаете некоторые скромные выигрыши в производительности. Позвольте мне изучить некоторые ... – ErikR

+0

Знаете ли вы о книге [Параллельное и параллельное программирование в Haskell] (http://chimera.labs.oreilly.com/books/1230000000929)? – ErikR

Смежные вопросы