为什么这段代码不是恒定空间?

时间:2015-09-02 12:59:00

标签: performance haskell

我目前正在学习Haskell(通过交易成为程序员,但这是我第一次使用函数式语言)。

我想编写一个扫描列表并返回该列表的最小和最大元素的函数。 Prelude函数minimummaximum的功能排序,但两者同时进行。我提出了以下代码:

import Data.List

-- Declaration of rand

minMax :: [Int] -> Maybe (Int, Int)
minMax []   = Nothing
minMax (x:xs) = Just (foldl' f (x, x) xs)
                where
                  f (a, b) c = (if c < a then c else a, if c > b then c else b)

rand是一个生成无限数字列表的函数。 问题是当我附加以下main函数时:

main = print $ minMax $ take 1000000 $ rand 7666532

使用性能分析编译并运行所有这些,它显示它使用超过200 MB的内存,所以它绝对不是一个恒定空间函数(我喜欢它)。

问题是为什么以及应该修改什么来修复它。据我所知,foldl'从左侧折叠列表(它生成的方式相同)并且不是懒惰的,所以我不明白为什么内存使用率如此之高。我很确定minMax函数不正确,只需打印上述列表,使用

main = print $ take 1000000 $ rand 7666532

给了我1MB的使用量,这是我理解和期待的。

2 个答案:

答案 0 :(得分:26)

请注意foldl'强制累加器为头部正常形式。由于累加器是一个元组,它强制评估元组的两个元素。

如果明确强制使用这两个元素,则会得到一个常量空间函数:

f (a, b) c = a `seq` b `seq` (if c < a then c else a, if c > b then c else b)

在原始程序中,您正在构建一个类型的元组:(<thunk>, <thunk>)每次应用f时,您只需构建一个具有越来越大的thunk的元组。最后由print消耗时,对show的调用会强制对元组进行全面评估,并在此时进行所有比较。

使用seq代替强制f来评估当时的比较,从而在执行比较之前评估累加器中包含的thunk。因此结果是存储在累加器中的thunk具有恒定的大小。

foldl'所做的只是避免构建thunk:f (f (f ...) y) x

Jubobs建议的另一种避免明确使用seq的解决方案是使用具有严格字段的​​数据类型:

data Pair a b = Pair !a !b
    deriving Show

因此代码将成为:

minMax :: [Int] -> Maybe (Pair Int Int)
minMax []   = Nothing
minMax (x:xs) = Just (foldl' f (Pair x x) xs)
                where
                  f (Pair a b) c = Pair (if c < a then c else a) (if c > b then c else b)

这完全避免了砰砰声。

答案 1 :(得分:12)

seq中使用的foldl'函数实质上强制将其第一个参数评估为WHNF(弱头范式)。

正如here所解释的那样,WHNF评估会在每次应用构造函数时停止。因此,(a, b)始终位于WHNF,即使ab是thunk,因为您在访问(,)之前遇到了元组构造函数a并且b

因此,尽管使用了foldl'

,但这个空间也会泄漏
foldl' (\ (a, b) x -> (a + x, b + x)) (0, 1) [1..1000000]

但这并不是:

foldl' (\ (a, b) x -> a `seq` b `seq` (a + x, b + x)) (0, 1) [1..10000000]

有时使用-XBangPatterns扩展程序来编写此代码也很方便:

foldl' (\ (!a, !b) x -> (a + x, b + x)) (0, 1) [1..10000000]