Являются ли массивы более чем в два раза медленнее, чем массивы?
Я написал тест, который пытается проверить две вещи:
- Независимо от того, влияет ли размер массива буферов на его производительность, даже если вы не используете весь буфер
- Относительная производительность массивов и
ArrayList
Я был удивлен результатами
- В штучной упаковке массивы (т.е.
Integer
vs int
) не намного медленнее, чем примитивная версия
- Размер базового массива не имеет большого значения.
-
ArrayList
более чем в два раза медленнее соответствующего массива.
Вопросы
- Почему
ArrayList
намного медленнее?
- Хорошо ли работает мой бенчмарк? Другими словами, мои результаты точны?
Результаты
0% Scenario{vm=java, trial=0, benchmark=SmallArray} 34.57 ns; ?=0.79 ns @ 10 trials
17% Scenario{vm=java, trial=0, benchmark=SmallBoxed} 40.40 ns; ?=0.21 ns @ 3 trials
33% Scenario{vm=java, trial=0, benchmark=SmallList} 105.78 ns; ?=0.09 ns @ 3 trials
50% Scenario{vm=java, trial=0, benchmark=BigArray} 34.53 ns; ?=0.05 ns @ 3 trials
67% Scenario{vm=java, trial=0, benchmark=BigBoxed} 40.09 ns; ?=0.23 ns @ 3 trials
83% Scenario{vm=java, trial=0, benchmark=BigList} 105.91 ns; ?=0.14 ns @ 3 trials
benchmark ns linear runtime
SmallArray 34.6 =========
SmallBoxed 40.4 ===========
SmallList 105.8 =============================
BigArray 34.5 =========
BigBoxed 40.1 ===========
BigList 105.9 ==============================
vm: java
trial: 0
Код
Этот код был написан в Windows с использованием Java 7 и Google caliper 0.5-rc1 (потому что последнее, что я проверил 1.0, еще не работает в Windows).
Краткое описание: во всех 6 тестах на каждой итерации цикла он добавляет значения в первые 128 ячеек массива (независимо от того, насколько велик массив) и добавляет это к общему значению. Caliper сообщает мне, сколько раз тест должен выполняться, поэтому я повторяю это дополнение 128 раз.
6 тестов имеют большую (131072) и небольшую (128) версию int[]
, Integer[]
и ArrayList<Integer>
. Вероятно, вы можете определить, что из имен.
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import com.google.caliper.Runner;
import com.google.caliper.SimpleBenchmark;
public class SpeedTest {
public static class TestBenchmark extends SimpleBenchmark {
int[] bigArray = new int[131072];
int[] smallArray = new int[128];
Integer[] bigBoxed = new Integer[131072];
Integer[] smallBoxed = new Integer[128];
List<Integer> bigList = new ArrayList<>(131072);
List<Integer> smallList = new ArrayList<>(128);
@Override
protected void setUp() {
Random r = new Random();
for(int i = 0; i < 128; i++) {
smallArray[i] = Math.abs(r.nextInt(100));
bigArray[i] = smallArray[i];
smallBoxed[i] = smallArray[i];
bigBoxed[i] = smallArray[i];
smallList.add(smallArray[i]);
bigList.add(smallArray[i]);
}
}
public long timeBigArray(int reps) {
long result = 0;
for(int i = 0; i < reps; i++) {
for(int j = 0; j < 128; j++) {
result += bigArray[j];
}
}
return result;
}
public long timeSmallArray(int reps) {
long result = 0;
for(int i = 0; i < reps; i++) {
for(int j = 0; j < 128; j++) {
result += smallArray[j];
}
}
return result;
}
public long timeBigBoxed(int reps) {
long result = 0;
for(int i = 0; i < reps; i++) {
for(int j = 0; j < 128; j++) {
result += bigBoxed[j];
}
}
return result;
}
public long timeSmallBoxed(int reps) {
long result = 0;
for(int i = 0; i < reps; i++) {
for(int j = 0; j < 128; j++) {
result += smallBoxed[j];
}
}
return result;
}
public long timeBigList(int reps) {
long result = 0;
for(int i = 0; i < reps; i++) {
for(int j = 0; j < 128; j++) {
result += bigList.get(j);
}
}
return result;
}
public long timeSmallList(int reps) {
long result = 0;
for(int i = 0; i < reps; i++) {
for(int j = 0; j < 128; j++) {
result += smallList.get(j);
}
}
return result;
}
}
public static void main(String[] args) {
Runner.main(TestBenchmark.class, new String[0]);
}
}
Ответы
Ответ 1
Во-первых...
Являются ли ArrayLists более чем в два раза медленнее, чем массивы?
Как обобщение, нет. Для операций, которые потенциально связаны с "изменением" длины списка/массива, ArrayList
будет быстрее, чем массив... если не использовать отдельную переменную для представления логического размера массива.
Для других операций, ArrayList
, скорее всего, будет медленнее, хотя коэффициент производительности, скорее всего, будет зависеть от операции и реализации JVM. Обратите внимание, что вы проверили только одну операцию/шаблон.
Почему ArrayList намного медленнее?
Поскольку у ArrayList есть отдельный объект массива внутри него.
-
Операции обычно включают дополнительные указания (например, для получения размера списка и внутреннего массива), а также проверки дополнительных границ (например, проверка списка size
и длины массива). Типичный JIT-компилятор (по-видимому) не в состоянии оптимизировать их. (И на самом деле, вы бы не хотели оптимизировать внешний массив, потому что это позволяет ArrayList расти.)
-
Для массива примитивов соответствующие типы списков включают завернутые примитивные типы/объекты, и это добавляет накладные расходы. Например, ваш result += ...
включает распаковку в случаях "списка".
Хорошо ли работает мой бенчмарк? Другими словами, мои результаты точны?
Нет ничего плохого в этом технически. Но это недостаточно, чтобы продемонстрировать свою точку зрения. Для начала вы измеряете только один вид операции: выбор элемента массива и его эквивалент. И вы измеряете только примитивные типы.
Наконец, это в значительной степени пропускает смысл использования типов List
. Мы используем их, потому что они почти всегда проще в использовании, чем простые массивы. Разница в производительности (скажем) 2 обычно не важна для общей производительности приложения.
Ответ 2
Имейте в виду, что при использовании ArrayList вы фактически вызываете функцию, которая в случае get()
фактически выполняет два других вызова функций. (Один из них - это проверка диапазона, которая, как я подозреваю, может быть частью задержки).
Важная вещь с ArrayList заключается не столько в том, насколько быстрее или медленнее она сравнивается с прямыми массивами, а в том, что время доступа всегда постоянное (например, массивы). В реальном мире вы почти всегда обнаружите, что добавленная задержка незначительна. Особенно, если у вас есть приложение, которое даже думает о подключении к базе данных.:)
Короче говоря, я думаю, что ваш тест (и результаты) являются законными.
Ответ 3
Эти результаты меня не удивляют. List.get(int)
включает литой, который медленный. Java-дженерики реализуются с помощью стирания типа, что означает, что любой тип List<T>
на самом деле является List<Object>
, и единственная причина, по которой вы получаете свой тип, - это результат. Источник для ArrayList
выглядит следующим образом:
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
// snip...
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// snip...
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
rangeCheck
и накладные расходы на вызовы функций тривиальны, а это приводит к E
, которая убивает вас.