Java迭代与递归

Java迭代与递归,java,recursion,Java,Recursion,有人能解释一下为什么下面的递归方法比迭代方法快(两者都是字符串连接)?迭代方法不应该打败递归方法吗?另外,每次递归调用都会在堆栈顶部添加一个新层,这可能会导致空间效率低下 private static void string_concat(StringBuilder sb, int count){ if(count >= 9999) return; string_concat(sb.append(count), count+1); }

有人能解释一下为什么下面的递归方法比迭代方法快(两者都是字符串连接)?迭代方法不应该打败递归方法吗?另外,每次递归调用都会在堆栈顶部添加一个新层,这可能会导致空间效率低下

    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);

    }
私有静态无效字符串\u concat(StringBuilder sb,int count){
如果(计数>=9999)返回;
字符串concat(sb.append(count),count+1);
}
公共静态void main(字符串[]arg){
长s=System.currentTimeMillis();
StringBuilder sb=新的StringBuilder();
对于(int i=0;i<9999;i++){
某人(i);
}
System.out.println(System.currentTimeMillis()-s);
s=系统.currentTimeMillis();
字符串_concat(新的StringBuilder(),0);
System.out.println(System.currentTimeMillis()-s);
}
我多次运行该程序,递归程序的速度总是比迭代程序快3-4倍。导致迭代速度变慢的主要原因是什么?

请参阅


确保您了解如何正确使用微基准。您应该对这两种方法的多次迭代进行计时,并将其平均化。除此之外,您应该确保VM不会因为不编译第一个而给第二个带来不公平的优势


事实上,默认的热点编译阈值(可通过
-XX:CompileThreshold
配置)是10000次调用,这可能解释了您在这里看到的结果。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);
  }
}
这是我的结果

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

基本上,您的基准测试的问题在于,它在许多试验()中并不平均,而且它高度依赖于各个基准测试的顺序。我得到的最初结果是:

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

现在,这里发生的有趣的事情是,默认的热点JIT编译阈值是10000次调用。在编译
append
之前,您的迭代基准很可能大部分时间都在执行。另一方面,您的递归方法应该相对较快,因为它在编译后很可能更喜欢
append
。为了避免影响结果,我传递了
-XX:CompileThreshold=0
,并找到了


所以,归根结底,它们的速度大致相等。但是,请注意,如果以更高的精度进行平均,则迭代似乎要快一点。顺序可能也会对我的基准测试产生影响,因为后一个基准测试的优点是虚拟机为其动态优化收集了更多的统计数据。

请确保您了解如何正确使用微基准测试。您应该对这两种方法的多次迭代进行计时,并将其平均化。除此之外,您应该确保虚拟机不会因为不编译第一个而给第二个带来不公平的优势。此外,更改它们的顺序,在循环中重复整个测试至少五次(放弃前两次进行预热),并使用System.nanoTime事实上,默认的热点编译阈值(可通过
-XX:CompileThreshold
配置)是10000次调用,这可能解释了您在这里看到的重用。HotSpot实际上没有进行任何尾部优化,因此递归解决方案速度更快是很奇怪的。尝试反转递归方法和迭代方法的位置。您将看到观察结果的相反:)您的递归方法更快,只是因为在运行迭代方法时JVM还没有预热。第2段的意思是什么?递归有时可能会更快,甚至与预热后的迭代方法结合起来?@user1389813您观察到递归解决方案更快,因为默认情况下,HotSpot将在10000次调用后将方法编译为本机代码。如果你交换订单,你很可能会看到相反的结果。
C:\dev\scrap>java AppendMicrobenchmark
recursive: 405.41us
iterative: 313.20us

C:\dev\scrap>java -server AppendMicrobenchmark
recursive: 397.43us
iterative: 312.14us
C:\dev\scrap>java StringBuilderBenchmark
80
41
C:\dev\scrap>java -XX:CompileThreshold=0 StringBuilderBenchmark
8
8