Eva*_*ins 17 c performance haskell ffi
我注意到在C中调用Haskell函数的开销很大,远远大于本机C函数调用的开销.为了提炼问题的本质,我编写了一个程序,它只是初始化Haskell运行时,运行一个循环,调用一个空函数100,000,000次,然后返回.
内联功能,程序需要0.003秒.调用用C编写的空函数需要0.18秒.调用用Haskell编写的空函数需要15.5秒.(奇怪的是,如果我在链接之前单独编译空的Haskell文件,则需要几秒钟.子问题:这是为什么?)
所以看起来调用C函数和调用Haskell函数之间的速度差了大约100倍.这是什么原因,有没有办法缓解这种放缓?
编辑:我发现了一个版本的测试中的NoFib基准套件,
callback002.有一个很好的博客文章由爱德华·杨Z.提到这个测试在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;
}
Run Code Online (Sandbox Code Playgroud)
test2.c中:
void test() {
}
Run Code Online (Sandbox Code Playgroud)
Test.hs:
{-# LANGUAGE ForeignFunctionInterface #-}
module Test where
foreign export ccall test :: ()
test :: ()
test = ()
Run Code Online (Sandbox Code Playgroud)
TestDummy.hs:
module Test where
Run Code Online (Sandbox Code Playgroud)
Zet*_*eta 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
Run Code Online (Sandbox Code Playgroud)
这给了我们以下(平面)简介:
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
Run Code Online (Sandbox Code Playgroud)
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
这是一个很大简单.但为什么?
也许你想知道为什么test甚至出现在之前的个人资料中.好吧,gprof本身增加了一些开销,可以看出objdump:
$ objdump -D ./test_c | grep -A5 "<test>:"
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
呼叫mcount是由gcc添加的.因此,对于下一部分,您要删除-pg选项.让我们首先检查testC中的反汇编程序:
$ 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>:"
Run Code Online (Sandbox Code Playgroud)
0000000000405510 <test>:
405510: f3 c3 repz retq
Run Code Online (Sandbox Code Playgroud)
这repz retq实际上是一些优化魔术,但在这种情况下,您可以将其视为(主要)无操作返回.
这与Haskell版本相比如何?
$ ghc -no-hs-main -O2 -optc-O3 -fforce-recomp \
main.c Test.hs -o test_hs
$ objdump -D ./Test.o | grep -A18 "<test>:"
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
这看起来很不一样.实际上,RTS功能看起来很可疑.我们来看看它们:
rts_checkSchedStatus只检查状态是否正常,否则退出.该Success路径没有什么太大的开销,所以这个功能是不是真正的罪魁祸首.rts_unlock并且rts_lock基本上声明并释放一种能力(虚拟CPU).他们呼吁newBoundTask和boundTaskExiting,这需要一定的时间(见上文配置文件).rts_apply调用allocate,整个程序中最常用的函数之一(这并不奇怪,Haskell被垃圾收集).rts_evalIO 最后创建实际线程并等待其完成.所以我们可以估计rts_evalIO单独需要大约27%.所以我们发现了所有正在使用的功能.STG和RTS为每次呼叫150ns的开销承担全部功劳.
......有没有办法减轻这种放缓?
好吧,你test的基本上是无操作.你称它为1亿次,总运行时间为15秒.与C版本相比,这是每次通话约149ns的开销.
解决方案非常简单:不要在C世界中使用Haskell函数来完成琐碎的任务.使用正确的工具以适应正确的情况.毕竟,如果您需要添加两个保证小于10的数字,则不要使用GMP库.
除了这个范例解决方案:没有.上面显示的程序集是由GHC创建的,目前无法创建没有RTS调用的变体.