编程语言中的堆栈性能

时间:2010-11-08 06:45:44

标签: java python c performance ocaml

为了好玩,我尝试比较使用朴素递归算法计算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发布了测试平台。如果您愿意,可以随意添加:)

8 个答案:

答案 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>

尝试近似所需的堆栈大小,并强制链接器分配那么多的堆栈空间。然后禁用堆栈检查并重新生成程序。