为什么有从C调用Haskell函数的开销?

时间:2015-07-29 21:36:21

标签: c performance haskell ffi

我注意到在C中调用Haskell函数的开销很大,远远大于本机C函数调用的开销。为了将问题提炼到其本质,我编写了一个程序,它只是初始化Haskell运行时,运行一个循环,调用一个空函数100,000,000次,然后返回。

使用内联函数,程序需要0.003秒。调用用C编写的空函数需要0.18秒。调用用Haskell编写的空函数需要15.5秒。 (奇怪的是,如果我在链接之前单独编译空的Haskell文件,则需要几秒钟。子问题:为什么会这样?)

所以看起来调用C函数和调用Haskell函数之间的速度差了大约100倍。这是什么原因,有没有办法缓解这种放缓?

代码

  

编辑:我在NoFib benchmark suitecallback002中发现了此测试的一个版本。 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

1 个答案:

答案 0 :(得分:16)

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

这为我们提供了以下(平面)简介:

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)。他们致电newBoundTaskboundTaskExiting,这需要一些时间(请参阅上面的个人资料)。
  • rts_apply调用allocate,这是整个程序中最常用的函数之一(这并不令人惊讶,Haskell被垃圾收集)。
  • rts_evalIO最终创建实际线程并等待其完成。因此,我们可以估计rts_evalIO单独占用约27%。

所以我们发现了所有正在使用的功能。 STG和RTS每次通话都需要150ns的开销。

  

......有没有办法减轻这种放缓?

嗯,你的test基本上是无操作。您将其称为100000000次,总运行时间为15秒。与C版本相比,这是每次通话约149ns的开销。

解决方案非常简单:不要在C世界中使用Haskell函数来完成琐碎的任务。使用正确的工具以适应正确的情况。毕竟,如果您需要添加两个保证小于10的数字,则不要使用GMP库。

除了这种范式解决方案:没有。上面显示的程序集是由GHC创建的,并且目前无法在没有RTS调用的情况下创建变体。