关于java:调用array.length的成本是多少

What is the Cost of Calling array.length

在我们的应用程序中将for循环更新为for-each循环时,我遇到了很多这样的"模式":

1
2
3
for (int i = 0, n = a.length; i < n; i++) {
    ...
}

代替

1
2
3
for (int i = 0; i < a.length; i++) {
    ...
}

我可以看到您获得了集合的性能,因为您不需要在每个循环中调用size()方法。 但是有了数组?

因此出现了一个问题:array.length是否比常规变量贵?


不,对array.length的调用是O(1)或恒定时间操作。

由于.length是(作用类似于)arraypublic final成员,因此访问速度不会比局部变量慢。 (这与对size()之类的方法的调用非常不同)

无论如何,现代的JIT编译器很可能会立即优化对.length的调用。

您可以通过查看OpenJDK中的JIT编译器的源代码,或者通过使JVM转储JIT编译的本机代码并检查代码来确认这一点。

注意,在某些情况下,JIT编译器无法做到这一点;例如

  • 如果要调试封闭方法,或者
  • 如果循环体具有足够的局部变量以强制寄存器溢出。

  • 我在午餐时间有一点时间:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public static void main(String[] args) {
        final int[] a = new int[250000000];
        long t;

        for (int j = 0; j < 10; j++) {
            t = System.currentTimeMillis();
            for (int i = 0, n = a.length; i < n; i++) { int x = a[i]; }
            System.out.println("n = a.length:" + (System.currentTimeMillis() - t));

            t = System.currentTimeMillis();
            for (int i = 0; i < a.length; i++) { int x = a[i]; }
            System.out.println("i < a.length:" + (System.currentTimeMillis() - t));
        }
    }

    结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    n = a.length: 672
    i < a.length: 516
    n = a.length: 640
    i < a.length: 516
    n = a.length: 656
    i < a.length: 516
    n = a.length: 656
    i < a.length: 516
    n = a.length: 640
    i < a.length: 532
    n = a.length: 640
    i < a.length: 531
    n = a.length: 641
    i < a.length: 516
    n = a.length: 656
    i < a.length: 531
    n = a.length: 656
    i < a.length: 516
    n = a.length: 656
    i < a.length: 516

    笔记:

  • 如果您逆转测试,则n = a.length的显示速度比i < a.length快大约一半,这可能是由于垃圾回收(?)所致。
  • 我不能将250000000变得更大,因为在270000000处有OutOfMemoryError
  • 关键是,这是其他所有人都在做的事情,您必须在内存不足的情况下运行Java,并且您仍然看不到这两种选择之间的速度差异显着。将开发时间花在实际重要的事情上。


    我怀疑是否存在任何重大差异,即使存在差异,我敢打赌它可能在编译期间已被优化。当您尝试对诸如此类的事情进行微优化时,您正在浪费时间。首先使代码可读性和正确性,然后如果遇到性能问题,请使用探查器,然后担心在适当时选择更好的数据结构/算法,然后担心优化探查器突出显示的部分。


    数组的长度在Java中作为数组的成员变量(与元素不同)存储,因此获取该长度是恒定时间的操作,与从常规类中读取成员变量相同。许多较旧的语言(例如C和C ++)不会将长度存储为数组的一部分,因此您需要在循环开始之前存储长度。您无需在Java中执行此操作。


    在这种情况下,为什么不进行反向循环:

    1
    2
    3
    for (int i = a.length - 1; i >= 0; --i) {
        ...
    }

    这里有2个微优化:

    • 循环反转
    • 后缀递减


    将其存储在变量中可能稍微快一些。但是,如果分析器指出这是一个问题,我将感到非常惊讶。

    在字节码级别,使用arraylength字节码获取数组的长度。我不知道它是否比iload字节码慢,但是应该没有足够的差异来引起注意。


    这个答案是从C#的角度来看的,但我认为Java同样适用。

    在C#中,这个成语

    1
    for (int i = 0; i < a.length; i++) {    ...}

    被认为是遍历数组,因此在循环中访问数组时(而不是每次访问数组时)都避免了边界检查。

    这可能会或可能不会被以下代码识别:

    1
    for (int i = 0, n = a.length; i < n; i++) {    ...}

    要么

    1
    2
    3
    n = a.length;

    for (int i = 0; i < n; i++) {   ...}

    这种优化有多少是由编译器和我不知道的JITter执行的,特别是如果它是由JITter执行的,我希望所有这3种都能生成相同的本机代码。

    但是,第一种形式也可以说是人们更易读的,所以我想说一遍。


    Array.length是一个常量,JIT编译器应该在两种情况下都可以看到该常量。我希望两种情况下产生的机器代码都相同。至少对于服务器编译器。