Существует много измерений производительности Java Map, но критически важным является простое однопоточное сканирование. Вот простой тестовый код для Iterators и Java 8 Map.forEach()
вместе с некоторыми графическими результатами.
1. Тестирование производительности сложно
Тестирование производительности является общеизвестно трудным делом, а для точного повторяемого тестирования требуется такая структура, как Java Microbenchmarking Harness, для фильтрации многих источников шума и предоставления статистики, такой как доверительный интервал. Тем не менее, результаты этого простого тестового кода здесь относительно повторяемы и хорошо коррелируют с тестами JMH, которые автор сделал на картах JDK и AirConcurrentMap (на кипятильник.com и на githubilerbay / airconcurrentmap ). Есть также более обширные тесты JMH для потоков и так далее.
JMH не сложен, но требует переключения передач из IDE на модель maven, изучения набора контрольных аннотаций и работы с еще одним деревом каталогов, полным лжи и, возможно, новым проектом IDE. Кроме того, мы хотим автоматически охватить спектр размеров карт и набор карт за один прогон: для этого требуются аннотации «параметризации» в JMH. Формат выходных данных этого теста можно легко настроить в соответствии с нужным графическим инструментом.
1.1 Проблемы, вызывающие шум
Многие вопросы должны быть решены, чтобы получить хороший тест:
- Внутренний цикл не должен иметь значительных накладных расходов. Это решается с помощью простого отслеживания изменений в общей статической переменной volatile int, увеличиваемой потоком «heartbeat»;
- Может возникнуть проблема с сборкой мусора. Этому помогает распределение циклического перебора GC по тестам для разных карт. Мы также избегаем создания недоступных объектов. Используйте -Xloggc: <file> в ORACLE JVM для просмотра GC;
- Код может быть оптимизирован в любое время JVM во время выполнения. Мы используем прогрев каждого теста, чтобы это произошло. Трудно понять, когда это произойдет, но это всего лишь секунды. Это можно минимизировать с помощью более длительных прогревов. Типичный флаг JVM ‘-XX: + PrintCompilation’ показывает ход оптимизации. Также см. Статью ORACLE ;
- JVM будет расти беспорядочно, пока растут Карты, поэтому тест сразу после роста должен прогреться;
- Разные Карты используют разные объемы памяти, поэтому даже разветвление JVM для каждого теста будет зависеть от характеристик роста JVM. Мы держим все Карты в одной JVM;
- Разные карты должны иметь одинаковое содержимое. Мы используем синхронизированные генераторы случайных чисел, засеянные размером карты;
- Большие карты требуют больше времени для итерации, чем меньшие, конечно. Сердцебиение позволяет выполнять больше прогонов меньших Карт, чтобы каждый тест занимал постоянное время;
- Карты не должны обмениваться содержимым, поэтому более ранние тесты не переносят данные в кэш ЦП, которые доступны для последующих тестов;
- Компилятор Just In Time не должен оптимизировать цикл. Мы печатаем сумму содержимого, чтобы цикл имел ощутимый результат. Если вместо этого использовать счетчик циклов, JIT может распознать шаблон и изменить цикл на число больше единицы, пропуская, таким образом, много итераций! Да, это происходит! (В AirConcurrentMap как минимум).
2. Результаты
Эти предосторожности помогают нам прийти к некоторым предварительным, но повторяемым выводам. Результаты показывают:
- forEach () намного быстрее чем итераторы;
- ConcurrentSkipListMap выполняет итерацию быстрее всего около 30 тыс. Записей, а AirConcurrentMap — быстрее;
- ConcurrentSkipListMap forEach () быстрее всего около 100 записей, а AirConcurrentMap быстрее всего выше.
3. Детали кода
В тестовом коде используется оболочка класса Test
для хранения определенной карты вместе с состоянием выполнения теста, таким как время, размер карты, циклы и промежуточные итоги. Надеемся, что эта организация делает main () более читабельным.
Класс Test реализует BiConsumer
чтобы его можно было использовать при вызове forEach(this)
. Он просто переопределяет accept(Integer, Integer)
. Мы также тестируем forEach((k, v) -> total += v)
(автор пробовал, и результаты примерно такие же). В любом случае локальная переменная не может быть использована в качестве общего накопителя, потому что локальные переменные должны быть «эффективно конечными» для внутреннего класса или лямбды, поэтому нам нужна переменная экземпляра в содержащей области, как в классе в любом случае. Редукция с использованием потока тоже подойдет, но здесь нас интересуют Итераторы. (См. Https://github.com/boilerbay/airconcurrentmap для соответствующих потоковых тестов в / jmh).
Сердцебиение — это static int
увеличиваемое один раз в секунду отдельным потоком. Сердцебиение должно быть изменчивым, чтобы его изменения распространялись между потоками, иначе программа никогда не завершится.
USE_ITERATION
и USE_LAMBDA
являются static final
, поэтому javac заблаговременно оценивает код, на который он влияет, удаляя мертвый код. Это определено как необходимое поведение, таким образом, нет никаких накладных расходов. Конечно, вы должны перекомпилировать тесты, но не меняйте их на нестатичные или не финальные!
Внутренний цикл не замедляется динамическим вызовом метода test.testIterator()
потому что методы длиной 35 байтов или меньше всегда встроены. Кроме того, здесь нет полиморфизма — класс Test
не имеет расширений, поэтому нет vtable для диспетчеризации и нет параметров для загрузки в стек.
Тесты относительно повторяемы и показывают общую картину на диаграммах.
4. Код
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
|
public class JavaCodeGeeksIteratorArticle { static volatile int heartBeat = 0 ; // otherwise use forEach() static final boolean USE_ITERATION = false ; static final boolean USE_LAMBDA = true ; public static void main(String... args) { System.out.println((USE_ITERATION ? "Iterator" : USE_LAMBDA ? " forEach(lambda)" : "ForEach()" ) + " performance test" ); Test tests[] = { new Test( new HashMap<Integer, Integer>()), new Test( new TreeMap<Integer, Integer>()), new Test( new ConcurrentHashMap<Integer, Integer>()), new Test( new ConcurrentSkipListMap<Integer, Integer>()), new Test( new AirConcurrentMap<Integer, Integer>()) }; int sizes[] = new int [] { 1 , 3 , 10 , 30 , 100 , 300 , 1000 , 3000 , 10_000, 30_000, 100_000, 300_000, 1000_000, 3_000_000, 10_000_000 }; // Just increment heartBeat every so often. It is volatile. // Reading it is very fast. new Thread( new Runnable() { public void run() { while ( true ) { heartBeat++; try { Thread.sleep( 100 ); } catch (InterruptedException e) { } } } }).start(); for ( int i = 0 ; i < sizes.length; i++) { for (Test test : tests) test.fillTo(sizes[i]); // warmup for (Test test : tests) { int nextHeartBeat = heartBeat + 20 ; while (heartBeat < nextHeartBeat) if (USE_ITERATION) test.testIterator(); else if (USE_LAMBDA) test.testForEachLambda(); else test.testForEach(); } for (Test test : tests) { test.time = 0 ; test.loops = 0 ; long t0 = System.nanoTime(); int nextHeartBeat = heartBeat + 30 ; while (heartBeat < nextHeartBeat) { if (USE_ITERATION) test.testIterator(); else if (USE_LAMBDA) test.testForEachLambda(); else test.testForEach(); } long t1 = System.nanoTime(); test.time += (t1 - t0); } for (Test test : tests) test.printResult(); // System.out.println("---------------"); } } } class Test implements BiConsumer<Integer, Integer> { // The total provides a tangible result to prevent optimizing-out long total = 0 ; long time = 0 ; int size = 0 ; long loops = 1 ; Map<Integer, Integer> map; Test(Map<Integer, Integer> map) { this .map = map; } void fillTo( int newSize) { Random random = new Random(size); while (size < newSize) { Integer n = new Integer(random.nextInt()); map.put(n, n); size++; } } void testIterator() { for (Integer v : map.values()) { total += v.intValue(); } loops++; } // This has the same effect and is 'terser' void testForEachLambda() { map.forEach((k, v) -> total += v); loops++; } void testForEach() { map.forEach( this ); loops++; } // Implement BiConsumer for forEach() @Override public void accept(Integer k, Integer v) { total += k.intValue(); } void printResult() { double seconds = time / 1e9; System.out.printf( "%22s size=% 9d entries/s(K)=% 11.3f total=%d\n" , map.getClass().getSimpleName(), size, size * loops / seconds / 1e3, total); } } |
5. Данные результатов
Данные для графиков ниже. Это может быть импортировано, например, в Excel и отсортировано по классу и размеру Map для создания графиков вручную. Вам нужно вырезать и вставить данные, а затем использовать текст в столбцы. Можно использовать линейные диаграммы, вручную выбирая данные серии по одному для каждого класса карты. Можно также использовать ggplot на языке статистики ‘R’, изменив формат вывода данных. Игнорировать итоги: это дает только ощутимый результат, чтобы избежать оптимизации цикла.
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
|
JavaCodeGeeksIteratorArticle Iterator performance test HashMap size= 1 entries /s = 31290.754K total=-289049400605539520 TreeMap size= 1 entries /s = 50210.333K total=-331631386373881504 ConcurrentHashMap size= 1 entries /s = 15881.356K total=-91464608232057952 ConcurrentSkipListMap size= 1 entries /s = 42187.535K total=-254234286353018080 AirConcurrentMap size= 1 entries /s = 25577.125K total=-149805405784208032 --------------- HashMap size= 3 entries /s = 62664.626K total=-484691989675689270 TreeMap size= 3 entries /s = 66908.091K total=-550745245141063704 ConcurrentHashMap size= 3 entries /s = 38018.996K total=-211326922860746827 ConcurrentSkipListMap size= 3 entries /s = 71265.063K total=-488278692474832005 AirConcurrentMap size= 3 entries /s = 48540.146K total=-302163579336545082 --------------- HashMap size= 10 entries /s = 86701.181K total=-832795481348512598 TreeMap size= 10 entries /s = 87832.407K total=-916137658370092344 ConcurrentHashMap size= 10 entries /s = 73069.458K total=-502840045890573499 ConcurrentSkipListMap size= 10 entries /s = 96150.046K total=-880874881700377401 AirConcurrentMap size= 10 entries /s = 72001.056K total=-591224549191451578 --------------- HashMap size= 30 entries /s = 89419.363K total=-832238604657166224 TreeMap size= 30 entries /s = 92397.645K total=-915559545928720920 ConcurrentHashMap size= 30 entries /s = 71702.258K total=-502393457157098057 ConcurrentSkipListMap size= 30 entries /s = 103387.524K total=-880213560719307683 AirConcurrentMap size= 30 entries /s = 80807.271K total=-590716865563759348 --------------- HashMap size= 100 entries /s = 90540.307K total=-845663104709828079 TreeMap size= 100 entries /s = 96479.776K total=-930300717715488858 ConcurrentHashMap size= 100 entries /s = 69055.433K total=-512752383626539310 ConcurrentSkipListMap size= 100 entries /s = 111208.365K total=-897628029635465726 AirConcurrentMap size= 100 entries /s = 89071.481K total=-604453907970584431 --------------- HashMap size= 300 entries /s = 94846.852K total=-860347586557330269 TreeMap size= 300 entries /s = 94506.995K total=-944821983122037883 ConcurrentHashMap size= 300 entries /s = 65857.587K total=-522786710141245214 ConcurrentSkipListMap size= 300 entries /s = 112398.344K total=-915694233970137252 AirConcurrentMap size= 300 entries /s = 92168.052K total=-618862268508225735 --------------- HashMap size= 1000 entries /s = 74493.997K total=-852961390806337305 TreeMap size= 1000 entries /s = 80026.348K total=-937067262358689631 ConcurrentHashMap size= 1000 entries /s = 38450.309K total=-519070765727723030 ConcurrentSkipListMap size= 1000 entries /s = 112085.413K total=-904572392174355552 AirConcurrentMap size= 1000 entries /s = 89022.852K total=-609589031935380951 --------------- HashMap size= 3000 entries /s = 57470.417K total=-847037038798366713 TreeMap size= 3000 entries /s = 65963.172K total=-930168324830420495 ConcurrentHashMap size= 3000 entries /s = 41073.089K total=-514814334734654678 ConcurrentSkipListMap size= 3000 entries /s = 109217.866K total=-892841347702876464 AirConcurrentMap size= 3000 entries /s = 89175.845K total=-600361285087522951 --------------- HashMap size= 10000 entries /s = 46254.558K total=-846309210319384299 TreeMap size= 10000 entries /s = 49044.408K total=-929403633977808893 ConcurrentHashMap size= 10000 entries /s = 36385.473K total=-514246034592481772 ConcurrentSkipListMap size= 10000 entries /s = 99442.425K total=-891342788950136070 AirConcurrentMap size= 10000 entries /s = 85447.544K total=-599022098209904335 --------------- HashMap size= 30000 entries /s = 43723.556K total=-848942181585517584 TreeMap size= 30000 entries /s = 45253.915K total=-932143497084889069 ConcurrentHashMap size= 30000 entries /s = 32665.051K total=-516220137420939070 ConcurrentSkipListMap size= 30000 entries /s = 83393.494K total=-896231928805619954 AirConcurrentMap size= 30000 entries /s = 80619.262K total=-603845100973163554 --------------- HashMap size= 100000 entries /s = 40028.088K total=-849706795555554639 TreeMap size= 100000 entries /s = 41755.506K total=-932944794183923404 ConcurrentHashMap size= 100000 entries /s = 26064.027K total=-516724530444651670 ConcurrentSkipListMap size= 100000 entries /s = 46619.667K total=-897138307784594414 AirConcurrentMap size= 100000 entries /s = 75034.058K total=-605290263409285564 --------------- HashMap size= 300000 entries /s = 28271.140K total=-850157323063101369 TreeMap size= 300000 entries /s = 23442.635K total=-933312552546033574 ConcurrentHashMap size= 300000 entries /s = 22886.588K total=-517086645455936620 ConcurrentSkipListMap size= 300000 entries /s = 26852.530K total=-897567202447311134 AirConcurrentMap size= 300000 entries /s = 43406.800K total=-605991920028554584 --------------- HashMap size= 1000000 entries /s = 20762.874K total=-850266118577777400 TreeMap size= 1000000 entries /s = 21465.730K total=-933426629396373490 ConcurrentHashMap size= 1000000 entries /s = 17617.501K total=-517179596963620996 ConcurrentSkipListMap size= 1000000 entries /s = 17753.452K total=-897660153954995510 AirConcurrentMap size= 1000000 entries /s = 24726.115K total=-606121840885886155 --------------- HashMap size= 3000000 entries /s = 20859.307K total=-850290350569160265 TreeMap size= 3000000 entries /s = 17078.422K total=-933446707332090721 ConcurrentHashMap size= 3000000 entries /s = 19987.888K total=-517202444269781983 ConcurrentSkipListMap size= 3000000 entries /s = 23990.479K total=-897687847659433070 AirConcurrentMap size= 3000000 entries /s = 30472.006K total=-606157150359044044 --------------- HashMap size= 10000000 entries /s = 18594.336K total=-850335966429011695 TreeMap size= 10000000 entries /s = 14332.011K total=-933483200019971865 ConcurrentHashMap size= 10000000 entries /s = 17038.665K total=-517248060129633413 ConcurrentSkipListMap size= 10000000 entries /s = 18600.417K total=-897733463519284500 AirConcurrentMap size= 10000000 entries /s = 39037.289K total=-606248382078746904 --------------- ForEach() performance test HashMap size= 1 entries /s = 60469.332K total=-429010055848020608 TreeMap size= 1 entries /s = 162720.446K total=-1192873323002853184 ConcurrentHashMap size= 1 entries /s = 39683.288K total=-238381128098095008 ConcurrentSkipListMap size= 1 entries /s = 125216.579K total=-742139402594604448 AirConcurrentMap size= 1 entries /s = 40199.780K total=-223453462147226592 --------------- HashMap size= 3 entries /s = 154792.076K total=-907351702836558858 TreeMap size= 3 entries /s = 209809.380K total=-1866688472587176884 ConcurrentHashMap size= 3 entries /s = 100788.166K total=-558021636792810008 ConcurrentSkipListMap size= 3 entries /s = 202179.445K total=-1380005440196857173 AirConcurrentMap size= 3 entries /s = 99654.211K total=-536118642462089017 --------------- HashMap size= 10 entries /s = 297297.392K total=-2080956150412338142 TreeMap size= 10 entries /s = 228262.234K total=-2782965758065995472 ConcurrentHashMap size= 10 entries /s = 189641.651K total=-1313404093180619596 ConcurrentSkipListMap size= 10 entries /s = 304427.266K total=-2602702896415550485 AirConcurrentMap size= 10 entries /s = 179131.091K total=-1257952993772621945 --------------- HashMap size= 30 entries /s = 305634.315K total=-2079066757218703314 TreeMap size= 30 entries /s = 224584.862K total=-2781566150975000263 ConcurrentHashMap size= 30 entries /s = 174917.912K total=-1312321443993669200 ConcurrentSkipListMap size= 30 entries /s = 354581.676K total=-2600502266614288915 AirConcurrentMap size= 30 entries /s = 254150.967K total=-1256373143380530839 --------------- HashMap size= 100 entries /s = 295763.376K total=-2123996537948400465 TreeMap size= 100 entries /s = 225375.420K total=-2816005249627104526 ConcurrentHashMap size= 100 entries /s = 168596.386K total=-1337959745143386554 ConcurrentSkipListMap size= 100 entries /s = 366342.358K total=-2656351051389612808 AirConcurrentMap size= 100 entries /s = 336305.468K total=-1307783105877232718 --------------- HashMap size= 300 entries /s = 335940.642K total=-2176036047793281607 TreeMap size= 300 entries /s = 213798.860K total=-2849343024468137449 ConcurrentHashMap size= 300 entries /s = 196158.746K total=-1368245793652746886 ConcurrentSkipListMap size= 300 entries /s = 348762.198K total=-2711376732587358984 AirConcurrentMap size= 300 entries /s = 435378.640K total=-1375390632080685627 --------------- HashMap size= 1000 entries /s = 312285.030K total=-2145888100702813327 TreeMap size= 1000 entries /s = 157007.240K total=-2834013084542617045 ConcurrentHashMap size= 1000 entries /s = 178919.552K total=-1350929801563960438 ConcurrentSkipListMap size= 1000 entries /s = 253518.140K total=-2687136797657993712 AirConcurrentMap size= 1000 entries /s = 449868.858K total=-1331959489090096491 --------------- HashMap size= 3000 entries /s = 269579.050K total=-2118053337323592431 TreeMap size= 3000 entries /s = 129271.886K total=-2820412724828116661 ConcurrentHashMap size= 3000 entries /s = 95870.060K total=-1340856888537750166 ConcurrentSkipListMap size= 3000 entries /s = 203849.473K total=-2666107139705649888 AirConcurrentMap size= 3000 entries /s = 443682.852K total=-1286068695711899563 --------------- HashMap size= 10000 entries /s = 100969.734K total=-2116523986910706773 TreeMap size= 10000 entries /s = 81240.085K total=-2819237854014952091 ConcurrentHashMap size= 10000 entries /s = 74229.656K total=-1339726640597926192 ConcurrentSkipListMap size= 10000 entries /s = 147733.052K total=-2664068913299591178 AirConcurrentMap size= 10000 entries /s = 399269.901K total=-1279844336850624703 --------------- HashMap size= 30000 entries /s = 77830.586K total=-2121068856388826590 TreeMap size= 30000 entries /s = 73801.711K total=-2823183557187296024 ConcurrentHashMap size= 30000 entries /s = 63503.394K total=-1343522194696030345 ConcurrentSkipListMap size= 30000 entries /s = 148907.153K total=-2671403338078408622 AirConcurrentMap size= 30000 entries /s = 450044.679K total=-1305454406448994036 --------------- HashMap size= 100000 entries /s = 57919.591K total=-2122150626578319295 TreeMap size= 100000 entries /s = 39468.243K total=-2823932886520250879 ConcurrentHashMap size= 100000 entries /s = 30273.873K total=-1344103393021081000 ConcurrentSkipListMap size= 100000 entries /s = 48766.187K total=-2672332261897079327 AirConcurrentMap size= 100000 entries /s = 307460.503K total=-1311189966514089586 --------------- HashMap size= 300000 entries /s = 42127.664K total=-2122825947560403955 TreeMap size= 300000 entries /s = 26508.090K total=-2824353316156729769 ConcurrentHashMap size= 300000 entries /s = 27206.845K total=-1344518179306734670 ConcurrentSkipListMap size= 300000 entries /s = 19172.093K total=-2672628537815403377 AirConcurrentMap size= 300000 entries /s = 152485.548K total=-1313414387297697136 --------------- HashMap size= 1000000 entries /s = 40607.148K total=-2123037200986959355 TreeMap size= 1000000 entries /s = 25159.911K total=-2824487462082592448 ConcurrentHashMap size= 1000000 entries /s = 30257.523K total=-1344677675643783997 ConcurrentSkipListMap size= 1000000 entries /s = 37742.151K total=-2672825003502099899 AirConcurrentMap size= 1000000 entries /s = 148954.277K total=-1314169618297632691 --------------- HashMap size= 3000000 entries /s = 43941.038K total=-2123087741997557902 TreeMap size= 3000000 entries /s = 18891.646K total=-2824508924703531557 ConcurrentHashMap size= 3000000 entries /s = 32007.894K total=-1344715062144774703 ConcurrentSkipListMap size= 3000000 entries /s = 21722.543K total=-2672849927836093703 AirConcurrentMap size= 3000000 entries /s = 126084.363K total=-1314312240875486125 --------------- HashMap size= 10000000 entries /s = 35724.715K total=-2123169850545290476 TreeMap size= 10000000 entries /s = 12693.940K total=-2824540855805427558 ConcurrentHashMap size= 10000000 entries /s = 27254.306K total=-1344783485934551848 ConcurrentSkipListMap size= 10000000 entries /s = 13572.712K total=-2672886420523974847 AirConcurrentMap size= 10000000 entries /s = 92726.047K total=-1314522073830802703 --------------- |
6. Резюме
Можно получить повторяемые, значимые пользовательские тесты производительности Map, используя простой код с определенными мерами предосторожности. Этот код показывает некоторые из этих проблем. Код легко адаптируется к другим видам тестов Map и форматам вывода данных.