Java迭代与递归

时间:2012-09-05 03:28:53

标签: java recursion

任何人都可以解释为什么以下递归方法比迭代方法更快(两者都在进行字符串连接)?是不是迭代的方法想要打败递归的?加上每个递归调用在堆栈顶部添加一个新层,这可能是非常低效的空间。

    private static void string_concat(StringBuilder sb, int count){
        if(count >= 9999) return;
        string_concat(sb.append(count), count+1);
    }
    public static void main(String [] arg){

        long s = System.currentTimeMillis();
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i < 9999; i++){
            sb.append(i);
        }
        System.out.println(System.currentTimeMillis()-s);
        s = System.currentTimeMillis();
        string_concat(new StringBuilder(),0);
        System.out.println(System.currentTimeMillis()-s);

    }

我多次运行程序,递归的程序总是比迭代程序快3-4倍。可能导致迭代速度变慢的主要原因是什么?

1 个答案:

答案 0 :(得分:8)

请参阅my comments

  

确保您了解如何正确地进行微基准测试。您应该计算两次迭代的时间并对这些时间进行平均。除此之外,你应该确保VM没有通过不编译第一个来给第二个不公平的优势。

     

实际上,默认的HotSpot编译阈值(可通过-XX:CompileThreshold配置)是10,000次调用,这可能会解释您在此处看到的结果。 HotSpot并没有真正进行任何尾部优化,因此递归解决方案更快是很奇怪的。将StringBuilder.append编译为本机代码主要用于递归解决方案是非常合理的。

我决定重写基准并亲自查看结果。

public final class AppendMicrobenchmark {

  static void recursive(final StringBuilder builder, final int n) {
    if (n > 0) {
      recursive(builder.append(n), n - 1);
    }
  }

  static void iterative(final StringBuilder builder) {
    for (int i = 10000; i >= 0; --i) {
      builder.append(i);
    }
  }

  public static void main(final String[] argv) {
    /* warm-up */
    for (int i = 200000; i >= 0; --i) {
      new StringBuilder().append(i);
    }

    /* recursive benchmark */
    long start = System.nanoTime();
    for (int i = 1000; i >= 0; --i) {
      recursive(new StringBuilder(), 10000);
    }
    System.out.printf("recursive: %.2fus\n", (System.nanoTime() - start) / 1000000D);

    /* iterative benchmark */
    start = System.nanoTime();
    for (int i = 1000; i >= 0; --i) {
      iterative(new StringBuilder());
    }
    System.out.printf("iterative: %.2fus\n", (System.nanoTime() - start) / 1000000D);
  }
}

以下是我的结果......

C:\dev\scrap>java AppendMicrobenchmark
recursive: 405.41us
iterative: 313.20us

C:\dev\scrap>java -server AppendMicrobenchmark
recursive: 397.43us
iterative: 312.14us

这些是平均超过1000次试验的每种方法的时间。

基本上,您的基准测试的问题在于它不会在多次试验中平均(law of large numbers),并且它高度依赖于各个基准的排序。我给你的原始结果是:

C:\dev\scrap>java StringBuilderBenchmark
80
41

这对我来说没什么意义。 HotSpot VM上的递归很可能不会像迭代那样快,因为它还没有实现您可能用于函数式语言的任何尾部优化。

现在,这里发生的有趣事情是默认的HotSpot JIT编译阈值是10,000次调用。在编译 append之前,您的迭代基准测试很可能会在大部分中执行。另一方面,您的递归方法应该相对较快,因为在编译之后,它很可能会享受append 。为了消除这种影响结果,我通过了-XX:CompileThreshold=0并找到了......

C:\dev\scrap>java -XX:CompileThreshold=0 StringBuilderBenchmark
8
8

所以,当它归结为它时,它们的速度大致相等。但请注意,如果平均值具有更高的精度,则迭代似乎会更快一些。订单可能仍会对我的基准测试产生影响,因为后者基准测试将具有VM为其动态优化收集更多统计信息的优势。