使用惰性列表的Haskell性能很差

时间:2011-12-14 05:49:54

标签: performance haskell

我试图测试Haskell性能,但得到了一些意想不到的糟糕结果:

-- main = do
--  putStrLn $ show $ sum' [1..1000000]

sum' :: [Int] -> Int
sum' [] = 0
sum' (x:xs) = x + sum' xs

我首先从ghci -O2

运行它
> :set +s
> :sum' [1..1000000]
1784293664
(4.81 secs, 163156700 bytes)

然后我将代码编译为ghc -O3,使用time运行并获得此代码:

1784293664

real    0m0.728s
user    0m0.700s
sys     0m0.016s

毋庸置疑,与C代码相比,这些结果非常糟糕:

#include <stdio.h>

int main(void)
{
    int i, n;
    n = 0;
    for (i = 1; i <= 1000000; ++i)
        n += i;
    printf("%d\n", n);
}

使用gcc -O3进行编译并使用time运行后,我得到了:

1784293664

real    0m0.022s
user    0m0.000s
sys     0m0.000s

这种糟糕表现的原因是什么?我假设Haskell永远不会真正构建列表,我错误的假设是什么?这是别的吗?

UPD:问题是Haskell不知道添加是关联的吗?有没有办法让它看到并使用它?

2 个答案:

答案 0 :(得分:11)

首先,当你谈论表现时,不要费心去讨论GHCi。使用GHCi的-Ox标志是无稽之谈。

你正在建立一个巨大的计算

使用GHC 7.2.2 x86-64和-O2我得到:

Stack space overflow: current size 8388608 bytes.
Use `+RTS -Ksize -RTS' to increase it.

这会占用如此多的堆栈空间的原因在于你构建i+...表达式的每个循环,所以你的计算变成了一个巨大的thunk:

n = 1 + (2 + (3 + (4 + ...

这将占用大量内存。标准sum未定义为sum'

是有原因的

sum

的合理定义

如果我将您的sum'更改为sum或等同于foldl' (+) 0,我会得到:

$  ghc -O2 -fllvm so.hs
$ time ./so
500000500000

real    0m0.049s

这对我来说似乎完全合理。请记住,使用如此短暂的代码,您测量的大部分时间都是噪音(加载二进制文件,启动RTS和GC托儿所,misc初始化等)。如果您想要对小型Haskell计算进行精确测量,请使用Criterion(基准测试工具)。

与C相比

我的gcc -O3时间是不可估量的低(报告为0.002秒),因为主程序包含4条指令 - 整个计算在编译时进行评估,常量0x746a5a2920存储在二进制中

有一个相当长的Haskell线程(here,但它是一个史诗般的火焰战争,在人们的思想中差不多3年后仍在燃烧)人们在GHC开始讨论这样做的现实你确切的基准 - 它还没有,但他们确实提出了一些模板Haskell工作,如果你想有选择地达到相同的结果,这将做到这一点。

答案 1 :(得分:3)

GHC优化器似乎没有做得那么好。尽管如此,您仍然可以使用尾递归和严格值来构建更好的sum'实现。

像(使用Bang模式):

sum' :: [Int] -> Int
sum' = sumt 0

sumt :: Int -> [Int] -> Int
sumt !n [] = n
sumt !n (x:xs) = sumt (n + x) xs

我没有测试过,但我敢打赌它会更接近c版本。

当然,你仍然坚持优化器去除列表。您可以使用与c中相同的算法(使用int i和goto):

sumToX x = sumToX' 0 1 x
sumToX' :: Int -> Int -> Int -> Int
sumToX' !n !i x = if (i <= x) then sumToX' (n+i) (i+1) x else n

你仍然希望GHC在命令级别上进行循环展开。

我还没有测试过这个,顺便说一句。

编辑:我想我应该指出sum [1..1000000]确实应该是500000500000并且因为整数溢出而只是1784293664。为什么你需要计算这个成为一个悬而未决的问题。无论如何,使用ghc -O2和一个没有爆炸模式的天真尾递归版本(这应该是标准库中的总和)让我

real    0m0.020s
user    0m0.015s
sys     0m0.003s

这让我觉得问题只是你的GHC。但是,似乎我的机器速度更快,因为c运行在

real    0m0.005s
user    0m0.001s
sys     0m0.002s

我的sumToX(有或没有爆炸模式)到达中途

real    0m0.010s
user    0m0.004s
sys     0m0.003s

编辑2:在反汇编代码之后,我认为我的答案为什么c仍然快两倍(如列表免费版本)是这样的:GHC在调用main之前有更多的开销。 GHC产生了相当多的运行时垃圾。显然这会在真实代码上分摊,但与GCC产生的美容相比:

0x0000000100000f00 <main+0>:    push   %rbp
0x0000000100000f01 <main+1>:    mov    %rsp,%rbp
0x0000000100000f04 <main+4>:    mov    $0x2,%eax
0x0000000100000f09 <main+9>:    mov    $0x1,%esi
0x0000000100000f0e <main+14>:   xchg   %ax,%ax
0x0000000100000f10 <main+16>:   add    %eax,%esi
0x0000000100000f12 <main+18>:   inc    %eax
0x0000000100000f14 <main+20>:   cmp    $0xf4241,%eax
0x0000000100000f19 <main+25>:   jne    0x100000f10 <main+16>
0x0000000100000f1b <main+27>:   lea    0x14(%rip),%rdi        # 0x100000f36
0x0000000100000f22 <main+34>:   xor    %eax,%eax
0x0000000100000f24 <main+36>:   leaveq 
0x0000000100000f25 <main+37>:   jmpq   0x100000f30 <dyld_stub_printf>

现在,我不是一个X86汇编程序员,但看起来或多或少完美。

好的,我有研究生院的申请表。没有了。