Skip to content

Commit 4d493ed

Browse files
dougqhclaude
andcommitted
Move UnsynchronizedMapBenchmark lookup rotation to @State(Scope.Thread)
The shared static sharedLookupIndex was written by every thread under @threads(8), so its cache-line ping-pong capped the get_* benchmarks near a ~1.4B ops/s contention ceiling -- the same artifact SetBenchmark fixed by moving its rotation counter to per-thread @State(Scope.Thread). Apply the same fix here; off the ceiling the get_* numbers rise and the differences between map options stop being compressed. Replaces the two stale, unlabeled Java-21 result blocks (and the interim contention-capped get block) with a single env-stamped table from one run on a modern, representative JVM (Apple M1 Max, macOS 26.4.1, Zulu 17.0.7+7-LTS, 8 threads, 2 forks). Header conclusions re-checked against these numbers: the fixed TagSet map leads HashMap ~30%/~50% (rotating/same-key), forEach matches the fastest map iterators, and create's HashMap-vs-LinkedHashMap edge is ~1.4x. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 66ee8ed commit 4d493ed

1 file changed

Lines changed: 48 additions & 72 deletions

File tree

internal-api/src/jmh/java/datadog/trace/util/UnsynchronizedMapBenchmark.java

Lines changed: 48 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import org.openjdk.jmh.annotations.Benchmark;
1010
import org.openjdk.jmh.annotations.Fork;
1111
import org.openjdk.jmh.annotations.Measurement;
12+
import org.openjdk.jmh.annotations.Scope;
13+
import org.openjdk.jmh.annotations.State;
1214
import org.openjdk.jmh.annotations.Threads;
1315
import org.openjdk.jmh.annotations.Warmup;
1416
import org.openjdk.jmh.infra.Blackhole;
@@ -18,7 +20,9 @@
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
@@ -34,89 +38,60 @@
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)
12095
public 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

Comments
 (0)