我遇到了this question,它比较了各种编译器在计算斐波那契数字方面的表现,这是天真的方式。
我尝试用Haskell做这个,看看它与C的比较。
C代码:
#include <stdio.h>
#include <stdlib.h>
int fib (int n) {
if (n < 2) return 1;
return fib (n-1) + fib (n-2);
}
int main (int argc, char* argv[]) {
printf ("%i\n", fib (atoi(argv[1])));
return 0;
}
结果:
> gcc -O3 main.c -o fib
> time ./fib 40
165580141
real 0m0.421s
user 0m0.420s
sys 0m0.000s
Haskell中:
module Main where
import System.Environment (getArgs)
fib :: Int -> Int
fib n | n < 2 = 1
| otherwise = fib (n-1) + fib (n-2)
main = getArgs >>= print . fib . read . head
结果:
> ghc -O3 -fllvm -optlo-O3 Main.hs -o fib
> time ./fib 40
165580141
real 0m1.476s
user 0m1.476s
sys 0m0.000s
使用
进行分析> ghc -O3 -fllvm -optlo-O3 -prof -auto-all -caf-all -rtsopts Main.hs -fforce-recomp -o fib
> ./fib 40 +RTS -prof
显示fib
需要100%的时间和分配,毫不奇怪。我拿了一些堆的配置文件,但不知道它们意味着什么:
> ./fib 40 +RTS -hc
> ./fib 40 +RTS -hd
所以我的问题:我可以做些什么来让我的Haskell程序的性能更接近于C,或者这就是GHC在这个微基准测试中做出的事情让它变得更慢的方式? (我不是要求渐近更快的算法来计算纤维。)
非常感谢。
[编辑]
事实证明,ghc -O3
在这种情况下比ghc -O3 -fllvm -optlo-O3
更快。但optlo-block-placement
为LLVM后端提供了可观察到的差异:
> ghc -O3 Main.hs -o fib -fforce-recomp
> time ./fib 40
165580141
real 0m1.283s
user 0m1.284s
sys 0m0.000s
> ghc -O3 -fllvm -optlo-O3 -o fib -fforce-recomp
> time ./fib 40
165580141
real 0m1.449s
user 0m1.448s
sys 0m0.000s
> ghc -O3 -fllvm -optlo-O3 -optlo-block-placement -o fib -fforce-recomp
> time ./fib 40
165580141
real 0m1.112s
user 0m1.096s
sys 0m0.016s
我想调查这个的原因是因为C和OCaml在这个程序中明显快于Haskell。我有点不能接受,并希望了解更多,以确保我已经尽我所能:D
> ocamlopt main.ml -o fib
> time ./fib 40
165580141
real 0m0.668s
user 0m0.660s
sys 0m0.008s
答案 0 :(得分:9)
堆配置文件在这里不是很有趣,因为GHC将fib
编译成一个在堆栈上运行soleley的函数。只需查看配置文件...只分配了800个字节,这是main
实现的小开销。
就GHC的核心级而言,这实际上已经尽可能地得到优化。低级代码生成是另一回事。让我们快速浏览一下GHC生成的代码:
_Main_zdwfib_info:
.Lc1RK:
leal -8(%ebp),%eax
cmpl 84(%ebx),%eax
jb .Lc1RM
这是对堆栈空间的检查。可能是C不需要的东西,因为它可以让操作系统处理堆栈空间分配。 Haskell具有用户级线程,因此可以手动管理堆栈空间。
cmpl $2,0(%ebp)
jl .Lc1RO
与代码中的2进行比较。
movl 0(%ebp),%eax
decl %eax
从堆栈重新加载参数并递减以获取递归调用的参数。重新加载可能是不必要的 - 不过确定它会有所不同。
movl %eax,-8(%ebp)
movl $_s1Qp_info,-4(%ebp)
addl $-8,%ebp
jmp _Main_zdwfib_info
参数和返回地址被推到堆栈顶部,我们直接跳转到标签以便递归。
.Lc1RO:
movl $1,%esi
addl $4,%ebp
jmp *0(%ebp)
参数小于2的情况的代码。返回值在寄存器中传递。
底线:一切似乎都应该如此运作,你不可能通过改变程序来挤出更多。自定义堆栈检查是一个明显的减速源,但不确定它是否可以归咎于全时差异。
答案 1 :(得分:6)
这些似乎是barsoap
所说的非常微弱的“基准”。假设我比较以下几乎同样天真的程序:
module Main where
import System.Environment (getArgs)
fib :: Int -> Int
fib 0 = 1
fib 1 = 1
fib 2 = 2
fib n = (\x y -> x + y + y ) (fib (n-3)) (fib (n-2) )
main = getArgs >>= print . fib . read . head
......在另一个角落......
#include <stdio.h>
#include <stdlib.h>
int fib (int n) {
if (n < 2) return 1;
if (n < 3) return n;
return (fib (n-3) + fib (n-2)) + fib (n-2);
}
int main (int argc, char* argv[]) {
printf ("%i\n", fib (atoi(argv[1])));
return 0;
}
然后,光荣的ghc
粉碎了gcc
,并不太令人惊讶,真的:
$ ghc --make -fforce-recomp fib.hs -o fibh
[1 of 1] Compiling Main ( fib.hs, fib.o )
Linking fibh ...
$ time ./fibh 40
165580141
real 0m0.013s
user 0m0.007s
sys 0m0.003s
$ gcc fib.c -o fibc
$ time ./fibc 40
165580141
real 0m1.495s
user 0m1.483s
sys 0m0.003s
现在开启优化,ghc
加快了速度:
$ ghc --make -fforce-recomp fib.hs -O3 -o fibhO
$ time ./fibhO 40
165580141
real 0m0.007s
user 0m0.002s
sys 0m0.004s
但gcc
终于得到了线索。
$ gcc fib.c -O3 -o fibcO
$ time ./fibcO 40
165580141
real 0m0.007s
user 0m0.004s
sys 0m0.002s
我认为解释是ghc
对共同子表达式消除的谨慎态度:'(几乎)所有东西都是表达式'是危险的,并且它表明程序员知道如何使用lambda。
答案 2 :(得分:3)
GHC编制此罚款。下一步是微观优化GHC后端的输出。使用各种LLVM标志可以在这里提供帮助。
为此,请使用ghc-core检查程序集,并尝试其他标志到LLVM以查看您获得的内容。
另一种方法是添加少量的并行性。
答案 3 :(得分:1)
试试这个:
fibs :: [Integer]
fibs = 0:1:zipWith (+) fibs (tail fibs)
fib n = fibs !! n
$ time ./fib 10000
33644[...]6875
./fib 10000 0.03s user 0.01s system 89% cpu 0.039 total
(这是一个好的赛道Athlon64 3200 +)
您使用的版本是,对于每个n
,计算fib (n-1)
和fib (n-2)
,即具有大致三角形性质的复杂性。上面的版本是线性的:每个fib只计算一次。尽管非Haskell编程hivemind似乎认为,Haskell并没有automatically memoise(总的来说,这比一般的动态编程要慢)。
the Haskell Wiki上有更快(使用数学技巧)的fibonnaci版本。
将C版本更改为非递归,我的赌注是你会看到Haskell和C的性能非常相似。紧密循环更容易优化。