当 运行 带有 -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
我编写了以下测试应用程序:
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