198

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

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

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

16 回答 16

219

来自Joshua Bloch在Effective Java中的第 46条:

在 1.5 版中引入的 for-each 循环通过完全隐藏迭代器或索引变量来消除混乱和出错的机会。由此产生的习语同样适用于集合和数组:

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

当您看到冒号 (:) 时,将其读作“in”。因此,上面的循环读作“对于元素中的每个元素 e”。请注意,使用 for-each 循环没有性能损失,即使是数组也是如此。事实上,在某些情况下,它可能比普通的 for 循环提供轻微的性能优势,因为它只计算一次数组索引的限制。虽然您可以手动完成(第 45 条),但程序员并不总是这样做。

于 2008-11-02T12:47:22.530 回答
31

所有这些循环都完全相同,我只想在投入两分钱之前展示这些。

一、循环遍历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 */ }

三、微优化的for循环:

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

现在实际的两美分:至少在我测试这些时,第三个是最快的,以毫秒计算每种类型的循环所需的时间,其中一个简单的操作重复了几百万次 - 这是使用 Java 5如果有人感兴趣,请在 Windows 上使用 jre1.6u10。

虽然至少看起来第三个是最快的,但你真的应该问自己是否愿意冒险在循环代码中到处实现这种窥视孔优化,因为从我所见,实际循环不是'这通常是任何实际程序中最耗时的部分(或者我可能只是在错误的领域工作,谁知道呢)。而且就像我在 Java for-each 循环的借口中提到的那样(有些人将其称为Iterator 循环,其他人称为for-in 循环),您在使用它时不太可能遇到一个特定的愚蠢错误。在讨论这甚至比其他的更快之前,请记住 javac 根本不优化字节码(好吧,几乎完全没有),它只是编译它。

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

于 2008-11-02T16:10:44.990 回答
12

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

我想这篇文章现在在这里: 新位置

此处显示的链接已失效。

于 2008-11-02T13:07:58.120 回答
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 循环不是最快的。

于 2009-02-24T02:13:53.493 回答
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

对于每个:

  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 进行了尝试。

于 2014-09-26T20:57:58.010 回答
5

foreach 使您的代码的意图更清晰,并且通常比非常小的速度改进更可取 - 如果有的话。

每当我看到一个索引循环时,我都必须对其进行更长时间的解析,以确保它完成我认为它所做的事情。例如它是否从零开始,它是否包含或排除终点等?

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

于 2008-11-03T08:22:38.510 回答
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

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

一般来说,HotSpot 优化了很多间接和简单的冗余操作,所以通常你不应该担心它们,除非它们有很多顺序或者它们是严重嵌套的。

另一方面,LinkedList 上的索引 get 比在 LinkedList 的迭代器上调用 next 慢得多,因此您可以在使用迭代器时(在 for-each 循环中显式或隐式地)在保持可读性的同时避免性能下降。

于 2012-08-28T21:53:42.170 回答
4

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

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

结果是存在差异,并且在具有非常大列表的非常受限的环境中,这可能是一个明显的差异。在他们的测试中,for each 循环花费了两倍的时间。然而,他们的测试是针对一个包含 400,000 个整数的数组列表。阵列中每个元素的实际差异为 6微秒。我没有测试过,他们也没有说,但我希望使用对象而不是原语的差异会稍微大一些,但即使是这样,除非你正在构建库代码,而你不知道你会被问到的规模重复一遍,我认为差异不值得强调。

于 2016-07-02T18:12:36.370 回答
3

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

于 2008-11-02T13:05:43.977 回答
2

即使使用 ArrayList 或 Vector 之类的东西,其中“get”是一个简单的数组查找,第二个循环仍然有第一个循环没有的额外开销。我希望它比第一个慢一点。

于 2008-11-02T12:54:21.690 回答
2

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

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

因此,除非循环中的逻辑需要索引,否则始终首选第一种方法。

于 2013-11-06T01:33:22.817 回答
1

使用迭代器而不是索引总是更好。这是因为迭代器最有可能针对 List 实现进行了优化,而索引(调用 get)可能不会。例如 LinkedList 是一个 List 但通过其元素进行索引将比使用迭代器进行迭代要慢。

于 2008-11-03T03:51:34.093 回答
1

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

于 2017-06-14T09:03:14.190 回答
1

在我看来,其他答案似乎是基于不正确的基准,这并没有考虑到 Hotspot 的编译和优化过程。

简短的回答

尽可能使用增强循环,因为大多数时候它是最快的。如果不能,请尽可能将整个数组拉入局部变量:

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

长答案

现在,通常没有区别,因为 Hotspot 非常擅长优化和摆脱 Java 需要做的检查。

但有时某些优化无法完成,通常是因为您在循环中有一个虚拟调用,无法内联。在这种情况下,某些循环确实可以比其他循环更快。

Java需要做的几件事:

  • 重新加载 this.array - 因为它可以被改变(通过调用或另一个线程)
  • 检查i是否在数组的边界内,如果没有则抛出 IndexOutOfBoundsException
  • 检查访问的对象引用是否为 null,如果是则抛出 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不需要每次都重新加载,因为局部变量不能被methodCall和/或另一个线程更改。局部变量只能在方法本身内部改变,java现在可以证明变量len不能改变。

但是现在循环条件i < this.array.length变成了i < len,之前的优化失败了,java 需要检查i是否在this.array的边界内。

更好的优化是将整个数组拉入局部变量:

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

现在java不需要重新加载数组,并且“ i in bounds”检查也被取消了。

那么增强循环呢?好吧,通常编译器会将您的增强循环重写为类似于最后显示的循环,如果不是更好的话。

于 2021-09-17T12:23:14.067 回答
0
1. for(Object o: objectArrayList){
    o.DoSomthing();
}
and

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

两者都做同样的事情,但为了方便和安全地使用 for-each 编程,第二种使用方式可能容易出错。

于 2016-01-22T14:09:21.047 回答
0

接受的答案回答了这个问题,除了 ArrayList 的例外情况......

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

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

直接来自开发者文档:-

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

遍历数组有几种选择:

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 还无法优化循环中每次迭代获取数组长度的成本。

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

two() 对于没有 JIT 的设备是最快的,并且对于带有 JIT 的设备与 one() 没有区别。它使用 Java 编程语言 1.5 版中引入的增强的 for 循环语法。

因此,您应该默认使用增强的 for 循环,但考虑为性能关键的 ArrayList 迭代使用手写的计数循环。

于 2017-08-21T05:37:44.670 回答