手工制作的微基准测试很容易被愚弄 – 你永远不知道他们实际测量的是什么.这就是为什么有像
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
Wrapper: 1.381 s
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 (on-stack replacement)模式下编译的.在第二次迭代期间,方法在正常模式下重新编译.从那时起,从第三次迭代开始,原语和包装器在执行速度上没有区别.
您实际测量的是OSR存根的性能.它通常与应用程序的真实性能无关,您不应该太在意它.
但问题仍然存在,为什么包装器的OSR存根编译得比原始变量好?要找到这个,我们需要了解生成的汇编代码:
-XX:CompileOnly = Test -XX:UnlockDiagnosticVMOptions -XX:PrintAssembly
我将省略所有不相关的代码,只留下编译循环.
原始:
0x00000000023e90d0: vmovsd 0x28(%rsp),%xmm1
0x00000000023e90d6: vaddsd -0x7e(%rip),%xmm1,%xmm1
0x00000000023e90de: test %eax,-0x21f90e4(%rip)
0x00000000023e90e4: vmovsd %xmm1,0x28(%rsp)
0x00000000023e90ea: vucomisd 0x28(%rsp),%xmm0
0x00000000023e90f0: ja 0x00000000023e90d0
包装:
0x00000000023ebe90: vaddsd -0x78(%rip),%xmm0,%xmm0
0x00000000023ebe98: vmovsd %xmm0,0x10(%rbx)
0x00000000023ebe9d: test %eax,-0x21fbea3(%rip)
0x00000000023ebea3: vucomisd %xmm0,%xmm1
0x00000000023ebea7: ja 0x00000000023ebe90
正如您所看到的,“原始”情况会在堆栈位置进行大量加载和存储,而“包装器”主要进行寄存器操作. OSR存根引用堆栈的原因是可以理解的:在解释模式下,局部变量存储在堆栈中,并且OSR存根与此解释帧兼容.在“包装器”的情况下,值存储在堆上,对象的引用已经缓存在寄存器中.