【问题标题】:Which idiom for iterating an array is most efficient?迭代数组的哪个习语最有效?
【发布时间】:2013-07-05 03:23:00
【问题描述】:

Java 5 为我们提供了 for-each 循环,应尽可能使用它。

但是如果你需要在块中使用数组的索引,什么是最有效的习语呢?

// Option (1)
for (int i = array.length - 1; i >= 0; i--) {
    // Code.
}

// Option (2)
for (int i = 0; i < array.length; i++) {
    // Code.
}

// Option (3)
for (int i = 0, n = array.length; i < n; i++) {
    // Code.
}

(显然,这在大多数程序中不会产生很大的性能差异,但让我很高兴。):-)

  1. 向后循环,非常可怕。也许甚至缓存不友好?或者现代处理器可以检测到内存中的倒退吗?

  2. 更短,我可以看到 JIT 编译器如何确定 array 永远不会改变,因此 length 是恒定的,因此它基本上可以用 (3) 替换它。但它会这样做吗? (假设 JVM 是 Oracle 的 Hotspot/Java 7。)

  3. 由 Joshua Bloch 在 Effective Java 的第 45 项中建议作为最快的选项,如果它是一些 Collection.size() 是上限。但它也适用于数组吗?从字节码(见下文)我可以看到每个周期保存一条arraylength 指令(预优化)。

This question 关于 Dalvik 虚拟机中的 for 循环,将 (1)-(3) 列为最快到最慢。但是,这些信息来自 2008 年,而 Dalvik 今天已经成熟得多,所以我认为情况仍然如此。

看上面的例子生成的字节码,有明显的区别:

Compiled from "ForLoops.java"
public class ForLoops extends java.lang.Object{
static int[] array;

public ForLoops();
  Code:
   0:   aload_0
   1:   invokespecial   #10; //Method java/lang/Object."<init>":()V
   4:   return

public static void forLoop1();
  Code:
   0:   getstatic   #17; //Field array:[I
   3:   arraylength
   4:   iconst_1
   5:   isub
   6:   istore_0
   7:   goto    13
   10:  iinc    0, -1
   13:  iload_0
   14:  ifge    10
   17:  return

public static void forLoop2();
  Code:
   0:   iconst_0
   1:   istore_0
   2:   goto    8
   5:   iinc    0, 1
   8:   iload_0
   9:   getstatic   #17; //Field array:[I
   12:  arraylength
   13:  if_icmplt   5
   16:  return

public static void forLoop3();
  Code:
   0:   iconst_0
   1:   istore_0
   2:   getstatic   #17; //Field array:[I
   5:   arraylength
   6:   istore_1
   7:   goto    13
   10:  iinc    0, 1
   13:  iload_0
   14:  iload_1
   15:  if_icmplt   10
   18:  return

}

【问题讨论】:

  • 理论上,1 和 3 比 2 更有效,在一般情况下,因为 array.length 只需要评估一次。但是,在 Java 中评估 length 非常简单,尤其是在 JITCed 的情况下,因此理论上的差异在实践中几乎消失了。这很可能取决于循环体中的内容以及 JITC 优化器如何将循环控制与循环体融合。
  • @Nicholas 提取字节码做得很好!但重要的是要记住,字节码最终并不是由于 JIT 而被执行的。如果我们能得到 JIT 指令,那 是最终答案,但这可能是不现实的,因为 (a) 它是特定于平台的,并且 (b) JIT 有不同的“级别”。另外,我不知道有任何 JIT 可以让运行时生成的代码具有任何透明度,至少在你自己在 JVM 内存空间中四处寻找的情况下是这样。
  • 我当然应该指出,“最有效”的选择通常是最适合其余代码的选择。例如,向后迭代有时很尴尬,有时非常有用。同样,如果数组对象在循环内被替换,实际上可能需要在每次迭代中检查array.length
  • 好点。但这个问题是关于许多人使用的典型案例。如果直截了当的解决方案是最有效的,那么很多人可以继续以他们的方式编写代码,并且心存平静(包括我自己)。

标签: java arrays performance for-loop


【解决方案1】:

您可以自己轻松地进行测试;如果你这样做了,你可能应该看看 HotSpot 创作者自己的these tips about performance testingThis answer 也可能有用。如果您决定测试这些实现,请告诉我们您的发现!

不过,总的来说,您不必过于担心这些事情。相反,专注于编写可读的代码并完成工作。大多数时候,您会发现您的代码运行速度足够快,没有任何“技巧”。现代硬件非常快,JIT 也非常好。

如果您确实发现您的代码运行速度太慢,先配置文件,然后再进行优化。其他任何事情都为时过早。请记住,来自一个比我们任何人都聪明的人:

“过早的优化是万恶之源。” ——唐纳德·克努斯

编辑:因为您似乎对“我应该如何编写我的代码?”这个问题不太感兴趣。以及更多的思想实验,我希望所有这些选项都以或多或少相同的速度运行。

这些循环都不太可能调整分支预测器(无论如何,对于大小合理的数组)。我希望底层 JIT 将任何重复的数组长度引用从 (2) 样式转换为 (3) 样式。在所有条件相同的情况下,(1)的缓存性能并不比(2)或(3)的差,因为它向后运行;对于给定的数组,相同的缓存行将被加载和命中(或不命中)。

当然,我的期望无关紧要。唯一知道的方法就是测试!但是,在测试时,请记住 writing good microbenchmarks is hard

【讨论】:

  • 我可以测试它,但我宁愿对 JVM 的功能有一般的了解。我们还假设它对性能至关重要。
  • @Nicholas 我觉得你想用 C 语言编写代码,但要求使用 Java :)
  • 不,一点也不——我认为 Java 很棒,但在性能方面有时不太确定。 (罪魁祸首当然是 GC,这也是快速生成新功能的最大福气)。
  • @Nicholas 好的,..我的答案是:: 根据定义,数组处于继续内存分配中(所以 --/++ 没关系),.length 不是函数调用,所以我认为都应该同样快。还要考虑优化阶段。
  • @GrijeshChauhan -- 实际上,对于优化器++-- 在寻址数组时可能会很重要,因为在移动计算时必须考虑溢出和其他问题。但是,要预测哪个更好,至少需要了解特定的优化器算法和目标指令集。
猜你喜欢
  • 1970-01-01
  • 2015-06-12
  • 2012-08-07
  • 1970-01-01
  • 2017-10-20
  • 1970-01-01
  • 1970-01-01
  • 2023-03-11
  • 2021-02-18
相关资源
最近更新 更多