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

时间:2014-02-11 18:46:47

标签: java arrays collections

我运行了两个测试用例(多次),似乎迭代地向我的列表添加值比使用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 - 使用更大的馆藏进行基准测试

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

有100个数字:

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

5 个答案:

答案 0 :(得分:18)

for-each循环解析为等效于

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

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

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

答案 1 :(得分:6)

我认为Collection.addAll()更快,至少有两个原因:

ArrayList

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

答案 2 :(得分:4)

这是一个基准测试,阵列中的元素略多一些。结果清楚地表明addAll方法赢得了保证金:

public static void main(String[] args) {

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

    /******** WARM UP JVM *********/

    for (int i = 0; i < 1000; ++i) {
        arrayToListLoop(rawArgs);
    }
    for (int i = 0; i < 1000; ++i) {
        arrayToListAddAll(rawArgs);
    }


    /** Actual measurement **/      
    long start = System.nanoTime();
    for (int i = 0; i < 1000; ++i) {
        arrayToListLoop(rawArgs);
    }
    long end = System.nanoTime();

    System.out.println((end - start) / 1000);

    start = System.nanoTime();
    for (int i = 0; i < 1000; ++i) {
        arrayToListAddAll(rawArgs);
    }
    end = System.nanoTime();

    System.out.println((end - start) / 1000);
}

public static void arrayToListLoop(String[] arr) {
    List<String> list = new ArrayList<>();
    for (String s : arr) {
        list.add(s);
    }
}

public static void arrayToListAddAll(String[] arr) {
    List<String> list = new ArrayList<>();
    list.addAll(Arrays.asList(arr));
}

<强>结果:

1首次运行:

2280
812

2 Second Run:

1336
613

3第三次运行:

2088
751

答案 3 :(得分:2)

我尝试用大量的迭代重复这个实验,并且给你带来了不同的结果:

public static void main(String[] args) {
    String[] rawArgs = new String[]{"one", "two", "three", "four", "five"};

    long start = System.nanoTime();
    for (int i = 0; i < 10000000; i++) {
        List<String> list = new ArrayList<>();
        for (String s : rawArgs) {
            list.add(s);
        }
    }
    long end = System.nanoTime();
    System.out.println("add():    " + (end - start));

    start = System.nanoTime();
    for (int i = 0; i < 10000000; i++) {
        List<String> list = new ArrayList<>();
        list.addAll(Arrays.asList(rawArgs));
    }
    end = System.nanoTime();
    System.out.println("addAll(): " + (end - start));

}

结果:

add():    310726674
addAll(): 233785566

确切的数字会有所不同,但我的JVM(在Windows上运行的Sun JDK 1.7)上addAll 始终总是。两者运行的顺序也没有区别(我试过两种方式),所以这与预热无关。如果增加元素数量,结果会更加显着,可能是因为ArrayList后面的数组必须调整大小(因此需要额外的arraycopy)。

答案 4 :(得分:1)

有两种计时基准的方法+正如其他人所说,你应该在进行真正的计算之前预热JVM。我个人从未决定基准时间何时不到1秒,我试图创建一个更大的问题来查看实际结果。所以,

如果您的时间太小(某些ns或ms),则必须增加问题的大小,例如在您的情况下添加N个元素,例如N> 1000。

 int size = 10000;
 String[] rawArgs = new String[size];
 //add some elements for this test
 for (int i=0; i<size; i++) {
     rawArgs[i] = String.valueOf(i);
 }

要测试较小的尺寸,您必须预热JVM(在实际测量之前进行一些测试测量)并创建一个循环来测量问题的多次运行,然后将总时间除以这​​些循环重复次数。例如,

 //after the warm up try the following
 int repetitions = 1000;
 start = System.nanoTime();
 for (int i=0; i<repetitions; i++) {
     //your calculations
 }
 end = System.nanoTime();
 System.out.println("Cost per repetition: " + (end - start)/repetitions);