【问题标题】:Where is the space leak when using scanl? (Haskell)使用scanl时空间泄漏在哪里? (哈斯克尔)
【发布时间】: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。

【问题讨论】:

    标签: haskell memory-leaks


    【解决方案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)。如果可能的话(根据你的实际情况,我的意思是)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-24
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多