Ste*_*han 12 parallel-processing performance haskell
我试图了解Haskell的并行化性能.
我有一个长列表(长度> 1000),我正在使用parallel并行评估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
Run Code Online (Sandbox Code Playgroud)
如果我运行两个线程,使用+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
Run Code Online (Sandbox Code Playgroud)
并在四个线程上:
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
Run Code Online (Sandbox Code Playgroud)
因此,根据经过的时间(每个输出中的最后一个数字),使用两个内核,程序占用单线程版本的约66%,而四个内核占54%的时间.这种加速并不算太糟糕,但远远低于理论上预期的内核数量的线性改进,这将导致4个内核的运行时间为25%.
现在,当查看上面的统计输出时,我可以看到程序的实际工作CPU时间(以行开头的行MUT)随着使用更多内核而显着增加.有了1,2和4个内核,我得到的CPU时间分别为19.76秒,25.31秒和37.26秒,这个增加就是 - 我相信 - 正在耗尽我的并行化性能.
我想到的具有多个内核的CPU运行时开销的典型原因是:
parListChunked从parallel包装,用10块大小但结果是非常相似的,所以我不会在那一刻觉得开销是由于太细的粒度.这种强大开销的其他原因是什么,我该如何减轻它们?
我看到人们投票结束这个问题,因为没有足够的细节,但我相信可以使用已经提供的信息找到答案(尽管总是欢迎更多细节。)
我的鼻子告诉我你受到内存吞吐量的限制。我将尝试描述为什么我这么认为,但我不是硬件专家,所以我可能部分或完全错误。毕竟,它是基于我个人的一套硬件架构神话。
让我们假设限制在每秒 50-100Gb 之间(我不确定这是正确的数字,如果您有更好的数字,请纠正我。)
您在 10 秒内分配 54Gb(本-N4例),因此吞吐量为 5Gb/秒。它相当高,但通常它本身不是问题。
大多数分配通常都是短暂的,一旦 gen0 分配区域(托儿所)被填满,它们就会被 GC。默认情况下,nursery 大小为 512 Kb,因此所有分配都发生在 L2 缓存中。如此短暂的数据永远不会进入主内存,这就是为什么它几乎是免费的。
但是您将 Nursery 大小增加到 100Mb。它不适合二级缓存,将被转移到主内存。这已经是一个不好的迹象了。
嗯,5Gb/秒还远未达到极限。但增加苗圃规模是有原因的——您的数据并不是短暂的。经过一段时间的延迟后,它将在其他地方使用。这意味着这 54Gb 迟早会从主内存加载回缓存。所以你至少有 10Gb/秒的吞吐量。
它距离限制还很远,但请注意,这是最好的情况——顺序内存访问模式。实际上,您正在以随机顺序访问内存,因此相同的缓存行会被加载和卸载多次,并且您可以轻松达到 100Gb/秒。
要解决这个问题,您应该确定为什么我们的数据不是短暂的,并尝试解决它。如果这是不可能的,您可以尝试增加数据局部性并更改内存访问模式以使其顺序化。
我想知道硬件专家对我天真的解释有何看法:)