-
Notifications
You must be signed in to change notification settings - Fork 344
Add FlatHashtable #11980
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dougqh
wants to merge
13
commits into
dougqh/strategy-annotation
Choose a base branch
from
dougqh/flat-hashtable
base: dougqh/strategy-annotation
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add FlatHashtable #11980
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
4591577
Add FlatHashtable: open-addressed find-or-create with static-polymorp…
dougqh c1d7b16
Add FlatHashtable collision/probe coverage tests
dougqh f39ac32
Reshape FlatHashtable into KeyStrategy/CreateStrategy strategies
dougqh fb84b62
Benchmark FlatHashtable in the single-threaded map comparison
dougqh f92aab4
Benchmark FlatHashtable's lock-free read in the thread-safe map compa…
dougqh 779de80
Make ThreadSafeMapBenchmark lookup index per-thread (remove shared-co…
dougqh e5674eb
Adopt INSTANCE-singleton convention in FlatHashtable test strategies
dougqh 68791d6
Defeat CHA in the FlatHashtable map benchmarks to prove structural de…
dougqh f9f9d22
Add case-insensitive strategy, load-factor control, and resize to Fla…
dougqh a8701fc
Split FlatHashtable strategy into HashStrategy/MatchingStrategy + spe…
dougqh 1cf81d8
Finalize iterator benchmark numbers at F5 (2.10x, tight CIs)
dougqh ba5e1c6
Refresh CaseInsensitiveMapBenchmark Javadoc with post-reshape F5 numbers
dougqh de8082d
Cover FlatHashtable resize empty-slot skip and iterator full-table wrap
dougqh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
181 changes: 181 additions & 0 deletions
181
internal-api/src/jmh/java/datadog/trace/util/FlatHashtableIteratorBenchmark.java
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| package datadog.trace.util; | ||
|
|
||
| import java.util.Iterator; | ||
| import org.openjdk.jmh.annotations.Benchmark; | ||
| import org.openjdk.jmh.annotations.Fork; | ||
| import org.openjdk.jmh.annotations.Level; | ||
| import org.openjdk.jmh.annotations.Measurement; | ||
| import org.openjdk.jmh.annotations.Scope; | ||
| import org.openjdk.jmh.annotations.Setup; | ||
| import org.openjdk.jmh.annotations.State; | ||
| import org.openjdk.jmh.annotations.Threads; | ||
| import org.openjdk.jmh.annotations.Warmup; | ||
|
|
||
| /** | ||
| * Tests the static-polymorphism iterator specialization: walking a shared-hash bucket through the | ||
| * <b>general</b> {@code iterator(table, hash, hashStrategy)} (strategy held in a field -> {@code | ||
| * hashOf} dispatched via the shared type profile) vs the <b>Entry-specialized</b> {@code | ||
| * iterator(table, hash)} (feeds a constant strategy into the shared traversal template -> {@code | ||
| * hashOf} inlines to {@code entry.hash} by constant type-flow). Both return {@code Iterator<E>} and | ||
| * reuse the same core; only the strategy call's dispatch differs. | ||
| * | ||
| * <p><b>Why the general path is fragile.</b> When only one strategy type flows through it, C2 | ||
| * devirtualizes {@code hashOf} from the type profile and the two arms tie — type-profile does the | ||
| * specialization's job. But HotSpot keeps <i>one</i> {@code hashOf} profile per bytecode index, | ||
| * shared across every inlining context (no context-sensitive profiling), so <b>{@code setUp} | ||
| * poisons that profile</b> by driving the general iterator with four distinct strategy types. After | ||
| * that, {@code iterate_general} — though it uses a single strategy and looks monomorphic — pays a | ||
| * virtual {@code hashOf} per entry, degraded by callers it has nothing to do with. {@code | ||
| * iterate_specialized} is immune: the constant gives exact-type devirt regardless of the profile. | ||
| * That contrast is the point — the specialization turns a profile-dependent inline into a | ||
| * structural one. Confirm with {@code -XX:+PrintInlining}. | ||
| */ | ||
| @Fork(2) | ||
| @Warmup(iterations = 3, time = 1) | ||
| @Measurement(iterations = 3, time = 1) | ||
| @Threads(1) | ||
| @State(Scope.Thread) | ||
| public class FlatHashtableIteratorBenchmark { | ||
| // All entries share this hash -> one contiguous probe-run "bucket" the iterator walks. | ||
| static final long BUCKET_HASH = 0x123456789ABCDEF0L; | ||
| static final int BUCKET_SIZE = 16; | ||
|
|
||
| static final class ItEntry extends FlatHashtable.Entry { | ||
| final int value; | ||
|
|
||
| ItEntry(long hash, int value) { | ||
| super(hash); | ||
| this.value = value; | ||
| } | ||
| } | ||
|
|
||
| // Four DISTINCT hashOf implementors, all reading entry.hash (identical behavior, different | ||
| // types). | ||
| // Loading them defeats CHA; driving all four through the shared traversal in setUp poisons the | ||
| // hashOf profile to megamorphic, so type-profile can't cleanly devirtualize it for the general | ||
| // path — the case the Entry-specialized iterator is immune to. | ||
| // | ||
| // Measured (MacBook M1, Zulu 21, F5, poisoned profile): iterate_general 18.06M ±0.18 vs | ||
| // iterate_specialized 37.88M ±0.16 ops/s (2.10x, tight non-overlapping CIs). Unpoisoned, the two | ||
| // tie (~37.8M) — type-profile does the general path's devirt then; the poisoning is what | ||
| // type-profile can't survive and the constant can. | ||
| static final class ItHashStrategyA implements FlatHashtable.HashStrategy<ItEntry> { | ||
| static final ItHashStrategyA INSTANCE = new ItHashStrategyA(); | ||
|
|
||
| private ItHashStrategyA() {} | ||
|
|
||
| @Override | ||
| public long hashOf(ItEntry entry) { | ||
| return entry.hash; | ||
| } | ||
| } | ||
|
|
||
| static final class ItHashStrategyB implements FlatHashtable.HashStrategy<ItEntry> { | ||
| static final ItHashStrategyB INSTANCE = new ItHashStrategyB(); | ||
|
|
||
| private ItHashStrategyB() {} | ||
|
|
||
| @Override | ||
| public long hashOf(ItEntry entry) { | ||
| return entry.hash; | ||
| } | ||
| } | ||
|
|
||
| static final class ItHashStrategyC implements FlatHashtable.HashStrategy<ItEntry> { | ||
| static final ItHashStrategyC INSTANCE = new ItHashStrategyC(); | ||
|
|
||
| private ItHashStrategyC() {} | ||
|
|
||
| @Override | ||
| public long hashOf(ItEntry entry) { | ||
| return entry.hash; | ||
| } | ||
| } | ||
|
|
||
| static final class ItHashStrategyD implements FlatHashtable.HashStrategy<ItEntry> { | ||
| static final ItHashStrategyD INSTANCE = new ItHashStrategyD(); | ||
|
|
||
| private ItHashStrategyD() {} | ||
|
|
||
| @Override | ||
| public long hashOf(ItEntry entry) { | ||
| return entry.hash; | ||
| } | ||
| } | ||
|
|
||
| @SuppressWarnings({"unchecked", "rawtypes"}) | ||
| static final FlatHashtable.HashStrategy<ItEntry>[] STRATEGIES = | ||
| new FlatHashtable.HashStrategy[] { | ||
| ItHashStrategyA.INSTANCE, | ||
| ItHashStrategyB.INSTANCE, | ||
| ItHashStrategyC.INSTANCE, | ||
| ItHashStrategyD.INSTANCE, | ||
| }; | ||
|
|
||
| ItEntry[] table; | ||
|
|
||
| // Kept live so the profile-poisoning loop below can't be dead-code-eliminated. | ||
| static long POISON_SINK; | ||
|
|
||
| @Setup(Level.Trial) | ||
| public void setUp() { | ||
| // Sparse so the bucket is one contiguous run with a clean terminating empty slot. | ||
| table = FlatHashtable.create(ItEntry.class, BUCKET_SIZE, FlatHashtable.LOW_LOAD_FACTOR); | ||
| for (int i = 0; i < BUCKET_SIZE; ++i) { | ||
| FlatHashtable.insert(table, new ItEntry(BUCKET_HASH, i)); // all share the hash => one bucket | ||
| } | ||
| // Poison the shared HashIterator.advanceWith hashOf profile: drive the GENERAL iterator with | ||
| // four distinct strategy types, hot enough that C2 records the site as megamorphic. HotSpot | ||
| // keeps one per-bci profile shared across all inlining contexts (no context-sensitive profiling | ||
| // — tried in Zing, never robust), so those unrelated callers permanently pollute the profile. | ||
| long sink = 0; | ||
| for (int r = 0; r < 200_000; ++r) { | ||
| sink += poison(); | ||
| } | ||
| POISON_SINK = sink; | ||
| } | ||
|
|
||
| /** Drives the general iterator once per distinct strategy type — the profile poisoner. */ | ||
| private long poison() { | ||
| long sink = 0; | ||
| for (FlatHashtable.HashStrategy<ItEntry> s : STRATEGIES) { | ||
| Iterator<ItEntry> it = FlatHashtable.iterator(table, BUCKET_HASH, s); | ||
| while (it.hasNext()) { | ||
| sink += it.next().value; | ||
| } | ||
| } | ||
| return sink; | ||
| } | ||
|
|
||
| /** | ||
| * General iterator with a SINGLE strategy — looks monomorphic, but its {@code hashOf} still | ||
| * dispatches virtually because {@code setUp} already poisoned the shared profile with other | ||
| * strategy types. This is the cross-caller pollution failure mode: a caller degraded by callers | ||
| * it has nothing to do with. (Unpoisoned, this ties {@code iterate_specialized} — type-profile | ||
| * then devirtualizes it; the poisoning is what type-profile can't survive and the constant can.) | ||
| */ | ||
| @Benchmark | ||
| public long iterate_general() { | ||
| long sum = 0; | ||
| Iterator<ItEntry> it = FlatHashtable.iterator(table, BUCKET_HASH, ItHashStrategyA.INSTANCE); | ||
| while (it.hasNext()) { | ||
| sum += it.next().value; | ||
| } | ||
| return sum; | ||
| } | ||
|
|
||
| /** | ||
| * Entry-specialized iterator: feeds the constant Entry-hash strategy into the shared template, so | ||
| * {@code hashOf} inlines to {@code entry.hash} <b>structurally</b> (constant type-flow, not | ||
| * profile) — immune to the poisoned profile that sinks {@code iterate_general}. | ||
| */ | ||
| @Benchmark | ||
| public long iterate_specialized() { | ||
| long sum = 0; | ||
| Iterator<ItEntry> it = FlatHashtable.iterator(table, BUCKET_HASH); // Entry overload | ||
| while (it.hasNext()) { | ||
| sum += it.next().value; // hashOf inlined to entry.hash via the constant strategy | ||
| } | ||
| return sum; | ||
| } | ||
| } |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the creation benchmark, the flat version stops here after 16 distinct inserts, but
_create_hashMapand_create_treeMapabove also run theUPPER_PREFIXESloop, which performs 8 extra case-insensitive overwrite puts. That meanscreate_flatHashtableis measuring less construction/update work while the results table compares creation throughput, so the experiment overstates the flat path; either model the duplicate lookup/update work or avoid comparing these create numbers.Useful? React with 👍 / 👎.