Почему Collection.addAll медленнее, чем добавление вручную?

Я запускал два тестовых примера (несколько раз), и кажется, что итеративное добавление значений в мои списки происходит быстрее, чем использование 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

Ответы

Ответ 1

Для каждого цикла разрешается эквивалент

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.

Ответ 2

Здесь находится эталон, содержащий несколько элементов в массиве. Результаты ясно показывают, что подход 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 Второй прогон:

1336
613

3 Третий прогон:

2088
751

Ответ 3

Я думаю, Collection.addAll() быстрее по крайней мере по двум причинам:

Для ArrayList

  • Он использует ensureCapacityInternal(size + numNew);, поэтому, если добавленный список большой, а в ручном добавлении он будет вызываться много раз, но в этом случае он будет вызываться один раз с необходимой емкостью.
  • Он использует метод System.arraycopy(a, 0, elementData, size, numNew); для копирования, который является собственным и высокопроизводительным методом.

Ответ 4

Я попытался повторить этот эксперимент с большим количеством итераций и имел для вас разные результаты:

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

Точные числа меняются, но addAll всегда работает быстрее на моем JVM (Sun JDK 1.7 работает в Windows). Порядок, с которым выполняются эти два, не имеет никакого значения (я пробовал в обоих направлениях), поэтому он не должен прогреваться. Если вы увеличиваете количество элементов, результаты становятся еще более драматичными, возможно, из-за того, что массив за ArrayList должен быть изменен (следовательно, дополнительный arraycopy).

Ответ 5

Есть два подхода к контрольным показателям времени +, как отмечают другие люди, вы должны разогреть 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);