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

Java直接数组索引访问与for循环访问的性能差异,java,arrays,performance-testing,benchmarking,Java,Arrays,Performance Testing,Benchmarking,我在试验谓词。我尝试在分布式系统中实现序列化问题的谓词。我写了一个简单的例子,其中test函数只返回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] = n

我在试验谓词。我尝试在分布式系统中实现序列化问题的谓词。我写了一个简单的例子,其中test函数只返回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));
类测试{
公共布尔测试(对象o){return true;}
}
长计数=100000000升;
试验[]试验=新试验[3];
测试[0]=新测试();
测试[1]=新测试();
测试[2]=新测试();
长毫秒=System.currentTimeMillis();
for(int i=0;i
但是,下面的代码几乎快了10倍。可能是什么 原因

毫秒=System.currentTimeMillis();
for(int i=0;i
i5上的基准测试结果

  • 环路访问为4567毫秒

  • 297毫秒用于直接访问


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

除了第一个解决方案中的循环头开销之外,每个迭代需要计算3个条件,而第二个解决方案中只有2个条件


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

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

将结果与以下
测试
等级进行比较:

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));

p.s.:查看有关JIT编译器优化的更多信息。

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

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

好的,在第一段代码中有一个内循环,它总是在外循环的每次迭代中迭代3次。在第二段代码中,不存在这样的循环,整个循环可以立即短路,而循环本身不能。为了使事情更具可比性,将第一段代码中的内循环条件更改为
!result&&j
和初始化
boolean result=false
以获得相同的短路机会?在我看来,您刚刚得到了一个坏的微基准,优化器正在做一些事情来消除第二个代码段中的大部分工作。将一种方法放入名为
foo()
的方法中,另一个方法是调用名为
bar()
的方法,然后从
main
方法中交替调用这些方法,可能是20种类型。对我来说,在几个调用之后,这些方法将花费完全相同的时间。(从
java-server开始…
可能会有所帮助)。第一种方法可能是一个更复杂的标记,这可能是JIT稍后启动的原因,但最终没有区别。在这两种情况下,不使用
结果
变量,编译器基本上可以消除整个程序。
result
变量在第一种方法中读取和写入,但仅在第二种方法中写入,这一事实可能是优化器更容易发现整个代码实际上毫无用处的另一个原因……虽然这是事实,但我不确定它是否有价值:关键问题是(或可能是)为什么它能够在一种情况下比在另一种情况下更好地优化原始代码。(我在评论中做了一些猜测,现在没有时间给出正确的答案)我不得不承认,是我的直觉引导我找到了上面的答案。比较此帖子的实例:。接受答案的作者假设(类似的)优化是由JIT编译器完成的。根本原因肯定不是附加回路。为什么变型2短路?所有这些都是真实的,需要回避吗?我遗漏了什么吗?@Mustafa我的陈述是不正确的(一定是从注释中得到的),但效果是真实的:因为您从test()返回了一个常量,所以整行表达式在通过JIT内联后成为一个常量。
    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)