【问题标题】:Using collection size in for loop comparison在 for 循环比较中使用集合大小
【发布时间】:2010-12-14 11:36:45
【问题描述】:

Java 中 Collections 的 size() 方法是否有编译器优化?

考虑以下代码:

for(int i=0;i<list.size();i++)
      ...some operation.....

每个 i 都会调用 size() 方法。找出尺寸并重复使用不是更好吗? (方法调用有开销)。

final int len = list.size()
for(int i=0;i<len;i++)
      ...some operation.....

但是,当我对这两个代码段进行计时时,没有明显的时间差异,即使 i 高达 10000000。 我在这里遗漏了什么吗?

Update1:​​我知道除非集合发生变化,否则不会再次计算大小。但是必须有一些与方法调用相关的开销。编译器是否总是内联这些(参见 Esko 的答案)?

更新 2: 我的好奇心被进一步激发。从给出的答案中,我看到好的 JIT 编译器通常会内联这个函数调用。但是他们仍然必须确定该集合是否被修改。我不接受答案,希望有人能给我一些关于编译器如何处理的指针。

【问题讨论】:

  • 最好不要担心这样的事情,直到分析器向您显示这是您的应用程序的实际瓶颈,可能永远不会出现这种情况。拥有更易读的代码比速度快得可以忽略不计的代码要好。但从纯学术的角度来看,这仍然是一个很好的问题。
  • @Sergey:是的。我进行的简单测试表明我不应该担心效率。因此,更新。但这激起了我的好奇心。请参阅我对汤姆安德森评论的回复。

标签: java collections


【解决方案1】:

好的,这里是 JDK 源代码的摘录(JDK 文件夹中的 src.zip):

public int size() {
    return size;
}

这是来自 ArrayList,但我认为其他集合也有类似的实现。现在,如果我们想象编译器内联 size() 调用(这很有意义),你的循环会变成这样:

for(int i=0;i<list.size;i++)
// ...

(好吧,让我们忘记大小是私有的。)编译器如何检查集合是否被修改?答案是它不需要也不需要这样做,因为字段中已经存在大小,所以它所要做的就是在每次迭代时访问 size 字段,但是访问 int 变量非常快手术。请注意,它可能只计算一次地址,因此它甚至不必在每次迭代时取消引用列表。

当集合被修改时会发生什么,比如通过 add() 方法?

public boolean add(E e) {
    ensureCapacity(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

如您所见,它只是增加了 size 字段。所以编译器实际上不需要做任何事情来确保它可以访问最新的大小。唯一的例外是,如果您从需要同步的另一个线程修改集合,否则循环线程可能会看到其本地缓存的 size 值,该值可能会更新,也可能不会更新。

【讨论】:

    【解决方案2】:

    集合的.size() 方法返回的值通常仅在实际集合被修改(添加新元素或删除旧元素)时才被缓存和重新计算。

    不要比较for 循环控制范围,而是尝试使用for each 循环,因为它实际上使用Iterator,这在某些集合实现中比使用索引进行迭代要快得多。

    【讨论】:

    • 例如使用 LinkedList 时。
    • @Esko:缓存是指像 ArrayList 这样的子类中的字段“大小”吗?那仍然是一个方法调用,不是吗?还是这样,方法调用在java中没有太多开销?
    • @athena:是的,该字段的值仅根据需要重新计算。随着时间的推移,JVM 通过内联实际字段访问来优化方法调用,因此从技术上讲,尤其是在长时间运行的应用程序中,大多数方法调用根本没有任何开销。
    • @Esko:这不是问题所在,但是您能否举一个集合的示例,其中使用迭代器比通过索引迭代更快(除了 LinkedList)?
    • @Esko:谢谢。你能给我一些关于这种字段内联的文章/文档的指针吗?
    【解决方案3】:

    调用集合的 size() 方法只是返回一个已经被跟踪的整数值。没有太大的时间差异,因为 size() 实际上并没有计算项目的数量,而是在您添加或删除它们时跟踪项目的数量。

    【讨论】:

    • 是和不是。 Collection 不需要在 O(1) 中返回它自己的大小。但大多数实现都是这样做的。
    【解决方案4】:

    java language specification 解释说,在每个迭代步骤中都会评估表达式。以您为例,list.size() 被调用了 10.000.000 次。

    这在您的情况下无关紧要,因为列表实现(通常)具有存储实际列表大小的私有属性。但是,如果评估真的需要时间,它可能会引起麻烦。在这些情况下,建议将表达式的结果存储到局部变量中。

    【讨论】:

    • 那还是方法调用?还是这样,方法调用在java中没有太多开销?
    • @athena:一个好的 JIT 编译器 - 就像 Sun 的 JVM 中的编译器 - 经常能够内联方法调用,把它变成一个简单的加载,它的速度差不多。
    • @Tom:但是编译器仍然需要确定集合是否被修改。你能给我一些关于找出(Sun 的)JVM 做什么的指示吗?
    • @athena - 使用内联,优化后执行代码可能包含对(私有)大小字段的引用。如果该方法是返回私有字段的引用/值的简单 getter,那么这是非常安全的。像大多数 size() 方法一样。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-25
    • 1970-01-01
    • 1970-01-01
    • 2020-05-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多