我尝试在haskell中解析大型日志文件。我使用System.IO.Streams
,但当我折叠输入时,它似乎占用了很多内存。这里有两个(丑陋的)例子:
首先将1M Int
加载到列表中的内存中。
let l = foldl (aux p -> p:aux) [] [1..1000000]
return (sum l)
内存消耗是美丽的。Ints吃了3Mb,列表需要6Mb:
参见1M Int 的构建列表的内存消耗
然后对Stream of ByteStrings进行同样的尝试。我们需要一次丑陋的来回对话,但我认为没有任何区别
let s = Streams.fromList $ map (B.pack . show) [1..1000000]
l <- s >>=
Streams.map bsToInt >>=
Streams.fold (aux p -> p:aux) []
return (sum l)
查看从流构建Int列表的内存消耗
为什么它需要更多的内存?如果我从文件中读到它,情况会更糟。它需要90Mb
result <- withFileAsInput file load
putStrLn $ "loaded " ++ show result
where load is = do
l <- Streams.lines is >>=
Streams.map bsToInt >>=
Streams.fold (aux p -> p:aux) []
return (sum l)
我的假设是Streams.fold存在一些问题。因为库的内置countInput方法没有使用它。知道吗?
编辑
经过调查,我把问题归结为:为什么这个代码需要额外的50Mb?
do
let l = map (Builder.toLazyByteString . intDec ) [1..1000000]
let l2 = map (fst . fromJust . B.readInt) l
return (foldl' (aux p -> p:aux) [] l2)
在没有转换的情况下,它只需要30Mb,而转换为90Mb。
在第一个示例中,foldl (aux p -> p:aux) []
是多余的。它构造了一个列表,其元素与作为参数的列表相同!在没有冗余的情况下,该示例等效于sum [1..1000000]
或foldl (+) 0 [1..1000000]
。此外,最好使用严格的左折叠foldl'
来避免可约表达式在堆上的累积。请参阅Haskell维基上的Foldr Foldl Foldl。
在上一个例子中,您使用System.IO.Streams.Combinators.fold
来构建从文件中读取的所有整数的列表,然后像在第一个例子中那样尝试对列表求和。
问题是,由于IO
monad强制执行文件读取操作的顺序,在开始对列表求和之前,文件中的所有数据都已被读取,并且潜伏在堆中,可能仍然未从原始字符串转换,甚至占用更多的内存。
解决方案是在每个新元素到达时,在折叠内执行实际和;这样,您就不需要在任何时候都在内存中有完整的列表,只需要当前元素(在执行I/O时能够做到这一点是流库的目标之一)。并且io-streams
提供的折叠是严格的,类似于foldl'
。因此,您也不会在堆上累积可约表达式。
试试System.IO.Streams.Combinators.fold (+) 0
之类的东西。
所以问题是ByteString
s的懒惰创建,而不是迭代器。看见为什么创建和处理时态ByteStrings会占用我在Haskell中的内存?