【问题标题】:Stack performance in programming languages编程语言中的堆栈性能
【发布时间】:2011-05-06 12:38:57
【问题描述】:

只是为了好玩,我尝试比较几种使用朴素递归算法计算斐波那契数列的编程语言的堆栈性能。代码在所有语言中基本相同,我将发布一个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 上发布了测试平台。如果您愿意,请随时对其进行添加:)

【问题讨论】:

  • 除了适用于基准测试的通常规则... (1) OCaml (native) 编译器 非常激进,在处理时不应比 C 慢六倍具有递归这样重要的FP概念。您是否使用了字节码解释器? (2) C 有哪些优化设置?
  • 您是否执行了多个样本?您是否删除了异常值?你平均成绩了吗?您测量的是时钟时间还是 CPU 时间?你有没有听说过统计? :-)
  • 令我惊讶的是java运行时间。我以前见过这个……在 C 和 Java 中做一个 Quicksort 方法,Java 每次都优于 C。
  • @paxdiablo:统计数据是在谎言和该死的谎言之后出​​现的,对吧? ;-)
  • @Nicholas:听起来有些可疑。很高兴看到您的 C 代码并知道您使用了哪些编译器和优化设置。

标签: java python c performance ocaml


【解决方案1】:

您可能希望提高 C 编译器的优化级别。使用gcc -O3,差别很大,从 2.015 秒下降到 0.766 秒,减少了大约 62%。

除此之外,您还需要确保已正确测试。您应该将每个程序运行 10 次,删除异常值(最快和最慢),然后平均其他 8 次。

此外,请确保您测量的是 CPU 时间而不是时钟时间。

除此之外,我不会考虑进行体面的统计分析,并且很可能会受到噪音的影响,从而使您的结果毫无用处。

不管怎样,上面的 C 次计时是七次运行,在平均之前取出异常值。


事实上,这个问题说明了在追求高性能时算法选择的重要性。尽管递归解决方案通常很优雅,但这个解决方案的缺点是您重复了 lot 的计算。迭代版本:

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.5*(Q3 - Q1) 时,这些值才是异常值...
  • 嗯,这是 one 定义,可能是一个很好的定义,但我不相信统计数据中有 rigid 定义。如果大多数数据点都在一个合理的范围内,那么使用简单的定义是没有问题的。 YMMV。
  • 如果您愿意,您可以对异常值使用不同的标准,但您必须使用标准。去掉最快和最慢绝对不是标准。
  • 如果您要挑选所使用的算法而不是基准,那么在不提及斐波那契数列存在封闭形式的情况下,任何挑剔都是不完整的。因此,有一个直接函数可以确定序列中的第 n 个数字,这是迄今为止的首选算法。
  • @user268396:实际上,鉴于返回类型是 Java int(32 位有符号),最好的方法是对每个值使用查找表。请参阅我对上一个问题的回答:stackoverflow.com/questions/3165293/fibonacci-sequence-in-c/…
【解决方案2】:

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

当我尝试为 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,我会得到与您的结果相似的时间(11 秒)。但是当然,为了运行速度比较,没有理由使用字节码编译器,除非您想对编译速度本身进行基准测试(ocamlc 编译速度惊人)。

【讨论】:

  • 很好,我不知道有两个ocaml编译器,实际上我使用了字节码一个..感谢您的解释!
  • @hoheinzollern 有四个!还有ocamlc.opt,使用ocamlopt 编译的字节码生成编译器,和ocamlopt.opt,使用自身编译的本机编译器。当然,这两个生成的代码与其各自的字节码编译版本相同。
  • @hoheinzollern - 您能否使用 ocamlopt 更新上述结果,以便我们进行更好的比较?
【解决方案3】:

一种可能性是 C 编译器正在优化第一个分支 (n &lt; 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 的全部优势......将外部调用置于循环中以获得一分钟左右的运行时间可能会很有趣。)

【讨论】:

  • 更改 c 代码中的分支顺序不会显着改变性能。肯定还有其他一些我现在想不出来的优化。
  • 另外,为什么 c# 实现慢两倍? (尝试使用 .net 我得到相同的结果,这不是单声道的错)
  • @hoheinzollem:不知道。请注意,这是一个很小的测试,很容易成为 Java 团队以一种方式进行权衡,.NET 团队以另一种方式进行权衡的事情,在这种特殊情况下,Java 方法效果很好。
  • 我刚刚在玩具基准测试(IDE 外部、发布模式、多次运行、桌面版本)上尝试了您的优化:C# 1.73s, Java .95s 和您的优化:C# 1.35s, Java .94s。一个适当包含缺陷但很好的小基准:)
  • @Jon - 这可能是 CLR 没有解释代码以获取跟踪信息的结果,因此猜测了错误的首选路径吗?我在某处读到 CLR 总是先编译。
【解决方案4】:

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

至于 Java 的性能,那是惊人的。 Java 击败 C 的情况很少见(即使 C 端的编译器优化很少)...... JIT 可能正在做的是记忆化或尾递归......

【讨论】:

  • 吹毛求疵:CPython 的递归限制默认为 1000,至少在常见的桌面架构上是这样。否则,是的。
  • 是的,递归对 Python 不利,尤其是在深入研究的时候。我决定尝试使用 Psyco(JIT 编译器)和 Cython。 Psyco 将时间从 75.6 秒降至 3.36 秒。使用 Cython,我能够将其降低到 1.27。如果时间真的与 OP 中的时间成正比,那么 Cython 代码将在他的机器上运行大约 1.79 秒。以我的标准来看,这相当不错,但也许时代不会那么好。
  • @delnan:它因操作系统和版本而异,但是是的,它出现在我现在使用的机器上是 1000。
  • 还需要花费时间计算每行开头的空格:)
  • @JeremyP:如果您真的相信这一点(这听起来像是在开玩笑,但人们永远无法确定...):,Python 不会被解释.它在执行前编译为字节码(并且所有被导入的字节码都缓存/写入磁盘,因此实际上它很少编译)。
【解决方案5】:

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

【讨论】:

  • 我很聪明,可以“记住”fib 的电话。我将它们替换为static const int fib[] = { 1, 1, 2, 3, ... };,它比int 范围内的代码中任何可能的实现都小。
  • 但是如果 JVM 使用滑动窗口或其他东西来记忆对 fib() 的最后 N 次调用,它将优于您的静态查找表。
  • @user268396:没有。R.. 只需要一个由 47 个斐波那契数组成的数组。第 48 位溢出一个 32 位整数。
【解决方案6】:

我编写了一个 C 版本的原始斐波那契函数,并将其编译为 gcc(4.3.2 Linux)中的汇编程序。然后我用 gcc -O3 编译它。

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

优化后的版本有 190 行长,而且(很难说,但)它似乎内联了至少对大约 5 的值的调用。

【讨论】:

    【解决方案7】:

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

    编辑 您至少需要指定 -O/-O1 才能进行递归内联,如果函数声明为内联也是如此。实际上,在测试自己时,我发现声明函数内联并使用-O 作为编译标志,或者仅使用-O -finline-functions,我的递归斐波那契代码比-O2-O2 -finline-functions 更快。

    【讨论】:

      【解决方案8】:

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

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

      【讨论】:

        猜你喜欢
        • 2016-02-29
        • 2015-12-30
        • 2019-02-23
        • 2012-04-05
        • 2012-11-19
        • 2023-01-03
        • 2013-11-05
        • 2013-05-17
        • 1970-01-01
        相关资源
        最近更新 更多