23

首先,我是 Haskell 初学者。

我计划将 Haskell 集成到 C 中以进行实时游戏。Haskell 做逻辑,C 做渲染。为此,我必须在每个滴答声中相互传递大量复杂的结构化数据(游戏状态)(每秒至少 30 次)。所以传递的数据应该是轻量级的。该状态数据可以放在内存的顺序空间上。Haskell 和 C 部分都应该自由访问州的每个区域。

在最好的情况下,传递数据的代价可能是复制指向内存的指针。在最坏的情况下,通过转换复制整个数据。

我正在阅读 Haskell 的 FFI(http://www.haskell.org/haskellwiki/FFICookBook#Working_with_structs)Haskell 代码看起来明确指定了内存布局。

我有几个问题。

  1. Haskell 可以明确指定内存布局吗?(与 C 结构完全匹配)
  2. 这是真正的内存布局吗?或者需要任何类型的转换?(性能损失)
  3. 如果 Q#2 为真,那么显式指定内存布局时是否会降低性能?
  4. 语法是#{alignment foo}什么?我在哪里可以找到有关此的文档?
  5. 如果我想以最佳性能传递大量数据,我应该怎么做?

*PS 显式内存布局功能,我所说的只是 C# 的 [StructLayout] 属性。这是明确指定内存中的位置和大小。 http://www.developerfusion.com/article/84519/mastering-structs-in-c/

我不确定 Haskell 是否具有与 C 结构的字段匹配的语言结构。

4

3 回答 3

25

我强烈建议使用预处理器。我喜欢 c2hs,但 hsc2hs 很常见,因为它包含在 ghc 中。绿卡似乎被放弃了。

要回答您的问题:

1) 是的,通过 Storable 实例的定义。使用 Storable 是通过 FFI 传递数据的唯一安全机制。Storable 实例定义了如何在 Haskell 类型和原始内存(Haskell Ptr、ForeignPtr 或 StablePtr 或 C 指针)之间编组数据。这是一个例子:

data PlateC = PlateC {
  numX :: Int,
  numY :: Int,
  v1   :: Double,
  v2   :: Double } deriving (Eq, Show)

instance Storable PlateC where
  alignment _ = alignment (undefined :: CDouble)
  sizeOf _ = {#sizeof PlateC#}
  peek p =
    PlateC <$> fmap fI ({#get PlateC.numX #} p)
           <*> fmap fI ({#get PlateC.numY #} p)
           <*> fmap realToFrac ({#get PlateC.v1 #} p)
           <*> fmap realToFrac ({#get PlateC.v2 #} p)
  poke p (PlateC xv yv v1v v2v) = do
    {#set PlateC.numX #} p (fI xv)
    {#set PlateC.numY #} p (fI yv)
    {#set PlateC.v1 #}   p (realToFrac v1v)
    {#set PlateC.v2 #}   p (realToFrac v2v)

{# ... #}片段是 c2hs 代码 。fIfromIntegral。get 和 set 片段中的值从包含的标头引用以下结构,而不是同名的 Haskell 类型:

struct PlateCTag ;

typedef struct PlateCTag {
  int numX;
  int numY;
  double v1;
  double v2;
} PlateC ;

c2hs 将其转换为以下普通的 Haskell:

instance Storable PlateC where
  alignment _ = alignment (undefined :: CDouble)
  sizeOf _ = 24
  peek p =
    PlateC <$> fmap fI ((\ptr -> do {peekByteOff ptr 0 ::IO CInt}) p)
           <*> fmap fI ((\ptr -> do {peekByteOff ptr 4 ::IO CInt}) p)
           <*> fmap realToFrac ((\ptr -> do {peekByteOff ptr 8 ::IO CDouble}) p)
           <*> fmap realToFrac ((\ptr -> do {peekByteOff ptr 16 ::IO CDouble}) p)
  poke p (PlateC xv yv v1v v2v) = do
    (\ptr val -> do {pokeByteOff ptr 0 (val::CInt)}) p (fI xv)
    (\ptr val -> do {pokeByteOff ptr 4 (val::CInt)}) p (fI yv)
    (\ptr val -> do {pokeByteOff ptr 8 (val::CDouble)})   p (realToFrac v1v)
    (\ptr val -> do {pokeByteOff ptr 16 (val::CDouble)})   p (realToFrac v2v)

偏移量当然取决于架构,因此使用预处理器可以让您编写可移植的代码。

您可以通过为您的数据类型( 、 等)分配空间new并将malloc数据poke放入 Ptr(或 ForeignPtr)中来使用它。

2)这是真正的内存布局。

peek3) 用/读/写会受到惩罚poke。如果您有大量数据,最好只转换您需要的数据,例如,只从 C 数组中读取一个元素,而不是将整个数组编组为 Haskell 列表。

4) 语法取决于您选择的预处理器。 c2hs 文档hsc2hs 文档。令人困惑的是,hsc2​​hs 使用语法#stuffor #{stuff},而 c2hs 使用{#stuff #}.

5)@sclv 的建议也是我会做的。写一个 Storable 实例并保存一个指向数据的指针。您可以编写 C 函数来完成所有工作并通过 FFI 调用它们,或者(不太好)使用 peek 和 poke 编写低级 Haskell 来仅对您需要的数据部分进行操作。来回编组整个事情(即调用peekpoke在整个数据结构上)将是昂贵的,但如果你只传递指针,成本将是最小的。

通过 FFI 调用导入的函数会受到很大的惩罚,除非它们被标记为“不安全”。声明导入“不安全”意味着该函数不应回调到 Haskell 或未定义的行为结果。如果您使用并发性或并行性,这也意味着具有相同功能(即 CPU)的所有 Haskell 线程将阻塞,直到调用返回,因此它应该很快返回。如果这些条件是可接受的,则“不安全”调用相对较快。

Hackage 上有很多处理这类事情的包。我可以推荐hsndfilehCsound作为 c2hs 的良好实践。如果您查看与您熟悉的小型 C 库的绑定,这可能会更容易。

于 2010-12-21T22:47:55.070 回答
7

即使您可以获得严格的未装箱 Haskell 结构的确定性内存布局,也不能保证,这是一个非常糟糕的主意。

如果您愿意接受转换,可以使用 Storeable:http ://www.haskell.org/ghc/docs/6.12.3/html/libraries/base-4.2.0.2/Foreign-Storable.html

我要做的是构造 C 结构,然后构造使用 FFI 直接对它们进行操作的 Haskell 函数,而不是尝试为它们生成 Haskell“等价物”。

或者,您可以决定只需要向 C 传递一些选择的信息——不是整个游戏状态,而只是关于世界上哪些对象的一些信息,以及关于如何传递的实际信息把他们画在等式的 C 边。然后,您在 Haskell 中执行所有逻辑,在本地 Haskell 结构上运行,并且只将 C 实际需要渲染的一小部分数据投影到 C 世界。

编辑:我应该补充一点,矩阵和其他常见的 c 结构已经具有出色的库/绑定,可以让 c 方面的繁重工作。

于 2010-12-21T17:44:04.527 回答
2

hsc2hsc→hsGreen Card都提供自动化的 Haskell⇆C 结构 peek/poke 或编组。我建议使用它们而不是手动确定大小和偏移量以及在 Haskell 中使用指针操作,尽管这也是可能的。

  1. 据我所知,如果我理解正确的话。Haskell 没有对外部聚合数据结构的任何内置处理。
  2.  
  3.  
  4. 正如该页面所描述的,它hsc2hs具有一些 C 魔法。
于 2010-12-21T17:49:34.717 回答