99import org .openjdk .jmh .annotations .Benchmark ;
1010import org .openjdk .jmh .annotations .Fork ;
1111import org .openjdk .jmh .annotations .Measurement ;
12+ import org .openjdk .jmh .annotations .Scope ;
13+ import org .openjdk .jmh .annotations .State ;
1214import org .openjdk .jmh .annotations .Threads ;
1315import org .openjdk .jmh .annotations .Warmup ;
1416import org .openjdk .jmh .infra .Blackhole ;
1820 *
1921 * <ul>
2022 * Benchmark comparing different Map-s...
21- * <li>(RECOMMENDED) HashMap - for fastest lookups - (not typically needed for tags)
23+ * <li>(RECOMMENDED) HashMap - fastest lookups among general-purpose (mutable) maps - (not
24+ * typically needed for tags; a fixed TagSet map below is faster still when the keys are
25+ * known)
2226 * <li>(RECOMMENDED) TagMap - for storing tags - especially if copying between maps or using
2327 * builders
2428 * <li>TreeMap - better for custom Comparators - case-insensitive Maps (see
3438 *
3539 * <p>TagMap excels at storing primitives, copying between TagMap instances, and builder idioms.
3640 *
37- * <p>Iterator traversal with TagMap is relatively slow, but TagMap#forEach is on par (and slightly)
38- * faster than traditional map entry iteration.
41+ * <p>Iterator traversal with TagMap is relatively slow, but TagMap#forEach matches the fastest map
42+ * iterators (LinkedHashMap/TreeMap) and far outpaces HashMap entry-set iteration.
3943 *
4044 * <p>HashMap & LinkedHashMap perform equally well on get operations.
4145 *
42- * <p>HashMap is 2x faster throughput-wise to create and has less memory overhead because there's no
43- * linked list to capture insertion order.
46+ * <p>HashMap is ~1.4x faster throughput-wise to create than LinkedHashMap and has less memory
47+ * overhead because there's no linked list to capture insertion order.
4448 *
4549 * <p>TreeMap is useful when a custom Comparator is needed -- see CaseInsensitiveMapBenchmark
4650 *
4751 * <p>HashMap & TagMap also perform exceedingly well in cases where the exact same object is used
48- * for put & get operations. e.g. when using String literals or Class literals as keys <code>
49- * MacBook M1 1 thread (Java 21)
52+ * for put & get operations. e.g. when using String literals or Class literals as keys.
5053 *
51- * Benchmark Mode Cnt Score Error Units
52- * UnsynchronizedMapBenchmark.clone_hashMap thrpt 6 12482267.775 ± 236852.198 ops/s
53- * UnsynchronizedMapBenchmark.clone_linkedHashMap thrpt 6 12414187.888 ± 224418.265 ops/s
54- * UnsynchronizedMapBenchmark.clone_tagMap thrpt 6 49638156.234 ± 2972608.986 ops/s
55- * UnsynchronizedMapBenchmark.clone_treeMap thrpt 6 16201216.086 ± 619985.352 ops/s
56- *
57- * UnsynchronizedMapBenchmark.create_hashMap thrpt 6 22534042.260 ± 819970.046 ops/s
58- * UnsynchronizedMapBenchmark.create_hashMap_sized thrpt 6 21871270.375 ± 893842.109 ops/s
59- * UnsynchronizedMapBenchmark.create_linkedHashMap thrpt 6 12905731.242 ± 8930007.156 ops/s
60- * UnsynchronizedMapBenchmark.create_tagMap thrpt 6 15794277.380 ± 6069426.265 ops/s
61- * UnsynchronizedMapBenchmark.create_treeMap thrpt 6 4711961.814 ± 48582.934 ops/s
62- *
63- * UnsynchronizedMapBenchmark.get_hashMap thrpt 6 212201631.841 ± 6223069.782 ops/s
64- * UnsynchronizedMapBenchmark.get_hashMap_sameKey thrpt 6 392053406.085 ± 3938305.125 ops/s
65- * UnsynchronizedMapBenchmark.get_linkedHashMap thrpt 6 210734968.352 ± 3627805.282 ops/s
66- * UnsynchronizedMapBenchmark.get_tagMap thrpt 6 201864656.534 ± 4596147.771 ops/s
67- * UnsynchronizedMapBenchmark.get_tagMap_sameKey thrpt 6 256311645.716 ± 13315886.308 ops/s
68- * UnsynchronizedMapBenchmark.get_treeMap thrpt 6 94606404.423 ± 806879.890 ops/s
69- * </code> <code>
70- * MacBook M1 with 8 threads (Java 21)
54+ * <p>A TagSet + parallel int[] used as a fixed (build-once) map is the fastest get here -- ~30%
55+ * ahead of HashMap on the rotating-key path and ~50% ahead on the same-key path, where it sustains
56+ * 5.4B ops/s at the tightest error in the table (±1.3%). It pays no boxing (int[] values), chases
57+ * no node, and allocates nothing per lookup. It only applies when the key set is fixed at
58+ * construction. <code>
59+ * Apple M1 Max (10 core), macOS 26.4.1 -- 8 threads (per-thread state), 2 forks -- Java 17 (Zulu 17.42.19, 17.0.7+7-LTS)
7160 *
7261 * Benchmark Mode Cnt Score Error Units
73- * UnsynchronizedMapBenchmark.clone_hashMap thrpt 6 89645484.526 ± 6546683.185 ops/s
74- * UnsynchronizedMapBenchmark.clone_linkedHashMap thrpt 6 78233577.417 ± 7204526.742 ops/s
75- * UnsynchronizedMapBenchmark.clone_tagMap thrpt 6 315228772.058 ± 20689692.104 ops/s
76- * UnsynchronizedMapBenchmark.clone_treeMap thrpt 6 102416350.341 ± 7258040.561 ops/s
77- *
78- * UnsynchronizedMapBenchmark.create_hashMap thrpt 6 150462966.692 ± 11243713.572 ops/s
79- * UnsynchronizedMapBenchmark.create_hashMap_sized thrpt 6 111213025.138 ± 4593366.916 ops/s
80- * UnsynchronizedMapBenchmark.create_linkedHashMap thrpt 6 80882399.133 ± 19567359.487 ops/s
81- * UnsynchronizedMapBenchmark.create_tagMap thrpt 6 93026443.634 ± 11831456.794 ops/s
82- * UnsynchronizedMapBenchmark.create_tagMap_via_ledger thrpt 6 70769351.353 ± 3821543.185 ops/s
83- * UnsynchronizedMapBenchmark.create_treeMap thrpt 6 32737595.187 ± 2638992.844 ops/s
62+ * UnsynchronizedMapBenchmark.clone_hashMap thrpt 6 69903436.959 ± 8570506.651 ops/s
63+ * UnsynchronizedMapBenchmark.clone_linkedHashMap thrpt 6 85857078.271 ± 6500998.510 ops/s
64+ * UnsynchronizedMapBenchmark.clone_tagMap thrpt 6 293226423.495 ± 40922964.340 ops/s
65+ * UnsynchronizedMapBenchmark.clone_treeMap thrpt 6 95345978.653 ± 19359076.478 ops/s
8466 *
85- * UnsynchronizedMapBenchmark.get_hashMap thrpt 6 1154522356.093 ± 116525174.735 ops/s
86- * UnsynchronizedMapBenchmark.get_hashMap_sameKey thrpt 6 1760800709.734 ± 33551896.166 ops/s
87- * UnsynchronizedMapBenchmark.get_linkedHashMap thrpt 6 1191208257.933 ± 49810465.132 ops/s
88- * UnsynchronizedMapBenchmark.get_tagMap thrpt 6 933455574.646 ± 154146815.295 ops/s
89- * UnsynchronizedMapBenchmark.get_tagMap_sameKey thrpt 6 1138764608.359 ± 88352911.617 ops/s
90- * UnsynchronizedMapBenchmark.get_treeMap thrpt 6 490872723.682 ± 87017311.892 ops/s
91- *
92- * UnsynchronizedMapBenchmark.iterate_hashMap thrpt 6 351222668.708 ± 35242914.752 ops/s
93- * UnsynchronizedMapBenchmark.iterate_linkedHashMap thrpt 6 406635839.285 ± 55990655.235 ops/s
94- * UnsynchronizedMapBenchmark.iterate_tagMap thrpt 6 185264584.604 ± 15137886.028 ops/s
95- * UnsynchronizedMapBenchmark.iterate_tagMap_forEach thrpt 6 422407681.630 ± 19493455.109 ops/s
96- * UnsynchronizedMapBenchmark.iterate_treeMap thrpt 6 392884747.896 ± 80190674.417 ops/s
97- * </code>
67+ * UnsynchronizedMapBenchmark.create_hashMap thrpt 6 154092654.362 ± 4062613.480 ops/s
68+ * UnsynchronizedMapBenchmark.create_hashMap_sized thrpt 6 146583930.032 ± 4457830.615 ops/s
69+ * UnsynchronizedMapBenchmark.create_linkedHashMap thrpt 6 111282881.273 ± 11735323.503 ops/s
70+ * UnsynchronizedMapBenchmark.create_tagMap thrpt 6 92286566.881 ± 16770930.695 ops/s
71+ * UnsynchronizedMapBenchmark.create_tagMap_via_ledger thrpt 6 71399936.094 ± 8077504.597 ops/s
72+ * UnsynchronizedMapBenchmark.create_treeMap thrpt 6 35930407.162 ± 590070.611 ops/s
9873 *
99- * <p>A TagSet + parallel int[] used as a fixed (build-once) map is the fastest get here -- ahead of
100- * HashMap in both the rotating-key and same-key cases -- and the most predictable (±1-2% vs
101- * HashMap's ~5% and TagMap's ~12%). It pays no boxing (int[] values), chases no node, and allocates
102- * nothing per lookup. It only applies when the key set is fixed at construction. <code>
103- * Fixed-map get comparison incl. TagSet + parallel int[] (Apple M1 Max, 8 threads, Java 8 Zulu 8.0.382)
74+ * UnsynchronizedMapBenchmark.get_hashMap thrpt 6 1897568575.023 ± 47092787.708 ops/s
75+ * UnsynchronizedMapBenchmark.get_hashMap_sameKey thrpt 6 3544716454.416 ± 192397957.653 ops/s
76+ * UnsynchronizedMapBenchmark.get_linkedHashMap thrpt 6 1871397460.306 ± 51848940.996 ops/s
77+ * UnsynchronizedMapBenchmark.get_tagMap thrpt 6 1706422514.145 ± 91472057.777 ops/s
78+ * UnsynchronizedMapBenchmark.get_tagMap_sameKey thrpt 6 2205821374.441 ± 108659512.329 ops/s
79+ * UnsynchronizedMapBenchmark.get_tagSetMap thrpt 6 2448917198.752 ± 105399021.596 ops/s
80+ * UnsynchronizedMapBenchmark.get_tagSetMap_sameKey thrpt 6 5358887465.195 ± 70196881.552 ops/s
81+ * UnsynchronizedMapBenchmark.get_treeMap thrpt 6 704782343.575 ± 52129796.457 ops/s
10482 *
105- * Benchmark Mode Cnt Score Error Units
106- * UnsynchronizedMapBenchmark.get_hashMap thrpt 6 1086382687.356 ± 52784044.910 ops/s
107- * UnsynchronizedMapBenchmark.get_hashMap_sameKey thrpt 6 1254600761.612 ± 33664831.344 ops/s
108- * UnsynchronizedMapBenchmark.get_linkedHashMap thrpt 6 1055047178.664 ± 44355203.950 ops/s
109- * UnsynchronizedMapBenchmark.get_tagMap thrpt 6 906953688.631 ± 112749682.884 ops/s
110- * UnsynchronizedMapBenchmark.get_tagMap_sameKey thrpt 6 1055399886.424 ± 162974805.646 ops/s
111- * UnsynchronizedMapBenchmark.get_tagSetMap thrpt 6 1168134502.026 ± 23922240.061 ops/s
112- * UnsynchronizedMapBenchmark.get_tagSetMap_sameKey thrpt 6 1379540570.008 ± 15177042.759 ops/s
113- * UnsynchronizedMapBenchmark.get_treeMap thrpt 6 590656771.941 ± 63177685.351 ops/s
83+ * UnsynchronizedMapBenchmark.iterate_hashMap thrpt 6 132215205.201 ± 9079485.505 ops/s
84+ * UnsynchronizedMapBenchmark.iterate_linkedHashMap thrpt 6 343848177.356 ± 13084730.321 ops/s
85+ * UnsynchronizedMapBenchmark.iterate_tagMap thrpt 6 140210161.690 ± 13645098.317 ops/s
86+ * UnsynchronizedMapBenchmark.iterate_tagMap_forEach thrpt 6 354398629.295 ± 5906534.357 ops/s
87+ * UnsynchronizedMapBenchmark.iterate_treeMap thrpt 6 344428565.523 ± 11100737.787 ops/s
11488 * </code>
11589 */
11690@ Fork (2 )
11791@ Warmup (iterations = 2 )
11892@ Measurement (iterations = 3 )
11993@ Threads (8 )
94+ @ State (Scope .Thread )
12095public class UnsynchronizedMapBenchmark {
12196 static final String [] INSERTION_KEYS = {
12297 "foo" , "bar" , "baz" , "quux" , "foobar" , "foobaz" , "key0" , "key1" , "key2" , "key3"
@@ -132,18 +107,19 @@ public class UnsynchronizedMapBenchmark {
132107 return keys ;
133108 });
134109
135- static int sharedLookupIndex = 0 ;
110+ int lookupIndex = 0 ; // per-thread (Scope.Thread) — no shared-counter contention under @Threads(8)
136111
137- static String nextLookupKey () {
112+ String nextLookupKey () {
138113 return nextLookupKey (EQUAL_KEYS );
139114 }
140115
141- static String nextLookupKey (String [] keys ) {
142- int localIndex = ++ sharedLookupIndex ;
143- if (localIndex >= keys .length ) {
144- sharedLookupIndex = localIndex = 0 ;
116+ String nextLookupKey (String [] keys ) {
117+ int i = lookupIndex + 1 ;
118+ if (i >= keys .length ) {
119+ i = 0 ;
145120 }
146- return keys [localIndex ];
121+ lookupIndex = i ;
122+ return keys [i ];
147123 }
148124
149125 static <T > T init (Supplier <T > supplier ) {
0 commit comments