繁体   English   中英

编程语言中的堆栈性能

[英]Stack performance in programming languages

只是为了好玩,我试图比较使用朴素递归算法计算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上发布了测试平台。 如果你想要随意添加它:)

您可能希望提高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;
}

这再次缩短了时间,但我怀疑我们在这里达到了收益递减的程度。

你对你的配置说的很少(在基准测试中,细节就是一切:命令行,使用的计算机,......)

当我尝试为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对于编译速度ocamlc是惊人的)。

一种可能性是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的全部好处...将外部调用放在一个循环中以获得一分钟左右的运行可能会很有趣。)

我可以解释一下Python的性能。 Python的递归性能充其量是极其糟糕的,并且应该避免像瘟疫一样编码。 特别是因为默认情况下堆栈溢出只发生在1000的递归深度...

至于Java的表现,这太棒了。 Java击败C很少见(即使C端的编译器优化很少)...... JIT可能正在做的是memoization或tail recursion ......

请注意,如果Java Hotspot VM足够智能以记忆fib()调用,它可以将算法的指数成本降低到更接近线性成本的值。

使用C,您应该声明fibonacci函数“inline”,或者使用gcc将-finline-functions参数添加到编译选项中。 这将允许编译器执行递归内联。 这也是为什么使用-O3可以获得更好的性能,它激活-finline-functions

编辑您需要至少指定-O / -O1来进行递归内联,如果函数是内联声明的话。 实际上,测试自己我发现声明函数内联并使用-O作为编译标志,或者只使用-O -finline-functions ,我的递归fibonacci代码比使用-O2-O2 -finline-functions更快。

我编写了一个天真的Fibonacci函数的C版本,并将其编译为gcc(4.3.2 Linux)中的汇编程序。 然后我用gcc -O3编译它。

未经优化的版本长34行,看起来像是C代码的直接翻译。

优化版本长达190行(很难说但是)它似乎至少要求最多约5个值的调用。

您可以尝试的一个C技巧是禁用堆栈检查(即内置代码,确保堆栈足够大,以允许额外分配当前函数的局部变量)。 这对于递归函数来说可能是冒险的,并且实际上可能是C次缓慢的原因:执行程序可能已经耗尽了堆栈空间,这迫使堆栈检查在实际运行期间多次重新分配整个堆栈。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM