Skip to content

Commit d916762

Browse files
dougqhclaude
andcommitted
Drop benchmarks from StringIndex PR; revert SetBenchmark/UnsynchronizedMapBenchmark
StringIndex's benchmark integration is moving to the dedicated benchmark PRs (set overhaul #11721, map overhaul #11679) and will be folded in there later. Revert both benchmark files to master so this PR is purely the StringIndex data structure + tests. Avoids the #11679/#11721 deletions-vs-edits conflicts too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0b1b9bd commit d916762

2 files changed

Lines changed: 99 additions & 219 deletions

File tree

Lines changed: 44 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,44 @@
11
package datadog.trace.util;
22

33
import java.util.Arrays;
4+
import java.util.Collections;
45
import java.util.HashSet;
56
import java.util.TreeSet;
7+
import java.util.concurrent.ThreadLocalRandom;
68
import java.util.function.Supplier;
79
import org.openjdk.jmh.annotations.Benchmark;
810
import org.openjdk.jmh.annotations.Fork;
911
import org.openjdk.jmh.annotations.Measurement;
10-
import org.openjdk.jmh.annotations.Scope;
11-
import org.openjdk.jmh.annotations.State;
1212
import org.openjdk.jmh.annotations.Threads;
1313
import 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)
6142
public 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

Comments
 (0)