【问题标题】:Defining Storable for Recursive Data Structure Involving Vectors为涉及向量的递归数据结构定义可存储
【发布时间】:2023-09-22 10:51:01
【问题描述】:

我有一个如下形式的数据结构(V是Data.Storable.Vector):

data Elems = I {-# UNPACK #-} !GHC.Int.Int32
             | S {-# UNPACK #-} !GHC.Int.Int32 {-# UNPACK #-} !(Ptr CChar)
             | T {-# UNPACK #-} !(V.Vector Elems)
                deriving (Show)

我首先为非递归形式编写了一个自定义的可存储定义(即,没有T 构造函数)。然后,我尝试使用来自VectorForeignPtrlength 信息为T 添加自定义peek 和poke 定义(代码如下)。 GHC 编译器抱怨没有为ForeignPtr Elems 类型定义Storable 实例。我的问题是是否可以在 Storable 定义中将 ptr 存储到 Vector,而不必为 ForeignPtr 编写 Storable 实例定义。

Haddocs 文档来看,ForeignPtr 似乎只是一个分配有终结器的 Ptr:

ForeignPtrs 和 vanilla memory 的本质区别 Ptr a 类型的引用是前者可能与 终结者。

我不想通过使用Ptr 而不是ForeignPtr 来解决此问题,因为存在最终确定的问题。所以,我更喜欢存储 ForeignPtr 的位置(通过Ptr (ForeignPtr a)),以便 GHC 垃圾收集器知道对它的引用。但是,这种方法将迫使我定义一个Storable instance(因为约束(Storable a) => Ptr a 是有道理的)。

有没有办法在 Storable 中将 ptr 存储和检索到 Vector 中,而无需为 ForeignPtr 定义 Storable 实例?如果没有,那么编写 ForeignPtr 的 Storable 定义是必须的。在那种情况下,它会是什么样子?我的猜测是它只会将 Ptr 存储到 ForeignPtr。

完整代码如下:

{-# LANGUAGE MagicHash #-}
import qualified Data.Vector.Storable as V
import Foreign
import Foreign.C.Types (CChar)
import Foreign.Marshal.Array (lengthArray0)
import GHC.Int

data Elems = I {-# UNPACK #-} !GHC.Int.Int32
             | S {-# UNPACK #-} !GHC.Int.Int32 {-# UNPACK #-} !(Ptr CChar)
             | T {-# UNPACK #-} !(V.Vector Elems)
                deriving (Show)

instance Storable Elems where
  sizeOf _ = sizeOf (undefined :: Word8) + sizeOf (undefined :: Int32) + sizeOf (undefined :: Ptr CChar)
  alignment _ = 4

  {-# INLINE peek #-}
  peek p = do
      let p1 = (castPtr p::Ptr Word8) `plusPtr` 1 -- get pointer to start of the element. First byte is type of element
      t <- peek (castPtr p::Ptr Word8)
      case t of
        1 -> do 
          x <- peek (castPtr p1 :: Ptr GHC.Int.Int32) 
          return (I x)
        2 -> do 
          x <- peek (castPtr p1 :: Ptr GHC.Int.Int32) 
          y <- peek (castPtr (p1 `plusPtr` 4) :: Ptr (Ptr CChar)) -- increment pointer by 4 bytes first
          return (S x y)
        _ -> do
          x <- peek (castPtr p1 :: Ptr Int)
          y <- peek (castPtr (p1 `plusPtr` 8) :: Ptr (ForeignPtr Elems)) 
          return (T (V.unsafeFromForeignPtr y 0 x)) -- return vector

  {-# INLINE poke #-}
  poke p x = case x of
      I a -> do
        poke (castPtr p :: Ptr Word8) 1  
        poke (castPtr p1) a
      S a b -> do
        poke (castPtr p :: Ptr Word8) 2
        poke (castPtr p1) a
        poke (castPtr (p1 `plusPtr` 4)) b -- increment pointer by 4 bytes first
      T x -> do
        poke (castPtr p :: Ptr Word8) 3
        let (fp,_,n) = V.unsafeToForeignPtr x
        poke (castPtr p1) n
        poke (castPtr (p1 `plusPtr` 8)) fp

      where  p1 = (castPtr p :: Ptr Word8) `plusPtr` 1 -- get pointer to start of the element. First byte is type of element

【问题讨论】:

  • 您可能需要考虑使用-funbox-strict-fields,而不是在每个字段上都添加{-# UNPACK #-} pragma。

标签: haskell vector storable


【解决方案1】:

ForeignPtrs 不能成为Storable,因为它们的实现需要一种将一个或多个终结器指针关联到原始指针的方法,并且这种关联是依赖于运行时的。要使ForeignPtr 可存储,您需要存储关联的Ptr(这很容易)和关联的终结器数组(这是不可能的,因为终结器是运行时内部的,并且可能绑定到 GHC 的 GC运行时)。

不过,这不是这里需要解决的问题。

问题是没有合理的方法可以将包含Vector 的东西变成Storable 的东西。 Vector 需要 托管 内存来存储其内容(Storable.Vector 的定义是 data Vector a = Vector Int (ForeignPtr a) 加上一些严格的注释),但 Storable 的全部目的是将一些值存储到 非托管内存。此外,Vector 根据其长度使用不同数量的内存,但Storable 数据结构必须使用恒定数量的内存。

您需要重新考虑您的数据结构试图建模的内容。你真的需要像这样存储Vector吗?请记住,您正在存储ElemsVector,这意味着您可以拥有一个包含Vector 的值Vector,其中包含一个包含VectorVector,其中包含一个T 等.

我认为您可能正在尝试对以下数据结构进行建模,但我可能错了:

data Elems = OneElem Elem | ManyElems (Vector Elem)

data Elem
    = I !GHC.Int.Int32
    | S !GHC.Int.Int32 !(Ptr CChar)

如果您确实需要您描述的递归数据结构,请尝试实现它:

data Elems
    = I !GHC.Int.Int32
    | S !GHC.Int.Int32 !(Ptr CChar)
    | T !GHC.Int.Int32 !(Ptr Elems)

指向某个Elems 的指针使用常量内存,并且可以指向非托管内存,因此您可以为其创建可存储的实例。

【讨论】:

  • 感谢您的详尽帖子。我正在尝试对递归向量进行建模-结果是因为混合向量(例如向量(字符向量,整数向量,浮点向量,...))我必须在交互时处理与现实世界中的数据。我简化了我的例子。它会更像: data Elems = I !Int32 | IV !(向量 Int32) |...| T !(向量元素)。如果我没记错的话,您对 Elems 的第二个定义仍然无法处理。但是,我从你的第一个定义中得到了一些想法。现在正在处理它们