Add FlatHashtable#11980
Conversation
|
🎯 Code Coverage (details) 🔗 Commit SHA: 60a69a3 | Docs | Datadog PR Page | Give us feedback! |
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
8e3862c to
2d79fe8
Compare
2d79fe8 to
07425af
Compare
4a05183 to
fbb0085
Compare
07425af to
b9c8860
Compare
…orphism strategies Marker-only (no enforcement yet): telegraphs the static-polymorphism strategy pattern and gives a future checker targets. @strategy marks strategy types/parameters; @StrategyConsumer marks the higher-order methods that must inline for them to specialize. Contracts live in the javadoc. Applications land in stacked PRs (FlatHashtable first). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fbb0085 to
da64961
Compare
…hism helpers A reusable open-addressed table over self-contained entries (one reference per slot), designed so callers specialize it via a concrete-typed static-final Helper that the JIT devirtualizes and inlines (C++-template-style static polymorphism). Cardinality cap / overflow / size are caller policy; this class is pure mechanism (capacityFor, create, get, getOrCreate). Includes a String-key StringHelper that seals a spread hash. Intended as the shared backing for several open-addressed caches/tables (per-operation sizing hints, UTF8BytesString caches, etc.). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Force hash collisions via fixed-hash helpers to exercise the linear-probe paths the original tests missed: probe-past-occupied + match-after-probe (in both get and getOrCreate), wraparound to the front, and get()'s full-table wrap-to-null branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
b9c8860 to
5cb9a38
Compare
KeyStrategy (abstract class): hash/matches/hashOf, long hashes for family consistency with Hashtable/ConcurrentHashtable. StringKeyStrategy seals the spread hash; EntryKeyStrategy seals hashOf to a cached Entry.hash (Entry is an optional structure-free base). CreateStrategy (bespoke @FunctionalInterface): create, cold+per-use, lambda-able. @strategy on both strategy types; @StrategyConsumer on the inlining consumers (get/getOrCreate/insert). Surface: get/getOrCreate (key-taking) + two insert flavors (Entry-based / KeyStrategy-based) over a shared comparison-free placement core; forEach (+ context variant); hash-filtered read-only iterator (not a StrategyConsumer -- interface-dispatched cold traversal). No removal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5cb9a38 to
f39ac32
Compare
Adds FlatHashtable to SingleThreadedMapBenchmark on the ops it supports (build via comparison-free insert, get, iterate) — its self-contained entry holds the value unboxed vs HashMap<String,Integer>. Fixed-capacity so the table is sized to the key count. Uses the INSTANCE-singleton strategy style (private ctor, lazy class-init). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rison Adds FlatHashtable to ThreadSafeMapBenchmark: build + concurrent get on a shared, once-published table (lock-free, no volatile) alongside ConcurrentHashMap / volatile-HashMap / synchronizedHashMap. Fixture mirrors SingleThreadedMapBenchmark (self-contained per benchmark). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…unter contention) nextLookupKey used a shared static counter incremented from all @threads(8) — a cache-line-contended race that floors the fastest reads and masks the very differences the benchmark compares (mirrors the per-thread index SingleThreadedMapBenchmark already uses, and the set-benchmark fix in #11721). Maps stay static/shared; only the lookup index goes per-thread via @State(Scope.Thread). Existing Javadoc numbers predate this and need a rerun. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each concrete key strategy carries a static final INSTANCE of its exact type + a private ctor (lazy class-init singleton); call sites use X.INSTANCE, dropping the separate module-level constants. Matches the benchmark fixture and the strategy-class style. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…virt Load two decoy KeyStrategy implementors alongside the real one in both map benchmarks so KeyStrategy.hash and KeyStrategy.matches each have >=2 concrete implementors before the hot method compiles. This denies C2 the single- implementor CHA devirtualization of keyStrat.hash/matches inside get(), so the steady-state numbers reflect the no-CHA regime rather than a deopt-guarded bet. Verified with -XX:+PrintInlining (Zulu 21; Java 8/ARM64 is fine for inlining- decision inspection but not for throughput): with CHA impossible, the concrete StringKeyStrategy::hash / IntEntryKeyStrategy::matches still inline (hot) and no type-profile/morphic markers appear on any FlatHashtable/KeyStrategy method. The devirtualization is therefore exact-type, from the constant INSTANCE propagated through the inlined get -- the structural monomorphization the @strategy contract promises, not a CHA or type-profile speculation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tHashtable Rounds out the FlatHashtable toolbox and adds a case-insensitive map arm to the comparison benchmark: - Strings.caseInsensitiveHashCode: a hashCode consistent with String.equalsIgnoreCase (same two-way fold as regionMatches(ignoreCase)), allocation-free. Reusable primitive; the strategy below composes it. - FlatHashtable.CaseInsensitiveStringKeyStrategy: case-insensitive sibling of StringKeyStrategy, sealing hash to the primitive. - The table now owns the spread (home()): a golden-ratio Fibonacci mix robust to weak/int-derived and full 64-bit hashes alike, so a KeyStrategy returns a plain hashCode without pre-mixing. StringKeyStrategy/CI/Entry now carry raw hashes. - Load-factor control: DEFAULT_LOAD_FACTOR (0.5) / LOW_LOAD_FACTOR (0.25) constants plus create(Class,int,float) / capacityFor(int,float); the 2-arg forms are kept and delegate to the default. - resize(...) and resizingInsert(...) (Entry and KeyStrategy flavors): explicit, caller-invoked growth for the rare full case, keeping get/insert resize-free on the hot path. resizingInsert returns the (possibly new) array either way. Benchmark: CaseInsensitiveMapBenchmark gains a FlatHashtable arm (dogfooding the shared strategy + primitive) and a DEFAULT-vs-LOW load-factor pair, and moves the lookup index per-thread (@State(Scope.Thread)) so the shared counter doesn't floor the fastest reads. On Zulu 21 the CI FlatHashtable is ~2x the (previously recommended) TreeMap at the same zero allocation, and matches HashMap's throughput without HashMap's per-lookup folded-String garbage; LOW_LOAD_FACTOR is a wash for the fold-dominated CI lookup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cialize the iterator Reshape the single KeyStrategy into concern-split roles so each operation asks only for what it needs, and so the iterator can be specialized without stubbing out unused methods: - HashStrategy<E> (hashOf) — entry side: insert / iterator / resize. - MatchingStrategy<E,K> (matches, + hashKey defaulting to key.hashCode()) — key side: get / getOrCreate. A functional interface; override hashKey only for custom hashing (e.g. case-insensitive). - EntryStrategy<E,K> implements both — the do-everything base a full user extends. - CaseInsensitiveStringStrategy seals hashKey to Strings.caseInsensitiveHashCode; StringKeyStrategy/EntryKeyStrategy are gone (the String hash is now the default). Specialize the iterator via the CacheHelper static-polymorphism move: HashIterator is an abstract base with final template methods (advanceWith/nextWith) taking the strategy; StrategyHashIterator holds it in a field (general), EntryHashIterator feeds the constant Entry-hash singleton so hashOf inlines to entry.hash. The public iterator(...) API and Iterator<E> return type are unchanged; the call site specializes by its own monomorphism. FlatHashtableIteratorBenchmark demonstrates why the specialization is more robust: @setup poisons the shared hashOf profile with four distinct strategy types, after which iterate_general (single strategy, looks monomorphic) pays ~2.1x because HotSpot keeps one per-bci profile shared across all inlining contexts, while iterate_specialized is immune (constant type-flow, not profile). Unpoisoned the two tie. Tests and the three map benchmarks updated to the new hierarchy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the two jacoco branch gaps the reshape left: resize now runs over a partially-filled table (exercising the null-slot skip in the rehash loop), and two iterator tests walk a full colliding table (the match-on-wrapping-slot and absent-hash walked-whole-table paths in HashIterator.advanceWith). All FlatHashtable classes are now 100% instruction- and branch-covered. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
More details
The FlatHashtable implementation is logically sound across every adversarial scenario tested — full-table iteration with no null sentinel, iterator wrap-around starting at a non-zero home slot, non-ASCII case folding (Turkish ı/İ) staying consistent with equalsIgnoreCase, and resize preserving findability of collided entries. No behavioral regressions or contract violations were found.
📊 Validated against 10 scenarios · Open Bits AI session
🤖 Datadog Autotest · Commit ba5e1c6 · What is Autotest? · Any feedback? Reach out in #autotest
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ba5e1c6b60
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…nsitiveHashCode semantics - CaseInsensitiveMapBenchmark: _create_flat now mirrors the HashMap/TreeMap builds' second loop (8 UPPER_PREFIXES case-insensitive collisions) via getOrCreate — all hits, so it does the same probe/match work the maps' overwrite puts do (the non-capturing create never fires, nothing allocates). All three create arms now run the same 24 ops, so create_* is comparable. Refreshed create numbers (create_flatHashtable 2.16M, was an unfair 3.79M measuring only 16 inserts). - Strings.caseInsensitiveHashCode javadoc: note it folds per-char exactly as String.equalsIgnoreCase itself does, so a supplementary case pair (U+10400 / U+10428) is treated as distinct by both and the two stay consistent — a code-point fold would make the hash inconsistent with equalsIgnoreCase, not more correct. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e + default Class javadoc: frame fixed capacity as a feature — create() forces you to size the table (surfacing the cardinality question that, unasked, becomes an unbounded-growth leak in an embedded agent). It caps rather than churns; growth is an explicit opt-in. Positions it as the general-purpose default of the family. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Executed 12 adversarial scenarios covering capacityFor edge cases, caseInsensitiveHashCode consistency with equalsIgnoreCase across HTTP headers and Unicode (including Turkish-I and German ß), iterator wrap-around on full tables, and mixed-hash probe filtering. One real defect found: capacityFor silently returns Integer.MIN_VALUE (-2^31) for cardinalityLimit >= 536870913 at DEFAULT_LOAD_FACTOR (or >= 268435457 at LOW_LOAD_FACTOR) because Integer.highestOneBit(min-1) << 1 overflows; the caller's subsequent Array.newInstance then throws an opaque NegativeArraySizeException. Fixed with a post-shift guard that throws a clear IllegalArgumentException, and a test case was added to lock in the rejection.
📊 Validated against 12 scenarios · Open Bits AI session
🤖 Datadog Autotest · Commit 60a69a3 · What is Autotest? · Any feedback? Reach out in #autotest
| throw new IllegalArgumentException("loadFactor must be in (0, 1): " + loadFactor); | ||
| } | ||
| int min = (int) Math.ceil(cardinalityLimit / (double) loadFactor); | ||
| return Integer.highestOneBit(min - 1) << 1; |
There was a problem hiding this comment.
capacityFor silently returns negative capacity on large cardinalities
Any caller that passes a cardinality > 536870912 (at DEFAULT_LOAD_FACTOR) or > 268435456 (at LOW_LOAD_FACTOR) gets a confusing NegativeArraySizeException from deep inside create(), not a clear argument error. Fixed by adding a post-shift guard.
Assertion details
- Input: FlatHashtable.capacityFor(536870913, DEFAULT_LOAD_FACTOR) — first cardinality at 0.5 load factor where required capacity (2^31) exceeds Integer.MAX_VALUE
- Expected:
IllegalArgumentException with clear message, or a valid positive power-of-two capacity - Actual:
Returns Integer.MIN_VALUE (-2147483648); any subsequent Array.newInstance call throws an opaque NegativeArraySizeException rather than a clear precondition violation
Was this helpful? React 👍 or 👎
🤖 Datadog Autotest · What is Autotest? · Any feedback? Reach out in #autotest
| return Integer.highestOneBit(min - 1) << 1; | |
| int cap = Integer.highestOneBit(min - 1) << 1; | |
| if (cap <= 0) { | |
| throw new IllegalArgumentException( | |
| "cardinalityLimit " + cardinalityLimit + " at loadFactor " + loadFactor | |
| + " requires capacity > Integer.MAX_VALUE"); | |
| } | |
| return cap; |
What Does This Do
Adds a light weight map-like FlatHashtable suitable for a variety of use cases
FlatHashtable is often suitable replacement for both HashMap and TreeMap
Motivation
Provide a safer, faster, lighter alternative to regular JDK Map-s. Unlike JDK Map-s, FlatHashtable is bounded in size by default, but the caller can still resize if necessary.
FlatHashtable's API is also designed to work with static polymorphism patterns to allow for optimal performance while serving as...
Additional Notes
From Claude:
A reusable open-addressed, single-array find-or-create table over self-contained entries — one reference per slot, so entry publication is a single reference store and readers see null-or-complete (no torn reads, no
volatile/atomics when the payload makes a stale/lost read benign). Pure mechanism; cardinality cap / overflow / live-size counting are caller policy.Intended as the general-purpose primitive of the flat-collection family — the one to reach for by default in perf-critical core code (works mutable and immutable, with POJO or
Entryobjects, and with custom hashing/matching). The narrower members are triggered specialists:StringIndex(expert dense name→ID interner), a futureTreeTable(ordered),ConcurrentHashtable(concurrent writers); simple immutable cases stay onSet.copyOf/Map.copyOf.Bounded by construction (a safety primitive, not just a fast one)
createtakes a cardinality budget, so you can't build one without deciding how big it may get — the question whose unasked version becomes an unbounded-growth leak in a long-lived agent embedded in someone else's process. A regularMap's auto-resize lets you forget that (fine when you own the heap; the wrong default when you're a guest). The table never grows on its own —get/getOrCreate/insertcap (a full table degrades to recompute-on-miss with bounded memory, no reallocation); growth is an explicitresize/resizingInsert. It defaults to the bounded-footprint posture the agent needs, with unbounded growth an opt-in you must reach for. (Trade pays when a miss is benign — a cache/interner — not a must-hold-everything map.)Static-polymorphism strategies
Split by concern, each held as a concrete-typed
static finalsingleton the JIT devirtualizes and inlines (one algorithm, one monomorphic instantiation per call site):MatchingStrategy<E,K>— key side (get/getOrCreate):matches(entry, key)+hashKey(key)defaulting tokey.hashCode()(override only for custom hashing, e.g. case-insensitive). A@FunctionalInterface.HashStrategy<E>— entry side (insert/iterator/resize):hashOf(entry). ForEntry-based tables it's the cachedEntry.hash, so those get strategy-free overloads.EntryStrategy<E,K>— both, the abstract base a do-everything user extends.CaseInsensitiveStringStrategy<E>— sealshashKeyto the new, allocation-freeStrings.caseInsensitiveHashCode(consistent withString.equalsIgnoreCase).CreateStrategy<E,K>— cold entry minting (non-capturing lambda / ctor-ref).The table owns the spread (
home— a golden-ratio/Fibonacci mix), so a strategy returns a plainhashCode;hashKey(key)must stay consistent withhashOf(entry).Operations
capacityFor/create(typed backing array) with load-factor control —DEFAULT_LOAD_FACTOR(0.5) /LOW_LOAD_FACTOR(0.25) +(…, float)overloads;get/getOrCreate;insert(Entry and strategy flavors);resize/resizingInsert;forEach;iterator(hash-filtered, specialized via the same static-polymorphism move soEntryiteration inlineshashOf— plainIterator<E>API unchanged).Benchmarks (JMH, Zulu 21)
SingleThreadedMapBenchmark/ThreadSafeMapBenchmark): FlatHashtable vs HashMap/TreeMap/ConcurrentHashMap. A CHA-defeat rig +-XX:+PrintInliningconfirms the strategy calls devirtualize structurally (constantINSTANCE), not via a CHA/type-profile bet. Plaingetruns in the billions of ops/s (cachedString.hashCode, single probe).CaseInsensitiveMapBenchmark: the CI FlatHashtable is ~2× TreeMap at the same zero allocation and matches HashMap's throughput without HashMap's per-lookuptoLowerCasegarbage. (All three create arms do equal work;LOW_LOAD_FACTORis a wash — the char-fold dominates.)FlatHashtableIteratorBenchmark: shows the iterator specialization is robust —@Setuppoisons the sharedhashOftype profile, after which the single-strategy general iterator pays 2.1× (18.1M vs 37.9M ops/s, F5), while the specialized iterator's constant type-flow is immune. Unpoisoned the two tie.Stacked on #11984 (the
@Strategy/@StrategyConsumermarkers). Real callers land separately (tick-tock).🤖 Generated with Claude Code