【问题标题】:What is GHC doing when run with -N (parallel) flag?使用 -N(并行)标志运行时 GHC 会做什么?
【发布时间】:2020-06-29 15:39:20
【问题描述】:

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

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

【问题讨论】:

  • 你真的不应该认为在没有优化的情况下编译的代码是有意义的。它可能没有优化计算中的分配,您会看到尝试在 12 个内核上运行并行 GC 的开销。你可以在+RTS -s 中包含输出以进行快速 GC 统计吗? (编译时需要添加-rtsopts,iirc。)
  • @Carl 我在问题中添加了 GC 统计信息。我会尝试看看我是否可以找到一个最小的工作示例来显示使用 -O2 的这种行为,因为即使使用 -O2,我似乎在我的实际应用程序中也遇到了同样的问题。
  • @Carl,我找到了一个适用于 -O2 的示例。我将编辑这个问题,因为我相信另一个问题会太相似,但如果你认为这样更好,我会回复并提出一个新问题。
  • 您可以使用 threadScope 实用程序查看实际的核心使用情况。我的猜测是垃圾收集器正在并行化减慢整个过程。您的代码不应并行运行,因为 map 函数不会并行运行。此外,在您的-N12 执行中创建零火花,这意味着您没有并行运行代码。我认为...
  • @lsmor 知道为什么垃圾收集器这么慢,以及如何避免这种情况?在我的实际代码中,我确实并行运行了一些东西,但是性能改进为零,因为这里发生的任何事情都会发生在我的真实代码中。

标签: multithreading haskell parallel-processing ghc


【解决方案1】:

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

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

据观察,当在大量功能上运行时,并行 GC 在某些工作负载上的性能非常差。例如,请参阅issue #14981。当然,该问题讨论的是 32 或 64 核机器。

但是,我观察到性能非常差尤其是在默认运行时 GC 设置下,即使在相对少量的内核上也是如此。例如,使用您的测试用例和 GHC 版本,我在我的 8 核、16 线程英特尔 i9-9980HK 笔记本电脑上获得了类似的性能不佳-N12 或更多。这是 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 个功能也可能发生这种情况。

建议您使用运行时garbage collector controls,而不是完全禁用并行GC。效果可能令人吃惊。例如,将第 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

【讨论】:

    猜你喜欢
    • 2011-05-04
    • 1970-01-01
    • 1970-01-01
    • 2015-04-03
    • 1970-01-01
    • 2017-08-20
    • 1970-01-01
    • 2018-09-18
    • 2011-02-19
    相关资源
    最近更新 更多