Skip to content

Commit 24a9fdc

Browse files
dougqhclaude
andcommitted
Overhaul set benchmarks: split Immutable / SingleThreaded, add Set.copyOf
Mirror the map-benchmark overhaul for sets. Replace the single SetBenchmark (shared mutable counter under @threads(8); contains_treeSet bug that queried HASH_SET) with two classes that each pick the right threading model: - ImmutableSetBenchmark: fixed read-only membership shared across threads (@State(Scope.Benchmark)); array / sortedArray / HashSet / TreeSet / Set.copyOf (the JDK compact SetN the agent actually uses for config sets, via CollectionUtils.tryMakeImmutableSet). hit/miss split, per-thread cursor. - SingleThreadedSetBenchmark: per-thread mutable lifecycle (@State(Scope.Thread)); create/clone + contains/iterate, plus a Collections.synchronizedSet case for the uncontended synchronization tax (per-thread => bias never revoked; biased-locking story across JVMs). StringIndex rows fold in later. Result blocks empty pending a fresh multi-JVM run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d875e4f commit 24a9fdc

3 files changed

Lines changed: 337 additions & 128 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package datadog.trace.util;
2+
3+
import java.util.Arrays;
4+
import java.util.HashSet;
5+
import java.util.Set;
6+
import java.util.TreeSet;
7+
import org.openjdk.jmh.annotations.Benchmark;
8+
import org.openjdk.jmh.annotations.Fork;
9+
import org.openjdk.jmh.annotations.Level;
10+
import org.openjdk.jmh.annotations.Measurement;
11+
import org.openjdk.jmh.annotations.Scope;
12+
import org.openjdk.jmh.annotations.Setup;
13+
import org.openjdk.jmh.annotations.State;
14+
import org.openjdk.jmh.annotations.Threads;
15+
import org.openjdk.jmh.annotations.Warmup;
16+
17+
/**
18+
* Membership over a small, fixed, read-only string set shared across threads — split into hit and
19+
* miss lookups (different cost shapes per structure).
20+
*
21+
* <p>The set is built once and only read, so a single shared instance ({@link Scope#Benchmark})
22+
* read by all {@code @Threads} is realistic and contention-free. This is the read-mostly
23+
* counterpart to the per-thread mutable {@link SingleThreadedSetBenchmark}, and mirrors {@link
24+
* ImmutableMapBenchmark} on the set side. Sets in the tracer skew strongly toward this fixed,
25+
* read-only shape.
26+
*
27+
* <p>Strategies compared:
28+
*
29+
* <ul>
30+
* <li>{@code array} / {@code sortedArray} — linear scan / binary search; slow on miss.
31+
* <li>{@link HashSet} — idiomatic, fast; node-based, allocates per element.
32+
* <li>{@link TreeSet} — comparator-ordered; worth it only for a custom comparator, not speed.
33+
* <li>{@link java.util.Set#copyOf} (via {@link CollectionUtils#tryMakeImmutableSet}) — the JDK's
34+
* compact, array-backed immutable set ({@code ImmutableCollections.SetN}), which is what the
35+
* agent actually uses for fixed config sets. Java 10+; falls back to {@code HashSet} pre-10.
36+
* The realistic baseline for any flat/immutable set comparison.
37+
* </ul>
38+
*
39+
* <p>Lookups are interned (the {@code ==} fast path where a structure has one); misses are short
40+
* and never present. (Results pending a fresh multi-JVM run — {@code Set.copyOf} only materializes
41+
* the compact form on Java 10+.)
42+
*/
43+
@Fork(2)
44+
@Warmup(iterations = 2)
45+
@Measurement(iterations = 3)
46+
@Threads(8)
47+
@State(Scope.Benchmark)
48+
public class ImmutableSetBenchmark {
49+
static final String[] STRINGS = {
50+
"foo", "bar", "baz", "quux", "hello", "world",
51+
"service", "queryString", "lorem", "ipsum", "dolem", "sit"
52+
};
53+
54+
/** Distinct String instances that are never present, for the miss path. */
55+
static final String[] MISSES = newMisses();
56+
57+
static String[] newMisses() {
58+
String[] misses = new String[STRINGS.length * 4];
59+
for (int i = 0; i < misses.length; ++i) {
60+
misses[i] = "dne-" + i;
61+
}
62+
return misses;
63+
}
64+
65+
// Built once, never mutated -- safe to share across the reader threads.
66+
String[] array;
67+
String[] sortedArray;
68+
HashSet<String> hashSet;
69+
TreeSet<String> treeSet;
70+
Set<String> copyOfSet;
71+
72+
@Setup(Level.Trial)
73+
public void setUp() {
74+
array = STRINGS;
75+
sortedArray = Arrays.copyOf(STRINGS, STRINGS.length);
76+
Arrays.sort(sortedArray);
77+
hashSet = new HashSet<>(Arrays.asList(STRINGS));
78+
treeSet = new TreeSet<>(Arrays.asList(STRINGS));
79+
copyOfSet = CollectionUtils.tryMakeImmutableSet(Arrays.asList(STRINGS));
80+
}
81+
82+
/** Per-thread lookup cursor so each reader thread cycles keys independently. */
83+
@State(Scope.Thread)
84+
public static class Cursor {
85+
int hitIndex = 0;
86+
int missIndex = 0;
87+
88+
String nextHit() {
89+
int i = hitIndex + 1;
90+
if (i >= STRINGS.length) {
91+
i = 0;
92+
}
93+
hitIndex = i;
94+
return STRINGS[i];
95+
}
96+
97+
String nextMiss() {
98+
int i = missIndex + 1;
99+
if (i >= MISSES.length) {
100+
i = 0;
101+
}
102+
missIndex = i;
103+
return MISSES[i];
104+
}
105+
}
106+
107+
static boolean arrayContains(String[] array, String needle) {
108+
for (String s : array) {
109+
if (needle.equals(s)) {
110+
return true;
111+
}
112+
}
113+
return false;
114+
}
115+
116+
@Benchmark
117+
public boolean array_hit(Cursor cursor) {
118+
return arrayContains(array, cursor.nextHit());
119+
}
120+
121+
@Benchmark
122+
public boolean array_miss(Cursor cursor) {
123+
return arrayContains(array, cursor.nextMiss());
124+
}
125+
126+
@Benchmark
127+
public boolean sortedArray_hit(Cursor cursor) {
128+
return Arrays.binarySearch(sortedArray, cursor.nextHit()) >= 0;
129+
}
130+
131+
@Benchmark
132+
public boolean sortedArray_miss(Cursor cursor) {
133+
return Arrays.binarySearch(sortedArray, cursor.nextMiss()) >= 0;
134+
}
135+
136+
@Benchmark
137+
public boolean hashSet_hit(Cursor cursor) {
138+
return hashSet.contains(cursor.nextHit());
139+
}
140+
141+
@Benchmark
142+
public boolean hashSet_miss(Cursor cursor) {
143+
return hashSet.contains(cursor.nextMiss());
144+
}
145+
146+
@Benchmark
147+
public boolean treeSet_hit(Cursor cursor) {
148+
return treeSet.contains(cursor.nextHit());
149+
}
150+
151+
@Benchmark
152+
public boolean treeSet_miss(Cursor cursor) {
153+
return treeSet.contains(cursor.nextMiss());
154+
}
155+
156+
@Benchmark
157+
public boolean copyOf_hit(Cursor cursor) {
158+
return copyOfSet.contains(cursor.nextHit());
159+
}
160+
161+
@Benchmark
162+
public boolean copyOf_miss(Cursor cursor) {
163+
return copyOfSet.contains(cursor.nextMiss());
164+
}
165+
}

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

Lines changed: 0 additions & 128 deletions
This file was deleted.

0 commit comments

Comments
 (0)