为了好玩,我尝试比较使用朴素递归算法计算Fibonacci系列的几种编程语言的堆栈性能。代码在所有语言中都是相同的,我将发布一个java版本:
public class Fib {
public static int fib(int n) {
if (n < 2) return 1;
return fib(n-1) + fib(n-2);
}
public static void main(String[] args) {
System.out.println(fib(Integer.valueOf(args[0])));
}
}
好的关键是使用输入40的算法我得到了这些时间:
C: 2.796s
Ocaml: 2.372s
Python: 106.407s
Java: 1.336s
C#(mono): 2.956s
使用官方存储库中可用的每种语言版本,在双核英特尔计算机上,将它们放入Ubuntu 10.04框中。
我知道像ocaml这样的函数式语言会因为将函数视为一阶公民而减速,并且解释CPython的运行时间没有问题,因为它是这个测试中唯一的解释语言,但我印象深刻java运行时间是同一算法的一半c!你会把它归结为JIT编译吗?
您如何解释这些结果?
编辑:谢谢你的回复!我认识到这不是一个合适的基准(从来没有说过:P),也许我可以做一个更好的基准,并根据我们讨论的内容,下次发布给你:)编辑2:我使用优化编译器ocamlopt更新了ocaml实现的运行时。我还在https://github.com/hoheinzollern/fib-test发布了测试平台。如果您愿意,可以随意添加:)
答案 0 :(得分:17)
您可能希望提高C编译器的优化级别。使用gcc -O3
,这会产生很大的影响,从2.015秒降至0.766秒,减少约62%。
除此之外,您需要确保已经正确测试。你应该运行每个程序十次,删除异常值(最快和最慢),然后平均其他八个。
此外,请确保您正在测量CPU时间而不是时钟时间。
除此之外,我不会考虑一个不错的统计分析,它可能会受到噪音的影响,使你的结果变得毫无用处。
对于它的价值,上面的那些C时间是七次运行,并且在平均之前取出异常值。
事实上,这个问题显示了在针对高性能时算法选择的重要性。虽然递归解决方案通常很优雅,但是这个解决方案会遇到复制批次计算的错误。迭代版本:
int fib(unsigned int n) {
int t, a, b;
if (n < 2) return 1;
a = b = 1;
while (n-- >= 2) {
t = a + b;
a = b;
b = t;
}
return b;
}
进一步缩短所需的时间,从0.766秒减少到0.078秒,进一步减少89%,并且高达从原始代码减少96%。
并且,作为最后的尝试,您应该尝试以下内容,它将查找表与超出某一点的计算结合起来:
static int fib(unsigned int n) {
static int lookup[] = {
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377,
610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657,
46368, 75025, 121393, 196418, 317811, 514229, 832040,
1346269, 2178309, 3524578, 5702887, 9227465, 14930352,
24157817, 39088169, 63245986, 102334155, 165580141 };
int t, a, b;
if (n < sizeof(lookup)/sizeof(*lookup))
return lookup[n];
a = lookup[sizeof(lookup)/sizeof(*lookup)-2];
b = lookup[sizeof(lookup)/sizeof(*lookup)-1];
while (n-- >= sizeof(lookup)/sizeof(*lookup)) {
t = a + b;
a = b;
b = t;
}
return b;
}
这又缩短了时间,但我怀疑我们在这里的收益正在减少。
答案 1 :(得分:11)
你对你的配置说的很少(在基准测试中,细节就是一切:命令行,使用的计算机,......)
当我尝试为OCaml重现时,我得到:
let rec f n = if n < 2 then 1 else (f (n-1)) + (f (n-2))
let () = Format.printf "%d@." (f 40)
$ ocamlopt fib.ml
$ time ./a.out
165580141
real 0m1.643s
这是在2.66GHz的Intel Xeon 5150(Core 2)上。另一方面,如果我使用字节码OCaml编译器ocamlc
,我会得到类似于结果的时间(11s)。但是,当然,对于运行速度比较,没有理由使用字节码编译器,除非你想对编译本身的速度进行基准测试(ocamlc
对于编译速度来说是惊人的。)
答案 2 :(得分:4)
一种可能性是C编译器正在优化猜测第一个分支(n < 2
)是更频繁采用的分支。它必须纯粹在编译时才这样做:猜测并坚持下去。
Hotspot可以运行代码,查看实际更频繁发生的事情,并根据该数据重新优化。
你可能能够通过颠倒if
的逻辑来看到差异:
public static int fib(int n) {
if (n >= 2) return fib(n-1) + fib(n-2);
return 1;
}
无论如何,值得一试:)
与往常一样,请检查所有平台的优化设置。显然,C和Java上的编译器设置尝试使用Hotspot的客户端版本与服务器版本。 (请注意,您需要运行超过一秒左右才能真正获得Hotspot的全部好处...将外部调用放在一个循环中以获得一分钟左右的运行可能会很有趣。)
答案 3 :(得分:4)
我可以解释Python的性能。 Python的递归性能充其量是极其糟糕的,并且应该避免像瘟疫一样编码。特别是因为默认情况下堆栈溢出在递归深度仅为1000 ...
对于Java的表现,这太棒了。 Java击败C很少见(即使C端的编译器优化很少)...... JIT可能正在做的是memoization或尾递归......
答案 4 :(得分:2)
请注意,如果Java Hotspot VM足够智能以便记住fib()调用,它可以将算法的指数成本降低到更接近线性成本的值。
答案 5 :(得分:1)
使用C,您应该声明fibonacci函数“inline”,或者使用gcc将-finline-functions
参数添加到编译选项中。这将允许编译器执行递归内联。这也是为什么使用-O3可以获得更好的性能,它会激活-finline-functions
。
编辑您需要至少指定-O / -O1来进行递归内联,如果函数是内联声明的话。实际上,测试自己我发现内联函数声明并使用-O
作为编译标记,或者只使用-O -finline-functions
,我的递归斐波纳契代码比-O2
或-O2 -finline-functions
更快
答案 6 :(得分:1)
我编写了一个天真的Fibonacci函数的C版本,并将其编译为gcc(4.3.2 Linux)中的汇编程序。然后我用gcc -O3编译它。
未经优化的版本长度为34行,看起来像是C代码的直接翻译。
优化版本的长度为190行(很难说但是)它似乎至少要求最多约5个值的调用。
答案 7 :(得分:0)
你可以尝试的一个C技巧是禁用堆栈检查(内置代码,确保堆栈足够大,允许额外分配当前函数的局部变量)。这对于递归函数来说可能是冒险的,事实上可能是C次缓慢的原因:执行程序可能已经耗尽了堆栈空间,这迫使堆栈检查在实际运行期间多次重新分配整个堆栈。 / p>
尝试近似所需的堆栈大小,并强制链接器分配那么多的堆栈空间。然后禁用堆栈检查并重新生成程序。