就像在previous question中一样,我正在尝试将Data.Binary.Put monad包装到另一个monad中,以便以后可以向它询问诸如“它将要写入多少个字节”或“文件中的当前位置是什么”之类的问题。

以前,我认为了解为什么在使用琐碎的(IdentityT?)包装器时会泄漏内存会导致我解决问题。但是,即使你们已经帮助我解决了琐碎的包装器问题,但使用诸如StateT或WriterT之类的有用包装器将其包装起来仍会占用过多内存(并且通常会崩溃)。

例如,这是我尝试包装它的一种方法,该方法会泄漏大量输入的内存:

输入Out = StateT整数P.PutM()

writeToFile::字符串->输出-> IO()
writeToFile路径输出= BL.writeFile路径$ P.runPut $ do runStateT输出0
返回 ()

Here是演示问题的更完整的代码示例。

我想知道的是:

  • 程序内部正在发生什么导致内存泄漏?
  • 我该怎么解决?

  • 对于第二个问题,我想我应该更详细地解释数据在磁盘上的显示内容:它基本上是树结构,其中树的每个节点都表示为其子节点的偏移表(加上一些其他数据)。因此,要将第n个子代的偏移量计算到偏移量表中,我需要知道0到n-1子代的大小加上当前偏移量(为简化起见,假设每个节点都有固定数量的子代)。

    感谢您的光临。

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

    像这样:

    类型偏移=整数

    新类型MyPut a = MyPut
    {unS::forall r。 (偏移-> a-> P.PutM r)->偏移-> P.PutM r}

    实例Monad MyPut,其中
    返回a = MyPut $\f s-> f s a
    ma >> = f = MyPut $\fb s-> unS ma(\s'a-> unS(f a)fb s')s

    writeToFile::字符串-> MyPut()-> IO()
    writeToFile路径put =
    BL.writeFile path $ P.runPut $ peal put >> return()
    其中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
    orgetSize::MyPut a-> Int

    我的方法是将MyPut monad包装在WriterT转换器内(类似于this)。但这又开始消耗过多的内存。正如sclv在nominolos答案下的评论中提到的那样,WriterT以某种方式抵消了延续的影响。他还提到应该直接从我已经拥有的MyPut monad中获取大小,但是我的所有尝试都以无法编译的代码或无限循环:-|结尾。

    有人可以帮忙吗?

    最佳答案

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

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

    在这种情况下,它并不是特别有用,因为它仅显示许多PAPFUN_1_0FUN_2_0。这意味着堆由许多部分应用的函数以及一个参数和两个参数的函数组成。这通常意味着对某些事物的评估不够。 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实例,并且某些懒惰技巧也不再起作用。

    关于haskell - 为什么包装Data.Binary.Put monad会导致内存泄漏? (第2部分),我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/5019655/

    10-10 14:01