关于java:for循环和for-each循环之间是否存在性能差异?

Is there a performance difference between a for loop and a for-each loop?

以下两个循环之间的性能差异是什么(如果有)?

1
2
3
for (Object o: objectArrayList) {
    o.DoSomething();
}

1
2
3
for (int i=0; i<objectArrayList.size(); i++) {
    objectArrayList.get(i).DoSomething();
}


从Joshua Bloch的有效Java中的第46项:

The for-each loop, introduced in
release 1.5, gets rid of the clutter
and the opportunity for error by
hiding the iterator or index variable
completely. The resulting idiom
applies equally to collections and
arrays:

1
2
3
4
// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
    doSomething(e);
}

When you see the colon (:), read it as
"in." Thus, the loop above reads as
"for each element e in elements." Note
that there is no performance penalty
for using the for-each loop, even for
arrays. In fact, it may offer a slight
performance advantage over an ordinary
for loop in some circumstances, as it
computes the limit of the array index
only once. While you can do this by
hand (Item 45), programmers don’t
always do so.


所有这些循环都是完全一样的,我只想在投入我的两分钱之前展示一下。

首先,循环遍历列表的经典方法:

1
for(int i=0;i<strings.size();i++) { /* do something using strings.get(i) */ }

第二,因为它不容易出错,所以首选的方法是(你做了多少次"oops,在循环中混合变量i和j"的操作?).

1
for(String s : strings) { /* do something using s */ }

第三,微循环优化:

1
2
int size = strings.size();
for(int i=0;++i<=size;) { /* do something using strings.get(i) */ }

现在实际的两分钱:至少当我测试这些时,第三毫秒是最快的,在毫秒数上,对于每种类型的循环花费了多长时间,其中一个简单的操作重复了几百万次,这是在Windows上使用JR1.1.6U10的Java 5,以防有人感兴趣。

虽然第三个循环看起来是最快的,但你真的应该问问自己,你是否愿意冒险在循环代码中的任何地方实现这个窥视孔优化,因为从我所看到的来看,实际的循环通常不是任何实际程序中最耗时的部分(或者可能我只是在错误的F上工作)。菲尔德,谁知道呢)。就像我在Java中为每个循环的借口所提到的(有些称之为迭代器循环,而其他的则指在循环中),在使用它时,不太可能碰到一个特殊的愚蠢错误。在讨论这种方法甚至比其他方法更快之前,请记住javac根本没有优化字节码(好吧,几乎完全没有),它只是编译它。

如果您正在进行微优化和/或您的软件使用了大量的递归循环,那么您可能对第三种循环类型感兴趣。只要记住在改变for循环之前和之后都要对软件进行很好的基准测试,就可以得到这个奇怪的、微优化的循环。


通常应首选for each循环。如果您使用的列表实现不支持随机访问,"get"方法可能会慢一些。例如,如果使用LinkedList,则会产生遍历成本,而for-each方法使用一个迭代器来跟踪它在列表中的位置。有关每个循环的细微差别的详细信息。

我想这篇文章现在在这里:新地点

这里显示的链接已断开。


好吧,性能影响主要是微不足道的,但不是零。如果您查看RandomAccess接口的javadoc:

As a rule of thumb, a List
implementation should implement this
interface if, for typical instances of
the class, this loop:

1
2
for (int i=0, n=list.size(); i < n; i++)
    list.get(i);

runs faster than this loop:

1
2
for (Iterator i=list.iterator(); i.hasNext();)
      i.next();

因为每个循环都使用带有迭代器的版本,所以对于ArrayList,对于每个循环来说,不是最快的。


不幸的是,这似乎有区别。

如果您查看两种循环的生成字节代码,它们是不同的。

下面是log4j源代码的一个示例。

在/Log4JAP/SRC/Ma/Java/Org/Apache/LogC/Log4J/MARKMARMAREJ.java中,我们有一个静态的内部类,称为Log4JMeX,定义了:

1
2
3
4
5
6
7
8
9
10
11
12
    /*
     * Called from add while synchronized.
     */

    private static boolean contains(final Marker parent, final Marker... localParents) {
        //noinspection ForLoopReplaceableByForEach
        for (final Marker marker : localParents) {
            if (marker == parent) {
                return true;
            }
        }
        return false;
    }

带标准回路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  private static boolean contains(org.apache.logging.log4j.Marker, org.apache.logging.log4j.Marker...);
    Code:
       0: iconst_0
       1: istore_2
       2: aload_1
       3: arraylength
       4: istore_3
       5: iload_2
       6: iload_3
       7: if_icmpge     29
      10: aload_1
      11: iload_2
      12: aaload
      13: astore        4
      15: aload         4
      17: aload_0
      18: if_acmpne     23
      21: iconst_1
      22: ireturn
      23: iinc          2, 1
      26: goto          5
      29: iconst_0
      30: ireturn

每个都有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  private static boolean contains(org.apache.logging.log4j.Marker, org.apache.logging.log4j.Marker...);
    Code:
       0: aload_1
       1: astore_2
       2: aload_2
       3: arraylength
       4: istore_3
       5: iconst_0
       6: istore        4
       8: iload         4
      10: iload_3
      11: if_icmpge     34
      14: aload_2
      15: iload         4
      17: aaload
      18: astore        5
      20: aload         5
      22: aload_0
      23: if_acmpne     28
      26: iconst_1
      27: ireturn
      28: iinc          4, 1
      31: goto          8
      34: iconst_0
      35: ireturn

那神谕是怎么回事?

我在Windows 7上用Java 7和8尝试过。


最好使用迭代器而不是索引。这是因为迭代器很可能是列表实现的最佳选择,而索引(调用get)可能不是。例如,LinkedList是一个列表,但通过其元素进行索引的速度将比使用迭代器进行迭代的速度慢。


foreach使代码的意图更加清晰,这通常比非常小的速度改进(如果有的话)更受欢迎。

每当我看到一个索引循环时,我必须对它进行更长时间的分析,以确保它能像我认为的那样工作,例如,它是从零开始的,它是否包括或排除端点等?

我的大部分时间似乎都花在阅读代码上(我写的或者别人写的),而清晰性几乎总是比性能更重要。现在人们很容易忽略性能,因为Hotspot做得非常出色。


以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;

interface Function<T> {
    long perform(T parameter, long x);
}

class MyArray<T> {

    T[] array;
    long x;

    public MyArray(int size, Class<T> type, long x) {
        array = (T[]) Array.newInstance(type, size);
        this.x = x;
    }

    public void forEach(Function<T> function) {
        for (T element : array) {
            x = function.perform(element, x);
        }
    }
}

class Compute {
    int factor;
    final long constant;

    public Compute(int factor, long constant) {
        this.factor = factor;
        this.constant = constant;
    }

    public long compute(long parameter, long x) {
        return x * factor + parameter + constant;
    }
}

public class Main {

    public static void main(String[] args) {
        List<Long> numbers = new ArrayList<Long>(50000000);
        for (int i = 0; i < 50000000; i++) {
            numbers.add(i * i + 5L);
        }

        long x = 234553523525L;

        long time = System.currentTimeMillis();
        for (int i = 0; i < numbers.size(); i++) {
            x += x * 7 + numbers.get(i) + 3;
        }
        System.out.println(System.currentTimeMillis() - time);
        System.out.println(x);
        x = 0;
        time = System.currentTimeMillis();
        for (long i : numbers) {
            x += x * 7 + i + 3;
        }
        System.out.println(System.currentTimeMillis() - time);
        System.out.println(x);
        x = 0;
        numbers = null;
        MyArray<Long> myArray = new MyArray<Long>(50000000, Long.class, 234553523525L);
        for (int i = 0; i < 50000000; i++) {
            myArray.array[i] = i * i + 3L;
        }
        time = System.currentTimeMillis();
        myArray.forEach(new Function<Long>() {

            public long perform(Long parameter, long x) {
                return x * 8 + parameter + 5L;
            }
        });
        System.out.println(System.currentTimeMillis() - time);
        System.out.println(myArray.x);
        myArray = null;
        myArray = new MyArray<Long>(50000000, Long.class, 234553523525L);
        for (int i = 0; i < 50000000; i++) {
            myArray.array[i] = i * i + 3L;
        }
        time = System.currentTimeMillis();
        myArray.forEach(new Function<Long>() {

            public long perform(Long parameter, long x) {
                return new Compute(8, 5).compute(parameter, x);
            }
        });
        System.out.println(System.currentTimeMillis() - time);
        System.out.println(myArray.x);
    }
}

在我的系统上提供以下输出:

1
2
3
4
5
6
7
8
224
-699150247503735895
221
-699150247503735895
220
-699150247503735895
219
-699150247503735895

我正在运行带有OracleJDK 1.7更新6的Ubuntu 12.10 Alpha。

一般来说,Hotspot优化了很多间接操作和简单的还原操作,所以一般来说,除非seqence中有很多间接操作或它们被大量嵌套,否则不应该担心它们。

另一方面,LinkedList上的indexed get比为LinkedList在迭代器上调用next慢得多,因此在使用迭代器(显式或隐式地在每个循环中)时,可以避免性能受到影响,同时保持可读性。


以下是对Android开发团队提出的差异的简要分析:

https://www.youtube.com/watch?V=mzof3poam6a

其结果是存在差异,并且在列表非常大的非常受限的环境中,这可能是一个显著的差异。在他们的测试中,for-each循环花费了两倍的时间。然而,他们的测试超过了40万个整数的数组列表。数组中每个元素的实际差异为6微秒。我还没有测试过,他们也没有说,但是我希望使用对象而不是原语的差异会稍微大一点,但是即使你在构建库代码的时候,如果你不知道你将被要求重复的内容的规模,我认为这种差异也不值得去强调。


唯一可以确定的方法就是对它进行基准测试,即使这样也不像听起来那么简单。JIT编译器可以对代码执行非常意外的操作。


即使使用类似arraylist或vector的方法,其中"get"是一个简单的数组查找,第二个循环仍然有第一个循环没有的额外开销。我希望它比第一个循环慢一点。


通过变量名objectArrayList,我假设这是java.util.ArrayList的一个实例。在这种情况下,性能差异将是不明显的。

另一方面,如果是java.util.LinkedList的实例,第二种方法将慢得多,因为List#get(int)是O(N)操作。

所以第一种方法总是首选的,除非循环中的逻辑需要索引。


1
2
3
4
5
6
7
8
1. for(Object o: objectArrayList){
    o.DoSomthing();
}
and

2. for(int i=0; i<objectArrayList.size(); i++){
    objectArrayList.get(i).DoSomthing();
}

两者都是相同的,但为了方便和安全地为每个编程使用,有可能在第二种使用方式中出现错误。


奇怪的是,没有人提到明显的foreach分配内存(以迭代器的形式),而普通for循环不分配任何内存。对于Android上的游戏,这是一个问题,因为这意味着垃圾收集器将定期运行。在游戏中,你不希望垃圾收集器运行…永远。所以不要在绘制(或渲染)方法中使用foreach循环。


接受的回答回答了这个问题,除了例外的ArrayList…

因为大多数开发人员都依赖于ArrayList(至少我相信如此)

所以我有义务在这里添加正确的答案。

直接从开发人员文档中:

增强的for循环(有时也称为"for each"循环)可用于实现iterable接口的集合和数组。对于集合,将分配一个迭代器来对hasNext()和next()进行接口调用。使用arraylist,手写计数循环的速度大约快3倍(有或没有jit),但对于其他集合,增强的for循环语法将完全等同于显式迭代器的使用。

迭代数组有几种选择:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static class Foo {
    int mSplat;
}

Foo[] mArray = ...

public void zero() {
    int sum = 0;
    for (int i = 0; i < mArray.length; ++i) {
        sum += mArray[i].mSplat;
    }
}

public void one() {
    int sum = 0;
    Foo[] localArray = mArray;
    int len = localArray.length;

    for (int i = 0; i < len; ++i) {
        sum += localArray[i].mSplat;
    }
}

public void two() {
    int sum = 0;
    for (Foo a : mArray) {
        sum += a.mSplat;
    }
}

Zero()是最慢的,因为JIT还不能优化每次循环中获取数组长度一次的成本。

一个()更快。它将所有内容提取到局部变量中,避免查找。只有阵列长度提供了性能优势。

对于没有JIT的设备,two()最快,对于有JIT的设备,two()与one()不可区分。它使用Java编程语言第1.5版中介绍的增强型for循环语法。

因此,您应该在默认情况下使用增强的for循环,但是考虑为性能关键的ArrayList迭代编写一个手工编写的计数循环。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FirstJavaProgram {

    public static void main(String[] args)
    {
        int a[]={1,2,3,45,6,6};

// Method 1: this is simple way to print array

        for(int i=0;i<a.length;i++)
        {
            System.out.print(a[i]+"");
        }

// Method 2: Enhanced For loop

        for(int i:a)
        {
            System.out.print(i+"");
        }
    }
}


是的,for-each变体比正常index-based-for-loop变体更快。

for-each变体使用iterator。因此,遍历比基于索引的普通for循环更快。这是因为iterator针对遍历进行了优化,因为它只指向下一个元素之前和前一个元素之后。作为index-based-for-loop的一个原因是,它必须计算并移动到每次不与iterator一起的元素位置。