为什么包装Data.Binary.Put monad会造成内存泄漏? (第2部分)

时间:2011-02-16 17:11:30

标签: haskell memory-leaks binary monads monad-transformers

在我的previous question中,我正在尝试将Data.Binary.Put monad包装到另一个monad中,以便稍后我可以问它“它要写多少字节”或“什么是文件中的当前位置“。

之前,我认为理解为什么它在使用一个简单的(IdentityT?)包装器时会泄漏内存会导致我解决我的问题。但即使你们帮我解决了琐碎的包装问题,用StateT或WriterT之类的东西包装它仍然会消耗太多内存(通常会崩溃)。

例如,这是我试图包装它的一种方式,它会泄漏内存以获得大输入:

type Out = StateT Integer P.PutM ()

writeToFile :: String -> Out -> IO ()
writeToFile path out = BL.writeFile path $ P.runPut $ do runStateT out 0
                                                         return ()

Here是一个更完整的代码示例,用于演示此问题。

我想知道的是:

  1. 导致内存泄漏的程序内部发生了什么?
  2. 我该怎么做才能解决它?
  3. 对于我的第二个问题,我想我应该更详细地解释一下我打算在磁盘上查看数据:它基本上是一个树结构,其中树的每个节点都表示为它的子节点的偏移表(加上一些额外的数据)。因此,要计算第n个孩子到偏移表中的偏移量,我需要知道子项0到n-1的大小加上当前的偏移量(为了简化事情,假设每个节点都有固定数量的子项)。

    感谢您的光临。

    更新: 感谢nominolo,我现在可以创建一个包裹Data.Binary.Put的monad,跟踪当前偏移并几乎不使用任何内存。这是通过放弃使用StateT转换器来支持使用Continuations的不同状态线程机制来完成的。

    像这样:

    type Offset = Int
    
    newtype MyPut a = MyPut
      { unS :: forall r . (Offset -> a -> P.PutM r) -> Offset -> P.PutM r }
    
    instance Monad MyPut where
      return a = MyPut $ \f s -> f s a
      ma >>= f = MyPut $ \fb s -> unS ma (\s' a -> unS (f a) fb s') s
    
    writeToFile :: String -> MyPut () -> IO ()
    writeToFile path put =
      BL.writeFile path $ P.runPut $ peal put >> return ()
      where peal myput = unS myput (\o -> return) 0
    
    getCurrentOffset :: MyPut Int
    getCurrentOffset = MyPut $ \f o -> f o o
    
    lift' n ma = MyPut $ \f s -> ma >>= f (s+n)
    

    但是我仍然遇到跟踪MyPut要在磁盘上写入多少字节的问题。特别是,我需要一个带有签名的函数,如下所示:

    getSize :: MyPut a -> MyPut Int
    
    getSize :: MyPut a -> Int
    

    我的方法是将MyPut monad包装在WriterT变换器中(类似this)。但是这开始再次消耗太多内存。正如sclv在nominolos回答中的评论中提到的那样,WriterT以某种方式取消了延续的效果。他还提到可以直接从我已经拥有的MyPut monad中获取大小,但是我所做的所有尝试都以不可编译的代码或无限循环结束: - |。

    有人可以请进一步帮忙吗?

2 个答案:

答案 0 :(得分:8)

看起来monad变压器太懒了。您可以通过以下命令运行程序来创建堆配置文件(无需专门构建它):

$ ./myprog +RTS -hT
$ hp2ps myprog.hp
$ open hp2ps.ps    # Or whichever viewer you have

在这种情况下,它不是特别有用,因为它只显示了很多PAP s,FUN_1_0FUN_2_0 s。这意味着堆由许多部分应用的函数以及一个参数和两个参数的函数组成。这通常意味着某些事情的评估不够。 Monad变形金刚因此而臭名昭着。

解决方法是使用continuation passing style使用更严格的monad变换器。 (他要求{-# LANGUAGE Rank2Types #-}

newtype MyStateT s m a =
  MyStateT { unMyStateT :: forall r. (s -> a -> m r) -> s -> m r }

延续传递样式意味着我们不是直接返回结果,而是使用我们的结果调用另一个函数 continuation ,在本例中为sa。实例定义看起来有点滑稽。要理解它,请阅读上面的链接(维基百科)。

instance Monad m => Monad (MyStateT s m) where
  return x = MyStateT (\k s -> k s x)
  MyStateT f >>= kk = MyStateT (\k s ->
    f (\s' a -> unMyStateT (kk a) k s') s)

runMyStateT :: Monad m => MyStateT s m a -> s -> m (a, s)
runMyStateT (MyStateT f) s0 = f (\s a -> return (a, s)) s0

instance MonadTrans (MyStateT s) where
  lift act = MyStateT (\k s -> do a <- act; k s a)

type Out = MyStateT Integer P.PutM ()

现在运行它会给出恒定的空间(“最大驻留”位):

$ ./so1 +RTS -s 
begin
end
   8,001,343,308 bytes allocated in the heap
     877,696,096 bytes copied during GC
          46,628 bytes maximum residency (861 sample(s))
          33,196 bytes maximum slop
            2 MB total memory in use (0 MB lost due to fragmentation)

Generation 0: 14345 collections,     0 parallel,  3.32s,  3.38s elapsed
Generation 1:   861 collections,     0 parallel,  0.08s,  0.08s elapsed

使用这种严格的变形金刚的缺点是你不能再定义MonadFix个实例,某些懒惰技巧不再起作用。

答案 1 :(得分:2)

我开始玩这个,并意识到更大的问题是什么 - 你的算法具有可怕的复杂性。不是每次计算每个子树的大小,而是每次调用getSize时计算一次。你递归调用getSize。对于每个叶节点,每次在其父节点上调用getSize时,都会调用一次getSize。并且getSize在每个父级上为自己调用一次,每次在父级中调用getSize时调用一次。因此,在树的深度中至少几何上调用getsize。您需要缓存大小以获得类似于合理运行时的内容。

也就是说,这是一个核心功能的版本,看起来运行正常而没有泄漏,尽管由于上述原因它实际上正在爬行:

type MyPut = S (Offset,Size) P.PutM

peal_1 :: (Monad m, Num t, Num t1) => S (t, t1) m a -> m a
peal_1 put = unS put (\o -> return) (0,0)

writeToFile :: String -> MyPut () -> IO ()
writeToFile path put =
  BL.writeFile path $ P.runPut $ (peal_1 put) >> return ()

getSize :: MyPut a -> MyPut Int
getSize x = S $ \f os -> unS (x >> getCurrentSize) f os

getCurrentOffset :: MyPut Int
getCurrentOffset = S $ \f os -> f os (fst os)

getCurrentSize :: MyPut Int
getCurrentSize = S $ \f os -> f os (snd os)

我还必须说我不确定你的逻辑是否正确。我的代码在修复泄漏时保留了当前的行为。我通过在一个简化的数据集上运行它和你的代码并生成逐位相同的文件来测试它。

但是对于你的大型测试数据,这段代码在我杀死它之前编写了6.5G(在此之前提供的代码已用完了)。我怀疑但是没有测试过put monad中的底层调用每次调用getSize都会运行一次,即使getSize的结果被抛弃了。

我建议的正确解决方案是作为您的其他问题的答案发布的:How do you save a tree data structure to binary file in Haskell