容易被手工制作的微基准所迷惑-您永远不知道它们的 实际
测量值。这就是为什么有诸如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 sn", (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 sn", (end - start) / 1e9);}结果有点类似于您的结果:
Primitive: 3.618 sWrapper: 1.380 s
现在重复测试几次:
public static void main(String[] args) { for (int i = 0; i < 5; i++) { primitive(); wrapper(); }}它变得更加有趣:
Primitive: 3.661 sWrapper: 1.382 sPrimitive: 3.461 sWrapper: 1.380 sPrimitive: 1.376 s <-- starting from 3rd iterationWrapper: 1.381 s <-- the timings become equalPrimitive: 1.371 sWrapper: 1.372 sPrimitive: 1.379 sWrapper: 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 entrantPrimitive: 3.589 s 3748 2 %Test::wrapper @ 17 (73 bytes) 5122 2 %Test::wrapper @ -2 (73 bytes) made not entrantWrapper: 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 sPrimitive: 1.372 sWrapper: 1.375 sPrimitive: 1.378 sWrapper: 1.373 sPrimitive: 1.375 sWrapper: 1.378 s
注意
%在第一次迭代时登录编译日志。这意味着这些方法是在OSR
(堆栈替换)模式下编译的。在第二次迭代中,方法以正常模式重新编译。从那时起,从第三次迭代开始,原语和包装器之间的执行速度就没有区别。
您实际测量的是OSR存根的性能。它通常与应用程序的实际性能无关,因此您不必在意它。
但是问题仍然存在,为什么包装的OSR存根比原始变量的编译好?为了找出答案,我们需要深入研究生成的汇编代码:
-XX:Compileonly=Test -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
我将忽略所有不相关的代码,仅保留编译循环。
原始:
0x00000000023e90d0: vmovsd 0x28(%rsp),%xmm1 <-- load double from the stack0x00000000023e90d6: vaddsd -0x7e(%rip),%xmm1,%xmm10x00000000023e90de: test %eax,-0x21f90e4(%rip)0x00000000023e90e4: vmovsd %xmm1,0x28(%rsp) <-- store to the stack0x00000000023e90ea: vucomisd 0x28(%rsp),%xmm0 <-- compare with the stack value0x00000000023e90f0: ja 0x00000000023e90d0
包装器:
0x00000000023ebe90: vaddsd -0x78(%rip),%xmm0,%xmm00x00000000023ebe98: vmovsd %xmm0,0x10(%rbx) <-- store to the object field0x00000000023ebe9d: test %eax,-0x21fbea3(%rip)0x00000000023ebea3: vucomisd %xmm0,%xmm1 <-- compare registers0x00000000023ebea7: ja 0x00000000023ebe90
如您所见,“原始”情况会产生许多负载并存储到堆栈位置,而“包装器”主要执行寄存器内操作。为什么OSR存根引用堆栈是完全可以理解的:在解释模式下,局部变量存储在堆栈中,并使OSR存根与此解释帧兼容。在“包装器”情况下,该值存储在堆中,并且对该对象的引用已缓存在寄存器中。



