为什么 Collection 的 .addAll 比手动添加慢?

2022-09-02 09:09:05

我运行了两个测试用例(多次),似乎迭代地向我的列表添加值比使用更快addAll

String[] rawArgs = new String[]{"one", "two", "three", "four", "five"};

// More efficient - 894 ns
List<String> list = new ArrayList<>();
for (String s : rawArgs) {
    list.add(s);
}


// Less efficient - 1340 ns
List<String> list = new ArrayList<>();
list.addAll(Arrays.asList(rawArgs));

我通过我的IDE以及其他人得到的注释,后一种方法是将数组转换为该数据结构的“正确”方法。但是,如果它实际上比第一个慢,有什么优势(一些晦涩难懂的类型安全?),出于什么原因我应该使用第二个?

编辑 - 代码基准测试:

JVM 预热,首先重新创建主类对象:

public static void main(String[] args) {
    Internet test;
    for (int i = 0; i < 15; i++) {
        test = new Internet(); // JVM warmup
    }
    test = new Internet();
    test.printOutput();
}

我只是在操作的两端采用系统纳米时间:

start = System.nanoTime();
/* function */
end = System.nanoTime();
result = end - start;

其中,测试用例中,每个开始/结束都有单独的字段,并在操作后计算结果(JVM也在运行测试之前通过循环实例抢占预热)。

编辑 2 - 对较大集合进行基准测试

经过一些测试(使用Integed代替,不会手写所有数字),看起来更大的集合确实更慢:

使用 100 个数字:

First operation: 18759ns
Second operation: 2680ns
Total operation: 21439ns

答案 1

for-each 循环解析为等效于

for (int i = 0; i < rawArgs.length; i++) {
  list.add(rawArgs[i]);
}

...而实际调用的实现,所以它最终调用,这做了一个冗余的副本。也就是说,它也做了一个 ,这可能最终使它比for循环更快 - 它可以走任何一种方式,并且根据其他一些基准测试,它实际上可能在不同的上下文中以不同的方式进行。ArrayList.addAlltoArray()Arrays.asList(rawArgs).toArray()System.arraycopy

Collections.addAll(Collection<E>, E...) 静态方法实际上旨在解决此特定问题,并且比 Javadoc 中明确说明的更快。addAll(Arrays.asList))


答案 2

我认为至少出于两个原因而更快:Collection.addAll()

ArrayList

  1. 它使用这样,如果添加的列表很大,并且在手动添加中,它将被调用多次,但在这种情况下,它将被调用一次,具有必要的容量。ensureCapacityInternal(size + numNew);
  2. 它使用复制方法,这是一种本机和高性能的方法。System.arraycopy(a, 0, elementData, size, numNew);