【问题标题】:Why is there overhead calling Haskell functions from C?为什么从 C 调用 Haskell 函数会有开销?
【发布时间】:2015-10-21 02:12:43
【问题描述】:

我注意到在 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

【问题讨论】:

  • 需要做很多工作来确保您可以在 Haskell 函数中分配内存等。
  • 拆解由 Haskell 代码生成的 test 符号既具有教育意义,又是一篇很棒的博文。
  • 出于反常的好奇心......使用-threaded 编译是否有任何不同?
  • @MathematicalOrchid:不。也许在链接的 RTS 变体中,但不在目标代码中。
  • @Zeta 我只是想知道它是否会影响时间。我知道这两种 RTS 方式在处理外来电话的方式上有所不同……也就是说,如果没有可衡量的差异,我不会感到非常惊讶。

标签: c performance haskell ffi


【解决方案1】:

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 函数似乎很可疑。让我们看看它们:

  • 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 调用的情况下创建变体。

【讨论】:

  • 所以,总而言之,占用时间的是 GHC 的绿色线程调度。 (?)
  • 那种。如果 CPU(又名能力)或线程事先存在,那也不会那么糟糕,但它们不存在。并且createThread 并不是真的要为每次调用调用。在常规的 Haskell 程序中,没有那么多线程,所以这不是问题。不过,这仍然比为每个调用创建一个操作系统线程要好。
  • 很有趣,我认为异步承诺会缓解这个问题。 RTS 可以保留它的(主要)功能和线程。但是,这会带来其他问题。
  • 从我的源代码阅读中,我认为该功能仅在第一次调用 Haskell 时初始化一次(请参阅allocTaskmyTask() 返回每​​个操作系统线程全局)。不过,Haskell 线程及其堆栈确实会为每个外部调用重新创建。看起来可以通过重用单个线程,使用 RtsAPI.c 中的函数,以更复杂的 C 代码为代价来避免这种开销。
  • @ReidBarton:如果使用非线程RTS,是的。由于waitForCapability 实际执行某些操作,线程化 RTS 甚至应该有更多的开销。我仍然认为罪魁祸首主要是rts_evalIO,但可能还有一些其他方面需要考虑。无论哪种方式,我想必须修补 GHC/RTS 以消除开销,但 OP 的情况似乎不太可能是诚实的。
猜你喜欢
  • 2015-10-25
  • 2015-07-30
  • 2017-01-23
  • 1970-01-01
  • 1970-01-01
  • 2013-07-24
  • 2013-08-01
  • 2020-07-03
  • 1970-01-01
相关资源
最近更新 更多