В стандартной библиотеке Java отсутствует возможность оперировать коллекциями примитивных типов, таких как int, long и т.д. Стандартный выход — использовать объекты классов Integer, Long и т.д.
Такой подход хорошо работает на небольшом количестве элементов, поскольку, во-первых, при любой операции происходит autoboxing/autounboxing и во-вторых, в коллекции хранятся ссылки на объекты в heap. Объекты в heap не только вносят дополнительный overhead по памяти, но и создают нагрузку на GC.
Есть еще один неочевидный минус объектов в heap — кэширование в современных процессорах. Процессор загружает данные в кэш блоками. В случае последовательной обработки массива, в кэш загружается сразу несколько элементов. В случае же объектов разбросанных по heap, попаданий в кэш будет меньше. Подробнее про кэширование и иерархию памяти здесь.
Библиотека Trove представляет стандартный интерфейс коллекций для работы с примитивными типами. Для большинства применений, коллекции Trove работают быстрее и потребляют меньше памяти.
В набор коллекций входят:
- List — ArrayList и LinkedList
- Map — HashMap
- Set — HashSet
- Stack — ArrayStack
В отличии от jdk, в Hash'ах Trove используется Open addressing метод разрешения коллизий.
Принцип именования — T<Type><CollectionType>.
Например:
Интерфейс TIntList — List of int, реализация TIntArrayList:
TIntList l = new TIntArrayList();
Интерфейс TLongLongMap — Map c ключами long и значениями long, реализация TLongLongHashMap:
TLongLongMap m = new TLongLongHashMap();
В jdk коллекциях, в случае если элемент не найден — возвращается null. В Trove возвращается «NoEntryValue», по умолчанию — 0. Узнать NoEntryValue, задать NoEntryValue можно при создании коллекции.
Плюсом коллекций Trove являются методы обработки — forEach,
public static long troveEach() {
final long [] rvalue = {0};
// troveMap - TLongLongHashMap
// TLongProcedure будет вызываться для каждого элемента коллекции,
// пока не вернет false
// или не кончатся элементы
troveMap.forEachValue(new TLongProcedure() {
@Override
public boolean execute(long value) {
rvalue[0] += value;
return true;
}
});
return rvalue[0];
}
grep, inverseGrep — формирование списка по условию (для TList...) и transformValues — inplace операции изменения над элементами коллекции.
Полезная возможность — в случае HashMap/HashSet c объектом (наследником Object) в качестве ключа, можно использовать свою hash функцию Interface HashingStrategy<T>.
Для бенчмаркинга использовался отличный benchmark tool jmh.
Было бы замечательно, если бы разработчики опубликовали его в maven repository.
Вывод пришлось слегка подредактировать, что бы сохранить форматирование, одна операция — 1млн элементов (полный отчет здесь):
$ java -version
java version "1.7.0_21"
Java(TM) SE Runtime Environment (build 1.7.0_21-b11)
Java HotSpot(TM) 64-Bit Server VM (build 23.21-b01, mixed mode)
$ java -server -XX:+AggressiveOpts -Xms2048m
-Xmx2048m -jar microbenchmarks.jar ".*Trove.*" -prof gc -i 3 -r 5s
Benchmark Mode Thr Cnt Sec Mean Mean error Units // Вставка в List<Integer> IntListJdkInsert thrpt 1 3 5 104.950 6.756 ops/sec // Полный перебор List<Integer> IntListJdkTraverse thrpt 1 3 5 774.100 71.809 ops/sec // Вставка в TIntArrayList IntListTroveInsert thrpt 1 3 5 424.556 28.239 ops/sec // Полный перебор TIntArrayList IntListTroveTraverse thrpt 1 3 5 3548.806 7.712 ops/sec // Вставка в HashMap<Long, Long> LongHashMapJdkInsert thrpt 1 3 5 24.683 1.994 ops/sec // поиск всех элементов в HashMap<Long, Long> по очереди LongHashMapJdkSearch thrpt 1 3 5 67.789 1.119 ops/sec // полный перебор значений HashMap<Long, Long> LongHashMapJdkTraverse thrpt 1 3 5 99.761 0.882 ops/sec // Вставка в TLongLongMap LongHashMapTroveInsert thrpt 1 3 5 28.750 0.165 ops/sec // поиск всех элементов в TLongLongMap по очереди LongHashMapTroveSearch thrpt 1 3 5 145.933 0.416 ops/sec // полный перебор значений TLongLongMap, с использованием forEach LongHashMapTroveTraverse thrpt 1 3 5 318.528 0.980 ops/sec
Количество занятой памяти подсчитать не так просто, но косвенный вывод можно сделать по активности GC:
Вставка jdк в List<Integer>:
Iteration 1 (5s in 1 thread): 103,950 ops/sec
GC | wall time = 5,002 secs, GC time = 0,331 secs, GC% = 6,62%, GC count = +24
Вставка Trove в TIntArrayList:
Iteration 1 (5s in 1 thread): 428,400 ops/sec
GC | wall time = 5,002 secs, GC time = 0,019 secs, GC% = 0,38%, GC count = +32
Если посмотреть в исходники TIntArrayList, то станет понятно откуда прирост производительности — данные хранятся в виде массива:
public class TIntArrayList implements TIntList, Externalizable {
static final long serialVersionUID = 1L;
/** the data of the list */
protected int[] _data;
Скорость перебора TLongLongMap объясняется иcпользованием forEach — не создается временных объектов и исключен unboxing.
Тот же benchmark, но одна операция — 1тыс элементов:
Benchmark Mode Thr Cnt Sec Mean Mean error Units IntListJdkInsert thrpt 1 3 5 239478.011 871.469 ops/sec IntListJdkTraverse thrpt 1 3 5 1326701.717 1649.389 ops/sec IntListTroveInsert thrpt 1 3 5 315562.594 2483.415 ops/sec IntListTroveTraverse thrpt 1 3 5 3630599.806 10822.903 ops/sec LongHashMapJdkInsert thrpt 1 3 5 45315.689 47.630 ops/sec LongHashMapJdkSearch thrpt 1 3 5 114759.789 424.996 ops/sec LongHashMapJdkTraverse thrpt 1 3 5 210012.550 139.001 ops/sec LongHashMapTroveInsert thrpt 1 3 5 33078.583 119.127 ops/sec LongHashMapTroveSearch thrpt 1 3 5 148311.567 267.613 ops/sec LongHashMapTroveTraverse thrpt 1 3 5 351955.550 901.902 ops/sec
Видно, что при сокращении количества элементов в коллекции, разрыв в производительности падает.
Можно сделать вывод о том, что если коллекции небольшие и используются нечасто, то включать в проект дополнительную зависимость особого смысла нет.
Но если в проекте массово используются большие коллекции примитивных типов, то применение Trove может быть оправдано.
Код проекта доступен на GitHub.
PS. Значения количества элементов при создании коллекций не были заданы намеренно.
Автор: xlix123