Haskell FFI进入C并返回需要多少钱?

Thi*_*gri 29 c performance haskell ffi

如果我想调用多个C函数,每个函数取决于前一个函数的结果,是否最好创建一个处理三个调用的包装器C函数?它会花费与使用Haskell FFI而不转换类型相同的成本吗?

假设我有以下Haskell代码:

foo :: CInt -> IO CInt
foo x = do
  a <- cfA x
  b <- cfB a
  c <- cfC c
  return c
Run Code Online (Sandbox Code Playgroud)

每个函数cf*都是C调用.

在性能方面,创建单个C函数cfABC并在Haskell中只进行一次外部调用会更好吗?

int cfABC(int x) {
   int a, b, c;
   a = cfA(x);
   b = cfB(a);
   c = cfC(b);
   return c;
}
Run Code Online (Sandbox Code Playgroud)

Haskell代码:

foo :: CInt -> IO CInt
foo x = do
  c <- cfABC x
  return c
Run Code Online (Sandbox Code Playgroud)

如何衡量Haskell的C调用的性能成本?不是C函数本身的成本,而是从Haskell到C的"上下文切换"的成本.

Dan*_*her 20

答案主要取决于外国呼叫是呼叫safe还是unsafe呼叫.

一个unsafe调用C基本上是一个函数调用,因此,如果没有(平凡)类型转换,还有如果你让三家外资调用三个函数调用,当你写一至四个在C包装,这取决于有多少在编译C时可以内联组件函数,因为GHC无法内联到C的外部调用.这样的函数调用通常非常便宜(它只是参数的副本和跳转到代码),所以差别很小,当没有C函数可以内联到包装器时,包装器应该稍微慢一些,并且当所有内容都可以内联时稍微快一点[在我的基准测试中确实如此,+ 1.5ns分别为.-3.5ns,其中三个外国电话大约需要12.7ns才能返回参数].如果函数做了一些非常重要的事情,那么差异可以忽略不计(如果它们没有做任何重要的事情,你最好在Haskell中编写它们让GHC内联代码).

一个safe调用C涉及到节能状态的一些平凡的数量,锁定,可能产生一个新的操作系统线程,因此需要更长的时间.然后,与外部调用的成本相比,可能在C中调用一个函数的小开销可以忽略不计[除非传递参数需要不寻常的复制量,许多大的structs左右].在我的无所事事的基准

{-# LANGUAGE ForeignFunctionInterface #-}
module Main (main) where

import Criterion.Main
import Foreign.C.Types
import Control.Monad

foreign import ccall safe "funcs.h cfA" c_cfA :: CInt -> IO CInt
foreign import ccall safe "funcs.h cfB" c_cfB :: CInt -> IO CInt
foreign import ccall safe "funcs.h cfC" c_cfC :: CInt -> IO CInt
foreign import ccall safe "funcs.h cfABC" c_cfABC :: CInt -> IO CInt

wrap :: (CInt -> IO CInt) -> Int -> IO Int
wrap foo arg = fmap fromIntegral $ foo (fromIntegral arg)

cfabc = wrap c_cfABC

foo :: Int -> IO Int
foo = wrap (c_cfA >=> c_cfB >=> c_cfC)

main :: IO ()
main = defaultMain
            [ bench "three calls" $ foo 16
            , bench "single call" $ cfabc 16
            ]
Run Code Online (Sandbox Code Playgroud)

所有C函数只返回参数,单个包装调用的平均值略高于100ns [105-112],而三个单独调用的平均值约为300ns [290-315].

所以safec调用大约需要100ns,通常,将它们包装成单个调用会更快.但是,如果被调用函数做了一些非常重要的事情,那么差别就不重要了.

  • Edward Z. Yang的帖子["安全第一:FFI和线程"](http://blog.ezyang.com/2010/07/safety-first-ffi-and-threading/)说"不安全"的外国人呼叫不能从Haskell RTS抢占.用户指南说:"如果你需要对需要很长时间或无限期阻塞的函数进行外部调用,那么你应该将它标记为'safe`并使用`-threaded`." 我想作者认为"如果你使用`unsafe`,即使你使用'-threaded`"它也会被隐藏. (3认同)
  • 我认为"不安全"的呼叫不能被抢占,因为它在RTS中被内联.因此RTS无法前进并控制另一个Haskell线程,直到此调用返回.当没有线程可用于进行外部调用并保持Haskell RTS调度程序运行时,可能`safe`调用可能会打开一个新的OS线程. (2认同)