提高分块列表的性能

时间:2014-09-21 17:23:07

标签: performance haskell

我有一个简单的问题:给定一个整数列表,将第一行读为N。然后,读取接下来的N行并返回它们的总和。重复直到N = 0.

我的第一个方法是使用它:

main = interact $ unlines . f . (map read) . lines

f::[Int] -> [String]
f (n:ls)
  | n == 0    = []
  | otherwise = [show rr] ++ (f rest)
     where (xs, rest) = splitAt n ls
           rr = sum xs
f _ = []

但它相对较慢。我用

分析了它
ghc -O2 --make test.hs -prof -auto-all -caf-all -fforce-recomp -rtsopts
time ./test +RTS -hc -p -i0.001 < input.in

其中input.in是第一行为100k的测试输入,后跟100k随机数,后跟0.我们可以在下图中看到它正在使用O(N)内存:

enter image description here

已编辑我原来的问题是比较两种类似的慢速方法。我已将其更新为与

下面的优化方法进行比较

现在,如果我迭代地进行求和,而不是调用sum,我会得到恒定的内存量

{-# LANGUAGE BangPatterns #-}

main = interact $ unlines . g . (map read) . lines

g::[Int] -> [String]
g (n:ls)
  | n == 0    = []
  | otherwise = g' n ls 0
g _ = []

g' n (l:ls) !cnt
  | n == 0 = [show cnt] ++ (g (l:ls))
  | otherwise = g' (n-1) ls (cnt + l)

enter image description here

我试图了解第一个例子中导致性能损失的原因。我猜想可以懒得评估一切吗?

2 个答案:

答案 0 :(得分:3)

我不确切地知道导致差异的原因。但我可以告诉你:

Data.Map> sum [1 .. 1e8]
Out of memory.

Data.Map> foldl' (+) 0 [1 .. 1e8]
5.00000005e15

出于某种原因,sum = foldl (+) 0,而不是foldl'(使用撇号)。不同之处在于后者的功能更严格,因此几乎不使用任何内存。相比之下,懒惰版本就是这样做的:

sum [1..100]
1 + sum [2..100]
1 + 2 + sum [3..100]
1 + 2 + 3 + sum [4.100]
...

换句话说,它创造了一个巨大的表达,表示1 + 2 + 3 + ......然后,在最后,它试图评估所有。嗯,显然,这会占用大量内存。使用foldl'代替foldl,您可以立即添加 ,而不是毫无意义地将它们存储在RAM中。

您可能还希望使用ByteString而非String进行I / O操作;但懒惰的差异可能会让你自己大大加快速度。

答案 1 :(得分:1)

我认为懒惰是阻止你的第一版和第二版同等的原因。

考虑从输入&#34;数字&#34;

创建的结果
1
garbage_here
2
3
5
0

第一个版本会给出一个结果列表[错误&#34; ...一些解析错误&#34;,8],您可以安全地查看第二个元素,而第二个版本会立即错误。似乎很难以流媒体方式实现第一个。

即使没有懒惰,从第一个版本到第二个版本可能比GHC可以处理的更多 - 它需要在元组的第一个元素{{1}上组合foldl/foldl'的融合重写规则}}。 GHC已经only recently达到了可以融合splitAt的程度。