【问题标题】:Efficient loop through Java ListJava List 的高效循环
【发布时间】:2012-07-31 20:54:11
【问题描述】:

以下列表来自 2008 年名为“Dalvik Virtual Machine Internals”的谷歌 I/O 演讲,它列出了按效率从高到低依次循环一组对象的方法:

(1) for (int i = initializer; i >=0; i--) //hard to loop backwards
(2) int limit = calculate_limit(); for (int i= 0; i< limit; i++)
(3) Type[] array = get_array(); for (Type obj : array)
(4) for (int i =0; i< array.length; i++) //gets array.length everytime
(5) for (int i=0; i < this.var; i++) //has to calculate what this.var is
(6) for (int i=0; i < obj.size(); i++) //even worse calls function  each time
(7) Iterable list = get_list(); for (Type obj : list) //generic object based iterators slow!

前 3 个效率相同,如果可能,请避免 7 个。这主要是帮助延长电池寿命的建议,但也可能有助于 Java SE 代码。

我的问题是:为什么 (7) 慢,为什么 (3) 好?我认为这可能是(3)和(7)的数组和列表之间的区别。此外,正如 Dan 提到的 (7) 创建了大量必须进行 GC 处理的小型临时对象,我现在对 Java 有点生疏了,有人可以解释为什么吗?它在他的talk video 中,时间为 0:41:10。

【问题讨论】:

  • 请注意,这只适用于 ArrayList(如果有的话)。按索引迭代到 LinkedList 非常昂贵。 (7) 只会创建一个临时对象,我真的怀疑差异是否可测量。我大多数程序都花时间对列表中的每个对象做一些事情,而不是迭代机制本身......

标签: java android optimization dalvik


【解决方案1】:

我猜你必须将对象编组为基于“链表”的迭代器,然后支持 API,而不是内存块和指针(数组)

【讨论】:

    【解决方案2】:

    第三种变体比 7 更快,因为数组是具体类型,JVM 应该只分配一个指向正确值的指针。但是当你遍历一个集合时,编译器可以因为擦除而执行额外的强制转换。实际上,编译器将此强制转换插入到通用代码中以确定一些肮脏的技巧,例如尽快使用已弃用的原始类型。

    P.S. 这只是猜测。事实上,我认为编译器和 JIT 编译器可以执行任何优化(即使在运行时也可以进行 JIT),结果可能取决于 JVM 版本和供应商等特定细节。

    【讨论】:

      【解决方案3】:

      我猜编译器对此进行了优化 (3)(这是我猜测的部分):

      for (int i =0; i < array.length; ++i)
      {
          Type obj = array[i];
      
      }
      

      并且 (7) 无法优化,因为编译器不知道它是哪种 Iterable。这意味着它确实必须在堆上创建一个new Iterator。分配内存是昂贵的。每次你请求下一个对象时,它都会经过一些调用。

      粗略地描述一下 (7) 被编译时会发生什么(确定这一点):

      Iterable<Type> iterable = get_iterable();
      Iterator<Type> it = iterable.iterator(); // new object on the heap
      while (it.hasNext()) // method call, some pushing and popping to the stack
      {
          Type obj = it.next(); // method call, again pushing and popping
      
      
      }
      

      【讨论】:

      • JVM 是一个堆栈机器,为什么内存分配很昂贵? JVM 应该只是向前移动堆栈指针。
      • JVM 也有一个堆...您使用的每个new 都是在堆上创建的。想一想:不可能在堆栈上创建所有news。
      • Dalvik 是基于寄存器的,而不是基于堆栈的。
      • 所以它没有栈,但是有堆。
      • Iterator it = iterable.iterator();为什么这会发生在堆上?如果是这样,如果我理解正确,它只分配迭代器对象一次(而不是每个项目一次)?
      【解决方案4】:

      我觉得我的第一个答案并不令人满意,真的无助于解释问题;我已经发布了指向this site 的链接并进行了详细说明,其中涵盖了一些基本用例,但并未涉及问题的本质。因此,我继续进行了一些动手研究。

      我运行了两个单独的代码:

          // Code 1
          int i = 0;
          Integer[] array = { 1, 2, 3, 4, 5 };
          for (Integer obj : array) {
              i += obj;
          }
          System.out.println(i);
      
          // Code 2
          int i = 0;
          List<Integer> list = new ArrayList<>();
          list.add(1);
          list.add(2);
          list.add(3);
          list.add(4);
          list.add(5);
          for (Integer obj : list) {
              i += obj;
          }
          System.out.println(i);
      

      当然,两者都打印出15,并且都使用Integer 数组(没有ints)。

      接下来,我使用javap 来反汇编这些并查看字节码。 (我忽略了初始化;for 循环之前的所有内容都被注释掉了。)由于这些内容很长,我将它们发布在 PasteBin here

      现在,虽然代码 1 的字节码实际上更长,但它的密集度较低。它只使用一次invokevirtual(除了println),不需要其他调用。在代码 1 中,似乎将迭代优化为基本循环;检查数组长度,并加载到我们的变量中,然后添加到i。这似乎被优化为行为完全for (int i = 0; i &lt; array.length; i++) { ... }

      现在,在代码 2 中,字节码变得更加密集。除了上面需要的所有其他调用之外,它还必须进行 2 次 invokeinterface 调用(都到 Iterator)。此外,代码 2 必须调用 checkcast,因为它是通用的 Iterator(正如我上面提到的,它没有经过优化)。现在,尽管对loadstore 操作的调用减少了,但上述这些调用涉及更多的开销。

      正如他在视频中所说,如果您发现自己需要很多做这些,您可能会遇到问题。例如,在Activity 的开头运行一个可能没什么大不了的。请注意创建其中的许多,尤其是在 onDraw 中进行迭代,例如。

      【讨论】:

        【解决方案5】:

        这个列表有点过时了,今天应该没什么用。

        几年前这是一个很好的参考,当时 Android 设备运行缓慢且资源非常有限。 Dalvik VM 实现目前也缺乏很多可用的优化。

        在此类设备上,简单的垃圾回收很容易花费 1 或 2 秒(相比之下,当今大多数设备大约需要 20 毫秒)。在 GC 期间,设备刚刚冻结,因此开发人员必须非常小心内存消耗。

        你今天不必太担心,但如果你真的关心性能,这里有一些细节:

        (1) for (int i = initializer; i >= 0; i--) //hard to loop backwards
        (2) int limit = calculate_limit(); for (int i=0; i < limit; i++)
        (3) Type[] array = get_array(); for (Type obj : array)
        

        这些很容易理解。 i &gt;= 0 的计算速度比 i &lt; limit 快​​,因为它在进行比较之前不会读取变量的值。它直接与整数文字一起使用,速度更快。

        我不知道为什么 (3) 应该比 (2) 慢。编译器应该会产生与 (2) 相同的循环,但可能 Dalvik 虚拟机此时没有正确优化它。

        (4) for (int i=0; i < array.length; i++) //gets array.length everytime
        (5) for (int i=0; i < this.var; i++) //has to calculate what this.var is
        (6) for (int i=0; i < obj.size(); i++) //even worse calls function  each time
        

        这些已经在 cmets 中解释过了。

        (7) Iterable list = get_list(); for (Type obj : list)
        

        Iterables 很慢,因为它们分配内存、进行一些错误处理、在内部调用多个方法……所有这些都比在每次迭代中只调用一个函数的 (6) 慢得多。

        【讨论】:

        • 非常感谢您的回答,关于为什么会出现“迭代器分配内存”的更多详细信息?谢谢
        • 我只知道这是一种在内部降低Iterator.remove() 复杂性的设计选择(也许还可以加快速度)。
        • 正在阅读这个 [java-temporary-iterators-are-slowing-my-android-game] (stackoverflow.com/questions/11490240/…)
        【解决方案6】:

        如果是 Android,这是 2015 年 Google Developers 的视频。

        索引还是迭代? (Android 性能模式第 2 季第 6 集) https://www.youtube.com/watch?v=MZOf3pOAM6A

        他们在 DALVIK 运行时 4.4.4 版本上进行了 10 次测试,以获得平均结果。 结果显示“For index”是最好的。

        int size = list.size();
        for (int index = 0; index < size; index++) {
            Object object = list.get(index);
            ...
        }

        他们还建议在视频结尾处自己在您的平台上进行类似的测试。

        【讨论】:

          猜你喜欢
          • 2021-10-23
          • 2016-01-03
          • 1970-01-01
          • 2017-04-03
          • 1970-01-01
          • 2012-03-19
          • 2015-10-08
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多