Skip to content

Commit 428e72f

Browse files
dougqhclaude
andcommitted
Add bloom-filter fast-path to dense store (skip knownIndexOf scan)
A per-map superset presence filter (one long): a clear bit means a known tag is definitely absent, so an append skips the O(n) linear scan -> O(1) in the common per-build case. Set on every add, never cleared on remove (stale bit only costs a scan, never a wrong answer), so correctness is independent of the position->bit collision rate; the scan is authoritative. Crude fieldPos-mod-64 mapping to start; per-type graph-coloring later raises the hit rate. WIP: tests + benchmark pending. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 27d42d4 commit 428e72f

1 file changed

Lines changed: 47 additions & 13 deletions

File tree

  • internal-api/src/main/java/datadog/trace/api

internal-api/src/main/java/datadog/trace/api/TagMap.java

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,11 +1225,11 @@ static final class EmptyHolder {
12251225

12261226
/**
12271227
* Dense known-tag store (dense-tagmap-design §5). Values for KNOWN tags (those {@link
1228-
* KnownTagCodec#keyOf} resolves to a stored id) live in these INSERTION-ORDERED parallel arrays with
1229-
* NO per-tag {@link Entry} object — the allocation win. Lazily allocated on the first known-tag
1230-
* write ({@code null} until then, so all-unknown maps pay nothing) and grown x2 from {@link
1231-
* #KNOWN_INIT_CAP}. Matched by globalSerial via a linear scan ({@link #knownIndexOf}); reads
1232-
* aren't hot, so O(knownCount) is fine and positional indexing is deferred. Dormant until a
1228+
* KnownTagCodec#keyOf} resolves to a stored id) live in these INSERTION-ORDERED parallel arrays
1229+
* with NO per-tag {@link Entry} object — the allocation win. Lazily allocated on the first
1230+
* known-tag write ({@code null} until then, so all-unknown maps pay nothing) and grown x2 from
1231+
* {@link #KNOWN_INIT_CAP}. Matched by globalSerial via a linear scan ({@link #knownIndexOf});
1232+
* reads aren't hot, so O(knownCount) is fine and positional indexing is deferred. Dormant until a
12331233
* resolver is registered: {@code keyOf} returns 0, so nothing routes here and production is
12341234
* byte-identical.
12351235
*
@@ -1247,6 +1247,15 @@ static final class EmptyHolder {
12471247
private Object[] knownValues;
12481248
private int knownCount;
12491249

1250+
/**
1251+
* Bloom-style presence filter over the dense store: a set bit means {@code tagId} MAY be present
1252+
* (scan to confirm), a clear bit means it is DEFINITELY absent (skip the scan, append in O(1) —
1253+
* the common per-build case). A superset of the present ids' bits: set on every add, never
1254+
* cleared on remove (a stale bit only costs a scan, never a wrong answer). So correctness never
1255+
* depends on the position→bit collision rate; only the fast-path hit rate does.
1256+
*/
1257+
private long knownBloom;
1258+
12501259
private static final int KNOWN_INIT_CAP = 8;
12511260

12521261
/**
@@ -1552,27 +1561,45 @@ private void ensureKnownCapacity() {
15521561
}
15531562
}
15541563

1564+
/**
1565+
* Presence-filter bit for {@code tagId}. Crude position→bit map ({@code fieldPos} mod 64) to
1566+
* start; a collision-minimizing per-type coloring later only raises the hit rate — correctness
1567+
* never depends on it, because the scan is authoritative.
1568+
*/
1569+
private static long knownBloomBit(long tagId) {
1570+
return 1L << (KnownTagCodec.fieldPos(tagId) & 63);
1571+
}
1572+
15551573
/**
15561574
* Stores a known tag's value densely (no {@link Entry} alloc). Overwrites in place when present
15571575
* (returning the prior value materialized as an Entry, per the {@code Map} contract — usually
1558-
* discarded by {@code set}); otherwise appends, growing x2 as needed.
1576+
* discarded by {@code set}); otherwise appends, growing x2 as needed. The bloom filter skips the
1577+
* {@link #knownIndexOf} scan when the tag is definitely absent (the common per-build case), so an
1578+
* append is O(1) instead of O(n).
15591579
*/
15601580
private Entry putKnownValue(long tagId, Object value) {
1561-
int i = this.knownIndexOf(tagId);
1562-
if (i >= 0) {
1563-
Object prior = this.knownValues[i];
1564-
this.knownValues[i] = value;
1565-
return materializeKnown(tagId, prior);
1581+
long bit = knownBloomBit(tagId);
1582+
if ((this.knownBloom & bit) != 0) {
1583+
// maybe present -> scan to overwrite in place
1584+
int i = this.knownIndexOf(tagId);
1585+
if (i >= 0) {
1586+
Object prior = this.knownValues[i];
1587+
this.knownValues[i] = value;
1588+
return materializeKnown(tagId, prior);
1589+
}
1590+
// bloom false positive (collision) -> fall through to append
15661591
}
15671592
this.ensureKnownCapacity();
15681593
int slot = this.knownCount++;
15691594
this.knownIds[slot] = tagId;
15701595
this.knownValues[slot] = value;
1596+
this.knownBloom |= bit;
15711597
return null;
15721598
}
15731599

15741600
/** Raw dense value for {@code tagId}, or {@code null} when absent (no Entry, no boxing). */
15751601
private Object knownRawValue(long tagId) {
1602+
if ((this.knownBloom & knownBloomBit(tagId)) == 0) return null; // definitely absent, no scan
15761603
int i = this.knownIndexOf(tagId);
15771604
return i < 0 ? null : this.knownValues[i];
15781605
}
@@ -1581,6 +1608,7 @@ private Object knownRawValue(long tagId) {
15811608
* Removes a known tag from the dense store (swap-with-last), returning the prior Entry or null.
15821609
*/
15831610
private Entry removeKnown(long tagId) {
1611+
if ((this.knownBloom & knownBloomBit(tagId)) == 0) return null; // definitely absent
15841612
int i = this.knownIndexOf(tagId);
15851613
if (i < 0) return null;
15861614
Object prior = this.knownValues[i];
@@ -1589,6 +1617,8 @@ private Entry removeKnown(long tagId) {
15891617
this.knownValues[i] = this.knownValues[last];
15901618
this.knownIds[last] = 0L;
15911619
this.knownValues[last] = null;
1620+
// knownBloom intentionally NOT cleared: a stale-set bit only costs a scan; clearing could drop
1621+
// a bit still shared (via collision) by a present id -> false negative.
15921622
return materializeKnown(tagId, prior);
15931623
}
15941624

@@ -1603,7 +1633,8 @@ private static Entry materializeKnown(long tagId, Object value) {
16031633
* local bucket entry — known tags never bucket — so no bucket check is needed here.)
16041634
*/
16051635
private boolean parentDenseHidden(long tagId) {
1606-
if (this.knownIndexOf(tagId) >= 0) return true; // shadowed by a local dense entry
1636+
// shadowed by a local dense entry (bloom prunes the scan when definitely absent)
1637+
if ((this.knownBloom & knownBloomBit(tagId)) != 0 && this.knownIndexOf(tagId) >= 0) return true;
16071638
return this.removedFromParent != null
16081639
&& this.removedFromParent.contains(KnownTagCodec.nameOf(tagId)); // tombstoned
16091640
}
@@ -2006,6 +2037,7 @@ private void putAllIntoEmptyMap(OptimizedTagMap that) {
20062037
this.knownIds = Arrays.copyOf(that.knownIds, that.knownIds.length);
20072038
this.knownValues = Arrays.copyOf(that.knownValues, that.knownValues.length);
20082039
this.knownCount = that.knownCount;
2040+
this.knownBloom = that.knownBloom;
20092041
}
20102042
}
20112043

@@ -2048,7 +2080,8 @@ public void fillStringMap(Map<? super String, ? super String> stringMap) {
20482080
}
20492081
for (int i = 0; i < this.knownCount; ++i) {
20502082
stringMap.put(
2051-
KnownTagCodec.nameOf(this.knownIds[i]), TagValueConversions.toString(this.knownValues[i]));
2083+
KnownTagCodec.nameOf(this.knownIds[i]),
2084+
TagValueConversions.toString(this.knownValues[i]));
20522085
}
20532086
}
20542087

@@ -2393,6 +2426,7 @@ public void clear() {
23932426
this.knownIds = null;
23942427
this.knownValues = null;
23952428
this.knownCount = 0;
2429+
this.knownBloom = 0L;
23962430
}
23972431

23982432
public OptimizedTagMap freeze() {

0 commit comments

Comments
 (0)