for循环和for-each循环之间有性能差异吗?

206

以下两个循环之间是否有性能差异?

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

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

1
@Keparo:这是一个“for each”循环,而不是“for-in”循环。 - Ande Turner
在Java中,它被称为“for each”,但是当涉及到Objective C时,它被称为“for in”循环。 - damithH
另请参见:https://docs.oracle.com/javase/8/docs/api/java/util/RandomAccess.html - Puce
这里讨论了扩展for循环的性能:https://dev59.com/tmfWa4cB1Zd3GeqPkMXN - eckes
即使存在差异,也是如此微小,以至于除非这段代码每秒执行数万亿次,否则您应该优先考虑可读性。即便如此,您仍需要进行适当的基准测试,以避免过早优化。 - Zabuzard
16个回答

223

来自 Joshua Bloch 的 Effective 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:

// 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.


54
值得一提的是,在 for-each 循环中无法访问索引计数器(因为它不存在)。 - basszero
89
使用 for-each 循环在许多不同(短期)的线程中分配临时迭代器会导致垃圾收集器产生大量工作,从而造成性能损失。在 Android Live Wallpaper 中,我有一些高度并行化的代码,发现垃圾收集器出现了问题。将其改为基于普通索引的循环就解决了这个问题。 - gsgx
1
@gsingh2011 但这也取决于您是否使用随机访问列表。对非随机访问列表使用基于索引的访问将比对随机访问列表使用for-each更糟糕,我想。如果您正在使用接口List并且不知道实际的实现类型,则可以检查列表是否是(实现)RandomAccess的实例,如果您真的很在意:https://docs.oracle.com/javase/8/docs/api/java/util/RandomAccess.html - Puce
3
Android文档中提到,使用foreach循环遍历ArrayList时会导致性能下降,但不会影响其他集合。我想知道这是否适用于您的情况。 - Viccari
1
很难相信Bloch会给出如此误导性的建议;我希望这在未来的版本中已经被修复或将被修复。特别是,“使用for-each循环没有性能惩罚”的说法是完全错误的,正如@Viccari和其他人所指出的那样。 - Don Hatch
显示剩余5条评论

32

这些循环的作用完全相同,我只是想在发表个人意见之前先展示这些。

首先,经典的通过List循环的方式:

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

其次,这是首选的方式,因为它更少出错(你有多少次在循环嵌套中混淆了变量i和j而犯错了吗?)。

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

第三,针对循环进行微优化:

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

现在进入正题:至少在我测试的时候,当计算毫秒级别的循环次数时,第三种循环方式是最快的。在其中每种循环中执行一个简单操作,并将其重复数百万次-在这个例子中使用的是Java 5和Windows上的jre1.6u10,如果有人感兴趣的话。
虽然第三种方式似乎是最快的,但你真的应该问问自己是否想要冒着在所有循环代码中都实现这种优化方法的风险,因为根据我的观察,实际的循环通常不是任何真正程序的耗时部分(或者我可能只是在错误的领域工作,谁知道呢)。 正如我在Java“for-each loop”的前言中提到的一样(有些人称其为“迭代器循环”,而其他人则称其为“for-in loop”),使用它时,你更不太可能遇到那个特定的愚蠢错误。在辩论它为什么比其他方法更快之前,请记住,javac根本不会优化字节码(好吧,几乎没有优化),它只会编译。
如果你喜欢微观优化和/或你的软件使用大量递归循环等技术,那么你可能会对第三种循环方式感兴趣。只是记得在将你的for循环改为这个奇怪的微优化代码之前,要好好测试一下你的软件,看看它的性能变化状况。

5
请注意,for循环中的++i<=size是“基于1”的,例如,在循环内部调用的get方法将被用于值1、2、3等。 - volley
20
一个更好的编写微优化循环的方法是:for(int i=0, size=strings.size();++i<=size;) {}这种方法更可取,因为它将 size 的作用域最小化。 - Dónal
1
第三个循环不是从i=1开始的吗?第一次循环时跳过了第一个元素。而且它是一个for循环是不必要的。int n = strings.length; while (n-- > 0) { System.out.println(" " + n + " " + strings[n]); } - Lassi Kinnunen
1
@Dónal,那个循环模式会漏掉第一个并导致IOOBE。这个可以用:for (int i = -1, size = list.size(); ++i < size;) - Nathan Adams
1
“所有这些循环都是完全相同的”是不正确的。其中一个使用随机访问get(int),另一个使用Iterator。考虑到LinkedList,因为它执行了n次get(int),所以for(int i=0;i<strings.size();i++) { /* do something using strings.get(i) */ }的性能要差得多。 - Steve Kuo

12

性能影响大多数情况下不明显,但并非为零。如果您查看RandomAccess接口的JavaDoc:

作为经验法则,如果List实现在类的典型实例中,以下循环:

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

比这个循环运行速度更快:

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

使用 for-each 循环的版本是使用迭代器,因此对于 ArrayList 等类型来说,for-each 循环不是最快的。


真的吗?甚至包含数组的一个?我在这里阅读到了:https://dev59.com/6nNA5IYBdhLWcg3wVcJx,它不涉及迭代器。 - Ondra Žižka
@OndraŽižka:for-each循环在遍历“Iterable”时使用迭代器,但不适用于数组。对于数组,使用带有索引变量的for循环。关于这方面的信息可以在JLS中找到。 - Lii
是的,所以你首先需要使用toArray将其创建为一个数组,并附带成本。 - Lassi Kinnunen

12
应该优先使用for-each循环语句。如果您正在使用的List实现不支持随机访问,则“get”方法可能会更慢。例如,如果使用LinkedList,则会产生遍历成本,而for-each方法使用迭代器来跟踪其在列表中的位置。有关for-each循环的细微差别的更多信息,请参见此处
我认为该文章现在在这个位置
此处显示的链接已经失效。

6

很不幸,这里存在一个差异。

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

以下是Log4j源代码的示例。

在 /log4j-api/src/main/java/org/apache/logging/log4j/MarkerManager.java 中,我们有一个名为 Log4jMarker 的静态内部类,其中定义了:

    /*
     * 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;
    }

使用标准循环:

  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

使用for-each:

  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

Oracle是怎么了?

我在Windows 7上尝试了Java 7和8。


9
对于那些试图阅读反汇编代码的人来说,总体结果是循环内部生成的代码相同,但使用for-each语法会创建一个额外的临时变量,其中包含了对第二个参数的引用。如果这个隐藏的变量被注册到寄存器中,而参数本身在代码生成期间没有被注册,那么使用for-each将更快;如果在 for(;;) 的例子中参数被注册到寄存器中,则执行时间将是相同的。需要进行基准测试吗? - Robin Davies

5

使用foreach循环可以更清晰地表达代码的意图,通常比微小的速度提升更受欢迎。

每当我看到一个索引循环时,我都需要花费更长的时间来解析它是否按照我想象中的那样进行。例如,它从零开始吗?它包括还是排除终点等等?

我的大部分时间似乎都花在阅读代码上(无论是我自己编写的还是别人编写的),清晰度几乎总是比性能更重要。这些天容易忽略性能,因为Hotspot做得非常出色。


4
下面的代码:
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);
    }
}

在我的系统上,输出以下内容:
224
-699150247503735895
221
-699150247503735895
220
-699150247503735895
219
-699150247503735895

我正在运行Ubuntu 12.10 alpha,并使用OracleJDK 1.7更新6。
一般来说,HotSpot会优化许多间接引用和简单冗余操作,因此您不必担心它们,除非它们在顺序中出现很多次或者嵌套很深。
另一方面,LinkedList上的索引获取比调用LinkedList迭代器上的next要慢得多,因此当您使用迭代器时(在for-each循环中显式或隐式地使用),可以避免这种性能损失同时保持可读性。

4
以下是Android开发团队发布的差异分析简介:

https://www.youtube.com/watch?v=MZOf3pOAM6A

结果是有差异的,在非常大的列表中,使用for each循环会比较慢。在测试中,for each循环需要两倍的时间。但是他们测试的数组列表包含了400000个整数,每个元素的实际差异为6微秒。我没有进行测试,他们也没有提到,但我认为如果使用对象而不是基本数据类型,差异可能会更大。但是,除非您正在构建库代码并且无法确定要迭代的规模,否则我认为这种差异不值得担心。

3

我认为其他答案的基础是错误的基准测试,这并没有考虑Hotspot的编译和优化过程。

简短回答

如果可能的话,请使用增强型循环,因为大多数情况下它是最快的。如果无法使用增强型循环,则尽可能将整个数组提取到本地变量中:

int localArray = this.array;
for (int i = 0; i < localArray.length; i++) { 
    methodCall(localArray[i]); 
}

长篇回答

通常情况下没有区别,因为Hotspot在优化和消除Java需要执行的检查方面非常出色。

但是有时候某些优化只是无法完成,通常是因为您在循环内部有一个虚拟调用,这不能被内联。

在这种情况下,一些循环确实比其他循环更快。

Java 需要做的事情之一:

  • 重新加载该数组——因为它可能已被更改(通过调用或其他线程)
  • 检查i是否在数组范围内,如果不在则抛出IndexOutOfBoundsException异常
  • 检查所访问的对象引用是否为空,如果是,则抛出NullPointerException异常

考虑以下 C 风格循环:

for (int i = 0; i < this.array.length; i++) { //now java knows i < this.array.length
    methodCall(this.array[i]);// no need to check
}

通过评估循环条件i < this.array.length,Java 知道i必须在边界内(i仅在调用后更改),因此下一行不需要再次检查它。但是在这种情况下,Java 需要重新加载this.array.length
你可能会尝试通过将this.array.length的值提取到局部变量中来“优化”循环:
int len = this.array.length;//len is now a local variable
for (int i = 0; i < len; i++) { //no reload needed
    methodCall(this.array[i]); //now java will do the check
}

现在Java不需要每次重新加载,因为局部变量可能无法被方法调用和/或另一个线程改变。局部变量只能在方法内部更改,Java现在可以证明变量len不会改变。

但现在循环条件i < this.array.length已更改为i < len,以前的优化失败了,Java需要检查i是否在this.array的边界内。

更好的优化是将整个数组提取到局部变量中:

ArrayType[] localArray = this.array;
for (int i = 0; i < localArray.length; i++) { 
    methodCall(localArray[i]); 
}

现在Java不需要重新加载数组,也消除了“索引在边界内”的检查。
那么增强型循环呢? 通常情况下,如果不是更好的话,编译器会将您的增强型循环重写为类似于上面显示的最后一个循环。

因此,使用数组和循环结合会更快,并且每次循环都会带来更好的性能? - Tần Quảng
@TầnQuảng 我不确定你的意思。请详细说明你的问题,或者提供一些代码。 - Jerry Lundegaard
短回答和长回答中的最后优化代码“int localArray = this.array;”不应该是int类型。我最想知道的是ArrayList localArray = this.array是否会使用虚拟内存。我不知道它是复制一个新数组还是只是一个引用。 - Raii
1
@Raii 是的,类型错了; 使用 ArrayList localArray = this.array,你只是复制了一个引用,没有进行额外的内存分配,除了在栈上创建一个对象引用之外,基本上没有开销。 - Jerry Lundegaard

3
唯一确定的方法是进行基准测试,即使这听起来并不像那么简单。JIT编译器可能会对您的代码做出非常意外的事情。

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