任何人都可以解释为什么以下递归方法比迭代方法更快(两者都在进行字符串连接)?是不是迭代的方法想要打败递归的?加上每个递归调用在堆栈顶部添加一个新层,这可能是非常低效的空间。
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倍。可能导致迭代速度变慢的主要原因是什么?
答案 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为其动态优化收集更多统计信息的优势。