@@ -1038,6 +1038,15 @@ public EntryChange next() {
10381038 private Object [] knownValues ;
10391039 private int knownCount ;
10401040
1041+ /**
1042+ * Bloom-style presence filter over the dense store: a set bit means {@code tagId} MAY be present
1043+ * (scan to confirm), a clear bit means it is DEFINITELY absent (skip the scan, append in O(1) —
1044+ * the common per-build case). A superset of the present ids' bits: set on every add, never
1045+ * cleared on remove (a stale bit only costs a scan, never a wrong answer). So correctness never
1046+ * depends on the position→bit collision rate; only the fast-path hit rate does.
1047+ */
1048+ private long knownBloom ;
1049+
10411050 private static final int KNOWN_INIT_CAP =
10421051 12 ; // generous per-type max stopgap; exact per-type sizing comes with the tag registry
10431052
@@ -1354,27 +1363,45 @@ private void ensureKnownCapacity() {
13541363 }
13551364 }
13561365
1366+ /**
1367+ * Presence-filter bit for {@code tagId}. Crude position→bit map ({@code fieldPos} mod 64) to
1368+ * start; a collision-minimizing per-type coloring later only raises the hit rate — correctness
1369+ * never depends on it, because the scan is authoritative.
1370+ */
1371+ private static long knownBloomBit (long tagId ) {
1372+ return 1L << (KnownTagCodec .fieldPos (tagId ) & 63 );
1373+ }
1374+
13571375 /**
13581376 * Stores a known tag's value densely (no {@link Entry} alloc). Overwrites in place when present
13591377 * (returning the prior value materialized as an Entry, per the {@code Map} contract — usually
1360- * discarded by {@code set}); otherwise appends, growing x2 as needed.
1378+ * discarded by {@code set}); otherwise appends, growing x2 as needed. The bloom filter skips the
1379+ * {@link #knownIndexOf} scan when the tag is definitely absent (the common per-build case), so an
1380+ * append is O(1) instead of O(n).
13611381 */
13621382 private Entry putKnownValue (long tagId , Object value ) {
1363- int i = this .knownIndexOf (tagId );
1364- if (i >= 0 ) {
1365- Object prior = this .knownValues [i ];
1366- this .knownValues [i ] = value ;
1367- return materializeKnown (tagId , prior );
1383+ long bit = knownBloomBit (tagId );
1384+ if ((this .knownBloom & bit ) != 0 ) {
1385+ // maybe present -> scan to overwrite in place
1386+ int i = this .knownIndexOf (tagId );
1387+ if (i >= 0 ) {
1388+ Object prior = this .knownValues [i ];
1389+ this .knownValues [i ] = value ;
1390+ return materializeKnown (tagId , prior );
1391+ }
1392+ // bloom false positive (collision) -> fall through to append
13681393 }
13691394 this .ensureKnownCapacity ();
13701395 int slot = this .knownCount ++;
13711396 this .knownIds [slot ] = tagId ;
13721397 this .knownValues [slot ] = value ;
1398+ this .knownBloom |= bit ;
13731399 return null ;
13741400 }
13751401
13761402 /** Raw dense value for {@code tagId}, or {@code null} when absent (no Entry, no boxing). */
13771403 private Object knownRawValue (long tagId ) {
1404+ if ((this .knownBloom & knownBloomBit (tagId )) == 0 ) return null ; // definitely absent, no scan
13781405 int i = this .knownIndexOf (tagId );
13791406 return i < 0 ? null : this .knownValues [i ];
13801407 }
@@ -1383,6 +1410,7 @@ private Object knownRawValue(long tagId) {
13831410 * Removes a known tag from the dense store (swap-with-last), returning the prior Entry or null.
13841411 */
13851412 private Entry removeKnown (long tagId ) {
1413+ if ((this .knownBloom & knownBloomBit (tagId )) == 0 ) return null ; // definitely absent
13861414 int i = this .knownIndexOf (tagId );
13871415 if (i < 0 ) return null ;
13881416 Object prior = this .knownValues [i ];
@@ -1391,6 +1419,8 @@ private Entry removeKnown(long tagId) {
13911419 this .knownValues [i ] = this .knownValues [last ];
13921420 this .knownIds [last ] = 0L ;
13931421 this .knownValues [last ] = null ;
1422+ // knownBloom intentionally NOT cleared: a stale-set bit only costs a scan; clearing could drop
1423+ // a bit still shared (via collision) by a present id -> false negative.
13941424 return materializeKnown (tagId , prior );
13951425 }
13961426
@@ -1405,7 +1435,8 @@ private static Entry materializeKnown(long tagId, Object value) {
14051435 * local bucket entry — known tags never bucket — so no bucket check is needed here.)
14061436 */
14071437 private boolean parentDenseHidden (long tagId ) {
1408- if (this .knownIndexOf (tagId ) >= 0 ) return true ; // shadowed by a local dense entry
1438+ // shadowed by a local dense entry (bloom prunes the scan when definitely absent)
1439+ if ((this .knownBloom & knownBloomBit (tagId )) != 0 && this .knownIndexOf (tagId ) >= 0 ) return true ;
14091440 return this .removedFromParent != null
14101441 && this .removedFromParent .contains (KnownTagCodec .nameOf (tagId )); // tombstoned
14111442 }
@@ -1801,6 +1832,7 @@ private void putAllIntoEmptyMap(TagMap that) {
18011832 this .knownIds = Arrays .copyOf (that .knownIds , that .knownIds .length );
18021833 this .knownValues = Arrays .copyOf (that .knownValues , that .knownValues .length );
18031834 this .knownCount = that .knownCount ;
1835+ this .knownBloom = that .knownBloom ;
18041836 }
18051837 }
18061838
@@ -2182,6 +2214,7 @@ public void clear() {
21822214 this .knownIds = null ;
21832215 this .knownValues = null ;
21842216 this .knownCount = 0 ;
2217+ this .knownBloom = 0L ;
21852218 }
21862219
21872220 public TagMap freeze () {
0 commit comments