Java for循环优化

我用java for循环做了一些运行时测试,并发现了一个奇怪的行为。 对于我的代码,我需要原始类型的包装器对象,如int,double等,以模拟io和输出参数,但这不是重点。 只需看我的代码。 具有字段访问权限的对象如何比原始类型更快?

具有prtimitive类型的for循环:

 public static void main(String[] args) { double max = 1000; for (int j = 1; j < 8; j++) { double i; max = max * 10; long start = System.nanoTime(); for (i = 0; i < max; i++) { } long end = System.nanoTime(); long microseconds = (end - start) / 1000; System.out.println("MicroTime primitive(max: ="+max + "): " + microseconds); } } 

结果:

MicroTime原语(最大值:= 10000.0):110
MicroTime原语(最大值:= 100000.0):1081
MicroTime原语(最大值:= 1000000.0):2450
MicroTime原语(max:= 1.0E7):28248
MicroTime原语(最大值:= 1.0E8):276205
MicroTime原语(max:= 1.0E9):2729824
MicroTime原语(最大值:= 1.0E10):27547009

for loop with simple type(wrapper object):

 public static void main(String[] args) { HDouble max = new HDouble(); max.value = 1000; for (int j = 1; j < 8; j++) { HDouble i = new HDouble(); max.value = max.value*10; long start = System.nanoTime(); for (i.value = 0; i.value <max.value; i.value++) { } long end = System.nanoTime(); long microseconds = (end - start) / 1000; System.out.println("MicroTime wrapper(max: ="+max.value + "): " + microseconds); } } 

结果:

MicroTime包装器(最大值:= 10000.0):157
MicroTime包装器(最大值:= 100000.0):1561
MicroTime包装器(最大值:= 1000000.0):3174
MicroTime包装器(最大值:= 1.0E7):15630
MicroTime包装器(最大值:= 1.0E8):155471
MicroTime包装器(最大值:= 1.0E9):1520967
MicroTime包装器(最大值:= 1.0E10):15373311

迭代次数越多,第二个代码就越快。 但为什么? 我知道java编译器和jvm正在优化我的代码,但我从未想过原始类型比具有字段访问权限的对象更慢。
有人有合理的解释吗?

编辑:HDouble类:

 public class HDouble { public double value; public HDouble() { } public HDouble(double value) { this.value = value; } @Override public String toString() { return String.valueOf(value); } } 

我还用代码测试了我的循环。 例如,我计算总和 – >相同的行为(差异不是那么大,但我认为原始算法必须更快?)。 首先我想,计算需要那么长时间,现场访问几乎没有差别。

包装for循环:

 for (i.value = 0; i.value <max.value; i.value++) { sum.value = sum.value + i.value; } 

结果:

MicroTime包装器(最大值:= 10000.0):243
MicroTime包装器(最大:= 100000.0):2805
MicroTime包装器(最大值:= 1000000.0):3409
MicroTime包装器(最大值:= 1.0E7):28104
MicroTime包装器(最大值:= 1.0E8):278432
MicroTime包装器(最大值:= 1.0E9):2678322
MicroTime包装器(最大值:= 1.0E10):26665540

原始for循环:

 for (i = 0; i < max; i++) { sum = sum + i; } 

结果:

MicroTime原语(最大值:= 10000.0):149
MicroTime原语(最大值:= 100000.0):1996
MicroTime原语(最大值:= 1000000.0):2289
MicroTime原语(最大值:= 1.0E7):27085
MicroTime原语(最大值:= 1.0E8):279939
MicroTime原语(最大值:= 1.0E9):2759133
MicroTime原语(最大值:= 1.0E10):27369724

手工制作的微基准测试很容易被愚弄 – 你永远不知道他们实际测量的是什么。 这就是为什么有像JMH这样的特殊工具。 但是让我们分析一下原始手工制作基准会发生什么:

 static class HDouble { double value; } public static void main(String[] args) { primitive(); wrapper(); } public static void primitive() { long start = System.nanoTime(); for (double d = 0; d < 1000000000; d++) { } long end = System.nanoTime(); System.out.printf("Primitive: %.3f s\n", (end - start) / 1e9); } public static void wrapper() { HDouble d = new HDouble(); long start = System.nanoTime(); for (d.value = 0; d.value < 1000000000; d.value++) { } long end = System.nanoTime(); System.out.printf("Wrapper: %.3f s\n", (end - start) / 1e9); } 

结果与您的结果有些相似:

 Primitive: 3.618 s Wrapper: 1.380 s 

现在重复几次测试:

 public static void main(String[] args) { for (int i = 0; i < 5; i++) { primitive(); wrapper(); } } 

它变得更有趣:

 Primitive: 3.661 s Wrapper: 1.382 s Primitive: 3.461 s Wrapper: 1.380 s Primitive: 1.376 s <-- starting from 3rd iteration Wrapper: 1.381 s <-- the timings become equal Primitive: 1.371 s Wrapper: 1.372 s Primitive: 1.379 s Wrapper: 1.378 s 

看起来两种方法最终都得到了优化。 再次运行它,现在使用日志记录JIT编译器活动: -XX:-TieredCompilation -XX:CompileOnly=Test -XX:+PrintCompilation

  136 1 % Test::primitive @ 6 (53 bytes) 3725 1 % Test::primitive @ -2 (53 bytes) made not entrant Primitive: 3.589 s 3748 2 % Test::wrapper @ 17 (73 bytes) 5122 2 % Test::wrapper @ -2 (73 bytes) made not entrant Wrapper: 1.374 s 5122 3 Test::primitive (53 bytes) 5124 4 % Test::primitive @ 6 (53 bytes) Primitive: 3.421 s 8544 5 Test::wrapper (73 bytes) 8547 6 % Test::wrapper @ 17 (73 bytes) Wrapper: 1.378 s Primitive: 1.372 s Wrapper: 1.375 s Primitive: 1.378 s Wrapper: 1.373 s Primitive: 1.375 s Wrapper: 1.378 s 

注意第一次迭代时编译日志中的%符号。 这意味着这些方法是在OSR (堆栈内替换)模式下编译的。 在第二次迭代期间,方法在正常模式下重新编译。 从那时起,从第三次迭代开始,原语和包装器在执行速度上没有区别。

您实际测量的是OSR存根的性能。 它通常与应用程序的真实性能无关,您不应该太在意它。

但问题仍然存在,为什么包装器的OSR存根编译得比原始变量好? 要找到这个,我们需要了解生成的汇编代码:
-XX:CompileOnly=Test -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly

我将省略所有不相关的代码,只留下编译循环。

原始:

 0x00000000023e90d0: vmovsd 0x28(%rsp),%xmm1 <-- load double from the stack 0x00000000023e90d6: vaddsd -0x7e(%rip),%xmm1,%xmm1 0x00000000023e90de: test %eax,-0x21f90e4(%rip) 0x00000000023e90e4: vmovsd %xmm1,0x28(%rsp) <-- store to the stack 0x00000000023e90ea: vucomisd 0x28(%rsp),%xmm0 <-- compare with the stack value 0x00000000023e90f0: ja 0x00000000023e90d0 

包装:

 0x00000000023ebe90: vaddsd -0x78(%rip),%xmm0,%xmm0 0x00000000023ebe98: vmovsd %xmm0,0x10(%rbx) <-- store to the object field 0x00000000023ebe9d: test %eax,-0x21fbea3(%rip) 0x00000000023ebea3: vucomisd %xmm0,%xmm1 <-- compare registers 0x00000000023ebea7: ja 0x00000000023ebe90 

正如您所看到的,“原始”情况会在堆栈位置进行大量加载和存储,而“包装器”主要进行寄存器操作。 OSR存根引用堆栈的原因是可以理解的:在解释模式下,局部变量存储在堆栈中,并且OSR存根与此解释帧兼容。 在“包装器”的情况下,值存储在堆上,对象的引用已经缓存在寄存器中。