Java For-Loop - 终止表达速度

时间:2015-11-05 13:47:04

标签: java performance for-loop

在我的java程序中,我的for循环看起来大致如下:

ArrayList<MyObject> myList = new ArrayList<MyObject>();
putThingsInList(myList);
for (int i = 0; i < myList.size(); i++) { 
     doWhatsoever(); 
}

由于列表的大小没有变化,我试图通过用变量替换循环的终止表达式来加速循环。

  

我的想法是:由于ArrayList的大小在迭代时可能会发生变化,因此必须在每个循环周期执行终止表达式。如果我知道(但JVM没有),它的大小将保持不变,变量的使用可能会加快速度。

ArrayList<MyObject> myList = new ArrayList<MyObject>();
putThingsInList(myList);
int myListSize = myList.size();
for (int i = 0; i < myListSize; i++) { 
     doWhatsoever(); 
}

然而,这种解决方案速度较慢,速度较慢;同时使myListSize最终不会改变任何东西!我的意思是我能理解,如果速度根本没有改变;因为JVM刚刚发现,大小并没有改变和优化代码。但为什么它会变慢?

然而,我改写了该计划;现在列表的大小随着每个周期而变化:if i%2==0,我删除列表的最后一个元素,否则我在列表的末尾添加一个元素。所以现在我必须在每次迭代中调用myList.size()操作,我猜想。

我不知道这是否真的正确,但myList.size()终止表达式仍然比仅使用一直保持不变的变量作为终止表达式更快......

任何想法为什么?

编辑(我在这里新建,我希望这是方法,怎么做) 我的整个测试程序看起来像这样:

ArrayList<Integer> myList = new ArrayList<Integer>();
for (int i = 0; i < 1000000; i++)
{
    myList.add(i);
}

final long myListSize = myList.size();
long sum = 0;
long timeStarted = System.nanoTime();
for (int i = 0; i < 500; i++)
{
    for (int j = 0; j < myList.size(); j++)
    {
        sum += j;

        if (j%2==0)
        {
            myList.add(999999);
        }
        else
        {
            myList.remove(999999);
        }
    }
}
long timeNeeded = (System.nanoTime() - timeStarted)/1000000;
System.out.println(timeNeeded);
System.out.println(sum);

发布代码的性能(平均10次执行): 4102ms for myList.size() myListSize的4230ms

没有if-then-else语句(因此使用常量myList大小) myList.size()的172ms myListSize为329ms

因此两种版本的速度差异仍然存在。在带有if-then-else部分的版本中,百分比差异当然较小,因为大量时间用于添加和删除列表操作。

4 个答案:

答案 0 :(得分:4)

问题在于这一行:

final long myListSize = myList.size();

将此更改为int并且看,运行时间相同。为什么?由于每次迭代都需要intlong进行比较,因此需要对int进行扩展转换,这需要时间。

请注意,编译和优化代码时,差异也很大(但可能不完全)消失,从以下JMH基准测试结果可以看出:

# JMH 1.11.2 (released 7 days ago)
# VM version: JDK 1.8.0_51, VM 25.51-b03
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
...
# Run complete. Total time: 00:02:01

Benchmark                           Mode  Cnt      Score      Error  Units
MyBenchmark.testIntLocalVariable   thrpt   20  81892.018 ±  734.621  ops/s
MyBenchmark.testLongLocalVariable  thrpt   20  74180.774 ± 1289.338  ops/s
MyBenchmark.testMethodInvocation   thrpt   20  82732.317 ±  749.430  ops/s

以下是它的基准代码:

public class MyBenchmark {

    @State( Scope.Benchmark)
    public static class Values {
        private final ArrayList<Double> values;

        public Values() {
            this.values = new ArrayList<Double>(10000);
            for (int i = 0; i < 10000; i++) {
                this.values.add(Math.random());
            }
        }
    }

    @Benchmark
    public double testMethodInvocation(Values v) {
        double sum = 0;
        for (int i = 0; i < v.values.size(); i++) {
            sum += v.values.get(i);
        }
        return sum;
    }

    @Benchmark
    public double testIntLocalVariable(Values v) {
        double sum = 0;
        int max = v.values.size();
        for (int i = 0; i < max; i++) {
            sum += v.values.get(i);
        }
        return sum;
    }

    @Benchmark
    public double testLongLocalVariable(Values v) {
        double sum = 0;
        long max = v.values.size();
        for (int i = 0; i < max; i++) {
            sum += v.values.get(i);
        }
        return sum;
    }
}

<强> P.S:

  

我的想法是:由于ArrayList的大小可能会改变   迭代它,终止表达式必须在每个循环中执行   周期。如果我知道(但JVM没有),它的大小将保持不变   常量,变量的使用可能会加快速度。

您的假设是错误的,原因有两个:首先,VM可以轻松确定通过转义分析,myList中存储的列表不会转义该方法(因此可以在堆栈中自由分配它)例子)。

更重要的是,即使列表是在多个线程之间共享的,因此在我们运行循环时可能会从外部进行修改,在没有任何同步的情况下,运行我们的循环的线程完全有效。根本没有发生变化。

答案 1 :(得分:2)

与往常一样,事情并非总是如此......

首先,ArrayList.size()不会在每次调用时重新计算,只有在调用适当的mutator时才会重新计算。所以经常打电话很便宜。

这些循环中哪一个最快?

// array1 and array2 are the same size.
int sum;
for (int i = 0; i < array1.length; i++) {
  sum += array1[i];
}

for (int i = 0; i < array2.length; i++) {
  sum += array2[i];
}

int sum;
for (int i = 0; i < array1.length; i++) {
  sum += array1[i];
  sum += array2[i];
}

本能地,你会说第二个循环是最快的,因为它不会迭代两次。但是,一些优化实际上会导致第一个循环最快,具体取决于内存行走步幅导致大量内存缓存未命中。

  

旁注:这种编译器优化技术称为循环   干扰

这个循环:

int sum;
for (int i = 0; i < 1000000; i++) {
    sum += list.get(i);
}

与:

不同
// Assume that list.size() == 1000000
int sum;
for (int i = 0; i < list.size(); i++) {
    sum += list.get(i);
}

在第一种情况下,编译绝对知道它必须迭代一百万次并将常量放在常量池中,因此可以进行某些优化。

更接近的等价物是:

int sum;
final int listSize = list.size();
for (int i = 0; i < listSize; i++) {
    sum += list.get(i);
}

但只有在JVM知道listSize的值是什么之后。 final关键字为编译器/运行时提供了可以利用的某些保证。如果循环运行的时间足够长,则JIT编译将启动,从而使执行更快。

答案 2 :(得分:0)

因为这引起了我的兴趣,所以我决定快速测试一下:

public class fortest {
    public static void main(String[] args) {
        long mean = 0;
        for (int cnt = 0; cnt < 100000; cnt++) {
            if (mean > 0)
                mean /= 2;

            ArrayList<String> myList = new ArrayList<String>();
            putThingsInList(myList);

            long start = System.nanoTime();


            int myListSize = myList.size();
            for (int i = 0; i < myListSize; i++) doWhatsoever(i, myList);

            long end = System.nanoTime();
            mean += end - start;
        }
        System.out.println("Mean exec: " + mean/2);
    }

    private static void doWhatsoever(int i, ArrayList<String> myList) {
        if (i % 2 == 0)
            myList.set(i, "0");
    }

    private static void putThingsInList(ArrayList<String> myList) {
        for (int i = 0; i < 1000; i++) myList.add(String.valueOf(i));
    }
}

我没有看到你所看到的那种行为。

使用myList.size()

1800ns 表示超过100000次迭代的执行时间 with myListSize

因此,我怀疑你的代码是由错误的函数执行的。在上面的示例中,如果只填充ArrayList一次,有时可以看到更快的执行,因为doWhatsoever()只会在第一个循环上执行某些操作。我怀疑其余部分正在被优化掉,因此大大减少了执行时间。你可能有类似的情况,但是没有看到你的代码就可能几乎不可能找出那个。

答案 3 :(得分:0)

还有另一种方法可以加速每个循环使用的代码

ArrayList<MyObject> myList = new ArrayList<MyObject>();
putThingsInList(myList);
for (MyObject ob: myList) { 
     doWhatsoever(); 
}

但我同意@ showp1984,其他一些部分正在减慢代码。