Skip to content

Commit 0b1b9bd

Browse files
dougqhclaude
andcommitted
Rename TagSet to StringIndex
The structure is more general than tags: a flat, open-addressed String set whose indexOf returns a stable dense slot for parallel value arrays. "Tag" was just one prospective consumer. Renames the class, test, and benchmark references (Support/Data nested types and the SetBenchmark / map-benchmark usages); leaves the unrelated propagation TagSetChanges in dd-trace-core alone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4d493ed commit 0b1b9bd

4 files changed

Lines changed: 68 additions & 60 deletions

File tree

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

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@
2020
* capped the fast structures at a ~1.4B contention ceiling (since superseded by the numbers below).
2121
*
2222
* <ul>
23-
* <li><b>tagSetSupport (static) is the fastest membership path</b> — 2336M hit / 2170M miss. It
24-
* beats the TagSet instance ({@code tagSet_*}) by ~7% (hit) to ~12% (miss): the instance pays
25-
* an instance-field load of hashes/names, while {@code Support.indexOf} over {@code static
26-
* final} arrays lets the refs fold to constants. Matches KeyOfBenchmark's ~12% static-vs-
27-
* instance gap. So when the set is fixed, pull {@code Data} into your own static finals.
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.
2829
* <li><b>vs HashSet</b> — the static path is ~12% faster on hit and ~par on miss. But HashSet was
29-
* noisy here (±22% error) while TagSet was tight (±2-7%), so TagSet also wins on
30+
* noisy here (±22% error) while StringIndex was tight (±2-7%), so StringIndex also wins on
3031
* predictability — and is allocation-free and positional-capable.
3132
* <li>array / sortedArray / treeSet cluster ~0.65-1.0B — they compare/scan per element, so they
3233
* slow on miss (hit early-exits; miss does the full scan / binary descent / tree walk).
@@ -44,10 +45,10 @@
4445
* SetBenchmark.hashSet_miss thrpt 6 2136501411.026 ± 474132929.024 ops/s
4546
* SetBenchmark.sortedArray_hit thrpt 6 837595967.794 ± 113538780.712 ops/s
4647
* SetBenchmark.sortedArray_miss thrpt 6 692064118.699 ± 25752553.077 ops/s
47-
* SetBenchmark.tagSet_hit thrpt 6 2184722734.028 ± 61054981.099 ops/s
48-
* SetBenchmark.tagSet_miss thrpt 6 1933588009.009 ± 159869680.982 ops/s
49-
* SetBenchmark.tagSetSupport_hit thrpt 6 2335685599.706 ± 52460762.937 ops/s
50-
* SetBenchmark.tagSetSupport_miss thrpt 6 2169715463.018 ± 141321499.862 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
5152
* SetBenchmark.treeSet_hit thrpt 6 798251906.675 ± 39041398.413 ops/s
5253
* SetBenchmark.treeSet_miss thrpt 6 667078954.487 ± 56517120.187 ops/s
5354
* </code>
@@ -174,37 +175,38 @@ public boolean treeSet_miss() {
174175
return TREE_SET.contains(nextMiss());
175176
}
176177

177-
static final TagSet TAG_SET = TagSet.of(STRINGS);
178+
static final StringIndex STRING_INDEX = StringIndex.of(STRINGS);
178179

179180
@Benchmark
180-
public boolean tagSet_hit() {
181-
return TAG_SET.contains(nextHit());
181+
public boolean stringIndex_hit() {
182+
return STRING_INDEX.contains(nextHit());
182183
}
183184

184185
@Benchmark
185-
public boolean tagSet_miss() {
186-
return TAG_SET.contains(nextMiss());
186+
public boolean stringIndex_miss() {
187+
return STRING_INDEX.contains(nextMiss());
187188
}
188189

189190
// The static Support path: hashes/names built once into static-final arrays (refs fold to
190-
// constants) and probed directly via Support.indexOf -- vs tagSet_* above, which loads them
191-
// through a TagSet instance. Mirrors KeyOfBenchmark's tagSet (static) vs tagSet_throughClass.
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.
192194
static final int[] SUPPORT_HASHES;
193195
static final String[] SUPPORT_NAMES;
194196

195197
static {
196-
TagSet.Data data = TagSet.Support.create(STRINGS);
198+
StringIndex.Data data = StringIndex.Support.create(STRINGS);
197199
SUPPORT_HASHES = data.hashes;
198200
SUPPORT_NAMES = data.names;
199201
}
200202

201203
@Benchmark
202-
public boolean tagSetSupport_hit() {
203-
return TagSet.Support.indexOf(SUPPORT_HASHES, SUPPORT_NAMES, nextHit()) >= 0;
204+
public boolean stringIndexSupport_hit() {
205+
return StringIndex.Support.indexOf(SUPPORT_HASHES, SUPPORT_NAMES, nextHit()) >= 0;
204206
}
205207

206208
@Benchmark
207-
public boolean tagSetSupport_miss() {
208-
return TagSet.Support.indexOf(SUPPORT_HASHES, SUPPORT_NAMES, nextMiss()) >= 0;
209+
public boolean stringIndexSupport_miss() {
210+
return StringIndex.Support.indexOf(SUPPORT_HASHES, SUPPORT_NAMES, nextMiss()) >= 0;
209211
}
210212
}

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

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,17 @@
2121
* <ul>
2222
* Benchmark comparing different Map-s...
2323
* <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
24+
* typically needed for tags; a fixed StringIndex map below is faster still when the keys are
2525
* known)
2626
* <li>(RECOMMENDED) TagMap - for storing tags - especially if copying between maps or using
2727
* builders
2828
* <li>TreeMap - better for custom Comparators - case-insensitive Maps (see
2929
* CaseInsensitiveMapBenchmark)
3030
* <li>LinkedHashMap - only when insertion order is needed
31-
* <li>TagSet + parallel value array - for a FIXED (build-once, read-only) map whose key set is
32-
* known up front: the keys go in a {@link TagSet} and the values in a parallel array indexed
33-
* by the slot {@code indexOf} returns. Fastest get, no per-lookup allocation, no node chasing
34-
* - but it can't change after construction (see get_tagSetMap).
31+
* <li>StringIndex + parallel value array - for a FIXED (build-once, read-only) map whose key set
32+
* is known up front: the keys go in a {@link StringIndex} and the values in a parallel array
33+
* indexed by the slot {@code indexOf} returns. Fastest get, no per-lookup allocation, no node
34+
* chasing - but it can't change after construction (see get_stringIndexMap).
3535
* </ul>
3636
*
3737
* <p>TagMap is the preferred way to store tags.
@@ -51,10 +51,10 @@
5151
* <p>HashMap & TagMap also perform exceedingly well in cases where the exact same object is used
5252
* for put & get operations. e.g. when using String literals or Class literals as keys.
5353
*
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
54+
* <p>A StringIndex + parallel int[] used as a fixed (build-once) map is the fastest get here --
55+
* ~30% ahead of HashMap on the rotating-key path and ~50% ahead on the same-key path, where it
56+
* sustains 5.4B ops/s at the tightest error in the table (±1.3%). It pays no boxing (int[] values),
57+
* chases no node, and allocates nothing per lookup. It only applies when the key set is fixed at
5858
* construction. <code>
5959
* 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)
6060
*
@@ -76,8 +76,8 @@
7676
* UnsynchronizedMapBenchmark.get_linkedHashMap thrpt 6 1871397460.306 ± 51848940.996 ops/s
7777
* UnsynchronizedMapBenchmark.get_tagMap thrpt 6 1706422514.145 ± 91472057.777 ops/s
7878
* 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
79+
* UnsynchronizedMapBenchmark.get_stringIndexMap thrpt 6 2448917198.752 ± 105399021.596 ops/s
80+
* UnsynchronizedMapBenchmark.get_stringIndexMap_sameKey thrpt 6 5358887465.195 ± 70196881.552 ops/s
8181
* UnsynchronizedMapBenchmark.get_treeMap thrpt 6 704782343.575 ± 52129796.457 ops/s
8282
*
8383
* UnsynchronizedMapBenchmark.iterate_hashMap thrpt 6 132215205.201 ± 9079485.505 ops/s
@@ -303,38 +303,42 @@ public TagMap clone_tagMap() {
303303
return TAG_MAP.copy();
304304
}
305305

306-
// TagSet + a parallel value array used as a FIXED (build-once, read-only) map. The keys are known
307-
// up front, so they go in a TagSet and the values in a plain array indexed by the slot that
306+
// StringIndex + a parallel value array used as a FIXED (build-once, read-only) map. The keys are
307+
// known
308+
// up front, so they go in a StringIndex and the values in a plain array indexed by the slot that
308309
// Support.indexOf returns. There is no node, no Entry, and no per-lookup allocation -- a get is a
309310
// hash probe (with an interned == fast path) plus one array load. Pull the Data into your own
310311
// static finals (as below) so the hashes/names refs fold to constants -- same static-vs-instance
311312
// win SetBenchmark and KeyOfBenchmark measure. The values live in a parallel int[] -- no boxing,
312313
// the same primitive-value advantage TagMap.getInt has over a HashMap<String, Integer>, whose
313314
// value type structurally forces the box. The trade-off: it cannot change after construction.
314-
static final int[] TAG_SET_HASHES;
315-
static final String[] TAG_SET_NAMES;
316-
static final int[] TAG_SET_VALUES;
315+
static final int[] STRING_INDEX_HASHES;
316+
static final String[] STRING_INDEX_NAMES;
317+
static final int[] STRING_INDEX_VALUES;
317318

318319
static {
319-
TagSet.Data data = TagSet.Support.create(INSERTION_KEYS);
320+
StringIndex.Data data = StringIndex.Support.create(INSERTION_KEYS);
320321
int[] values = new int[data.names.length];
321322
for (int i = 0; i < INSERTION_KEYS.length; ++i) {
322-
values[TagSet.Support.indexOf(data.hashes, data.names, INSERTION_KEYS[i])] = i;
323+
values[StringIndex.Support.indexOf(data.hashes, data.names, INSERTION_KEYS[i])] = i;
323324
}
324-
TAG_SET_HASHES = data.hashes;
325-
TAG_SET_NAMES = data.names;
326-
TAG_SET_VALUES = values;
325+
STRING_INDEX_HASHES = data.hashes;
326+
STRING_INDEX_NAMES = data.names;
327+
STRING_INDEX_VALUES = values;
327328
}
328329

329330
@Benchmark
330-
public int get_tagSetMap() {
331-
int slot = TagSet.Support.indexOf(TAG_SET_HASHES, TAG_SET_NAMES, nextLookupKey());
332-
return slot < 0 ? -1 : TAG_SET_VALUES[slot];
331+
public int get_stringIndexMap() {
332+
int slot =
333+
StringIndex.Support.indexOf(STRING_INDEX_HASHES, STRING_INDEX_NAMES, nextLookupKey());
334+
return slot < 0 ? -1 : STRING_INDEX_VALUES[slot];
333335
}
334336

335337
@Benchmark
336-
public int get_tagSetMap_sameKey() {
337-
int slot = TagSet.Support.indexOf(TAG_SET_HASHES, TAG_SET_NAMES, nextLookupKey(INSERTION_KEYS));
338-
return slot < 0 ? -1 : TAG_SET_VALUES[slot];
338+
public int get_stringIndexMap_sameKey() {
339+
int slot =
340+
StringIndex.Support.indexOf(
341+
STRING_INDEX_HASHES, STRING_INDEX_NAMES, nextLookupKey(INSERTION_KEYS));
342+
return slot < 0 ? -1 : STRING_INDEX_VALUES[slot];
339343
}
340344
}

internal-api/src/main/java/datadog/trace/util/TagSet.java renamed to internal-api/src/main/java/datadog/trace/util/StringIndex.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* path; nothing to dereference.
1212
* <li>{@link Data} — a <b>build-time carrier</b> for the placed {@code {hashes, names}} returned
1313
* by {@link Support#create}. Pull its fields into your own and discard it.
14-
* <li>The {@code TagSet} <b>instance</b> ({@link #of}) — a convenience wrapper holding the
14+
* <li>The {@code StringIndex} <b>instance</b> ({@link #of}) — a convenience wrapper holding the
1515
* arrays; {@link #indexOf}/{@link #contains} delegate to {@link Support}. Costs an
1616
* instance-field load per call (the indirection the static path removes) — fine off the hot
1717
* path.
@@ -23,12 +23,12 @@
2323
* <p>Slot 0-value is the empty sentinel: {@link Support#hash} never returns 0, so {@code hashes[i]
2424
* == 0} unambiguously means an empty slot.
2525
*/
26-
public final class TagSet {
26+
public final class StringIndex {
2727
private final int[] hashes;
2828
private final String[] names;
2929
public final int slots; // == hashes.length
3030

31-
private TagSet(int[] hashes, String[] names) {
31+
private StringIndex(int[] hashes, String[] names) {
3232
this.hashes = hashes;
3333
this.names = names;
3434
this.slots = hashes.length;
@@ -37,9 +37,9 @@ private TagSet(int[] hashes, String[] names) {
3737
/**
3838
* Convenience instance — wraps the placed arrays. For the hot path prefer raw {@link Support}.
3939
*/
40-
public static TagSet of(String... names) {
40+
public static StringIndex of(String... names) {
4141
Data data = Support.create(names);
42-
return new TagSet(data.hashes, data.names);
42+
return new StringIndex(data.hashes, data.names);
4343
}
4444

4545
/** Slot of {@code name}, or -1. Delegates to {@link Support} on the instance's arrays. */
@@ -67,7 +67,9 @@ public static final class Data {
6767
}
6868
}
6969

70-
/** Static algorithm over raw arrays. Query helpers take raw arrays, never a Data or a TagSet. */
70+
/**
71+
* Static algorithm over raw arrays. Query helpers take raw arrays, never a Data or a StringIndex.
72+
*/
7173
public static final class Support {
7274
private Support() {}
7375

internal-api/src/test/java/datadog/trace/util/TagSetTest.java renamed to internal-api/src/test/java/datadog/trace/util/StringIndexTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
import static org.junit.jupiter.api.Assertions.assertThrows;
77
import static org.junit.jupiter.api.Assertions.assertTrue;
88

9-
import datadog.trace.util.TagSet.Data;
10-
import datadog.trace.util.TagSet.Support;
9+
import datadog.trace.util.StringIndex.Data;
10+
import datadog.trace.util.StringIndex.Support;
1111
import org.junit.jupiter.api.Test;
1212

13-
class TagSetTest {
13+
class StringIndexTest {
1414

1515
@Test
1616
void hash_spread_and_zeroSentinel() {
@@ -32,7 +32,7 @@ void tableSizeFor_isPow2_andOversized() {
3232

3333
@Test
3434
void instance_contains_internedAndCopy_andMiss() {
35-
TagSet set = TagSet.of("foo", "bar", "baz");
35+
StringIndex set = StringIndex.of("foo", "bar", "baz");
3636

3737
assertEquals(8, set.slots()); // 3 names -> tableSizeFor(3) == 8
3838

@@ -84,7 +84,7 @@ void put_throwsWhenFull() {
8484
assertThrows(IllegalStateException.class, () -> Support.put(hashes, names, "c", 6));
8585
}
8686

87-
/** The documented usage: build a TagSet, attach a parallel payload indexed by slot. */
87+
/** The documented usage: build a StringIndex, attach a parallel payload indexed by slot. */
8888
@Test
8989
void parallelPayloadBySlot() {
9090
String[] names = {"a", "b", "c"};

0 commit comments

Comments
 (0)