通过Java List高效循环

时间:2012-08-02 20:12:21

标签: java android optimization dalvik

以下列表来自2008年的谷歌I / O演讲,名为“Dalvik虚拟机内部”,它列出了从最高效率到最低效率循环遍历一组对象的方法:

(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!

前三个处于相同的效率区域,如果可能,请避免使用7个。这主要是帮助电池寿命的建议,但也可能有助于java SE代码。

我的问题是:为什么(7)慢,为什么(3)好?我认为它可能是(3)和(7)的数组和列表之间的区别。另外,正如Dan提到的那样(7)创建了大量临时对象,这些对象必须是GCed,现在我对Java有点生疏,有人可以解释为什么吗?这是他talk video 0:41:10一分钟。

6 个答案:

答案 0 :(得分:7)

这个列表有点过时,今天不应该真的有用。

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

在这样的设备上,简单的垃圾收集很容易 1或2秒(相比之下,今天大多数设备需要大约20ms)。在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 >= 0的评估速度比i < limit更快,因为它在进行比较之前不会读取变量的值。它直接使用整数文字,速度更快。

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

(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)

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

答案 1 :(得分: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数组(无int s)。

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

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

现在,在代码2中,字节码变得更加密集。除了上面需要的每个其他呼叫之外,它还必须进行2次invokeinterface次呼叫(均为Iterator)。此外,代码2必须调用checkcast,因为它是通用Iterator(如上所述,它未经过优化)。现在,尽管对loadstore操作的调用较少,但上述调用涉及大量开销。

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

答案 2 :(得分:1)

我想编译器会优化(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


}

答案 3 :(得分:0)

我猜你必须将对象编组到基于Iterator的“链表”中,然后支持API,而不是一块内存和一个指针(数组)

答案 4 :(得分:0)

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

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

答案 5 :(得分:0)

对于Android,这是2015年Google开发者的视频。

要索引还是迭代? (Android Performance Patterns第2季ep6) 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);
    ...
}

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