当前位置: 首页 > 知识库问答 >
问题:

Java for-loop 优化

齐昊焱
2023-03-14

我用java为循环做了一些运行时测试,发现了一个奇怪的行为。对于我的代码,我需要像int、Double等原始类型的包装对象来模拟io和输出参数,但这不是重点。看看我的代码。具有字段访问权限的对象怎么会比原始类型更快?

对于带压缩型的循环:

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原语(max:=10000.0):110
MicroTime原语(max:=100000.0):1081
MicroTime原语(max:=100000.0):2450
MicroTime原语(max:=1.0E7):28248
MicroTime原语(max:=1.0E8):276205
MicroTime原语(max:=1.0E9):2729824
MicroTime原语(max:=1.0E10):27547009

用于具有简单类型(包装对象)的循环:

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包装器(max:=10000.0):157
MicroTime包装器(max:=100000.0):1561
MicroTime包装器(max:=100000.0):3174
MicroTime包装器(max:=1.0E7):15630
MicroTime包装器(max:=1.0E8):155471
MicroTime包装器(max:=1.0E9):1520967
MicroTime包装器(max:=1.0E10):15373311

迭代越多,第二个代码越快。但是为什么呢?我知道java编译器和jvm正在优化我的代码,但是我从来没有想过原始类型会比具有字段访问权限的对象慢。
有人对此有合理的解释吗?

编辑:h双类:

public class HDouble {
    public double value;

    public HDouble() {
    }

    public HDouble(double value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }
}

我还用其中的代码测试了我的循环。例如,我计算总和-

包装器for循环:

for (i.value = 0; i.value <max.value; i.value++) {
    sum.value = sum.value + i.value;
}

结果:

微时间包装器(最大: =10000.0):243
微时间包装器(最大: =100000.0): 2805
微时间包装器(最大: =1000000.0): 3409
微时间包装器(最大: =1.0E7): 28104
微时间包装器(最大: =1.0E8): 278432
微时间包装器(最大: =1.0E9): 2678322
微时间包装器(最大: =1.0E10): 26665540

原始for循环:

for (i = 0; i < max; i++) {
    sum = sum + i;
}

结果:

微时间基元(最大: =10000.0):149
微时间基元(最大: =100000.0):1996
微时间基元(最大值: =1000000.0): 2289
微时间基元(最大: =1.0E7): 27085
微时间基元(最大: =1.0E8): 279939
微时间基元(最大值: =1.0E9): 2759133
微时间基元(最大: =1.0E10): 27369724

共有1个答案

东方飞捷
2023-03-14

很容易被手工制作的微量印记所愚弄 - 你永远不知道它们实际测量的是什么。这就是为什么有像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:-分层编译 -XX:编译唯一=测试 -XX: 打印编译

    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

注< code>%在第一次迭代时登录编译日志。这意味着这些方法是在OSR(栈上替换)模式下编译的。在第二次迭代中,这些方法在正常模式下被重新编译。从那以后,从第三次迭代开始,原语和包装器在执行速度上没有区别。

您实际测量的是 OSR 存根的性能。它通常与应用程序的实际性能无关,您不应该太关心它。

但是问题仍然存在,为什么包装器的OSR存根比原始变量编译得更好?为了找出这一点,我们需要着手生成汇编代码:< br > < code >-XX:compile only = Test-XX:UnlockDiagnosticVMOptions-XX:print assembly

我将省略所有不相关的代码,只留下编译的循环。

原始:

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 存根与此解释的帧兼容。在“包装器”情况下,值存储在堆上,并且对对象的引用已缓存在寄存器中。

 类似资料:
  • loop结构是LISP提供的最简单的迭代形式。 最简单的形式它允许你重复执行一些语句,直到找到一个return语句。 它具有以下语法 - (loop (s-expressions)) 例子 (Example) 创建一个名为main.lisp的新源代码文件,并在其中键入以下代码。 (setq a 10) (loop (setq a (+ a 1)) (write a) (ter

  • Loop 是基于 JVM 的编程语言,提供了函数编程特性,简单有趣。 Loop 强调可读性、简洁、清晰,侧重于性能和并发性。 示例代码: greet(name) -> "hi, @{name}!"'Zaphod Beeblebrox'.greet()

  • JavaScript提供了完全控制来处理循环和switch语句。 可能存在这样一种情况,即您需要在没有到达底部的情况下退出循环。 当您想跳过代码块的一部分并开始循环的下一次迭代时,可能还会出现这种情况。 为了处理所有这些情况,JavaScript提供了break和continue语句。 这些语句用于立即从任何循环中出来或分别开始任何循环的下一次迭代。 休息声明 使用switch语句简要介绍的bre

  • 构造循环允许您实现像其他语言中最常见的for循环之类的迭代。 它允许你 为迭代设置变量 指定将有条件地终止迭代的表达式 指定用于在每次迭代中执行某些作业的表达式 指定表达式,以及在退出循环之前执行某些工作的表达式 构造的for循环遵循几种语法 - (loop for loop-variable in <a list> do (action) ) (loop for loop-variable

  • 实现无限循环滚动的选择器(picker)。比如,循环显示0、1、2、3、4、0、1、2、3、4、0... [Code4App.com]

  • 问题内容: 通过一些旧的公司代码,我遇到了一个如下所示的for循环: 我尝试了Google,但找不到任何答案。我是在编程课上睡着了还是这是一个不寻常的循环? 问题答案: 一个for在Java循环结构如下- for (initialization statement; condition check; update) loop body; 如你所见,这里有四个语句- 初始化语句:第一次进入循环时,该