通过 Java 列表的高效循环

2022-09-03 07:31:52

以下列表来自2008年的Google I / O谈话,称为“Dalvik Virtual Machine Internals”,它是按从最高到最低效率的顺序遍历一组对象的方法列表:

(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)创建了大量的小型临时对象,这些对象必须进行GCed,我现在在Java上有点生锈,有人可以解释为什么吗?这是他在0:41:10的谈话视频中一分钟。


答案 1

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

几年前,这是一个很好的参考,当时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 >= 0i < 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)慢得多,后者在每次迭代中只执行一次函数调用。


答案 2

我觉得我的第一个答案并不令人满意,真的无助于解释这个问题。我已经发布了这个网站的链接,并详细说明了一些,它涵盖了一些基本的用例,但不是问题的细节。所以,我继续做了一些动手研究。

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

    // 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);

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

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

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

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

正如他在视频中所说,如果你发现自己需要做很多这些,你可能会遇到问题。例如,在 开头运行一个 可能没什么大不了的。只是要小心创建其中的许多,特别是迭代 例如。ActivityonDraw


推荐