int steps = 256 * 1024 * 1024;
int[] a = new int[2];
// Loop 1
for (int i=0; i<steps; i++) { a[0]++; a[0]++; }
// Loop 2
for (int i=0; i<steps; i++) { a[0]++; a[1]++; }
有人可以解释为什么第二个循环比第一个循环慢20倍(19毫秒vs 232毫秒)?
这就是我计时的方式:
long start_time = System.currentTimeMillis();
// Loop
long end_time = System.currentTimeMillis();
System.out.println(end_time - start_time);
答案 0 :(得分:9)
JIT编译器正在将第一个循环转换为乘法,但不是非常优化第二个循环。
两个循环的字节码基本相同(您可以使用javap -c test.class
查看)。
在Java中,字节码由JIT编译器转换为x86指令,JIT编译器能够执行其他优化。
如果您有hsdis插件,您实际上可以通过java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly ...
查看JIT生成的程序集。
我将添加到每个元素的值更改为0xbad,以便更容易发现相关代码并将循环计数器更改为long
。
第一个循环产生:
mov r11d,dword ptr [r13+10h] Load from memory a[0]
...
add r11d,175ah Add 2 * 0xbad to the value
mov dword ptr [r13+10h],r11d Store to memory a[0]
第二个循环产生:
mov ebx,dword ptr [rax+10h] Load from memory a[0]
add ebx,0badh Add 0xbad
...
mov dword ptr [rax+10h],ebx Store to memory
...
mov ebx,dword ptr [rax+14h] Load from memory a[1]
add ebx,0badh Add 0xbad
...
mov dword ptr [rax+14h],ebx Store to memory a[1]
因此您可以看到编译器已经能够将第一个循环优化为更少的指令。
特别是,它发现同一个数组元素的两个加法可以合并为一次加值两次。
当我将循环计数器更改回int
时,我注意到编译器在第一次循环时设法做得更好:
mov r10d,dword ptr [r14+10h]
imul ecx,r13d,175ah This line converts lots of adds of 0xbad into a single multiply
mov r11d,r10d
sub r11d,ecx
add r10d,175ah
mov dword ptr [r14+10h],r10d
在这种情况下,它发现它可以通过使用乘法实际上在一次传递中实现循环的多次迭代!这解释了第一个循环如何比第二个循环快一个数量级。