使用scanl时空间泄漏在哪里? (哈斯克尔)
Posted
技术标签:
【中文标题】使用scanl时空间泄漏在哪里? (哈斯克尔)【英文标题】:Where is the space leak when using scanl? (Haskell) 【发布时间】:2014-10-11 02:20:21 【问题描述】:考虑这段代码:
incState state i = map (+ i) state
main =
sequence_ $ do
n <- map last $ scanl incState (replicate 1000000 0) (repeat 1)
return $ print $ n
当我运行它时,内存使用量不断增加,因为它会打印出数字。这里的内存泄漏在哪里?它应该只保持恒定的内存量,以前的 state
值应该在打印时丢弃。
即使sequence_
或列表单子保持n
的值,每次迭代也只有一个整数。我看到 6Gs 的内存在它只计数到 100 之后被使用。一百万个整数应该最多只占用 10Ms。
【问题讨论】:
【参考方案1】:没有什么会迫使中间计算通过列表的中间。如果将last
替换为sum
,它将停止泄漏内存。
要了解发生了什么,让我们考虑一个稍微简单的同一件事的陈述:
main = mapM (print . last) $ scanl incState (replicate 5 0) (repeat 1)
现在,让我们开始评估它。首先,我们状态的内存看起来像
scanl incState (map (+1) (replicate 5 0)) (repeat 1)
我们将跳过一些细节,但我们的第一个要求是mapM
模式与状态列表的开头匹配。
(:) -> replicate 5 0
| ^
| |_________________
v |
scanl incState (map (+1) | ) (repeat 1)
为了将last
项目变为print
,我们强制列表的脊椎和最后一个项目,而不是单个项目。我们将在下一步中更好地看到这一点。
0 : 0 : 0 : 0 : 0 : []
^
|_________________
|
scanl incState (map (+1) | ) (repeat 1)
对于mapM
的下一步,我们需要状态列表中的下一项。内存现在看起来像
0 : 0 : 0 : 0 : 0 : []
^
|________
|
(:) -> map (+1) |
| ^
| |_________________
v |
scanl incState (map (+1) | ) (repeat 1)
为了将 last
项目变为 print
,我们强制列表的脊椎和最后一个项目,而不是其他单个项目。
0 : 0 : 0 : 0 : 0 : []
^ ^ ^ ^
| | | |
|+1 : |+1 : |+1 : |+1 : 1 : []
^
|_________________
|
scanl incState (map (+1) | ) (repeat 1)
对于mapM
的下一步,我们需要状态列表中的下一项。内存现在看起来像
0 : 0 : 0 : 0 : 0 : []
^ ^ ^ ^
| | | |
|+1 : |+1 : |+1 : |+1 : 1 : []
^
|________
|
(:) -> map (+1) |
| ^
| |_________________
v |
scanl incState (map (+1) | ) (repeat 1)
为了将last
项目变为print
,我们强制列表的脊椎和最后一个项目,而不是其他单个项目。
0 : 0 : 0 : 0 : 0 : []
^ ^ ^ ^
| | | |
|+1 : |+1 : |+1 : |+1 : 1 : []
^ ^ ^ ^
| | | |
|+1 : |+1 : |+1 : |+1 : 2 : []
^
|_________________
|
scanl incState (map (+1) | ) (repeat 1)
每次重复此操作时,我们都会留下另一个 thunk 列表,参考之前的 thunk 列表等。这就是空间泄漏的来源。如果您在每一步之后强制所有结果,就像print . sum
所做的那样,就不会有空间泄漏。
【讨论】:
【参考方案2】:套用 Cirdec 的回答,计算下一个状态,incState state i = map (+ i) state
需要 上一个状态 state
,其结果列表中的每个非强制元素都将保留对前一个状态的引用。因此,这些状态都不会被垃圾回收丢弃,以防万一以后可能需要除 last
之外的其他元素(即使永远不会,但这是一个整个程序优化)。
要强制通过整个列表以便忘记之前的列表,我们可以使用seq
减少它,
mapM_ (print . foldl' seq 0) $
scanl (\s i-> map (+ i) s) (replicate 1000000 0) (repeat 1)
或者我们可以使用转换
map f xs = foldr (\x r-> f x : r) [] xs
= foldr (\x r-> let y=f x in y `seq` (y:r)) [] xs
定义一个“强制”map
(几年前我在 SO cmets 中看到了这一点,@luqui, IIRC):
mapM_ (print . last) $
scanl (\s i-> foldr (\x r-> let y=(+i) x in y `seq` (y:r)) [] s)
(replicate 1000000 0) (repeat 1)
当然,所有这些人为强制的多余计算都需要时间,因此最好更改代码,如果可能的话,不要携带任何不必要的东西——实际上是将map last
手动融合到scanl ... replicate ...
并进一步进入incState
(编译器无法执行的步骤),最终得到mapM_ print $ scanl (+) 0 (repeat 1)
。如果可能的话(根据你的实际情况,我的意思是)。
【讨论】:
以上是关于使用scanl时空间泄漏在哪里? (哈斯克尔)的主要内容,如果未能解决你的问题,请参考以下文章