当 运行 带有 -N(并行)标志时 GHC 在做什么?

What is GHC doing when run with -N (parallel) flag?

我编写了以下测试应用程序:

main = print $ sum $ map (read . show) [1 .. 10^7]

当我 运行 使用和不使用 -N 标志时,我得到以下结果:

$ ghc -O2 -threaded -rtsopts -o test test.hs
...
$ time ./test +RTS -s
50000005000000
real    0m12.411s
user    0m12.367s
sys     0m0.040s
$ time ./test +RTS -s -N12
50000005000000
real    0m22.702s
user    1m14.904s
sys     0m12.608s

GHC 似乎决定通过在不同的核心上分配计算(结果非常糟糕)来遵守 -N12 标志,但我找不到任何文档来说明当代码不这样做时它究竟是如何决定这样做的' 包含明确的说明。是否缺少某些文档?

我有 GHC 8.6.5 版。

垃圾收集统计:

$ ghc -O2 -threaded -rtsopts -o test test.hs
...
$ time ./test +RTS -s
50000005000000
  54,332,520,712 bytes allocated in the heap
      53,571,832 bytes copied during GC
          56,824 bytes maximum residency (2 sample(s))
          29,192 bytes maximum slop
               0 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0     52088 colls,     0 par    0.154s   0.150s     0.0000s    0.0001s
  Gen  1         2 colls,     0 par    0.000s   0.000s     0.0001s    0.0001s

  TASKS: 4 (1 bound, 3 peak workers (3 total), using -N1)

  SPARKS: 0(0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)

  INIT    time    0.000s  (  0.000s elapsed)
  MUT     time   12.250s  ( 12.249s elapsed)
  GC      time    0.155s  (  0.151s elapsed)
  EXIT    time    0.001s  (  0.010s elapsed)
  Total   time   12.406s  ( 12.410s elapsed)

  Alloc rate    4,435,169,879 bytes per MUT second

  Productivity  98.7% of total user, 98.7% of total elapsed


real    0m12.411s
user    0m12.367s
sys     0m0.040s
$ time ./test +RTS -s -N12
50000005000000
  54,332,687,840 bytes allocated in the heap
     214,001,248 bytes copied during GC
         183,360 bytes maximum residency (2 sample(s))
         146,696 bytes maximum slop
               0 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0     52088 colls, 52088 par   20.219s   0.975s     0.0000s    0.0001s
  Gen  1         2 colls,     1 par    0.001s   0.000s     0.0001s    0.0002s

  Parallel GC work balance: 0.15% (serial 0%, perfect 100%)

  TASKS: 26 (1 bound, 25 peak workers (25 total), using -N12)

  SPARKS: 0(0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)

  INIT    time    0.007s  (  0.003s elapsed)
  MUT     time   67.281s  ( 21.720s elapsed)
  GC      time   20.221s  (  0.975s elapsed)
  EXIT    time    0.002s  (  0.003s elapsed)
  Total   time   87.511s  ( 22.701s elapsed)

  Alloc rate    807,549,654 bytes per MUT second

  Productivity  76.9% of total user, 95.7% of total elapsed


real    0m22.702s
user    1m14.904s
sys     0m12.608s

GHC 不会自动并行化代码。 (运行时间系统本身可能会利用多个线程进行初始化,从而在启动时提供小的、固定的性能改进,但这是唯一“自动”发生的事情。)

因此,您的代码是 运行ning 顺序。正如一些评论中所指出的,奇怪的性能问题可能是并行垃圾收集。

据观察,当 运行 处理大量功能时,并行 GC 在某些工作负载上的表现非常差。例如,参见 issue #14981。当然,那个issue说的是32核还是64核的机器。

但是,我观察到性能非常差 ,尤其是默认 运行time GC 设置 即使在相对较少的内核上也是如此。例如,使用您的测试用例和 GHC 版本,我在 -N12 或更多的 8 核 16 线程英特尔 i9-9980HK 笔记本电脑上获得了类似的低性能。这是 1 能力和 12 能力的比较 运行。编译它:

$ cat test.hs
main = print $ sum $ map (read . show) [1 .. 10^7]
$ stack ghc --resolver=lts-14.27 -- -fforce-recomp -O2 -threaded -rtsopts -o test test.hs
[1 of 1] Compiling Main             ( test.hs, test.o )
Linking test ...

运行 它具有一项功能:

$ time ./test +RTS -N1
50000005000000

real    0m10.803s
user    0m10.770s
sys     0m0.037s

运行十二点吧:

$ time ./test +RTS -N12
50000005000000

real    0m15.655s
user    0m52.103s
sys     0m7.019s

要查看并行 GC 的错误,我们可以切换到顺序 GC:

$ time ./test +RTS -N12 -qg
50000005000000

real    0m11.175s
user    0m11.066s
sys     0m0.120s

我原以为这种糟糕的并行 GC 性能与超过物理核心数量有关,但您的经验表明,即使没有超过物理核心数量,这种情况也可能发生在大约 12 个功能的情况下。

不要完全禁用并行 GC,建议您使用 运行时间 garbage collector controls。效果可能是惊人的。例如,将第 0 代分配区域从默认的 1m 增加到 4m 会产生很大的改进:

$ time ./test +RTS -N12 -A4m
50000005000000

real    0m12.485s
user    0m25.219s
sys     0m2.053s

甚至更高到 16m 完全消除了性能问题,至少对于这个简单的测试用例是这样。

$ time ./test +RTS -N12 -A16m
50000005000000

real    0m11.481s
user    0m11.775s
sys     0m0.126s

我在切换到第二代压缩时获得了类似的改进:

$ time ./test +RTS -N12 -c
50000005000000

real    0m11.125s
user    0m11.043s
sys     0m0.089s

当然,运行在减少的内核数量上启用并行 GC 也可能有所帮助:

$ time ./test +RTS -N12 -qn4
50000005000000

real    0m14.092s
user    0m18.961s
sys     0m3.031s