TL;DR:原因:RTS 和 STG 调用。解决方案:不要从 C 中调用琐碎的 Haskell 函数。
这是什么原因……?
免责声明:我从未从 C 调用 Haskell。我熟悉 C 和 Haskell,但我很少将两者交织在一起,除非我正在编写包装器。现在我已经失去了我的信誉,让我们开始这个基准测试、拆卸和其他有趣的恐怖冒险吧。
使用 gprof 进行基准测试
一个简单的方法来检查是什么消耗了你的所有资源是使用 gprof。我们将稍微更改您的编译行,以便编译器和链接器都使用-pg(注意:我已将 test.c 重命名为 main.c 并将 test2.c 重命名为 test.c):
$ ghc -no-hs-main -O2 -optc-O3 -optc-pg -optl-pg -fforce-recomp \
main.c Test.hs -o test
$ ./test
$ gprof ./test
这为我们提供了以下(平面)配置文件:
平面轮廓:
每个样本计为 0.01 秒。
% 累计自我自我总计
time seconds seconds 呼叫 Ts/呼叫 Ts/呼叫名称
16.85 2.15 2.15 scheduleWaitThread
11.78 3.65 1.50 创建StrictIOThread
7.66 4.62 0.98 创建线程
6.68 5.47 0.85 分配
5.66 6.19 0.72 traverseWeakPtrList
5.34 6.87 0.68 活着
4.12 7.40 0.53 新绑定任务
3.06 7.79 0.39 stg_ap_p_fast
2.36 8.09 0.30 stg_ap_v_info
1.96 8.34 0.25 stg_ap_0_fast
1.85 8.57 0.24 rts_checkSchedStatus
1.81 8.80 0.23 stg_PAP_apply
1.73 9.02 0.22 rts_apply
1.73 9.24 0.22 stg_enter_info
1.65 9.45 0.21 stg_stop_thread_info
1.61 9.66 0.21 测试
1.49 9.85 0.19 stg_returnToStackTop
1.49 10.04 0.19 move_STACK
1.49 10.23 0.19 stg_ap_v_fast
1.41 10.41 0.18 rts_lock
1.18 10.56 0.15 boundTaskExiting
1.10 10.70 0.14 标准运行
0.98 10.82 0.13 rts_evalIO
0.94 10.94 0.12 stg_upd_frame_info
0.79 11.04 0.10 阻塞ThrowTo
0.67 11.13 0.09 StgReturn
0.63 11.21 0.08 创建IOThread
0.63 11.29 0.08 stg_bh_upd_frame_info
0.63 11.37 0.08 c5KU_info
0.55 11.44 0.07 stg_stk_save_n
0.51 11.50 0.07 线程暂停
0.47 11.56 0.06 脏_TSO
0.47 11.62 0.06 ghczmprim_GHCziCString_unpackCStringzh_info
0.47 11.68 0.06 停止HeapProfTimer
0.39 11.73 0.05 stg_threadFinished
0.39 11.78 0.05 分配组
0.39 11.83 0.05 base_GHCziTopHandler_runNonIO1_info
0.39 11.88 0.05 stg_catchzh
0.35 11.93 0.05 freeMyTask
0.35 11.97 0.05 rts_eval_
0.31 12.01 0.04 唤醒BlockedExceptionQueue
0.31 12.05 0.04 stg_ap_2_upd_info
0.27 12.09 0.04 s5q4_info
0.24 12.12 0.03 标记稳定表
0.24 12.15 0.03 rts_getSchedStatus
0.24 12.18 0.03 s5q3_info
0.24 12.21 0.03 清除堆栈
0.24 12.24 0.03 stg_ap_7_upd_info
0.24 12.27 0.03 stg_ap_n_fast
0.24 12.30 0.03 stg_gc_noregs
0.20 12.32 0.03 base_GHCziTopHandler_runIO1_info
0.20 12.35 0.03 stat_exit
0.16 12.37 0.02 垃圾收集
0.16 12.39 0.02 脏堆栈
0.16 12.41 0.02 免费GcThreads
0.16 12.43 0.02 rts_mkString
0.16 12.45 0.02 scavenge_capability_mut_lists
0.16 12.47 0.02 开始ProfTimer
0.16 12.49 0.02 stg_PAP_info
0.16 12.51 0.02 stg_ap_stk_p
0.16 12.53 0.02 stg_catch_info
0.16 12.55 0.02 stg_killMyself
0.16 12.57 0.02 stg_marked_upd_frame_info
0.12 12.58 0.02 中断所有能力
0.12 12.60 0.02 scheduleThreadOn
0.12 12.61 0.02 等待返回能力
0.08 12.62 0.01 退出存储
0.08 12.63 0.01 免费WSDeque
0.08 12.64 0.01 gcStableTables
0.08 12.65 0.01 重置终端设置
0.08 12.66 0.01 调整大小NurseriesEach
0.08 12.67 0.01 清除循环
0.08 12.68 0.01 split_free_block
0.08 12.69 0.01 startHeapProfTimer
0.08 12.70 0.01 stg_MVAR_TSO_QUEUE_info
0.08 12.71 0.01 stg_forceIO_info
0.08 12.72 0.01 zero_static_object_list
0.04 12.73 0.01 frame_dummy
0.04 12.73 0.01 rts_evalLazyIO_
0.00 12.73 0.00 1 0.00 0.00 stginit_export_Test_zdfstableZZC0ZZCmainZZCTestZZCtest
哇,这是一堆被调用的函数。这与您的 C 版本相比如何?
$ ghc -no-hs-main -O2 -optc-O3 -optc-pg -optl-pg -fforce-recomp \
main.c TestDummy.hs test.c -o test_c
$ ./test_c
$ gprof ./test_c
平面轮廓:
每个样本计为 0.01 秒。
% 累计自我自我总计
time seconds seconds 呼叫 Ts/呼叫 Ts/呼叫名称
75.00 0.05 0.05 测试
25.00 0.06 0.02 frame_dummy
这很多更简单。但为什么呢?
后面发生了什么?
也许您想知道为什么test 会出现在之前的个人资料中。好吧,gprof 本身增加了一些开销,如 objdump 所示:
$ objdump -D ./test_c | grep -A5 "<test>:"
0000000000405630 <test>:
405630: 55 push %rbp
405631: 48 89 e5 mov %rsp,%rbp
405634: e8 f7 d4 ff ff callq 402b30 <mcount@plt>
405639: 5d pop %rbp
40563a: c3 retq
mcount 的调用是 gcc 添加的。因此,对于下一部分,您要删除 -pg 选项。我们先来看看反汇编后的 C 中的test 例程:
$ ghc -no-hs-main -O2 -optc-O3 -fforce-recomp \
main.c TestDummy.hs test.c -o test_c
$ objdump -D ./test_c | grep -A2 "<test>:"
0000000000405510 <test>:
405510: f3 c3 repz retq
repz retq 实际上是some optimisation magic,但在这种情况下,您可以将其视为(大部分)无操作返回。
这与 Haskell 版本相比如何?
$ ghc -no-hs-main -O2 -optc-O3 -fforce-recomp \
main.c Test.hs -o test_hs
$ objdump -D ./Test.o | grep -A18 "<test>:"
0000000000405520 <test>:
405520: 48 83 ec 18 sub $0x18,%rsp
405524: e8 f7 3a 05 00 callq 459020 <rts_lock>
405529: ba 58 24 6b 00 mov $0x6b2458,%edx
40552e: be 80 28 6b 00 mov $0x6b2880,%esi
405533: 48 89 c7 mov %rax,%rdi
405536: 48 89 04 24 mov %rax,(%rsp)
40553a: e8 51 36 05 00 callq 458b90 <rts_apply>
40553f: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
405544: 48 89 c6 mov %rax,%rsi
405547: 48 89 e7 mov %rsp,%rdi
40554a: e8 01 39 05 00 callq 458e50 <rts_evalIO>
40554f: 48 8b 34 24 mov (%rsp),%rsi
405553: bf 64 57 48 00 mov $0x485764,%edi
405558: e8 23 3a 05 00 callq 458f80 <rts_checkSchedStatus>
40555d: 48 8b 3c 24 mov (%rsp),%rdi
405561: e8 0a 3b 05 00 callq 459070 <rts_unlock>
405566: 48 83 c4 18 add $0x18,%rsp
40556a: c3 retq
40556b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
405570: d8 ce fmul %st(6),%st
这看起来很不一样。事实上,RTS 函数似乎很可疑。让我们看看它们:
所以我们找到了所有一直占用的功能。 STG 和 RTS 承担了每次调用 150ns 开销的全部功劳。
...有没有办法缓解这种放缓?
嗯,你的test 基本上是一个空操作。您调用它 100000000 次,总运行时间为 15 秒。与 C 版本相比,每次调用的开销约为 149ns。
解决方案非常简单:不要在 C 世界中将 Haskell 函数用于琐碎的任务。在正确的情况下使用正确的工具。毕竟,如果你需要将两个保证小于 10 的数字相加,你就不要使用 GMP 库。
除了这个典型的解决方案:没有。上面显示的程序集是由 GHC 创建的,目前无法在没有 RTS 调用的情况下创建变体。