Java for循环优化

我使用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原语(最大值:= 10000.0):110

MicroTime原语(最大值:= 100000.0):1081

MicroTime原语(最大值:=

1000000.0 ):2450 MicroTime原语(最大值:= 1.0E7):28248

MicroTime原语(最大值:= 1.0E8) :276205

MicroTime原语(最大值:= 1.0E9):2729824

MicroTime原语(最大值:= 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包装器(最大值:= 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-compiler和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 (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存根与此解释帧兼容。在“包装器”情况下,该值存储在堆中,并且对该对象的引用已缓存在寄存器中。

以上是 Java for循环优化 的全部内容, 来源链接: utcz.com/qa/410791.html

回到顶部