【问题标题】:Lazy Evaluation - Space Leak懒惰的评估 - 空间泄漏
【发布时间】:2015-05-06 06:40:51
【问题描述】:

Thinking Functionally with Haskell 提供了以下代码,用于计算 Float 列表的 mean。

mean :: [Float] -> Float
mean [] = 0
mean xs = sum xs / fromIntegral (length xs)

教授。理查德伯德:

现在我们准备好看看 mean 的真正问题是什么:它有空间泄漏。计算mean [1..1000] 将导致列表在求和后扩展并保留在内存中,因为有第二个指针指向它,即在计算其长度时。

如果我正确理解了这段文字,他的意思是,如果在长度计算中没有指向 xs 的指针,那么 xs 内存可能已经被释放在计算 @ 987654327@?

我的困惑是 - 如果 xs 已经在内存中,那么 length 函数不是只会使用已经被占用的相同内存吗?

这里的空间泄漏我不明白。

【问题讨论】:

  • 嗯,“泄漏”对我来说似乎有点奇怪......但绝对要注意一件事:stackoverflow.com/a/2380437/667648 sum xs 被计算出来,然后长度 xs 被计算出来,通过两次而不是一次.
  • 与问题无关,但如果您要编写一个平均函数,请让它返回一个Maybe。这里无法区分真 0 和假 0。
  • Haskell 公文:计算sum xs 不是问题。计算length xs 不是问题。计算sum xs / length xs 是个问题。
  • @DanielWagner 我不明白这一点。鉴于sum 是用foldl 懒惰地定义的,它不会导致空间泄漏吗?在我的笔记本电脑中提供sum [1..10000000] 会增加笔记本电脑的内存使用率。而mysum = foldl' (+) 0 在恒定的内存空间上运行?
  • @Sibi 这确实是一个不幸的历史细节。但是,在任何地方都用foldl' (+) 0 代替sum 确实有损公案的美感,同时对新手几乎没有兴趣,你不同意吗? =)

标签: haskell


【解决方案1】:

sum 函数不需要将整个列表保存在内存中;它可以一次查看一个元素,然后在移动到下一个元素时忘记它。

因为默认情况下 Haskell 有惰性求值,如果你有一个创建列表的函数,sum 可以在整个列表都不在内存中的情况下使用它(每次生成函数生成一个新元素时,它会被sum 消费然后释放)。

length 也会发生完全相同的事情。

另一方面,mean 函数将列表提供给sum 和length。所以在sum的求值过程中,我们需要将列表保存在内存中,以便稍后length处理。

[更新]要明确,列表最终将被垃圾收集。问题是它停留的时间比需要的长。在这种简单的情况下,这不是问题,但在对无限流进行操作的更复杂的函数中,这很可能会导致内存泄漏。

【讨论】:

  • sum 在 Prelude 中被延迟实现。我认为它仍会将整个列表保留在内存中?
  • 是的,因为不是sum 将列表保存在内存中;它是mean,sum 和 length 都需要它。
  • 在这种情况下这是一个问题,因为这种分配比将列表计算为循环两次要昂贵得多,因为如果你内联了两次出现就会发生这种情况
  • @FrédéricDumont,对不起,我想我不是很清楚。有两种方法可以将整个列表保存在内存中:从懒惰产生的xs 和懒惰的foldl 操作,它将用 (+) 操作扩展整个列表。伯德可能指的是第一种情况。虽然,即使使用普通的sum 函数也会导致空间泄漏。
  • @Sibi 是的,很明显 Bird 指的是第一种类型。令人难以置信的是,sum 仍然使用非严格的累加器实现,尽管当严格分析开始时,这在实践中可能不是问题。
【解决方案2】:

其他人已经解释了问题所在。最干净的解决方案可能是使用 Gabriel Gonzalez 的foldl package。具体来说,您需要使用

import qualified Control.Foldl as L
import Control.Foldl (Fold)
import Control.Applicative

meanFold :: Fractional n => Fold n (Maybe n)
meanFold = f <$> L.sum <*> L.genericLength where
  f _ 0 = Nothing
  f s l = Just (s/l)

mean :: (Fractional n, Foldable f) => f n -> Maybe n
mean = L.fold meanFold

【讨论】:

    【解决方案3】:

    如果在length 计算中没有指向xs 的指针,那么在计算sum 之后 可以释放xs 内存?

    不,您在这里遗漏了惰性求值的重要方面。没错,length 将使用在 sum 调用期间分配的内存,即我们扩展整个列表的内存。

    但这里的重点是根本不需要为整个列表分配内存。如果没有length 计算而只有sum,则内存可能已经在计算sum期间被释放。请注意,[1..1000] 列表仅在被消耗时延迟生成,因此实际上mean [1..1000] 应该在恒定空间中运行。

    您可以编写如下函数,以了解如何避免此类空间泄漏:

    import Control.Arrow
    
    mean [] = 0
    mean xs = uncurry (/) $ foldr (\x -> (x+) *** (1+)) (0, 0) xs
    
    -- or more verbosely
    mean xs = let (sum, len) = foldr (\x (s, l) -> (x+s, 1+l)) (0, 0)
              in sum / len
    

    哪个应该只遍历xs一次。然而,Haskell 是该死的懒惰 - 并且仅在评估 sum 时计算第一个元组组件,并且仅在稍后为 len 计算第二个元组组件。我们需要use some more tricks 来实际强制评估:

    {-# LANGUAGE BangPatterns #-}
    import Data.List
    
    mean [] = 0
    mean xs = uncurry (/) $ foldl' (\(!s, !l) x -> (x+s, 1+l)) (0,0) xs
    

    它确实在恒定空间中运行,您可以在 ghci 中使用:set +s 进行确认。

    【讨论】:

      【解决方案4】:

      空间泄漏是整个评估的xs 保存在内存中以供length 函数使用。这很浪费,因为我们不会在评估 sum 之后使用列表的实际值,也不需要同时将它们全部存储在内存中,但 Haskell 不知道这一点。

      消除空间泄漏的一种方法是每次重新计算列表:

      sum [1..1000] / fromIntegral (length [1..1000])
      

      现在应用程序可以立即开始丢弃第一个列表中的值,因为它正在评估 sum,因为它没有在表达式中的其他任何地方引用。

      这同样适用于length。它生成的 thunk 可以立即标记为删除,因为没有其他东西可能希望它进一步评估。

      编辑:

      在Prelude中实现sum:

      sum l = sum' l 0
        where
          sum' []     a = a
          sum' (x:xs) a = sum' xs (a+x)
      

      【讨论】:

      • 我认为即使是普通的sum 函数也会导致空间泄漏。 sum 在序曲中被定义为foldl (+) 0。
      • 这种方法无法实现mean函数。
      猜你喜欢
      • 2011-10-02
      • 2019-01-07
      • 1970-01-01
      • 2015-09-06
      • 1970-01-01
      • 2016-12-14
      • 2021-12-25
      • 2018-04-01
      • 2018-03-31
      相关资源
      最近更新 更多