6

在 GHCI 中,我运行了这个简单的测试:

encodeFile "test" [0..10000000]

这条线运行得非常快(<10 秒),但我的内存使用量在它完成之前飙升至 ~500MB。由于它使用 ByteString.Lazy,因此 encodeFile 不应该是惰性的吗?


编辑:罗马下面的答案很棒!我还想指出另一个问题的答案,这解释了为什么 Data.Binary 对列表进行严格编码并提供了一种稍微优雅的解决方法。

4

1 回答 1

9

以下是列表序列化的定义方式:

instance Binary a => Binary [a] where
    put l  = put (length l) >> mapM_ put l

也就是先序列化列表的长度,再序列化列表本身。

为了找出列表的长度,我们需要评估整个列表。但是我们不能对它进行垃圾收集,因为第二部分需要它的元素,mapM_ put l. 所以整个列表必须在计算长度之后和元素序列化开始之前存储在内存中。

下面是堆配置文件的样子:

轮廓

请注意它在构建列表以计算其长度时如何增长,然后在元素被序列化并可以被 GC 收集时如何减少。

那么,如何解决这个问题?在您的示例中,您已经知道长度。因此,您可以编写一个采用已知长度的函数,而不是计算它:

import Data.Binary
import Data.ByteString.Lazy as L
import qualified Data.ByteString as B
import Data.Binary.Put

main = do
  let len = 10000001 :: Int
      bs = encodeWithLength len [0..len-1]
  L.writeFile "test" bs

putWithLength :: Binary a => Int -> [a] -> Put
putWithLength len list =
  put len >> mapM_ put list

encodeWithLength :: Binary a => Int -> [a] -> ByteString
encodeWithLength len list = runPut $ putWithLength len list

该程序在 53k 的堆空间内运行。

您还可以在其中包含一个安全功能putWithLength:在序列化列表时计算长度,并检查最后的第一个参数。如果不匹配,则抛出错误。

练习:为什么你仍然需要传递长度putWithLength而不是使用上面描述的计算值?

于 2012-07-26T08:08:09.717 回答