这是Performance considerations of Haskell FFI / C?的双重问题:我想以尽可能小的开销调用C函数。
要设置场景,我具有以下C函数:
typedef struct
{
uint64_t RESET;
} INPUT;
typedef struct
{
uint64_t VGA_HSYNC;
uint64_t VGA_VSYNC;
uint64_t VGA_DE;
uint8_t VGA_RED;
uint8_t VGA_GREEN;
uint8_t VGA_BLUE;
} OUTPUT;
void Bounce(const INPUT* input, OUTPUT* output);
让我们从C运行它,并用gcc -O3
计时它:
int main (int argc, char **argv)
{
INPUT input;
input.RESET = 0;
OUTPUT output;
int cycles = 0;
for (int j = 0; j < 60; ++j)
{
for (;; ++cycles)
{
Bounce(&input, &output);
if (output.VGA_HSYNC == 0 && output.VGA_VSYNC == 0) break;
}
for (;; ++cycles)
{
Bounce(&input, &output);
if (output.VGA_DE) break;
}
}
printf("%d cycles\n", cycles);
}
将其运行25152001个周期大约需要400毫秒:
$ time ./Bounce
25152001 cycles
real 0m0.404s
user 0m0.403s
sys 0m0.001s
现在让我们写一些Haskell代码来设置FFI(请注意,Bool
的Storable
实例确实使用完整的int
):
data INPUT = INPUT
{ reset :: Bool
}
data OUTPUT = OUTPUT
{ vgaHSYNC, vgaVSYNC, vgaDE :: Bool
, vgaRED, vgaGREEN, vgaBLUE :: Word64
}
deriving (Show)
foreign import ccall unsafe "Bounce" topEntity :: Ptr INPUT -> Ptr OUTPUT -> IO ()
instance Storable INPUT where ...
instance Storable OUTPUT where ...
并且让我们做我认为在功能上等同于我们之前的C代码的事情:
main :: IO ()
main = alloca $ \inp -> alloca $ \outp -> do
poke inp $ INPUT{ reset = False }
let loop1 n = do
topEntity inp outp
out@OUTPUT{..} <- peek outp
let n' = n + 1
if not vgaHSYNC && not vgaVSYNC then loop2 n' else loop1 n'
loop2 n = do
topEntity inp outp
out <- peek outp
let n' = n + 1
if vgaDE out then return n' else loop2 n'
loop3 k n
| k < 60 = do
n <- loop1 n
loop3 (k + 1) n
| otherwise = return n
n <- loop3 (0 :: Int) (0 :: Int)
printf "%d cycles" n
我使用-O3
使用GHC 8.6.5构建它,并且得到了超过3秒的时间!
$ time ./.stack-work/dist/x86_64-linux/Cabal-2.4.0.1/build/sim-ffi/sim-ffi
25152001 cycles
real 0m3.468s
user 0m3.146s
sys 0m0.280s
而且这也不是启动时的固定开销:如果我运行10次循环,我从C处得到大约3.5秒,从Haskell得到34秒。
如何减少Haskell-> C FFI开销?
我设法减少了开销,因此现在2500万个呼叫在1.2秒内完成。更改为:
loop1
参数中使loop2
,loop3
和n
严格(使用BangPatterns
)INLINE
的peek
实例中的OUTPUT
添加Storable
编译指示当然,#1点很愚蠢,但这就是我之前没有进行概要分析的结果。仅凭这一变化,我就可以节省1.5秒。...
然而,第2点具有很多意义,并且通常适用。它还解决了@Thomas M. DuBuisson的评论:
您是否曾经在haskell中需要Haskell结构?如果您只是将其保留为指向内存的指针,并且具有一些测试功能(例如
vgaVSYNC :: Ptr OUTPUT -> IO Bool
),那么它将保存复制,分配和GC在每次调用时的工作日志。
在最终的完整程序中,我确实需要查看OUTPUT
的所有字段。但是,内联peek
时,GHC很乐意进行大小写转换,因此我可以在Core中看到现在没有分配OUTPUT
值。 peek
的输出直接使用。