【问题标题】:Performance of traditional for loop vs Iterator/foreach in JavaJava 中传统 for 循环与 Iterator/foreach 的性能对比
【发布时间】:2015-07-25 05:19:40
【问题描述】:

在遍历 ArrayList、HashMap 和其他集合时,比较传统的 for 循环和迭代器是否有任何性能测试结果?

或者干脆为什么我应该使用迭代器而不是 for 循环,反之亦然?

【问题讨论】:

  • 请注意,链表的 for 循环速度较慢的原因是,每次对 get(i) 的调用都从列表的头部迭代 i 次。我相信这对这里的其他人来说是直观的,但我花了一分钟才弄清楚为什么会这样。
  • @Harish 有见地

标签: java map iterator arraylist for-loop


【解决方案1】:

对生成的代码使用JADJD-GUI,您会发现没有真正的区别。新迭代器形式的优点是它在您的代码库中看起来更干净。

编辑:我从其他答案中看到,您实际上是指使用 get(i) 与迭代器之间的区别。我将最初的问题理解为使用迭代器的新旧方式之间的区别。

使用 get(i) 并维护自己的计数器,尤其是对于 List 类不是一个好主意,原因在接受的答案中提到。

【讨论】:

    【解决方案2】:

    假设这就是你的意思:

    // traditional for loop
    for (int i = 0; i < collection.size(); i++) {
      T obj = collection.get(i);
      // snip
    }
    
    // using iterator
    Iterator<T> iter = collection.iterator();
    while (iter.hasNext()) {
      T obj = iter.next();
      // snip
    }
    
    // using iterator internally (confirm it yourself using javap -c)
    for (T obj : collection) {
       // snip
    }
    

    对于没有随机访问的集合(例如 TreeSet、HashMap、LinkedList),迭代器更快。对于数组和 ArrayList,性能差异应该可以忽略不计。

    编辑:我相信微基准测试是万恶之源,就像早期优化一样。但话又说回来,我认为对这些非常微不足道的事情的含义有一种感觉是件好事。因此我运行了a small test

    • 分别遍历 LinkedList 和 ArrayList
    • 包含 100,000 个“随机”字符串
    • 总结它们的长度(只是为了避免编译器优化整个循环)
    • 使用所有 3 种循环样式(迭代器、for each、for with counter)

    结果与 LinkedList 的“for with counter”不同。其他五个都用了不到 20 毫秒的时间来遍历整个列表。在 LinkedList 上使用 list.get(i) 100,000 次需要超过 2 分钟 (!) 才能完成(慢 60,000 倍)。哇! :) 因此,最好使用迭代器(显式或隐式地使用每个迭代器),特别是如果您不知道要处理的列表的类型和大小。

    【讨论】:

    • 您的 LinkedList 结果显示当您从 O(n) 变为 O(n^2)(或更多)时会发生什么
    • 所有其他五个迭代整个列表的时间不到 20 毫秒 看起来 JVM 死代码优化开始了...... LinkedList 的迭代和迭代之间的区别ArrayList 很重要(有利于 ArrayList)
    • @bestsss 不,当然没有。我生成了 100,000 个随机字符串(实际上是 UUID)并将它们的长度相加,在循环后打印到标准输出。当然,UUID 具有相同的长度,这使得输出可预测,但编译器并不那么聪明。信不信由你,现代 CPU 可以在 20 毫秒内完成。换个角度看:我的 CPU 每个内核有 4,000 个 BogoMips。所以我们说的是每秒数十亿条指令或每毫秒数百万条指令。因此,用数百万条指令迭代超过 100,000 个字符串是可行的。 CPU 比大多数开发人员想象的要快 :)
    • 总结这是一个可行的选择,编译器不会优化任何东西(除了像疯了一样预取)。该案例也非常适合 L2 缓存(即使带有 LinkedList)。如果不是所有元素都按顺序添加,则退出 L2 缓存将对 LinkedList 产生更大的影响。
    • 混合方式呢? )) Iterator&lt;T&gt; iter = collection.iterator(); int l = collection.size(); for (int i = 0, i &lt; l; i++) { T obj = iter.next(); // snip }
    【解决方案3】:

    在大多数情况下性能相似。

    但是,每当代码接收到 List 并在其上循环时,就会出现众所周知的情况:
    Iterator 对于所有未实现 RandomAccess 的 List 实现都更好(例如: 链表)。

    原因是对于这些列表,按索引访问元素并不是一个固定时间的操作。

    因此,您也可以认为迭代器更健壮(对于实现细节)。


    一如既往,性能不应隐藏可读性问题。
    java5 foreach 循环在这方面大受欢迎:-)

    【讨论】:

    • 谢谢,但是 ArrayList 呢?
    • ArrayList 实现了 RandomAccess,因此 list.get(i) 很快。性能差异应该可以忽略不计。
    • 注意:虽然我不知道 JDK 中的 LinkedList 是否以这种方式编写,但编写一个 LinkedList 实现将是微不足道的,因为传统 for 循环的执行速度与随机访问一样快.所需要的只是保持一个内部指针,指向请求随机访问的最后一个元素。这似乎是一个微不足道的实现,它会加速如此多的代码,我无法想象它不在那里。
    • @tster:实际上这正是迭代器所做的。
    【解决方案4】:

    在 i++ 语法上使用迭代器的最佳理由之一是,并非所有数据结构都支持随机访问,更不用说让它运行良好了。您还应该对列表或集合接口进行编程,这样如果您后来决定使用另一种数据结构更有效,您就可以在不进行大规模手术的情况下将其替换掉。在那种情况下(对接口进行编码的情况),您不一定知道实现细节,将其推迟到数据结构本身可能更明智。

    【讨论】:

      【解决方案5】:

      我学会坚持使用 for each 的原因之一是它简化了嵌套循环,尤其是超过 2 维的循环。您可能最终操纵的所有 i、j 和 k 都会很快变得混乱。

      【讨论】:

        【解决方案6】:

        使用迭代器的第一个原因是明显的正确性。如果您使用手动索引,可能会出现非常无害的逐一错误,只有仔细观察才能看到:您是从 1 开始还是从 0 开始?你在length - 1 完成了吗?您使用的是&lt; 还是&lt;=?如果您使用迭代器,则更容易看出它实际上是在迭代整个数组。 “说你做的,做你说的。”

        第二个原因是对不同数据结构的统一访问。可以通过索引有效地访问数组,但最好通过记住最后访问的元素来遍历链表(否则您会得到“Shlemiel the painter”)。哈希图更加复杂。通过提供来自这些和其他数据结构的统一接口(例如,您还可以进行树遍历),您再次获得明显的正确性。遍历逻辑只需要实现一次,使用它的代码可以简洁地“说它做什么,做它说什么”。

        【讨论】:

          【解决方案7】:

          +1 对 sfussenegger 所说的话。仅供参考,无论您使用显式迭代器还是隐式迭代器(即每个迭代器)都不会产生性能差异,因为它们编译为相同的字节码。

          【讨论】:

          • 它们不会被编译成相同的字节码。 forEach 循环遍历一个可迭代对象并获得遍历列表的迭代器。对于链表,get(i) 方法从第一个节点开始,一直遍历并返回对象。因此,如果您每次从头开始都使用 i=1 到 5 。请参阅下面的答案。
          • 我的答案是将 forEach 与显式使用迭代器进行比较,而不是将其与使用索引变量的传统 for 循环进行比较。 docs.oracle.com/javase/specs/jls/se7/html/…
          【解决方案8】:

          我不相信

          for (T obj : collection) {
          

          每次循环都会计算 .size() ,因此比

          for (int i = 0; i < collection.size(); i++) {
          

          【讨论】:

          • 使用for (int i = 0, l = collection.size(); i &lt; l; i++) {轻松修复
          • 第一个通过调用collection.iterator()方法获得集合迭代器,然后通过调用迭代器的next()和hasNext()方法进行迭代。
          【解决方案9】:

          是的,它确实对不基于随机访问的集合(如 LinkedList)产生了影响。内部链表由指向下一个(从头节点开始)的节点实现。

          链表中的 get(i) 方法从头节点开始,并通过链接一直导航到第 i 个节点。使用传统的for循环对链表进行迭代时,每次都是从头节点重新开始,这样整个遍历就变成了二次时间。

          for( int i = 0; i< list.size(); i++ ) {
              list.get(i); //this starts everytime from the head node instead of previous node
          }
          

          而 for each 循环遍历从链表获得的迭代器并调用其 next() 方法。迭代器维护上次访问的状态,因此不会每次都从头开始。

          for( Object item: list ) {
              //item element is obtained from the iterator's next method.
          }
          

          【讨论】:

            猜你喜欢
            • 2011-03-26
            • 2017-07-14
            • 1970-01-01
            • 1970-01-01
            • 2012-08-18
            • 2010-10-29
            • 2021-12-09
            • 2010-11-13
            相关资源
            最近更新 更多