Подтвердить что ты не робот

Являются ли массивы более чем в два раза медленнее, чем массивы?

Я написал тест, который пытается проверить две вещи:

  • Независимо от того, влияет ли размер массива буферов на его производительность, даже если вы не используете весь буфер
  • Относительная производительность массивов и 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]);
    }
}
4b9b3361

Ответ 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, которая убивает вас.