«1. Обзор

В этом руководстве мы собираемся сравнить производительность некоторых популярных библиотек примитивных списков в Java.

Для этого мы протестируем методы add(), get() и contains() для каждой библиотеки.

2. Сравнение производительности

Теперь давайте выясним, какая библиотека предлагает быстродействующий API коллекций примитивов.

Для этого сравним аналоги List от Trove, Fastutil и Colt. Мы будем использовать инструмент JMH (Java Microbenchmark Harness) для написания тестов производительности.

2.1. Параметры JMH

Мы запустим тесты со следующими параметрами:

@BenchmarkMode(Mode.SingleShotTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Measurement(batchSize = 100000, iterations = 10)
@Warmup(batchSize = 100000, iterations = 10)
@State(Scope.Thread)
public class PrimitivesListPerformance {
}

Здесь мы хотим измерить время выполнения для каждого метода тестирования. Кроме того, мы хотим отображать наши результаты в миллисекундах.

Аннотация @State указывает, что переменные, объявленные в классе, не будут участвовать в выполнении тестов производительности. Однако затем мы можем использовать их в наших методах тестирования.

Кроме того, давайте определим и инициализируем наши списки примитивов:

public static class PrimitivesListPerformance {
    private List<Integer> arrayList = new ArrayList<>(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9));
    private TIntArrayList tList = new TIntArrayList(new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
    private cern.colt.list.IntArrayList coltList = new cern.colt.list.IntArrayList(new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
    private IntArrayList fastUtilList = new IntArrayList(new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9});

    private int getValue = 4;
}

Теперь мы готовы написать наши тесты.

3. add()

Во-первых, давайте проверим добавление элементов в наши примитивные списки. Мы также добавим один для ArrayList в качестве нашего элемента управления.

3.1. Сравнительные тесты

Первый микротест для метода add() в ArrayList:

@Benchmark
public boolean addArrayList() {
    return arrayList.add(getValue);
}

Аналогично, для TIntArrayList.add() от Trove:

@Benchmark
public boolean addTroveIntList() {
    return tList.add(getValue);
}

Аналогично, для IntArrayList от Colt. add() выглядит так:

@Benchmark
public void addColtIntList() {
    coltList.add(getValue);
}

А для библиотеки Fastutil тест метода IntArrayList.add() будет выглядеть так:

@Benchmark
public boolean addFastUtilIntList() {
    return fastUtilList.add(getValue);
}

3.2. Результаты тестирования

Теперь запустим и сравним результаты:

Benchmark           Mode  Cnt  Score   Error  Units
addArrayList          ss   10  4.527 ± 4.866  ms/op
addColtIntList        ss   10  1.823 ± 4.360  ms/op
addFastUtilIntList    ss   10  2.097 ± 2.329  ms/op
addTroveIntList       ss   10  3.069 ± 4.026  ms/op

Из результатов ясно видно, что add() в ArrayList является самым медленным вариантом.

Это логично, как мы объяснили в статье о библиотеках примитивных списков, ArrayList будет использовать упаковку/автоупаковку для хранения значений int внутри коллекции. Поэтому у нас здесь значительное замедление.

С другой стороны, методы add() для Colt и Fastutil были самыми быстрыми.

Под капотом все три библиотеки хранят значения внутри int[]. Так почему же у нас разное время выполнения их методов add()?

Ответ заключается в том, как они увеличивают int[], когда емкость по умолчанию заполнена:

    Colt будет увеличивать свой внутренний int[] только тогда, когда он заполняется. Напротив, Trove и Fastutil будут использовать некоторые дополнительные вычисления при расширении емкости. int[] container

Вот почему Colt побеждает в результатах наших тестов.

4. get()

Теперь давайте добавим микротест операции get().

4.1. Сравнительные тесты

Во-первых, для операции get() ArrayList:

@Benchmark
public int getArrayList() {
    return arrayList.get(getValue);
}

Аналогично, для TIntArrayList Trove у нас будет:

@Benchmark
public int getTroveIntList() {
    return tList.get(getValue);
}

И для Colt cern.colt.list.IntArrayList, метод get() будет таким:

@Benchmark
public int getColtIntList() {
    return coltList.get(getValue);
}

Наконец, для IntArrayList Fastutil мы протестируем операцию getInt():

@Benchmark
public int getFastUtilIntList() {
    return fastUtilList.getInt(getValue);
}

4.2. Результаты тестирования

После этого мы запускаем тесты и видим результаты:

Benchmark           Mode  Cnt  Score   Error  Units
getArrayList        ss     20  5.539 ± 0.552  ms/op
getColtIntList      ss     20  4.598 ± 0.825  ms/op
getFastUtilIntList  ss     20  4.585 ± 0.489  ms/op
getTroveIntList     ss     20  4.715 ± 0.751  ms/op

Хотя разница в баллах невелика, мы можем заметить, что getArrayList() работает медленнее.

Для остальных библиотек у нас почти идентичные реализации метода get(). Они будут немедленно извлекать значение из int[] без какой-либо дополнительной работы. Вот почему Colt, Fastutil и Trove имеют одинаковые характеристики для операции get().

5. contains()

Наконец, давайте протестируем метод contains() для каждого типа списка.

5.1. Сравнительные тесты

Давайте добавим первый микротест для метода contains() в ArrayList:

@Benchmark
public boolean containsArrayList() {
    return arrayList.contains(getValue);
}

Аналогично, для TIntArrayList Trove тест contains() будет:

@Benchmark
public boolean containsTroveIntList() {
    return tList.contains(getValue);
}

Аналогично, тест для cern.colt.list.IntArrayList.contains() от Colt:

@Benchmark
public boolean containsColtIntList() {
    return coltList.contains(getValue);
}

А для IntArrayList от Fastutil тест метода contains() выглядит так:

@Benchmark
public boolean containsFastUtilIntList() {
    return fastUtilList.contains(getValue);
}

5.2. Результаты тестов

Наконец, мы запускаем наши тесты и сравниваем результаты:

Benchmark                  Mode  Cnt   Score    Error  Units
containsArrayList          ss     20   2.083  ± 1.585  ms/op
containsColtIntList        ss     20   1.623  ± 0.960  ms/op
containsFastUtilIntList    ss     20   1.406  ± 0.400  ms/op
containsTroveIntList       ss     20   1.512  ± 0.307  ms/op

Как обычно, метод containsArrayList имеет наихудшую производительность. Напротив, Trove, Colt и Fastutil имеют более высокую производительность по сравнению с основным решением Java.

На этот раз выбор библиотеки зависит от разработчика. Результаты для всех трех библиотек достаточно близки, чтобы считать их идентичными.

«6. Заключение

В этой статье мы исследовали фактическую производительность примитивных списков во время выполнения с помощью эталонных тестов JVM. Кроме того, мы сравнили результаты теста с ArrayList JDK.

Кроме того, имейте в виду, что цифры, которые мы представляем здесь, являются просто результатами тестов JMH — всегда тестируйте в рамках данной системы и среды выполнения.

Как обычно, полный код этой статьи доступен на GitHub.