在Java中迭代数组的最快方法:循环变量 vs 增强型for语句

37

在Java中,以传统的方式迭代数组是否更快?

for (int i = 0; i < a.length; i++)
    f(a[i]);

或者使用更简洁的形式:

for (Foo foo : a)
    f(foo);

对于ArrayList,答案是否相同?

当然,在大部分应用代码中,答案是没有明显差别的,因此应该使用更简洁的形式以增强可读性。然而,我关注的上下文是重度技术计算,需要执行数十亿次操作,因此即使微小的速度差异也可能具有重要意义。


这个问题之前已经被问过了:https://dev59.com/WnVC5IYBdhLWcg3wjx1d。 - RichardOD
为什么不运行基准测试?比讨论更准确。 - Deestan
那不是一个数组。在“之前询问的问题”中,内部将使用foreach循环创建迭代器,而根据这里的答案,这里不会创建迭代器。 - Albert Hendriks
除非 f() 非常短,否则您不太可能注意到差异。 - user207421
从技术上讲,这确实是一个不同的问题。数组和列表之间有区别。因此,技术实现是不同的,可能会导致不同的结果。Jon Skeet的答案对于数组非常好。 - Gunnar Bernstein
6个回答

68
如果您遍历一个数组,使用增强型for循环不会有任何问题——增强型for循环本身就使用数组访问方式。例如,考虑以下代码:
public static void main(String[] args)
{
    for (String x : args)
    {
        System.out.println(x);
    }
}

使用 javap -c Test 反编译后,我们得到以下内容(针对 main 方法):

public static void main(java.lang.String[]);
  Code:
   0:   aload_0
   1:   astore_1
   2:   aload_1
   3:   arraylength
   4:   istore_2
   5:   iconst_0
   6:   istore_3
   7:   iload_3
   8:   iload_2
   9:   if_icmpge   31
   12:  aload_1
   13:  iload_3
   14:  aaload
   15:  astore  4
   17:  getstatic   #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   20:  aload   4
   22:  invokevirtual   #3; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   25:  iinc    3, 1
   28:  goto    7
   31:  return

现在将其更改为使用显式的数组访问:

public static void main(String[] args)
{
    for (int i = 0; i < args.length; i++)
    {
        System.out.println(args[i]);
    }
}

这个反编译的结果是:

public static void main(java.lang.String[]);
  Code:
   0:   iconst_0
   1:   istore_1
   2:   iload_1
   3:   aload_0
   4:   arraylength
   5:   if_icmpge   23
   8:   getstatic   #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   11:  aload_0
   12:  iload_1
   13:  aaload
   14:  invokevirtual   #3; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   17:  iinc    1, 1
   20:  goto    2
   23:  return

增强型for循环中有一些额外的设置代码,但它们基本上在做相同的事情。 没有涉及到迭代器。 此外,我希望它们能够被JIT编译成更相似的代码。

建议:如果你真的认为这可能会产生显着的差异(只有当循环体绝对微不足道时),那么你应该用你的实际应用程序进行基准测试。 这是唯一重要的情况。


23

这完全属于 微观优化 的范畴。但其实并不重要。从风格上来讲,我倾向于第二种方式,因为它更加简明扼要,除非你需要循环计数器来做其他的事情。而这些远比这种微观优化更为重要:可读性。

话虽如此,对于ArrayList来说,两种方式的差别不大,但是对于LinkedList来说,第二种方式会更加高效。


5
它不会对数组使用迭代器。 - Jon Skeet
1
第一个样式的迭代次数受到整型最大值(Integer.MAX)的限制,而第二个样式则没有。 - Chii
2
第二种方式也受到此限制,因为数组最多只能有 Integer.MAX_VALUE 个元素,并且 ArrayList 是由一个数组支持的。 - cletus

8

测量它。所有性能问题的答案都可能取决于虚拟机版本、处理器、内存速度、缓存等因素。因此,您必须针对您特定的平台进行测量。

个人而言,我更喜欢第二种变体,因为意图更加清晰。如果性能成为问题,我随时可以优化它 - 如果该代码确实对整个应用程序的性能很重要。


2

1
在数组或RandomAccess集合上,您可以通过执行以下操作轻微提高速度:
List<Object> list = new ArrayList<Object>();

for (int i=0, d=list.size(); i<d; i++) {
    something(list.get(i));
}

但是一般情况下我不会担心这个问题。像这样的优化对你的代码影响不会超过0.1%。尝试使用-prof参数来调用Java,以查看你的代码实际花费时间的地方。


1

如果您有足够大的数据集,更快的方法是使用分叉-合并框架的ParallelArray。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接