我试图测试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不知道添加是关联的吗?有没有办法让它看到并使用它?
答案 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汇编程序员,但看起来或多或少完美。
好的,我有研究生院的申请表。没有了。