很容易被手工制作的微基准愚弄 - 你永远不知道它们是什么actually措施。这就是为什么有像这样的特殊工具JMH http://openjdk.java.net/projects/code-tools/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
Note %
在第一次迭代时登录编译日志。这意味着这些方法是在 OSR 中编译的(堆栈上替换) https://stackoverflow.com/questions/9105505/differences-between-just-in-time-compilation-and-on-stack-replacement模式。在第二次迭代期间,方法在正常模式下重新编译。此后,从第三次迭代开始,primitive 和wrapper 在执行速度上就没有区别了。
您实际测量的是 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
Wrapper:
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 存根与此解释框架兼容。在“包装器”情况下,值存储在堆上,并且对对象的引用已缓存在寄存器中。