为什么包装Data.Binary.Put单子会导致内存泄漏?

12

我正在尝试将 Data.Binary.Put monad 包装到另一个 monad 中,以便以后可以询问它类似 "它要写入多少字节" 或者 "文件中的当前位置是什么" 的问题。但即使是非常简单的包装,比如:

data Writer1M a = Writer1M { write :: P.PutM a }
or
data Writer2M a = Writer2M { write :: (a, P.Put) }

我尝试过以下方法,但程序通常会崩溃(占用4GB的内存),并导致大量空间泄漏。

-- This works well and consumes almost no memory.

type Writer = P.Put

writer :: P.Put -> Writer
writer put = put

writeToFile :: String -> Writer -> IO ()
writeToFile path writer = BL.writeFile path (P.runPut writer)

-- This one will cause memory leak.

data Writer1M a = Writer1M { write :: P.PutM a }

instance Monad Writer1M where
  return a = Writer1M $ return a
  ma >>= f = Writer1M $ (write ma) >>= \a -> write $ f a

type WriterM = Writer1M
type Writer = WriterM ()

writer :: P.Put -> Writer
writer put = Writer1M $ put

writeToFile :: String -> Writer -> IO ()
writeToFile path writer = BL.writeFile path (P.runPut $ write writer)
-- This one will crash as well with exactly the
-- same memory foot print as Writer1M

data Writer2M a = Writer2M { write :: (a, P.Put) }

instance Monad Writer2M where
  return a = Writer2M $ (a, return ())
  ma >>= f = Writer2M $ (b, p >> p')
                        where (a,p) = write ma
                              (b,p') = write $ f a

type WriterM = Writer2M
type Writer = WriterM ()

writer :: P.Put -> Writer
writer put = Writer2M $ ((), put)

writeToFile :: String -> Writer -> IO ()
writeToFile path writer = BL.writeFile path (P.runPut $ snd $ write writer)

我是Haskell的新手,包装器Monad对我来说毫无意义,但似乎很简单,所以我猜肯定有一些显而易见的问题我没有注意到。

谢谢您的关注。

更新: 以下是一个示例代码,展示了这个问题:http://hpaste.org/43400/why_wrapping_the_databinaryp

更新2: 此问题还有第二部分,请参见此处


1
你正在使用哪些编译器标志? - C. A. McCann
你能否发布一个简单的测试程序,这样其他人就不必自己构建了吗? - Thomas M. DuBuisson
好主意,我很快就会编译出来。我能在stackoverflow上发布吗?还是应该使用hpaste? - Peter Jankuliak
完成,附上一个示例程序的链接。 - Peter Jankuliak
3
我已经尝试了你的样例并且在我的 GHC 6.12.3 和 -O2 标志下,两个版本表现出几乎相同的时间/空间行为。用 newtype 替换“有问题”的包装器中的 data 可以进一步减少差异。没有 -O2 标志会非常占用内存。你确定你重新编译时使用了 -O2 吗?试试 -O2 -fforce-recomp - Ed'ka
显示剩余4条评论
2个回答

4

经过一番探索,我发现问题似乎在于使用二进制的(>>=)来实现(>>)。对于Writer1M单子实现,以下添加解决了这个问题:

  m >> k = Writer1M $ write m >> write k

然而,这个版本仍然存在内存泄漏问题:

  m >> k = Writer1M $ write m >>= const (write k)

binary的源代码来看,(>>)似乎明确地丢弃了第一个monad的结果。不过我不确定这样做如何防止泄漏。我最好的理论是,否则GHC将保留PairS对象,并且"a"引用泄漏,因为它永远不会被查看。


2

你尝试过使单子更加严格吗?例如,尝试使数据类型的构造函数变为严格/用新类型替换它们。

我不知道确切的问题在哪里,但这通常是泄漏的主要原因。

PS:尝试删除不必要的lambda表达式,例如:

  ma >>= f = Writer1M $ (write ma) >=> write . f

将数据类型更改为newtype是#haskell的专家建议,不幸的是,像您建议的那样更改并删除lambda并没有改变内存占用。但还是感谢您的建议。 - Peter Jankuliak
是的,这是结果:http://i.imgur.com/4Q2E3.png,当我使用其中一个包装器时,黄色区域出现。 - Peter Jankuliak
非常奇怪。我想我不能帮助你。 - fuz
1
我猜测Writer1M无意中充当了保留器的角色,持有输出指针,从而防止垃圾回收在途中清除字节串。尝试使用保留器分析进行运行,并查看是否能得到任何信息。 - Paul Johnson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接