【问题标题】:Performance difference between Java direct array index access vs. for loop accessJava 直接数组索引访问与 for 循环访问之间的性能差异
【发布时间】:2016-11-29 05:24:47
【问题描述】:

我正在试验谓词。我试图实现用于在分布式系统中序列化问题的谓词。我写了一个简单的例子,其中测试函数只返回 true。我正在测量开销,我偶然发现了这个有趣的问题。与直接访问相比,在 for 循环中访问数组要慢 10 倍。

class Test {
    public boolean test(Object o) { return true; }
}

long count = 1000000000l;
Test[] test = new Test[3];
test[0] = new Test();
test[1] = new Test();
test[2] = new Test();
long milliseconds = System.currentTimeMillis();
for(int i = 0; i < count; i++){
    boolean result = true;
    Object object = new Object();
    for(int j = 0; j < test.length; j++){
        result = result && test[j].test(object);
    }
}
System.out.println((System.currentTimeMillis() - milliseconds));

但是,下面的代码几乎快了 10 倍。可以是什么 原因?

milliseconds = System.currentTimeMillis();
for(int i=0 ; i < count; i++) {
    Object object = new Object();
    boolean result = test[0].test(object) && test[1].test(object) && test[2].test(object);
}
System.out.println((System.currentTimeMillis() - milliseconds));

在我的 i5 上进行基准测试。

  • 4567 毫秒 for 循环访问

  • 297 毫秒用于直接访问

【问题讨论】:

  • 好吧,在第一段代码中,您有一个内部循环,每次外部循环迭代总是迭代 3 次。在第二段代码中,不存在这样的循环,整个事情可以立即短路,而循环本身不能。
  • 只是为了让事情更具可比性,如何将第一段代码中的内循环条件更改为 !result &amp;&amp; j &lt; test.length 并初始化 boolean result = false 以具有相同的短路机会?
  • 在我看来,你刚刚获得了一个糟糕的微基准,优化器正在做一些事情来消除第二个 sn-p 中的大部分工作。
  • 将一种方法放入名为foo()的方法中,将另一种方法放入称为bar()的方法中,然后调用这些方法,可能有20种类型,交替,来自您的 main 方法。对我来说,在几次调用之后,这些方法将花费完全相同的时间。 (以java -server ... 开头可能会有所帮助)。第一种方法可能是一个更复杂的标记,这可能是JIT稍后启动的原因,但最终没有区别。
  • 附注:这不是一个合适的微基准。在这两种情况下,result 变量都没有使用,编译器可以基本上消除了整个程序。 result 变量在第一种方法中是读取和写入,但在第二种方法中只写入这一事实可能是优化器更容易的另一个原因找出整个代码实际上是无用的......

标签: java arrays performance-testing benchmarking


【解决方案1】:

由于test(Object o) 的可预测结果,编译器能够非常有效地优化第二段代码。第一段代码中的第二个循环使这种优化变得不可能。

将结果与以下Test 类进行比较:

static class Test {
    public boolean test(Object o) {
        return Math.random() > 0.5;
    }
}  

...和循环:

    long count = 100000000l;
    Test[] test = new Test[3];
    test[0] = new Test();
    test[1] = new Test();
    test[2] = new Test();

    long milliseconds = System.currentTimeMillis();

    for(int i = 0; i < count; i++){
        boolean result = true;
        Object object = new Object();
        for(int j = 0; j < test.length; j++){
            result = result && test[j].test(object);
        }
    }

    System.out.println((System.currentTimeMillis() - milliseconds));
    milliseconds = System.currentTimeMillis();

    for(int i=0 ; i < count; i++) {
        Object object = new Object();
        boolean result = test[0].test(object) && test[1].test(object) && test[2].test(object);
    }

    System.out.println((System.currentTimeMillis() - milliseconds));

现在两个循环需要几乎相同的时间:

run:
3759
3368
BUILD SUCCESSFUL (total time: 7 seconds)

p.s.:请查看this article,了解有关 JIT 编译器优化的更多信息。

【讨论】:

  • 虽然这是真的,但我不确定它是否值得 +1:关键问题是(或可能是)为什么它在一种情况下比在另一种情况下更好地优化原始代码。 (我在cmets中做了一些猜测,现在没有时间正确回答)
  • 我不得不承认,是我的直觉引导我得出上述答案。例如比较这篇文章:stackoverflow.com/questions/7772864/…。接受答案的作者假设(类似的)优化是由 JIT 编译器完成的。根本原因绝对不是额外的循环。
【解决方案2】:

您几乎犯了使用微基准测试可能犯的所有基本错误。

  • 您并不能通过确保实际使用计算结果来确保无法优化代码。
  • 你的两个代码分支有微妙但明显不同的逻辑(正如指出的变体二总是短路)。由于 test() 返回一个常量,第二种情况更容易针对 JIT 进行优化。
  • 您没有预热代码,导致 JIT 优化时间被包含在某处到执行时间中
  • 您的测试代码没有考虑对测试结果产生影响的测试用例的执行顺序。它公平地运行案例 1,然后使用相同的数据和对象运行案例 2。当案例 2 运行优化测试方法并收集有关其行为的运行时统计信息时,JIT 将(以案例 1 的执行时间为代价)。

【讨论】:

  • 为什么变体 2 会短路?所有这些都是真实的,需要评估吗?我错过了什么吗?
  • @Mustafa 我的说法不正确(肯定是从评论中得到的想法),但效果是真实的:由于您从test()返回一个常量,因此内联后整个行表达式变为常量通过 JIT。
【解决方案3】:

如果循环标头需要一个单位时间来执行第一个解决方案中的循环标头评估需要 3N 个单位时间。在直接访问时需要 N.

除了第一个解决方案中的循环头开销 3 && 每次迭代评估的条件,而在第二个解决方案中只有 2 个。

最后但并非最不重要的布尔短路评估会导致您的第二个更快的示例“过早”停止测试条件,即如果第一个 && 条件结果为假,则整个结果评估为假。

【讨论】:

    猜你喜欢
    • 2010-11-13
    • 2016-01-23
    • 1970-01-01
    • 2021-09-08
    • 2016-03-26
    • 2021-04-07
    • 2016-03-24
    • 1970-01-01
    相关资源
    最近更新 更多