Haskell FFI/C 的性能考虑?

Posted

技术标签:

【中文标题】Haskell FFI/C 的性能考虑?【英文标题】:Performance considerations of Haskell FFI / C? 【发布时间】:2011-08-05 15:09:34 【问题描述】:

如果使用 Haskell 作为 我的 C 程序调用,那么调用它会对性能产生什么影响?例如,如果我有一个包含 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 个参与者)进行操作,因此在 Haskell 函数中使用细粒度并行化并不比在 C 级别管理它好。正在运行 FFI Haskell 实例“线程安全”,我如何实现这一点 - 每次启动并行运行时是否需要初始化 Haskell 实例? (如果必须的话,似乎很慢..)如何以良好的性能实现这一目标?

【问题讨论】:

helper data pList = map (f data) pList 或类似的可能吗? @Dan,我真的不知道。也许您可以通过答案详细说明-谢谢 【参考方案1】:

Haskell can peek into that 20k blob if you pass the pointer.

【讨论】:

【参考方案2】:

调用它对性能有何影响

假设您在我的机器上只启动一次 Haskell 运行时 (like this),从 C 向 Haskell 进行函数调用,在边界上来回传递一个 Int,大约需要 80,000 个周期(在我的 Core 2 上 31,000 ns)- 通过 rdstc 寄存器实验确定

我是否可以将 20kB 的数据作为全局不可变引用保存在 Haskell 代码可以访问的某个地方

是的,这当然是可能的。如果数据确实是不可变的,那么无论您:

通过编组跨语言边界来回线程化数据; 来回传递对数据的引用; 或将其缓存在 Haskell 端的 IORef 中。

哪种策略最好?这取决于数据类型。最惯用的方法是来回传递对 C 数据的引用,在 Haskell 端将其视为 ByteStringVector

我想并行化这个

强烈建议在那时反转控件,并从 Haskell 运行时进行并行化——它会更加健壮,因为该路径已经过大量测试。

关于线程安全,对在同一运行时运行的foreign exported 函数进行并行调用显然是安全的——尽管相当肯定没有人尝试过这样做以获得并行性。调用获取一种能力,本质上是一个锁,因此多个调用可能会阻塞,从而减少并行的机会。在多核情况下(例如-N4 左右),您的结果可能会有所不同(有多种功能可用),但是,这几乎可以肯定是提高性能的不好方法。

再次重申,通过 forkIO 从 Haskell 进行许多并行函数调用是一种更好的文档记录、更好的测试路径,与在 C 端进行工作相比开销更少,并且最终可能更少的代码。

只需调用您的 Haskell 函数,该函数将通过许多 Haskell 线程进行并行处理。简单!

【讨论】:

从你在这里所说的(这一切听起来不错),我要解决的主要性能问题似乎是跨越 C/Haskell 之间的边界 - 对吗? 31usecs 是可以忍受的,但是这样做 100 或 1000 次变得非常昂贵。一旦我“越过边界”,我认为我将获得更快的处理速度,并且 31usec 应该是一个相当恒定的每批次运行一次命中。我明白了吗?此外,当从 C 到 Haskell 运行第二个函数调用时,您是否会得到同样的结果,或者 31usec 是否包括初始运行时设置?我说不出来 31usec 不包括启动运行时。一旦进入 Haskell 领域,函数调用就会以正常速度运行(例如跳跃的速度)。 我同意唐的观点。我会将那个循环移到 Haskell 端,这样你就只会交叉一次,并且更容易并行化。 @Don 如果你愿意,我可以打开一个新 Q,但我如何设计软件来避免这种昂贵的跨界。也就是说,如果我知道我在全局空间中引用参数,并且我知道我将以 30hz 调用 Haskell 函数,有没有办法将边界交叉传递到我的 C 代码中,向 Haskell 函数发出信号运行并将其结果放在全局内存空间引用中? 我绝对可以从 C 中并行调用外部导出函数,它的工作原理就像一个魅力。【参考方案3】:

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

但在我看来,如果您想重用 20 Kb 的数据而不是每次都传递它,那么您可以简单地使用一个方法来获取“个性”列表,并返回一个列表“决定”。

所以如果你有一个函数

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

那为什么不做一个辅助函数

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

然后调用它?但是,如果您想使用这种方式进行并行化,则需要在 Haskell 端使用并行列表和并行映射。

我请专家解释是否/如何将 C 数组轻松编组为 Haskell 列表(或类似结构)。

【讨论】:

【参考方案4】:

我在我的一个应用程序中混合使用了 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

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

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

哈斯克尔:

-# 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();

【讨论】:

以上是关于Haskell FFI/C 的性能考虑?的主要内容,如果未能解决你的问题,请参考以下文章

-bash: ghci: 找不到命令(Haskell 交互式 shell,Haskell 安装)

安全执行不受信任的 Haskell 代码

工具rest:Haskell的REST开源框架

用sublime text 3编译haskell

Haskell 中的单子——洪峰老师讲创客道(三十五)

FFI 可以处理数组吗?如果是这样,怎么做?