如何减少Haskell的并行化开销?

时间:2015-03-11 08:46:39

标签: performance haskell parallel-processing

我试图了解Haskell的并行化性能。

我有一个很长的列表(长度> 1000)我正在使用并行的parMap并行评估。

以下是使用+RTS -s表示单个帖子的完整统计信息输出(编辑:完整统计信息输出):

        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%的时间。这种加速速度并不算太差,但远远低于理论上预期的内核数量的线性改进,这将导致4个内核的运行时间为25%。

现在,在查看上面的统计信息输出时,我可以看到程序的实际工作CPU时间(以MUT开头的行)随着使用更多内核而显着增加。有了1,2和4个内核,我得到的CPU时间分别为19.76秒,25.31秒和37.26秒,这个增加就是 - 我相信 - 正在耗尽我的并行化性能。

我想到的具有多个内核的CPU运行时开销的典型原因是:

  • 工作负载分配的细粒度太小。但是,我使用parListChunked包中的parallel尝试了相同的程序,块大小为10.但结果非常相似,所以我现在不认为开销是由于太精细的粒度。
  • 垃圾收集:过去,这对我的代码来说是一个很大的性能杀手,但是由于我将GC的大小增加到100Mb,因此在GC中花费的总时间非常少,如上面的统计数据所示。

如此强大的开销有什么其他原因,我该如何减轻它们?

1 个答案:

答案 0 :(得分:3)

我看到有人投票决定关闭这个问题,因为没有足够的细节,但我相信可以使用已提供的信息找到答案(尽管欢迎提供更多细节。)

我的鼻子告诉我你受内存吞吐量的限制。我会试着描述为什么我这么认为,但我不是硬件专家,所以我可能部分或完全错误。毕竟,它基于我个人的硬件架构神话。

让我们假设限制介于每秒50-100Gb之间(我不确定它是否正确,如果你有更好的数字,请纠正我。)

您在10秒内分配54Gb(-N4情况),因此您的吞吐量为5Gb /秒。它非常高,但通常它本身不是问题。

大多数分配通常是短暂的生活,并且一旦gen0分配区域(托儿所)被填满它们就是GC。默认情况下,托儿所大小为512 Kb,因此所有分配都在L2缓存中进行。如此短暂的生活数据永远不会进入主存,这就是为什么它几乎是免费的。

但是你将幼儿园的大小增加到100Mb。它不适合L2缓存,并将被转移到主存储器。这已经是一个不好的迹象。

嗯,5Gb /秒远远没有限制。但是有一个原因可以增加幼儿园的数量 - 你的数据不是短暂的生活。它将在一些滞后后的其他地方使用。这意味着这个54Gb迟早会从主内存加载回缓存。所以你至少有10Gb /秒的吞吐量。

它仍然远离极限,但请注意,这是最好的情况 - 顺序内存访问模式。实际上,您是以随机顺序访问内存,因此多次加载和卸载相同的缓存行,您可以轻松达到100Gb /秒。

要解决此问题,您应该确定为什么我们的数据不会短暂生存并尝试解决这个问题。如果无法做到这一点,您可以尝试增加数据局部性并更改内存访问模式以使其顺序。

我想知道硬件专家对我的天真解释的看法:)