Java函数的递归版本在第一次调用时比迭代版本慢,但在第二次调用后更快。为什么会这样?
对于一项作业,我目前正试图衡量矩阵链问题的迭代解和递归解之间的性能(空间/时间)差异 我在迭代版本中使用的问题和解决方案的要点可以在这里找到: 我通过两个函数运行给定的输入10次,测量每个函数的空间和时间性能。非常有趣的是,尽管递归解决方案在第一次调用时运行得比迭代解决方案慢得多,但在连续调用时,它的性能要好得多,速度要快得多。这些函数不使用任何类全局变量,只使用一个来计算内存使用量。为什么会发生这种情况?这是编译器正在做的事情还是我遗漏了一些明显的东西 注:我知道我测量记忆的方法是错误的,计划改变它 Main:初始化数组并将其传递给运行函数Java函数的递归版本在第一次调用时比迭代版本慢,但在第二次调用后更快。为什么会这样?,java,performance,recursion,jvm,jit,Java,Performance,Recursion,Jvm,Jit,对于一项作业,我目前正试图衡量矩阵链问题的迭代解和递归解之间的性能(空间/时间)差异 我在迭代版本中使用的问题和解决方案的要点可以在这里找到: 我通过两个函数运行给定的输入10次,测量每个函数的空间和时间性能。非常有趣的是,尽管递归解决方案在第一次调用时运行得比迭代解决方案慢得多,但在连续调用时,它的性能要好得多,速度要快得多。这些函数不使用任何类全局变量,只使用一个来计算内存使用量。为什么会发生这种情况?这是编译器正在做的事情还是我遗漏了一些明显的东西 注:我知道我测量记忆的方法是错误的,计划
public static void main(String[] args) {
int s[] = new int[] {30,35,15,5,10,100,25,56,78,55,23};
runFunctions(s, 15);
}
runFunctions:运行两个函数2*n次,测量空间和时间,最后打印结果
private static void runFunctions(int[]arr , int n){
final Runtime rt = Runtime.getRuntime();
long iterativeTime[] = new long [n],
iterativeSpace[] = new long [n],
recursiveSpace[] = new long [n],
recursiveTime[] = new long [n];
long startTime, stopTime, elapsedTime, res1, res2;
for (int i = 0; i <n; i++){
System.out.println("Measuring Running Time");
//measure running time of iterative
startTime = System.nanoTime();
res1 = solveIterative(arr, false);
stopTime = System.nanoTime();
elapsedTime = stopTime - startTime;
iterativeTime[i] = elapsedTime;
//measure running time of recursive
startTime = System.nanoTime();
res2 = solveRecursive(arr, false);
stopTime = System.nanoTime();
elapsedTime = stopTime - startTime;
recursiveTime[i] = elapsedTime;
System.out.println("Measuring Space");
//measure space usage of iterative
rt.gc();
res1 = solveIterative(arr, true);
iterativeSpace[i] = memoryUsage;
//measure space usage of recursive
rt.gc();
res2 = solveRecursive(arr, true);
recursiveSpace[i] = memoryUsage;
rt.gc();
if (res1 != res2){
System.out.println("Error! Results do not match! Iterative Result: " + res1 + " Recursive Result: " + res2);
}
}
System.out.println("Time Iterative: " + Arrays.toString(iterativeTime));
System.out.println("Time Recursive: " + Arrays.toString(recursiveTime));
System.out.println("Space Iterative: " + Arrays.toString(iterativeSpace));
System.out.println("Space Recursive: " + Arrays.toString(recursiveSpace));
}
doRecursion:递归地求解函数
private static int doRecursion(int i, int j, int[][] m, int s[]){
if (m[i][j] != 0){
return m[i][j];
}
if (i == j){
return 0;
}
else
{
m[i][j] = Integer.MAX_VALUE / 3;
for (int k = i; k <= j - 1; k++){
int q = doRecursion(i, k, m, s) + doRecursion(k + 1, j, m, s) + (s[i] * s[k + 1] * s[j + 1]);
if (q < m[i][j]){
m[i][j] = q;
}
}
}
return m[i][j];
}
Java代码在您更多地使用它之后变得更快,这绝对是100%正常的。这或多或少就是JIT编译器的全部要点——在运行时优化使用率越来越高的代码。问题在于测试运行时间太短。JIT没有足够的时间来充分优化方法。
试着重复测试至少200次(而不是15次),你会看到不同之处
请注意,JIT编译不会只发生一次。当JVM收集更多的运行时统计信息时,可以多次重新编译方法。您遇到的情况是,solveRecursive
比solveIterative
经受了更多级别的优化
我已经描述了JIT如何决定编译一个方法。基本上有两个主要的编译触发器:方法调用阈值和后缘阈值(即循环迭代计数器) 请注意,这两个方法具有不同的编译触发器:
执行更多调用=>当达到调用阈值时编译它李>solveRecursive
运行更多循环=>当达到后缘阈值时编译它solveIterative
solveIterative
得到优化,它就开始表现得更好
还有一个技巧可以使
solveIterative
更早编译:将(k=i;k移动到一个单独的方法。是的,这听起来很奇怪,但是JIT在编译几个小方法时比编译一个大方法要好。更小的方法更容易理解和优化,不仅适用于人类,也适用于计算机:)欢迎来到动态优化、JIT编译Java运行时的世界。它可能不仅仅是JIT编译。这可能是一些其他昂贵的启动开销,如类加载。如果关闭JIT编译(使用VM arg-Djava.compiler=NONE
),您可能会得到更一致的并行比较。不管怎样,您的第一次迭代都可能是一个异常值。有趣的是,它是一个一致的异常值,每次我运行它时,递归调用都非常可怕,但在连续调用上比迭代版本要好得多。有趣的是,它比迭代版本改进了很多。谢谢你关于禁用JIT的提示,我会确保做到这一点,以确保准确性。这取决于你所说的“准确度”是什么意思。如果你想衡量哪一个在实际的程序使用中会更快,JIT应该是开启的。没有JIT的测量将得到一致的结果,但不一定是有意义的结果。然而,这并不能回答为什么solveRecursive
最终比solveIterative
更快的问题(这有点违反直觉)。JIT编译应该使这两种方法更快。什么比什么更好并不完全可以预测。为了确保代码是用-XX:+printcomilation
编译的,测试应该运行足够长的时间,以便您看到所有方法都已编译+1.
private static int doRecursion(int i, int j, int[][] m, int s[]){
if (m[i][j] != 0){
return m[i][j];
}
if (i == j){
return 0;
}
else
{
m[i][j] = Integer.MAX_VALUE / 3;
for (int k = i; k <= j - 1; k++){
int q = doRecursion(i, k, m, s) + doRecursion(k + 1, j, m, s) + (s[i] * s[k + 1] * s[j + 1]);
if (q < m[i][j]){
m[i][j] = q;
}
}
}
return m[i][j];
}
private static int solveIterative(int[] s, boolean measureMemory) {
memoryUsage = 0;
maxMemory = 0;
int n = s.length - 1;
int i = 0, j = 0, k= 0, v = 0;
int[][] m = new int[n][n];
for (int len = 2; len <= n; len++) {
for (i = 0; i + len <= n; i++) {
j = i + len - 1;
m[i][j] = Integer.MAX_VALUE;
for (k = i; k < j; k++) {
v = m[i][k] + m[k + 1][j] + s[i] * s[k + 1] * s[j + 1];
if (m[i][j] > v) {
m[i][j] = v;
}
}
}
}
if (measureMemory){
memoryUsage += MemoryUtil.deepMemoryUsageOf(n);
memoryUsage += MemoryUtil.deepMemoryUsageOf(m);
memoryUsage += MemoryUtil.deepMemoryUsageOf(i);
memoryUsage += MemoryUtil.deepMemoryUsageOf(j);
memoryUsage += MemoryUtil.deepMemoryUsageOf(k);
memoryUsage += MemoryUtil.deepMemoryUsageOf(v);
memoryUsage += MemoryUtil.deepMemoryUsageOf(s);
System.out.println("Memory Used: " + memoryUsage);
}
return m[0][n - 1];
}
Time Iterative: [35605, 12039, 20492, 17674, 17674, 12295, 11782, 19467, 16906, 18442, 21004, 19980, 18955, 12039, 13832]
Time Recursive: [79918, 4611, 8453, 6916, 6660, 6660, 4354, 6916, 18699, 7428, 13576, 5635, 4867, 3330, 3586]
Space Iterative: [760, 760, 760, 760, 760, 760, 760, 760, 760, 760, 760, 760, 760, 760, 760]
Space Recursive: [712, 712, 712, 712, 712, 712, 712, 712, 712, 712, 712, 712, 712, 712, 712]