诊断并行monad性能

时间:2015-08-03 22:52:26

标签: haskell parallel-processing

我使用Attoparsec库编写了一个Bytestring解析器:

import qualified Data.ByteString.Char8 as B
import qualified Data.Attoparsec.ByteString.Char8 as P

parseComplex :: P.Parser Complex

我的意图是使用这个解析大(> 5 Gb)文件,所以实现懒惰地使用了这个解析器:

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库时,我编写了一个简单,天真的静态分区函数,我认为它将并行映射我的解析:

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

我可以非常清楚地看到在HEC 1上运行的垃圾收集器正在中断HEC 2上的计算。此外,HEC 1显然比HEC 2分配的工作少。作为测试,我试图调整两个分裂的相对大小。列表重新平衡负载,但我看到这样做后程序的行为没有可察觉的差异。我还尝试在不同大小的输入上运行此操作,使用较大的最小堆分配,并且还使用parMap库中包含的Control.Monad.Par函数,但这些工作也没有对结果

我假设某处有空间泄漏,可能来自let (as,bs) = ...分配,因为在并行情况下内存使用率要高得多。这是问题吗?如果是这样,我该如何解决呢?

编辑:按照建议手动拆分输入数据,我现在看到时间上有一些小的改进。对于6m点输入文件,我手动将文件分成两个3m点文件和三个2m点文件,并分别使用2个和3个核心重新编码。粗略的时间如下:

1核心:6.5s

2核心:5.7s

3核心:4.5s

新的threadscope配置文件如下所示:

enter image description here

开始时的奇怪行为已经消失,但现在仍然有一些看起来像是仍有一些明显的负载平衡问题。

1 个答案:

答案 0 :(得分:4)

首先,我建议您参考代码审核帖子(link),以便为人们提供有关您尝试做的更多背景信息。

您的基本问题是您强制Haskell使用length x将整个文件读入内存。你想要做的是在结果中流式传输,以便在任何时候只有少量文件在内存中。

你所拥有的是典型的map-reduce计算,所以为了将工作量分成两部分,我的建议是:

  1. 打开输入文件两次,创建两个文件句柄。
  2. 将第二个手柄放在&#34;中间&#34;该文件。
  3. 创建两个计算 - 每个文件句柄一个。
  4. 第一个计算将从其句柄读取,直到它到达&#34;中间&#34 ;;第二个将从其句柄读取,直到它到达文件末尾。
  5. 每次计算都会创建Vector Int
  6. 当每个计算完成时,我们将两个向量组合在一起(按元素方式加上向量。)
  7. 当然,&#34;中&#34;该文件是一行的开头,它接近文件的中间位置。

    棘手的部分是第4步,因此为了简化操作,我们假设输入文件已经分成两个单独的文件part1part2。然后你的计算看起来像这样:

    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,但这种想法应该有用。