使用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时空间泄漏在哪里? (哈斯克尔)的主要内容,如果未能解决你的问题,请参考以下文章

哪种最大选择算法更快? (哈斯克尔)[重复]

哈斯克尔,再次。非法类型? [关闭]

哈斯克尔中类型的函数应用操作数($)?

`ghc-pkg` 和 `cabal` 程序有啥关系? (哈斯克尔)

哈斯克尔。我很困惑这个代码片段是如何工作的

我的 AVFoundation/AVCaptureSession 泄漏内存在哪里?