Add FlatHashtable open-addressed find-or-create collection#11980
Draft
dougqh wants to merge 12 commits into
Draft
Add FlatHashtable open-addressed find-or-create collection#11980dougqh wants to merge 12 commits into
dougqh wants to merge 12 commits into
Conversation
This comment has been minimized.
This comment has been minimized.
Contributor
🟢 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. |
dougqh
force-pushed
the
dougqh/flat-hashtable
branch
from
July 17, 2026 15:18
8e3862c to
2d79fe8
Compare
dougqh
commented
Jul 17, 2026
dougqh
commented
Jul 17, 2026
dougqh
commented
Jul 17, 2026
dougqh
commented
Jul 17, 2026
dougqh
commented
Jul 17, 2026
dougqh
force-pushed
the
dougqh/flat-hashtable
branch
from
July 17, 2026 16:25
2d79fe8 to
07425af
Compare
dougqh
commented
Jul 17, 2026
dougqh
commented
Jul 17, 2026
dougqh
commented
Jul 17, 2026
dougqh
force-pushed
the
dougqh/strategy-annotation
branch
from
July 17, 2026 16:28
4a05183 to
fbb0085
Compare
dougqh
commented
Jul 17, 2026
dougqh
commented
Jul 17, 2026
dougqh
commented
Jul 17, 2026
dougqh
commented
Jul 17, 2026
dougqh
force-pushed
the
dougqh/flat-hashtable
branch
from
July 17, 2026 16:31
07425af to
b9c8860
Compare
dougqh
force-pushed
the
dougqh/strategy-annotation
branch
from
July 17, 2026 16:36
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>
dougqh
force-pushed
the
dougqh/flat-hashtable
branch
from
July 17, 2026 16:44
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>
dougqh
force-pushed
the
dougqh/flat-hashtable
branch
from
July 17, 2026 16:50
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.Static-polymorphism strategies
The per-use policy is split by concern so each operation asks only for what it needs, and each is held as a concrete-typed
static finalsingleton the JIT devirtualizes and inlines (C++-template-style static polymorphism — one algorithm, one monomorphic instantiation per call site):MatchingStrategy<E,K>— key side (get/getOrCreate):matches(entry, key)plushashKey(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 this is the cachedEntry.hash, so those get strategy-free overloads.EntryStrategy<E,K>— both, the abstract base a do-everything user extends (invokevirtualfallback if inlining ever misses).CaseInsensitiveStringStrategy<E>— sealshashKeytoStrings.caseInsensitiveHashCode(a new, allocation-free primitive 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 plainhashCodewithout pre-mixing;hashKey(key)must stay consistent withhashOf(entry)(trivial when both default tohashCode).Operations
capacityFor/create(typed backing array viaArray.newInstance) with load-factor control —DEFAULT_LOAD_FACTOR(0.5) /LOW_LOAD_FACTOR(0.25) constants +(…, float)overloads;get/getOrCreate;insert(Entry and strategy flavors);resize/resizingInsert(explicit, caller-invoked growth — keepsget/insertresize-free on the hot path);forEach;iterator(hash-filtered).The
iteratoris specialized via the same static-polymorphism move (abstract base +finaltemplate methods; theEntryoverload feeds a constantEntry::hashsohashOfinlines structurally) while keeping the plainIterator<E>API.Benchmarks (JMH, Zulu 21)
SingleThreadedMapBenchmark/ThreadSafeMapBenchmark): FlatHashtablecreate/get/iteratevs HashMap/TreeMap/ConcurrentHashMap. A CHA-defeat rig (multiple strategy implementors loaded) confirms the strategy calls devirtualize structurally (from the constantINSTANCE), not via a CHA/type-profile bet — verified with-XX:+PrintInlining. 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 (which drives its multi-threaded GC pressure).LOW_LOAD_FACTORis a wash here (the char-fold dominates, not probe count).FlatHashtableIteratorBenchmark: demonstrates why the iterator specialization is robust.@Setuppoisons the sharedhashOftype profile with several strategy types; the single-strategy general iterator then pays 2.1× (18.1M vs 37.9M ops/s, F5, tight CIs) — HotSpot keeps one per-bci profile shared across all inlining contexts, so unrelated callers pollute it, 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