Haskell FFI / C 的性能考虑?

28

如果我作为一个C程序的库从中调用Haskell,那么向它进行调用的性能影响是多少?例如,如果我有一个大约20kB的问题数据集,并且想要运行类似于:

// Go through my 1000 actors and have them make a decision based on
// HaskellCode() function, which is compiled Haskell I'm accessing through
// the FFI.  As an argument, send in the SAME 20kB of data to EACH of these
// function calls, and some actor specific data
// The 20kB constant data defines the environment and the actor specific
// data could be their personality or state
for(i = 0; i < 1000; i++)
   actor[i].decision = HaskellCode(20kB of data here, actor[i].personality);
这里会发生什么情况 - 我是否可以将那20kB的数据作为全局不可变引用存储在Haskell代码中访问,或者我必须每次都复制该数据?问题在于这些数据可能会更大,我也希望编写算法以对更大的数据集进行操作,使用多个Haskell代码调用使用的不可变数据模式。此外,我想并行化这个过程,就像dispatch_apply() GCD或Parallel.ForEach(..) C#。 我在Haskell之外进行并行化的原因是我知道我总是在处理许多单独的函数调用,即1000个actors,因此在Haskell函数内部使用细粒度并行化与在C级别管理它没有太大区别。 运行FFI Haskell实例是否"线程安全",以及如何实现这一点 - 我需要每次启动并行运行时初始化Haskell实例吗?(如果必要,速度会很慢..) 如何在保持良好性能的同时实现这一点?

“helper data pList = map (f data) pList” 或类似的写法是否可行? - Dan Burton
@Dan,老实说我不知道。也许你可以用一个答案详细解释一下 - 谢谢。 - Nektarios
4个回答

20
假设您仅启动一次Haskell运行时(如此),在我的计算机上,从C调用函数进入Haskell,在边界上来回传递一个Int,需要约80,000个周期(在我的Core 2上为31,000纳秒)-通过rdstc寄存器进行实验确定。
是不是可以将这20kB的数据作为全局不可变引用保留在某个地方,由Haskell代码访问?
是的,这当然是可能的。如果数据确实是不可变的,则通过以下方式之一,您将获得相同的结果:
- 通过编组在语言边界上来回传递数据; - 传递对数据的引用来回传递; - 或者将其缓存在Haskell侧的IORef中。
哪种策略最好?这取决于数据类型。最惯用的方法是将对C数据的引用来回传递,将其视为Haskell侧的ByteString或Vector。
我想并行化这个问题。
我强烈建议您反转控制,然后从Haskell运行时开始并行化-这样会更加健壮,因为该路径经过了大量测试。
关于线程安全性,显然可以在同一个运行时中并行调用foreign exported函数而安全--尽管几乎肯定没有人尝试过这样做来获得并行性。调用会获取一个能力(capability),它本质上是一个锁,因此多个调用可能会被阻塞,从而降低并行性的机会。在多核情况下(例如,-N4之类的),您的结果可能不同(多个能力可用),但是这几乎肯定是改善性能的不良方式。
再次强调,通过forkIO从Haskell进行许多并行函数调用是一种更好记录、经过更好测试的路径,比在C端执行工作的开销要小,并且最终可能需要较少的代码。
只需调用您的Haskell函数,该函数将通过许多Haskell线程实现并行性。简单易行!

从你在这里说的(听起来都不错),我将遇到的主要性能问题似乎是跨越C/Haskell之间的边界 - 这正确吗?31微秒是可以容忍的,但是重复100或1000次就变得非常昂贵了。一旦我“跨越边界”,我假设我会获得更快的处理速度,并且31微秒应该是每个批处理运行中的一个恒定的一次性命中。我理解得对吗?此外,当从C调用Haskell的第二个函数调用时,您是否会遇到相同的问题,还是31微秒包括初始运行时设置?我无法确定。 - Nektarios
31usec不包括启动运行时。一旦进入Haskell领域,函数调用以常规速度运行(例如跳跃的速度)。 - Don Stewart
1
我同意Don的观点。我会将那个循环移到Haskell端,这样你只需要跨越一次,而且更容易并行化。 - augustss
@Don 我可以打开一个新的问题,如果你愿意的话,但是我要如何设计软件来避免这种昂贵的边界穿越。也就是说,如果我知道我在全局空间中引用参数,并且我知道我将以30hz调用Haskell函数,是否有一种方法将边界穿越传递转换为我的C代码向Haskell函数发出信号并将其结果放置在全局内存空间引用中呢? - Nektarios
1
我肯定会从C中发出并行调用到外部导出的函数,而且效果非常好。 - Nathan Howell
显示剩余3条评论

9

我在一个应用程序中使用了混合的C和Haskell线程,并没有发现在两者之间切换时有太大的性能损失。因此,我设计了一个简单的基准测试......它比Don的测试要快/便宜得多。这是在2.66GHz i7上测量1000万次迭代的结果:

$ ./foo
IO  : 2381952795 nanoseconds total, 238.195279 nanoseconds per, 160000000 value
Pure: 2188546976 nanoseconds total, 218.854698 nanoseconds per, 160000000 value

使用 GHC 7.0.3/x86_64 和 gcc-4.2.1 在 OSX 10.6 上编译

ghc -no-hs-main -lstdc++ -O2 -optc-O2 -o foo ForeignExportCost.hs Driver.cpp

Haskell:

{-# LANGUAGE ForeignFunctionInterface #-}

module ForeignExportCost where

import Foreign.C.Types

foreign export ccall simpleFunction :: CInt -> CInt
simpleFunction i = i * i

foreign export ccall simpleFunctionIO :: CInt -> IO CInt
simpleFunctionIO i = return (i * i)

同时还需要一个OSX C++应用程序来驱动它,很容易调整为Windows或Linux:

#include <stdio.h>
#include <mach/mach_time.h>
#include <mach/kern_return.h>
#include <HsFFI.h>
#include "ForeignExportCost_stub.h"

static const int s_loop = 10000000;

int main(int argc, char** argv) {
    hs_init(&argc, &argv);

    struct mach_timebase_info timebase_info = { };
    kern_return_t err;
    err = mach_timebase_info(&timebase_info);
    if (err != KERN_SUCCESS) {
        fprintf(stderr, "error: %x\n", err);
        return err;
    }

    // timing a function in IO
    uint64_t start = mach_absolute_time();
    HsInt32 val = 0;
    for (int i = 0; i < s_loop; ++i) {
        val += simpleFunctionIO(4);
    }

    // in nanoseconds per http://developer.apple.com/library/mac/#qa/qa1398/_index.html
    uint64_t duration = (mach_absolute_time() - start) * timebase_info.numer / timebase_info.denom;
    double duration_per = static_cast<double>(duration) / s_loop;
    printf("IO  : %lld nanoseconds total, %f nanoseconds per, %d value\n", duration, duration_per, val);

    // run the loop again with a pure function
    start = mach_absolute_time();
    val = 0;
    for (int i = 0; i < s_loop; ++i) {
        val += simpleFunction(4);
    }

    duration = (mach_absolute_time() - start) * timebase_info.numer / timebase_info.denom;
    duration_per = static_cast<double>(duration) / s_loop;
    printf("Pure: %lld nanoseconds total, %f nanoseconds per, %d value\n", duration, duration_per, val);

    hs_exit();
}


1

免责声明:我没有使用 FFI 的经验。

但是在我看来,如果您想重复使用这 20 Kb 的数据,以便不需要每次都传送它,那么您可以简单地编写一个方法,该方法接受“个性”列表,并返回“决策”列表。

因此,如果您有一个函数

f :: LotsaData -> Personality -> Decision
f data p = ...

那为什么不写一个辅助函数呢?

helper :: LotsaData -> [Personality] -> [Decision]
helper data ps = map (f data) ps

那么如何调用呢?使用这种方式,如果你想并行化,你需要在Haskell端使用并行列表和并行映射。

我会请专家解释C数组如何轻松地转换为Haskell列表(或类似结构)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接