Skip to content

Commit 9e76978

Browse files
dougqhclaude
andcommitted
Add TagSet: a generic open-addressed string set (+ benchmarks)
TagSet (datadog.trace.util) — a flat, allocation-free, open-addressed string set: Support (static algorithm over raw int[] hashes / String[] names — the hot path, folds to constants when held in static finals), Data (build-time carrier), and a convenience instance wrapper. 2x-oversized (load factor <= 0.5), linear probe, interned-== fast path, 0 = empty sentinel. Consumers attach a parallel payload array indexed by the returned slot. Benchmarks: SetBenchmark (membership vs HashSet/array/sortedArray/treeSet) and KeyOfBenchmark (name->id resolution vs HashMap and a hand-written switch). TagSetTest covers hashing, probing/wraparound, table-full, and parallel payload. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 26f1bcb commit 9e76978

4 files changed

Lines changed: 623 additions & 44 deletions

File tree

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
package datadog.trace.util;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
import java.util.function.Supplier;
6+
import org.openjdk.jmh.annotations.Benchmark;
7+
import org.openjdk.jmh.annotations.Fork;
8+
import org.openjdk.jmh.annotations.Measurement;
9+
import org.openjdk.jmh.annotations.Scope;
10+
import org.openjdk.jmh.annotations.State;
11+
import org.openjdk.jmh.annotations.Threads;
12+
import org.openjdk.jmh.annotations.Warmup;
13+
14+
/**
15+
* name -&gt; id resolution shootout (the {@code keyOf} path), built on the generic {@link TagSet}.
16+
*
17+
* <ul>
18+
* <li><b>tagSet</b> — {@code TagSet.Support.indexOf} over <b>static-final</b> {@code int[]
19+
* hashes} / {@code String[] names} (refs fold to constants) + a parallel {@code long[] ids}.
20+
* <li><b>tagSet_throughClass</b> — same, but via a {@code TagSet} <b>instance</b> (an
21+
* instance-field load of hashes/names) — isolates the wrapper indirection vs static fields.
22+
* <li><b>hashMap</b> — {@code HashMap<String,Long>} (boxes the value).
23+
* <li><b>switch</b> — hand-written string {@code switch} (the thing keyOf replaces). At 16 cases
24+
* it inlines fine; the at-scale degradation (hundreds of cases over FreqInlineSize) shows up
25+
* against the real generated keyOf, not here.
26+
* </ul>
27+
*
28+
* <p>Two term flavors: <b>interned</b> (realistic — instrumentation passes string literals → the
29+
* {@code ==} fast path in eq) and <b>copies</b> (non-interned → forces {@code String.equals}).
30+
* Terms are hit-dominated. <code>
31+
* Apple M1 Max (10 core) - 8 threads (per-thread state) - 2 forks - Java 8 (Zulu 8.0.382)
32+
*
33+
* Benchmark Mode Cnt Score Error Units
34+
* KeyOfBenchmark.aa_baseline_termSelection thrpt 6 2743246161.5 ± 29519843.7 ops/s
35+
* KeyOfBenchmark.tagSet thrpt 6 2275407420.3 ± 35217527.6 ops/s
36+
* KeyOfBenchmark.tagSet_throughClass thrpt 6 2036161909.9 ± 49813775.7 ops/s
37+
* KeyOfBenchmark.hashMap thrpt 6 1889985340.4 ± 46434121.2 ops/s
38+
* KeyOfBenchmark.switch_ thrpt 6 1132557957.9 ±128775728.2 ops/s
39+
* // copies (non-interned): tagSet 1843M, tagSet_throughClass 1708M, hashMap 1593M, switch_ 1137M
40+
* </code>
41+
*
42+
* <ul>
43+
* <li><b>tagSet ~2x the switch</b> (2275M vs 1133M) at only 16 cases — the gap widens toward the
44+
* generated hundreds, where the switch exceeds the inline budget. The keyOf swap's win.
45+
* <li><b>tagSet ~20% over HashMap</b> (2275M vs 1890M).
46+
* <li><b>static ~12% over the instance</b> (tagSet 2275M vs tagSet_throughClass 2036M) — folded
47+
* static-final arrays beat the instance-field load; pull {@code Data} into your own statics.
48+
* <li>The switch is interning-insensitive (1133≈1137, dispatch-bound); hash contenders gain
49+
* ~16-19% interned via the {@code ==} fast path.
50+
* </ul>
51+
*/
52+
@Fork(2)
53+
@Warmup(iterations = 2)
54+
@Measurement(iterations = 3)
55+
@Threads(8)
56+
@State(Scope.Thread)
57+
public class KeyOfBenchmark {
58+
static final long UNKNOWN = 0L;
59+
60+
static final String[] NAMES_IN = {
61+
"span.type", "component", "span.kind", "db.type", "db.instance", "db.statement",
62+
"peer.hostname", "peer.port", "http.method", "http.route", "http.status_code", "http.url",
63+
"error", "resource", "service", "operation"
64+
};
65+
66+
/** ids parallel to NAMES_IN — id == index+1, matched across all contenders. */
67+
static final long[] IDS_IN =
68+
init(
69+
() -> {
70+
long[] ids = new long[NAMES_IN.length];
71+
for (int j = 0; j < ids.length; j++) {
72+
ids[j] = j + 1L;
73+
}
74+
return ids;
75+
});
76+
77+
// fastest path: build once, pull into static final so the refs fold
78+
static final int[] HASHES;
79+
static final String[] NAMES;
80+
static final long[] IDS;
81+
82+
static {
83+
TagSet.Data data = TagSet.Support.create(NAMES_IN);
84+
long[] ids = new long[data.names.length];
85+
for (int j = 0; j < NAMES_IN.length; j++) {
86+
ids[TagSet.Support.indexOf(data.hashes, data.names, NAMES_IN[j])] = IDS_IN[j];
87+
}
88+
HASHES = data.hashes;
89+
NAMES = data.names;
90+
IDS = ids;
91+
}
92+
93+
static final Map<String, Long> HASH_MAP =
94+
init(
95+
() -> {
96+
Map<String, Long> m = new HashMap<>(NAMES_IN.length * 2);
97+
for (int j = 0; j < NAMES_IN.length; j++) {
98+
m.put(NAMES_IN[j], IDS_IN[j]);
99+
}
100+
return m;
101+
});
102+
103+
/** Convenience instance — the through-the-class path (instance-field loads vs folded statics). */
104+
static final TagSet TAG_SET = TagSet.of(NAMES_IN);
105+
106+
// hit-dominated, two misses; interned and non-interned copies
107+
static final String[] TERMS = {
108+
"span.type", "component", "span.kind", "db.type", "db.instance", "db.statement",
109+
"peer.hostname", "peer.port", "http.method", "http.route", "http.status_code", "http.url",
110+
"error", "resource", "service", "operation", "unknown.tag", "custom.attr"
111+
};
112+
113+
static final String[] TERM_COPIES =
114+
init(
115+
() -> {
116+
String[] copies = new String[TERMS.length];
117+
for (int i = 0; i < TERMS.length; i++) {
118+
copies[i] = new String(TERMS[i]); // defeat interning
119+
}
120+
return copies;
121+
});
122+
123+
int termIndex = 0; // per-thread (Scope.Thread) — no shared-counter contention under @Threads(8)
124+
125+
String nextTerm() {
126+
int i = termIndex + 1;
127+
if (i >= TERMS.length) {
128+
i = 0;
129+
}
130+
termIndex = i;
131+
return TERMS[i];
132+
}
133+
134+
String nextTermCopy() {
135+
int i = termIndex + 1;
136+
if (i >= TERM_COPIES.length) {
137+
i = 0;
138+
}
139+
termIndex = i;
140+
return TERM_COPIES[i];
141+
}
142+
143+
static <T> T init(Supplier<T> supplier) {
144+
return supplier.get();
145+
}
146+
147+
// ---- resolvers ----
148+
static long tagSetKeyOf(String t) {
149+
int slot = TagSet.Support.indexOf(HASHES, NAMES, t); // folded static-final refs
150+
return slot < 0 ? UNKNOWN : IDS[slot];
151+
}
152+
153+
static long tagSetThroughClassKeyOf(String t) {
154+
int slot = TAG_SET.indexOf(t); // instance-field load of hashes/names
155+
return slot < 0 ? UNKNOWN : IDS[slot];
156+
}
157+
158+
static long hashMapKeyOf(String t) {
159+
Long v = HASH_MAP.get(t);
160+
return v == null ? UNKNOWN : v.longValue();
161+
}
162+
163+
static long switchKeyOf(String t) {
164+
switch (t) {
165+
case "span.type":
166+
return 1L;
167+
case "component":
168+
return 2L;
169+
case "span.kind":
170+
return 3L;
171+
case "db.type":
172+
return 4L;
173+
case "db.instance":
174+
return 5L;
175+
case "db.statement":
176+
return 6L;
177+
case "peer.hostname":
178+
return 7L;
179+
case "peer.port":
180+
return 8L;
181+
case "http.method":
182+
return 9L;
183+
case "http.route":
184+
return 10L;
185+
case "http.status_code":
186+
return 11L;
187+
case "http.url":
188+
return 12L;
189+
case "error":
190+
return 13L;
191+
case "resource":
192+
return 14L;
193+
case "service":
194+
return 15L;
195+
case "operation":
196+
return 16L;
197+
default:
198+
return UNKNOWN;
199+
}
200+
}
201+
202+
// ---- interned terms (realistic) ----
203+
@Benchmark
204+
public String aa_baseline_termSelection() {
205+
return nextTerm();
206+
}
207+
208+
@Benchmark
209+
public long tagSet() {
210+
return tagSetKeyOf(nextTerm());
211+
}
212+
213+
@Benchmark
214+
public long tagSet_throughClass() {
215+
return tagSetThroughClassKeyOf(nextTerm());
216+
}
217+
218+
@Benchmark
219+
public long hashMap() {
220+
return hashMapKeyOf(nextTerm());
221+
}
222+
223+
@Benchmark
224+
public long switch_() {
225+
return switchKeyOf(nextTerm());
226+
}
227+
228+
// ---- non-interned copies (forces equals) ----
229+
@Benchmark
230+
public String aa_baseline_termSelectionCopy() {
231+
return nextTermCopy();
232+
}
233+
234+
@Benchmark
235+
public long tagSet_copy() {
236+
return tagSetKeyOf(nextTermCopy());
237+
}
238+
239+
@Benchmark
240+
public long tagSet_throughClass_copy() {
241+
return tagSetThroughClassKeyOf(nextTermCopy());
242+
}
243+
244+
@Benchmark
245+
public long hashMap_copy() {
246+
return hashMapKeyOf(nextTermCopy());
247+
}
248+
249+
@Benchmark
250+
public long switch_copy() {
251+
return switchKeyOf(nextTermCopy());
252+
}
253+
}

0 commit comments

Comments
 (0)