3535 * ({@code ImmutableCollections.SetN}), which is what the agent actually uses for fixed config
3636 * sets. Java 10+; falls back to {@code HashSet} pre-10. The realistic baseline for any
3737 * flat/immutable set comparison.
38+ * <li>{@code stringIndex} — {@link StringIndex#contains} on the instance wrapper (one field load
39+ * to reach the placed arrays, then an open-addressed probe).
40+ * <li>{@code support} — the same probe via {@link StringIndex.Support#indexOf} over {@code static
41+ * final} arrays, so the JIT folds the refs to constants and there is nothing to dereference
42+ * (the hot path StringIndex recommends). The {@code stringIndex}/{@code support} pair shows
43+ * the indirection cost of the wrapper.
3844 * </ul>
3945 *
4046 * <p>Lookups are interned (the {@code ==} fast path where a structure has one); misses are short
4147 * and never present.
4248 *
43- * <p>Java 17 results (Apple M1, {@code @Fork(2)}, {@code @Threads(8)}; M ops/s = millions):
49+ * <p>JDK 17 results (Apple M1, quiet machine, {@code @Fork(5)}, {@code @Threads(8)}; M ops/s =
50+ * millions):
4451 *
4552 * <pre>{@code
4653 * Structure hit miss
47- * hashSet 2159 1751 (fastest)
48- * tracerImmutableSet 1946 1633 (Set.copyOf / SetN)
49- * array 926 584
50- * sortedArray 664 588
51- * treeSet 642 593
54+ * support (static) 2320 2159 (fastest)
55+ * hashSet 2198 2134
56+ * stringIndex (inst) 2098 1548 * (* miss bimodal -- see caveat)
57+ * tracerImmutableSet 1914 1663 (Set.copyOf / SetN)
58+ * array 941 589
59+ * sortedArray 685 610
60+ * treeSet 657 610
5261 * }</pre>
5362 *
5463 * <p>Key findings:
5564 *
5665 * <ul>
57- * <li>{@code HashSet} is fastest; {@link java.util.Set#copyOf} ({@code SetN}) trails by only ~10%
58- * on hit and ~7% on miss — and it's the compact, array-backed form the agent already uses for
59- * fixed config sets, so it's a strong default when the set is immutable.
60- * <li>{@code array} / {@code sortedArray} / {@code treeSet} cluster at ~0.6–0.9B — they scan,
61- * binary-search, or tree-walk per lookup, so they trail the hashed structures, most visibly
62- * on the miss path.
66+ * <li>The static {@code Support} path is the fastest — it beats {@code HashSet} on hit and miss
67+ * and crushes the scan/search/tree forms.
68+ * <li>{@code stringIndex} (the instance wrapper) trails {@code Support} by the field-load
69+ * indirection (~10% on hit), landing near {@code HashSet} — fine off the hot path, prefer
70+ * {@code Support} on it.
71+ * <li>{@link java.util.Set#copyOf} ({@code SetN}, the agent's compact fixed-set form) is ~1.2x
72+ * behind {@code Support} on hit but the most <i>compact</i> (~27% smaller — no cached hashes,
73+ * no 2x table). So StringIndex's edge over {@code SetN} is speed + the {@code
74+ * indexOf}->parallel-array capability, not footprint; over {@code HashSet} it wins both.
75+ * <li>{@code array} / {@code sortedArray} / {@code treeSet} trail the hashed structures, most on
76+ * miss.
6377 * </ul>
78+ *
79+ * <p><b>Caveat — the instance {@code stringIndex} miss is bimodal across forks</b> (confirmed at
80+ * {@code @Fork(10)}: 6 forks fast, 4 slow, nothing between). ~60% of forks compile to a fast mode
81+ * (~2000, ≈ {@code support_miss} — the wrapper indirection is then free) and ~40% to a slow mode
82+ * (~1070, ~half); each fork locks one at warmup. So the {@code 1548 ±27%} above is a mode-mix, not
83+ * noise. Cause: C2 hoists the instance field-loads ({@code this.hashes}/{@code names}) out of the
84+ * miss-path probe loop only in the fast mode; the static {@code Support} path const-folds those
85+ * refs and is never bimodal ({@code support_miss} ±0.3%). Prefer {@code Support} where miss latency
86+ * matters.
6487 */
65- @ Fork (2 )
88+ @ Fork (5 ) // 5 forks settle the bimodal stringIndex_miss / interface-dispatch arms (see header )
6689@ Warmup (iterations = 2 )
6790@ Measurement (iterations = 3 )
6891@ Threads (8 )
@@ -84,12 +107,26 @@ static String[] newMisses() {
84107 return misses ;
85108 }
86109
110+ // StringIndex static-Support mode: the placed arrays pulled into static final fields, so the JIT
111+ // folds the refs to constants and Support.indexOf has nothing to dereference (the hot path the
112+ // StringIndex class Javadoc recommends). Contrast support_* (these) with stringIndex_* (the
113+ // instance wrapper, one field load) to see the indirection cost.
114+ static final int [] SI_HASHES ;
115+ static final String [] SI_NAMES ;
116+
117+ static {
118+ StringIndex .Data data = StringIndex .Support .create (STRINGS );
119+ SI_HASHES = data .hashes ;
120+ SI_NAMES = data .names ;
121+ }
122+
87123 // Built once, never mutated -- safe to share across the reader threads.
88124 String [] array ;
89125 String [] sortedArray ;
90126 HashSet <String > hashSet ;
91127 TreeSet <String > treeSet ;
92128 Set <String > tracerImmutableSet ;
129+ StringIndex stringIndex ;
93130
94131 @ Setup (Level .Trial )
95132 public void setUp () {
@@ -99,6 +136,7 @@ public void setUp() {
99136 hashSet = new HashSet <>(Arrays .asList (STRINGS ));
100137 treeSet = new TreeSet <>(Arrays .asList (STRINGS ));
101138 tracerImmutableSet = CollectionUtils .tryMakeImmutableSet (Arrays .asList (STRINGS ));
139+ stringIndex = StringIndex .of (STRINGS );
102140 }
103141
104142 /** Per-thread lookup cursor so each reader thread cycles keys independently. */
@@ -184,4 +222,24 @@ public boolean tracerImmutableSet_hit(Cursor cursor) {
184222 public boolean tracerImmutableSet_miss (Cursor cursor ) {
185223 return tracerImmutableSet .contains (cursor .nextMiss ());
186224 }
225+
226+ @ Benchmark
227+ public boolean stringIndex_hit (Cursor cursor ) {
228+ return stringIndex .contains (cursor .nextHit ());
229+ }
230+
231+ @ Benchmark
232+ public boolean stringIndex_miss (Cursor cursor ) {
233+ return stringIndex .contains (cursor .nextMiss ());
234+ }
235+
236+ @ Benchmark
237+ public boolean support_hit (Cursor cursor ) {
238+ return StringIndex .Support .indexOf (SI_HASHES , SI_NAMES , cursor .nextHit ()) >= 0 ;
239+ }
240+
241+ @ Benchmark
242+ public boolean support_miss (Cursor cursor ) {
243+ return StringIndex .Support .indexOf (SI_HASHES , SI_NAMES , cursor .nextMiss ()) >= 0 ;
244+ }
187245}
0 commit comments