我注意到在C中调用Haskell函数的开销很大,远远大于本机C函数调用的开销。为了将问题提炼到其本质,我编写了一个程序,它只是初始化Haskell运行时,运行一个循环,调用一个空函数100,000,000次,然后返回。
使用内联函数,程序需要0.003秒。调用用C编写的空函数需要0.18秒。调用用Haskell编写的空函数需要15.5秒。 (奇怪的是,如果我在链接之前单独编译空的Haskell文件,则需要几秒钟。子问题:为什么会这样?)
所以看起来调用C函数和调用Haskell函数之间的速度差了大约100倍。这是什么原因,有没有办法缓解这种放缓?
编辑:我在NoFib benchmark suite,
callback002
中发现了此测试的一个版本。 Edward Z. Yang有一个nice blog post在GHC调度程序的上下文中提到了这个测试。我仍然试图与Zeta的非常好的答案一起浏览这篇博文。我还不相信没有办法更快地做到这一点!
要编译“慢”Haskell版本,请运行
ghc -no-hs-main -O2 -optc-O3 test.c Test.hs -o test
要编译“快速”C版本,请运行
ghc -no-hs-main -O2 -optc-O3 test.c test2.c TestDummy.hs -o test
test.c的:
#include "HsFFI.h"
extern void __stginit_Test(void);
extern void test();
int main(int argc, char *argv[]) {
hs_init(&argc, &argv);
hs_add_root(__stginit_Test);
int i;
for (i = 0; i < 100000000; i++) {
test();
}
hs_exit();
return 0;
}
test2.c中:
void test() {
}
Test.hs:
{-# LANGUAGE ForeignFunctionInterface #-}
module Test where
foreign export ccall test :: ()
test :: ()
test = ()
TestDummy.hs:
module Test where
答案 0 :(得分:16)
TL; DR :原因:RTS和STG呼叫。解决方案:不要从C中调用简单的Haskell函数。
这是什么原因......?
免责声明:我从来没有从C调用Haskell。我熟悉C和Haskell,但我很少交织两者,除非我写了一个包装器。现在我已经失去了信誉,让我们开始这个基准测试,反汇编和其他漂亮恐怖的冒险。
检查吃什么资源的一种简单方法是使用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
这为我们提供了以下(平面)简介:
Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls Ts/call Ts/call name 16.85 2.15 2.15 scheduleWaitThread 11.78 3.65 1.50 createStrictIOThread 7.66 4.62 0.98 createThread 6.68 5.47 0.85 allocate 5.66 6.19 0.72 traverseWeakPtrList 5.34 6.87 0.68 isAlive 4.12 7.40 0.53 newBoundTask 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 test 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 StgRun 0.98 10.82 0.13 rts_evalIO 0.94 10.94 0.12 stg_upd_frame_info 0.79 11.04 0.10 blockedThrowTo 0.67 11.13 0.09 StgReturn 0.63 11.21 0.08 createIOThread 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 threadPaused 0.47 11.56 0.06 dirty_TSO 0.47 11.62 0.06 ghczmprim_GHCziCString_unpackCStringzh_info 0.47 11.68 0.06 stopHeapProfTimer 0.39 11.73 0.05 stg_threadFinished 0.39 11.78 0.05 allocGroup 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 awakenBlockedExceptionQueue 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 markStableTables 0.24 12.15 0.03 rts_getSchedStatus 0.24 12.18 0.03 s5q3_info 0.24 12.21 0.03 scavenge_stack 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 GarbageCollect 0.16 12.39 0.02 dirty_STACK 0.16 12.41 0.02 freeGcThreads 0.16 12.43 0.02 rts_mkString 0.16 12.45 0.02 scavenge_capability_mut_lists 0.16 12.47 0.02 startProfTimer 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 interruptAllCapabilities 0.12 12.60 0.02 scheduleThreadOn 0.12 12.61 0.02 waitForReturnCapability 0.08 12.62 0.01 exitStorage 0.08 12.63 0.01 freeWSDeque 0.08 12.64 0.01 gcStableTables 0.08 12.65 0.01 resetTerminalSettings 0.08 12.66 0.01 resizeNurseriesEach 0.08 12.67 0.01 scavenge_loop 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
Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls Ts/call Ts/call name 75.00 0.05 0.05 test 25.00 0.06 0.02 frame_dummy
lot 更简单。但为什么呢?
也许您已经想知道为什么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函数似乎很可疑。让我们来看看它们:
rts_checkSchedStatus
只检查状态是否正常,否则退出。 Success
路径没有太大的开销,所以这个功能并不是真正的罪魁祸首。rts_unlock
and rts_lock
基本上声明并释放capability(虚拟CPU)。他们致电newBoundTask
和boundTaskExiting
,这需要一些时间(请参阅上面的个人资料)。rts_apply
调用allocate
,这是整个程序中最常用的函数之一(这并不令人惊讶,Haskell被垃圾收集)。rts_evalIO
最终创建实际线程并等待其完成。因此,我们可以估计rts_evalIO
单独占用约27%。所以我们发现了所有正在使用的功能。 STG和RTS每次通话都需要150ns的开销。
......有没有办法减轻这种放缓?
嗯,你的test
基本上是无操作。您将其称为100000000次,总运行时间为15秒。与C版本相比,这是每次通话约149ns的开销。
解决方案非常简单:不要在C世界中使用Haskell函数来完成琐碎的任务。使用正确的工具以适应正确的情况。毕竟,如果您需要添加两个保证小于10的数字,则不要使用GMP库。
除了这种范式解决方案:没有。上面显示的程序集是由GHC创建的,并且目前无法在没有RTS调用的情况下创建变体。