Java for循环优化

4

我进行了一些Java运行时测试,发现了一种奇怪的行为。对于我的代码,我需要使用包装器对象来模拟原始类型(如int,double等)的输入和输出参数,但这不是重点。看看我的代码。如何使具有字段访问的对象比原始类型更快?

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编译器和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

该文本展示了使用不同的最大值来测试MicroTime原始值的结果。

我建议在循环中放置一些实时代码。在真实的程序甚至是严肃的基准测试中,空循环很少出现,因此为它们进行优化可能不太一致或经过充分测试。 - Patricia Shanahan
你能展示一下 HDouble 的代码吗? - M A
我编辑了我的帖子,并添加了Hdouble的代码 ;) - user3490546
2
这类测试的问题在于它们不是非常准确。您正在测量某种JVM性能,但不一定是代码在优化后的比较情况,因为这只有在运行时才会发生。https://dev59.com/hHRB5IYBdhLWcg3wz6UK 此外,测试之间的系统负载也可能会产生影响,因此您需要重复测试。最终运行时间较长的测试可能具有意义。尽管JVM经常重写代码,以至于很难确定它做了什么以及为什么这样做。 - zapl
@tibtof:你关闭了JIT编译器吗?当我关闭它时,我得到的结果几乎相同。 100亿次迭代:包装器(300秒),原始(132秒) - user3490546
显示剩余3条评论
1个回答

11

很容易被手工制作的微基准测试所欺骗——你永远不知道它们实际上测量了什么。这就是为什么有像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(on-stack replacement)模式下编译的。在第二次迭代中,方法以正常模式重新编译。从第三次迭代开始,原始类型和包装类型的执行速度没有区别。

实际上,你所测量的是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

你可以看到,“primitive”情况下,需要进行多次加载和存储到栈位置,而“wrapper”主要是寄存器操作。很容易理解为什么OSR存根会引用堆栈:在解释模式下,局部变量存储在堆栈上,并且OSR存根与此解释帧兼容。在“wrapper”情况下,值存储在堆上,并且对象的引用已经缓存在寄存器中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接