Java for 循环优化
Java for-loop optimization
我用 java for 循环进行了一些运行时测试,发现了一个奇怪的行为。
对于我的代码,我需要原始类型(如 int、double 等)的包装器对象来模拟 io 和输出参数,但这不是重点。
只看我的代码。具有字段访问权限的对象如何比原始类型更快?
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 primitive(max: =10000.0): 110
MicroTime primitive(max: =100000.0): 1081
MicroTime primitive(max: =1000000.0): 2450
MicroTime primitive(max: =1.0E7): 28248
MicroTime primitive(max: =1.0E8): 276205
MicroTime primitive(max: =1.0E9): 2729824
MicroTime primitive(max: =1.0E10): 27547009
for
简单类型循环(包装器对象):
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 wrapper(max: =10000.0): 157
MicroTime wrapper(max: =100000.0): 1561
MicroTime wrapper(max: =1000000.0): 3174
MicroTime wrapper(max: =1.0E7): 15630
MicroTime wrapper(max: =1.0E8): 155471
MicroTime wrapper(max: =1.0E9): 1520967
MicroTime wrapper(max: =1.0E10): 15373311
迭代次数越多,第二个代码越快。但为什么?我知道 java-编译器和 jvm 正在优化我的代码,但我从没想过原始类型会比具有字段访问权限的对象慢。
有人对此有合理的解释吗?
已编辑:
H双class:
public class HDouble {
public double value;
public HDouble() {
}
public HDouble(double value) {
this.value = value;
}
@Override
public String toString() {
return String.valueOf(value);
}
}
我还用其中的代码测试了我的循环。例如,我计算总和 -> 相同的行为(差异不是很大,但我认为原始算法必须快得多?)。一开始我以为,计算时间那么长,字段访问几乎没有区别。
循环包装器:
for (i.value = 0; i.value <max.value; i.value++) {
sum.value = sum.value + i.value;
}
结果:
MicroTime wrapper(max: =10000.0): 243
MicroTime wrapper(max: =100000.0): 2805
MicroTime wrapper(max: =1000000.0): 3409
MicroTime wrapper(max: =1.0E7): 28104
MicroTime wrapper(max: =1.0E8): 278432
MicroTime wrapper(max: =1.0E9): 2678322
MicroTime wrapper(max: =1.0E10): 26665540
原始for循环:
for (i = 0; i < max; i++) {
sum = sum + i;
}
结果:
MicroTime primitive(max: =10000.0): 149
MicroTime primitive(max: =100000.0): 1996
MicroTime primitive(max: =1000000.0): 2289
MicroTime primitive(max: =1.0E7): 27085
MicroTime primitive(max: =1.0E8): 279939
MicroTime primitive(max: =1.0E9): 2759133
MicroTime primitive(max: =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 编译器 activity:
-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) 模式下编译的。在第二次迭代期间,这些方法在正常模式下重新编译。此后,从第三次迭代开始,primitive和wrapper在执行速度上没有区别。
您实际测量的是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
如您所见,'primitive' 情况对堆栈位置进行了多次加载和存储,而 'wrapper' 主要执行寄存器内操作。 OSR stub 指的是栈是很好理解的:在解释模式下,局部变量存储在栈中,OSR stub 与这个解释框架兼容。在 'wrapper' 的情况下,值存储在堆上,并且对对象的引用已经缓存在寄存器中。
我用 java for 循环进行了一些运行时测试,发现了一个奇怪的行为。 对于我的代码,我需要原始类型(如 int、double 等)的包装器对象来模拟 io 和输出参数,但这不是重点。 只看我的代码。具有字段访问权限的对象如何比原始类型更快?
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 primitive(max: =10000.0): 110
MicroTime primitive(max: =100000.0): 1081
MicroTime primitive(max: =1000000.0): 2450
MicroTime primitive(max: =1.0E7): 28248
MicroTime primitive(max: =1.0E8): 276205
MicroTime primitive(max: =1.0E9): 2729824
MicroTime primitive(max: =1.0E10): 27547009
for
简单类型循环(包装器对象):
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 wrapper(max: =10000.0): 157
MicroTime wrapper(max: =100000.0): 1561
MicroTime wrapper(max: =1000000.0): 3174
MicroTime wrapper(max: =1.0E7): 15630
MicroTime wrapper(max: =1.0E8): 155471
MicroTime wrapper(max: =1.0E9): 1520967
MicroTime wrapper(max: =1.0E10): 15373311
迭代次数越多,第二个代码越快。但为什么?我知道 java-编译器和 jvm 正在优化我的代码,但我从没想过原始类型会比具有字段访问权限的对象慢。
有人对此有合理的解释吗?
已编辑: H双class:
public class HDouble {
public double value;
public HDouble() {
}
public HDouble(double value) {
this.value = value;
}
@Override
public String toString() {
return String.valueOf(value);
}
}
我还用其中的代码测试了我的循环。例如,我计算总和 -> 相同的行为(差异不是很大,但我认为原始算法必须快得多?)。一开始我以为,计算时间那么长,字段访问几乎没有区别。
循环包装器:
for (i.value = 0; i.value <max.value; i.value++) {
sum.value = sum.value + i.value;
}
结果:
MicroTime wrapper(max: =10000.0): 243
MicroTime wrapper(max: =100000.0): 2805
MicroTime wrapper(max: =1000000.0): 3409
MicroTime wrapper(max: =1.0E7): 28104
MicroTime wrapper(max: =1.0E8): 278432
MicroTime wrapper(max: =1.0E9): 2678322
MicroTime wrapper(max: =1.0E10): 26665540
原始for循环:
for (i = 0; i < max; i++) {
sum = sum + i;
}
结果:
MicroTime primitive(max: =10000.0): 149
MicroTime primitive(max: =100000.0): 1996
MicroTime primitive(max: =1000000.0): 2289
MicroTime primitive(max: =1.0E7): 27085
MicroTime primitive(max: =1.0E8): 279939
MicroTime primitive(max: =1.0E9): 2759133
MicroTime primitive(max: =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 编译器 activity:
-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) 模式下编译的。在第二次迭代期间,这些方法在正常模式下重新编译。此后,从第三次迭代开始,primitive和wrapper在执行速度上没有区别。
您实际测量的是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
如您所见,'primitive' 情况对堆栈位置进行了多次加载和存储,而 'wrapper' 主要执行寄存器内操作。 OSR stub 指的是栈是很好理解的:在解释模式下,局部变量存储在栈中,OSR stub 与这个解释框架兼容。在 'wrapper' 的情况下,值存储在堆上,并且对对象的引用已经缓存在寄存器中。