11package datadog .trace .util ;
22
33import java .util .Arrays ;
4+ import java .util .Collections ;
45import java .util .HashSet ;
56import java .util .TreeSet ;
7+ import java .util .concurrent .ThreadLocalRandom ;
68import java .util .function .Supplier ;
79import org .openjdk .jmh .annotations .Benchmark ;
810import org .openjdk .jmh .annotations .Fork ;
911import org .openjdk .jmh .annotations .Measurement ;
10- import org .openjdk .jmh .annotations .Scope ;
11- import org .openjdk .jmh .annotations .State ;
1212import org .openjdk .jmh .annotations .Threads ;
1313import org .openjdk .jmh .annotations .Warmup ;
1414
1515/**
16- * Ways to represent a small set of strings and test membership, split into hit and miss lookups
17- * (different cost shapes per structure). Lookups are interned (the {@code ==} fast path); misses
18- * are short and never present. Per-thread state ({@code @State(Scope.Thread)}) keeps the rotation
19- * counter off the shared-write path under {@code @Threads(8)} — an earlier shared-counter version
20- * capped the fast structures at a ~1.4B contention ceiling (since superseded by the numbers below).
16+ *
2117 *
2218 * <ul>
23- * <li><b>stringIndexSupport (static) is the fastest membership path</b> — 2336M hit / 2170M miss.
24- * It beats the StringIndex instance ({@code stringIndex_*}) by ~7% (hit) to ~12% (miss): the
25- * instance pays an instance-field load of hashes/names, while {@code Support.indexOf} over
26- * {@code static final} arrays lets the refs fold to constants. Matches KeyOfBenchmark's ~12%
27- * static-vs- instance gap. So when the set is fixed, pull {@code Data} into your own static
28- * finals.
29- * <li><b>vs HashSet</b> — the static path is ~12% faster on hit and ~par on miss. But HashSet was
30- * noisy here (±22% error) while StringIndex was tight (±2-7%), so StringIndex also wins on
31- * predictability — and is allocation-free and positional-capable.
32- * <li>array / sortedArray / treeSet cluster ~0.65-1.0B — they compare/scan per element, so they
33- * slow on miss (hit early-exits; miss does the full scan / binary descent / tree walk).
34- * TreeSet is NOT uniquely slowest — worth it only for a custom comparator (case-insensitive,
35- * dodging {@code toLowerCase}), not speed.
19+ * Benchmark showing possible ways to represent and check if a set includes an elememt...
20+ * <li>(RECOMMENDED) HashSet - on par with TreeSet - idiomatic
21+ * <li>(RECOMMENDED) TreeMap - on par with HashSet - better solution if custom comparator is
22+ * needed (see CaseInsensitiveMapBenchmark)
23+ * <li>array - slower than HashSet
24+ * <li>sortedArray - slowest - slower than array for common case of small arrays
3625 * </ul>
3726 *
3827 * <code>
39- * Apple M1 Max (10 core) - 8 threads (per-thread state) - 2 forks - Java 8 (Zulu 8.0.382)
28+ * MacBook M1 - 8 threads - Java 21
29+ * 1/3 not found rate
4030 *
41- * Benchmark Mode Cnt Score Error Units
42- * SetBenchmark.array_hit thrpt 6 995578895.732 ± 73709080.997 ops/s
43- * SetBenchmark.array_miss thrpt 6 649860848.470 ± 32489300.626 ops/s
44- * SetBenchmark.hashSet_hit thrpt 6 2081738804.271 ± 464349157.190 ops/s
45- * SetBenchmark.hashSet_miss thrpt 6 2136501411.026 ± 474132929.024 ops/s
46- * SetBenchmark.sortedArray_hit thrpt 6 837595967.794 ± 113538780.712 ops/s
47- * SetBenchmark.sortedArray_miss thrpt 6 692064118.699 ± 25752553.077 ops/s
48- * SetBenchmark.stringIndex_hit thrpt 6 2184722734.028 ± 61054981.099 ops/s
49- * SetBenchmark.stringIndex_miss thrpt 6 1933588009.009 ± 159869680.982 ops/s
50- * SetBenchmark.stringIndexSupport_hit thrpt 6 2335685599.706 ± 52460762.937 ops/s
51- * SetBenchmark.stringIndexSupport_miss thrpt 6 2169715463.018 ± 141321499.862 ops/s
52- * SetBenchmark.treeSet_hit thrpt 6 798251906.675 ± 39041398.413 ops/s
53- * SetBenchmark.treeSet_miss thrpt 6 667078954.487 ± 56517120.187 ops/s
31+ * Benchmark Mode Cnt Score Error Units
32+ * SetBenchmark.contains_array thrpt 6 645561886.327 ± 100781717.494 ops/s
33+ * SetBenchmark.contains_hashSet thrpt 6 1536236680.235 ± 114966961.506 ops/s
34+ * SetBenchmark.contains_sortedArray thrpt 6 571476939.441 ± 21334620.460 ops/s
35+ * SetBenchmark.contains_treeSet thrpt 6 1557663759.411 ± 95343683.124 ops/s
5436 * </code>
5537 */
5638@ Fork (2 )
5739@ Warmup (iterations = 2 )
5840@ Measurement (iterations = 3 )
5941@ Threads (8 )
60- @ State (Scope .Thread )
6142public class SetBenchmark {
6243 static final String [] STRINGS =
6344 new String [] {
@@ -79,60 +60,45 @@ static <T> T init(Supplier<T> supplier) {
7960 return supplier .get ();
8061 }
8162
82- /** Present in the set (interned). */
83- static final String [] HITS = STRINGS ;
84-
85- /** Never present. */
86- static final String [] MISSES =
63+ static final String [] LOOKUPS =
8764 init (
8865 () -> {
89- String [] misses = new String [STRINGS .length * 4 ];
90- for (int i = 0 ; i < misses .length ; ++i ) {
91- misses [i ] = "dne-" + i ;
66+ String [] lookups = Arrays .copyOf (STRINGS , STRINGS .length * 10 );
67+
68+ for (int i = 0 ; i < STRINGS .length ; ++i ) {
69+ lookups [STRINGS .length + i ] = new String (STRINGS [i ]);
70+ }
71+
72+ // 2 / 3 of the key look-ups miss the set
73+ for (int i = STRINGS .length * 2 ; i < lookups .length ; ++i ) {
74+ lookups [i ] = "dne-" + ThreadLocalRandom .current ().nextInt ();
9275 }
93- return misses ;
94- });
9576
96- int hitIndex = 0 ; // per-thread (Scope.Thread) — no shared-counter contention under @Threads(8)
97- int missIndex = 0 ;
77+ Collections .shuffle (Arrays .asList (lookups ));
78+ return lookups ;
79+ });
9880
99- String nextHit () {
100- int i = hitIndex + 1 ;
101- if (i >= HITS .length ) {
102- i = 0 ;
103- }
104- hitIndex = i ;
105- return HITS [i ];
106- }
81+ static int sharedLookupIndex = 0 ;
10782
108- String nextMiss () {
109- int i = missIndex + 1 ;
110- if (i >= MISSES .length ) {
111- i = 0 ;
83+ static String nextString () {
84+ int localIndex = ++ sharedLookupIndex ;
85+ if (localIndex >= LOOKUPS .length ) {
86+ sharedLookupIndex = localIndex = 0 ;
11287 }
113- missIndex = i ;
114- return MISSES [i ];
88+ return LOOKUPS [localIndex ];
11589 }
11690
11791 static final String [] ARRAY = STRINGS ;
11892
119- static boolean arrayContains (String needle ) {
93+ @ Benchmark
94+ public boolean contains_array () {
95+ String needle = nextString ();
12096 for (String str : ARRAY ) {
12197 if (needle .equals (str )) return true ;
12298 }
12399 return false ;
124100 }
125101
126- @ Benchmark
127- public boolean array_hit () {
128- return arrayContains (nextHit ());
129- }
130-
131- @ Benchmark
132- public boolean array_miss () {
133- return arrayContains (nextMiss ());
134- }
135-
136102 static final String [] SORTED_ARRAY =
137103 init (
138104 () -> {
@@ -142,71 +108,21 @@ public boolean array_miss() {
142108 });
143109
144110 @ Benchmark
145- public boolean sortedArray_hit () {
146- return Arrays .binarySearch (SORTED_ARRAY , nextHit ()) >= 0 ;
147- }
148-
149- @ Benchmark
150- public boolean sortedArray_miss () {
151- return Arrays .binarySearch (SORTED_ARRAY , nextMiss ()) >= 0 ;
111+ public boolean contains_sortedArray () {
112+ return (Arrays .binarySearch (SORTED_ARRAY , nextString ()) != -1 );
152113 }
153114
154115 static final HashSet <String > HASH_SET = new HashSet <>(Arrays .asList (STRINGS ));
155116
156117 @ Benchmark
157- public boolean hashSet_hit () {
158- return HASH_SET .contains (nextHit ());
159- }
160-
161- @ Benchmark
162- public boolean hashSet_miss () {
163- return HASH_SET .contains (nextMiss ());
118+ public boolean contains_hashSet () {
119+ return HASH_SET .contains (nextString ());
164120 }
165121
166122 static final TreeSet <String > TREE_SET = new TreeSet <>(Arrays .asList (STRINGS ));
167123
168124 @ Benchmark
169- public boolean treeSet_hit () {
170- return TREE_SET .contains (nextHit ());
171- }
172-
173- @ Benchmark
174- public boolean treeSet_miss () {
175- return TREE_SET .contains (nextMiss ());
176- }
177-
178- static final StringIndex STRING_INDEX = StringIndex .of (STRINGS );
179-
180- @ Benchmark
181- public boolean stringIndex_hit () {
182- return STRING_INDEX .contains (nextHit ());
183- }
184-
185- @ Benchmark
186- public boolean stringIndex_miss () {
187- return STRING_INDEX .contains (nextMiss ());
188- }
189-
190- // The static Support path: hashes/names built once into static-final arrays (refs fold to
191- // constants) and probed directly via Support.indexOf -- vs stringIndex_* above, which loads them
192- // through a StringIndex instance. Mirrors KeyOfBenchmark's stringIndex (static) vs
193- // stringIndex_throughClass.
194- static final int [] SUPPORT_HASHES ;
195- static final String [] SUPPORT_NAMES ;
196-
197- static {
198- StringIndex .Data data = StringIndex .Support .create (STRINGS );
199- SUPPORT_HASHES = data .hashes ;
200- SUPPORT_NAMES = data .names ;
201- }
202-
203- @ Benchmark
204- public boolean stringIndexSupport_hit () {
205- return StringIndex .Support .indexOf (SUPPORT_HASHES , SUPPORT_NAMES , nextHit ()) >= 0 ;
206- }
207-
208- @ Benchmark
209- public boolean stringIndexSupport_miss () {
210- return StringIndex .Support .indexOf (SUPPORT_HASHES , SUPPORT_NAMES , nextMiss ()) >= 0 ;
125+ public boolean contains_treeSet () {
126+ return HASH_SET .contains (nextString ());
211127 }
212128}
0 commit comments