Java直接数组索引访问和for循环访问的性能差异

3

我正在尝试使用谓词。我试图实现在分布式系统中序列化问题的谓词。我写了一个简单的示例,其中测试函数只返回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的基准测试结果。

  • 使用for循环访问的时间为4567毫秒

  • 直接访问的时间为297毫秒


好的,在第一段代码中,你有一个内部循环,它总是在外部循环的每次迭代中迭代3次。在第二段代码中,没有这样的循环存在,整个过程可以立即短路,而循环本身则不能。 - Crusha K. Rool
为了让事情更具可比性,我们可以将第一段代码中内部循环的条件更改为!result && j < test.length,并初始化boolean result = false以使其有同样的短路机会。 - Crusha K. Rool
3
在我看来,你只是遇到了一个糟糕的微基准测试,并且优化器正在做一些事情来消除第二个片段中大部分的工作。 - user2357112
1
将一种方法放入名为foo()的方法中,将另一种方法放入名为bar()的方法中,然后从您的main方法中交替调用这些方法,可能会调用20次。对我来说,在几次调用之后,这些方法将花费完全相同的时间。(从java -server ...开始可能有所帮助)。第一个方法可能会更复杂一些,这可能是JIT稍后启动的原因,但最终没有区别。 - Marco13
2
一个小提示:这不是一个合适的微基准测试。在两种情况下,result变量都没有被使用,编译器基本上可以消除整个程序。而第一种方法中result变量被读取和写入,但在第二种方法中只被写入,这可能是优化器更容易发现整个代码实际上是无用的另一个原因... - Marco13
3个回答

1

你在使用微基准测试时犯了几乎所有的基本错误。

  • 你没有确保代码不能被优化掉,要确保实际上使用了计算结果。
  • 你的两个代码分支逻辑有微妙但明显的不同 (第二个例子由于test()返回一个常数而更容易被JIT优化)
  • 你没有热身代码,会将JIT优化时间包含在执行时间中的某个地方。
  • 你的测试代码没有考虑到测试用例的执行顺序会影响测试结果。使用相同的数据和对象先运行case 1,再运行case 2是不公平的。当case 2运行时,JIT会已经优化测试方法并收集其行为的运行时统计信息(以case 1的执行时间为代价)。

为什么变量2会短路?它们都是真的,需要进行评估吗?我有什么遗漏吗? - Mustafa
1
@Mustafa 我的陈述是不正确的(可能是从评论中得到的想法),但效果是真实的:由于您从test()返回一个常量,所以在JIT内联后整个行表达式变成了一个常量。 - Durandal

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
虽然这是正确的,但我不确定它是否值得加1:关键问题是(或可能是)为什么在某种情况下它能够比其他情况更好地优化原始代码。 (我在注释中猜测了一些,现在没有时间给出正式答案) - Marco13
我必须承认,是我的直觉引导我得出了上面的答案。例如,比较一下这篇文章:https://dev59.com/CGsz5IYBdhLWcg3wn5RP。被接受的答案的作者认为(类似的)优化是由JIT编译器完成的。根本原因绝对不是额外循环。 - Trinimon

0

如果循环头在第一种解决方案中需要执行一个单位时间,则循环头评估需要3N个单位时间。而在直接访问中只需要N个单位时间。

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

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


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