懒惰评估 - 空间泄漏

时间:2015-03-05 01:51:41

标签: haskell

Thinking Functionally with Haskell提供以下代码,用于计算 Float 列表的平均值

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

教授。理查德伯德评论:

  

现在我们已经准备好了解真正意义上的错误:它有空间泄漏。评估mean [1..1000]将导致列表在求和后被扩展并保留在内存中,因为有一个指向它的第二个指针,即计算其长度。

如果我正确地理解了这个文本,他说,如果在长度计算中没有指向xs的指针,那么{/ 1}}内存可以在 >计算xs

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

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

4 个答案:

答案 0 :(得分:7)

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

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

length完全相同。

另一方面,mean功能会将列表提供给sumlength。因此,在评估sum期间,我们需要将列表保留在内存中,以便稍后length处理。

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

答案 1 :(得分:4)

其他人已经解释了问题所在。最干净的解决方案可能是使用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

答案 2 :(得分:2)

  

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

不,你在这里错过了懒惰评估的重要方面。你是对的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

真正在恒定空间中运行,因为您可以使用:set +s在ghci中确认。

答案 3 :(得分:0)

空间泄漏是整个评估的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)