【发布时间】:2011-10-06 16:13:34
【问题描述】:
我遇到了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
哈斯克尔:
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 的做法,碰巧让它在这个微基准测试中变慢? (我不是在要求一种渐近更快的算法来计算 fibs。)
非常感谢。
[编辑]
在这种情况下,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
【问题讨论】:
-
gcc -O3可能会将其变成非递归循环。我不知道 GHC 是否可以做到这一点。 -
哦,真的吗? Fib 不是尾递归的。它甚至不是线性递归的。怎么会变成循环呢?
-
@Po:参见stackoverflow.com/questions/405770/why-are-compilers-so-stupid/… 的示例,使用(也不是尾递归)阶乘,原理相同。你不会是第一个低估 C 编译器的人。您可以使用
-S制作 gcc 输出程序集,以检查它是否对其进行了优化(我会检查自己,但我无法轻松访问 Unix 机器 ATM)。 -
@delnan:我明白了。对于命令式语言编译器,
-O3的这种行为非常令人印象深刻。但是回到这个程序,这个fib函数怎么可能变成非递归的呢?您有 2 个递归调用,最终未决添加是不可避免的。并且原作者注意在运行时提供参数,因此编译器无法从您提供的链接执行像gcc -O3中那样的常量传播。 -
@Po:该常量与移除递归无关(继续,自己尝试一下 - 或者考虑一下 gcc 甚至没有费心在
-O2上传播它的事实,但是仍然生成循环)。虽然这两个递归调用似乎使它更难,但我只是看看而不是假设。同样,您不会是第一个低估编译器的人……
标签: performance haskell ghc micro-optimization microbenchmark