Почему 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);