【问题标题】:Is there a performance difference between a for loop and a for-each loop?for 循环和 for-each 循环之间是否存在性能差异?
【发布时间】:2010-09-20 09:19:49
【问题描述】:

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

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

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

【问题讨论】:

标签: java performance for-loop


【解决方案1】:

来自 Joshua Bloch 的 Effective Java 中的第 46 项:

for-each 循环,在 发布 1.5,摆脱混乱 和出错的机会 隐藏迭代器或索引变量 完全地。结果成语 同样适用于集合和 数组:

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

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

【讨论】:

  • 值得一提的是,在 for-each 循环中没有办法访问索引计数器(因为它不存在)
  • 分配迭代器会带来性能损失。我在 Android 动态壁纸中有一些高度并行的代码。我看到垃圾收集器快疯了。这是因为 for-each 循环在许多不同的(短期)线程中分配临时迭代器,导致垃圾收集器大量工作。切换到基于常规索引的循环解决了这个问题。
  • @gsingh2011 但这也取决于您是否使用随机访问列表。我猜,对非随机访问列表使用基于索引的访问比使用 for-each 和随机访问列表要糟糕得多。如果您正在使用接口 List 并因此不知道实际的实现类型,您可以检查列表是否是(实现)RandomAccess 的实例,如果您真的很在意:docs.oracle.com/javase/8/docs/api/java/util/RandomAccess.html
  • @gsingh2011 Android 文档 (developer.android.com/training/articles/perf-tips.html#Loops) 提到,仅在 ArrayList 上使用 foreach 会降低性能,而不是在其他集合上。我很好奇这是否是你的情况。
  • 很难相信布洛赫给出了这样的误导性建议;我希望这个问题在未来的版本中已经得到修复,或者将得到修复。特别是,正如@Viccari 和其他人所指出的那样,“使用 for-each 循环不会降低性能”的说法是完全错误的。
【解决方案2】:

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

【讨论】:

  • 第一个循环也必须获取每个元素。它在幕后创建了一个迭代器来执行此操作。它们真的是等价的。
  • 从 C 的角度考虑,迭代器可以只增加一个指针,但每次获取都必须将 i 的值乘以指针的宽度。
  • 这取决于您使用的列表类型。不过我认为你是对的,使用 get 永远不会更快,有时甚至更慢。
【解决方案3】:

确定的唯一方法是对其进行基准测试,即使是not as simple as it may sound。 JIT 编译器可以对您的代码做非常意想不到的事情。

【讨论】:

    【解决方案4】:

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

    我认为文章现在在这里:new location

    此处显示的链接已失效。

    【讨论】:

      【解决方案5】:

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

      首先,循环遍历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) */ }
      

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

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

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

      【讨论】:

      • 请注意,带有 ++i
      • 编写微优化循环的更好方法是 for(int i=0, size=strings.size();++i
      • 第三个不是在第一次通过循环时从 i=1 开始,跳过第一个元素。它是一个 for 循环是不必要的。 int n=strings.length; while(n-->0) { System.out.println(" "+n+" "+strings[n]); }
      • @Dónal 循环模式错过了第一个并给出了 IOOBE。这个有效: for (int i = -1, size = list.size(); ++i
      • “所有这些循环都完全相同”是不正确的。一种使用随机访问get(int),另一种使用Iterator。考虑LinkedList,其中for(int i=0;i&lt;strings.size();i++) { /* do something using strings.get(i) */ } 的性能要差得多,因为它执行了get(int) n 次。
      【解决方案6】:

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

      【讨论】:

      • 我认为性能优化中没有“总是”这样的东西。)
      • 我认为您需要在此处定义 better。如果我们谈论的是性能(这是问题的重点),那么在这里说 always 是错误的。基于迭代器的循环比RandomAccess 列表的索引循环慢。请注意,这包括 ArrayList,它是 Java 中最常用的数据结构之一。大多数时候,性能差异比循环的可读性更重要,但如果它在你的热门路径中,惩罚就在这里,所以当被问到关于性能的问题时,这个答案真的很误导。
      【解决方案7】:

      foreach 使您的代码意图更清晰,这通常优于非常小的速度改进(如果有的话)。

      每当我看到索引循环时,我都必须对其进行更长时间的解析,以确保它按照我的想法进行操作。是从零开始,是包括还是不包括终点等?

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

      【讨论】:

        【解决方案8】:

        嗯,性能影响大多是微不足道的,但不是零。如果你看RandomAccess接口的JavaDoc:

        根据经验,列表 实施应该实施这个 接口 if,对于典型的实例 类,这个循环:

        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 循环并不是最快的。

        【讨论】:

        • 真的每个?甚至有一个数组?我在这里读到stackoverflow.com/questions/1006395/… 它不涉及迭代器。
        • @OndraŽižka:for-each 循环在循环Iterables 时使用迭代器,但不使用数组。对于数组,使用带有索引变量的 for 循环。在JLS 中有相关信息。
        • 是的,所以您首先需要使用 toArray 将其创建到一个数组中,这是有代价的。
        【解决方案9】:

        以下代码:

        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 循环中显式或隐式地)保持可读性的同时避免性能下降。

        【讨论】:

          【解决方案10】:

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

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

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

          【讨论】:

            【解决方案11】:

            不幸的是,似乎有所不同。

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

            这是一个来自 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
            

            那个 Oracle 怎么了?

            我已经在 Windows 7 上使用 Java 7 和 8 进行了尝试。

            【讨论】:

            • 对于那些试图阅读反汇编的人,最终结果是循环内生成的代码是相同的,但是 for-each 设置似乎创建了一个额外的临时变量,其中包含对第二个论点。如果注册了额外的隐藏变量,但参数本身不在代码生成期间,那么 for-each 会更快;如果在 for(;;) 示例中注册了参数,则执行时间将相同。需要基准测试吗?
            【解决方案12】:
            1. for(Object o: objectArrayList){
                o.DoSomthing();
            }
            and
            
            2. for(int i=0; i<objectArrayList.size(); i++){
                objectArrayList.get(i).DoSomthing();
            }
            

            两者的作用相同,但为了方便安全地使用 for-each 进行编程,第二种使用方式可能容易出错。

            【讨论】:

              【解决方案13】:

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

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

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

              【讨论】:

                【解决方案14】:

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

                【讨论】:

                • 但不要忘记永远不会创建新迭代器的集合。
                【解决方案15】:

                已接受的答案回答了问题,除了 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 迭代使用手写计数循环。

                【讨论】:

                • 您所指的文档实际上是android开发人员的性能指南(link),仅适用于在android上运行的jvm,无论如何似乎已经过时了。
                【解决方案16】:

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

                简答

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

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

                长答案

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

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

                Java 需要做的几件事:

                • 重新加载 this.array - 因为它可以被更改(通过调用或其他线程)
                • 检查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 ,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 变成了 i ,并且之前的优化失败,java需要检查i是否在this.array的范围内。

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

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

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

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

                【讨论】:

                  猜你喜欢
                  • 2016-06-02
                  • 2012-08-06
                  • 2010-11-13
                  • 1970-01-01
                  • 2015-11-12
                  • 1970-01-01
                  • 2014-01-11
                  • 2010-11-08
                  相关资源
                  最近更新 更多