Skip to content

Updated ImmutableSetBenchmark and ImmutableMapBenchmark to be more realistic - #12294

Open
dougqh wants to merge 7 commits into
masterfrom
dougqh/stringindex-immutable-set-benchmark
Open

Updated ImmutableSetBenchmark and ImmutableMapBenchmark to be more realistic#12294
dougqh wants to merge 7 commits into
masterfrom
dougqh/stringindex-immutable-set-benchmark

Conversation

@dougqh

@dougqh dougqh commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

What Does This Do

Adds coverage of StringIndex to ImmutableSetBenchmark and ImmutableMapBenchmark
Fixes a systemic error in benchmarking of HashSet/HashMap by introducing type profile pollution
Demonstrating that the real performance of HashSet is ~20% worse than previously indicated.

Motivation

In a real system, HashSet, HashMap, etc are sharing among many different pieces of code.
That causes their methods to have dirty megamorphic profiles that aren't reflected in a simple benchmark.
BenchmarkingUtils aims to solve that by setting up those classes with more realistic profiles.

Additional Notes

  • Adds EmbeddingSupport.contains(hashes, names, name) to StringIndex, mirroring the existing instance StringIndex#contains, so callers of the static-arrays/embedded form don't need to spell out indexOf(...) >= 0 themselves. Used by ImmutableSetBenchmark's stringIndex_embedded_* arms.
  • Reworks ImmutableSetBenchmark to add a hitFresh scenario alongside the existing hit/miss scenarios: hit reuses the same interned literals used to build each structure (so String#equals's == fast path and String#hashCode's cached result pay nothing extra), while hitFresh looks up separate, never-touched String instances to measure a real, uncached hit.
  • Wires in BenchmarkUtils#polluteHashDispatch() so the JVM-shared hashCode/equals call sites are already megamorphic before each structure's own lookups are measured, matching production.
  • Replaces the javadoc's self-flagged stale/partial-run tables with a full hit/hitFresh/miss re-run across all six structures together (@Fork(5), @Threads(8)), which corrected an earlier guess: hitFresh turns out to be the slowest of the three scenarios for every hash-based structure (not merely slower than hit), because a miss usually short-circuits on the first hash mismatch while a fresh hit must probe to a match and pay a real, uncached equals().
  • Restructures StringIndex.EmbeddingSupport.put/indexOf around a single induction variable (i = (h + probes) & mask recomputed each iteration, instead of a separately-incremented cursor) for a more canonical loop shape. Investigated as a candidate fix for hitFresh's cross-fork bimodality; empirically it made no difference (root cause is a concurrent-warmup profile race governing whether C2 hoists the instance final field loads, not loop shape), but the cleaner induction-variable form is kept on its own merits.
  • Scopes down the ImmutableSetBenchmark javadoc's StringIndex-as-Set guidance: the instance wrapper reliably beats HashSet on hit (its actual design case -- repeated lookups of a known, fixed name set), but on miss/hitFresh it is bimodal across forks and its mean already sits at or below HashSet's steady figure. Miss- or fresh-key-heavy callers should reach for EmbeddingSupport directly rather than assume the wrapper is strictly better than a plain Set. This doesn't apply to StringIndex's parallel-value (map) use case, covered next.
  • Splits BenchmarkUtils#polluteHashDispatch into populateTypeProfile (drives only contains(), so it works against the actual collection instance under test, mutable or immutable) and populateTypeProfileMutable (keeps the old add()+contains() behavior for callers that need add() dispatch polluted too).
  • Adds the same polluteHashDispatch() treatment to ImmutableMapBenchmark (previously missing it) and reruns the full suite. Refreshed javadoc results table, reorganized one row per collection / one column per benchmark arm. Unlike the Set case, StringIndex's get win over HashMap/TagMap/Map.copyOf (MapN) holds up unconditionally here -- no bimodality, on both the equals() and identity-fast-path lookups.

Test plan

  • ./gradlew :internal-api:test --tests StringIndexTest
  • ./gradlew :internal-api:jmh -Pjmh.includes=ImmutableSetBenchmark run to completion and results folded into the javadoc
  • ./gradlew :internal-api:jmh -Pjmh.includes=ImmutableMapBenchmark run to completion and results folded into the javadoc
  • ./gradlew spotlessApply / spotlessCheck

🤖 Generated with Claude Code

@dougqh dougqh added comp: core Tracer core tag: performance Performance related changes tag: no release notes Changes to exclude from release notes type: refactoring tag: ai generated Largely based on code generated by an AI or LLM labels Aug 25, 2026
Comment thread internal-api/src/jmh/java/datadog/trace/util/BenchmarkUtils.java Outdated
Comment thread internal-api/src/jmh/java/datadog/trace/util/BenchmarkUtils.java
@datadog-datadog-prod-us1

This comment has been minimized.

@dd-octo-sts

dd-octo-sts Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 14.82 s 14.75 s [-0.4%; +1.4%] (no difference)
startup:insecure-bank:tracing:Agent 13.63 s 13.61 s [-0.7%; +1.1%] (no difference)
startup:petclinic:appsec:Agent 16.98 s 16.94 s [-0.6%; +1.1%] (no difference)
startup:petclinic:iast:Agent 16.94 s 17.04 s [-1.4%; +0.3%] (no difference)
startup:petclinic:profiling:Agent 16.54 s 17.00 s [-3.9%; -1.5%] (significantly better)
startup:petclinic:sca:Agent 17.01 s 16.62 s [+1.5%; +3.2%] (significantly worse)
startup:petclinic:tracing:Agent 16.09 s 16.11 s [-1.3%; +1.0%] (no difference)

Commit: bc1a97ae · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

@dougqh dougqh changed the title Add hitFresh scenario to ImmutableSetBenchmark and StringIndex.contains helper Updated ImmutableSetBenchmark and ImmutableMapBenchmark to be more realistic Aug 26, 2026
@dougqh
dougqh marked this pull request as ready for review August 26, 2026 00:12
@dougqh
dougqh requested a review from a team as a code owner August 26, 2026 00:12
@dougqh
dougqh requested review from amarziali and jbachorik and removed request for a team August 26, 2026 00:12

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b55310a4a1

ℹ️ 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".

Comment thread internal-api/src/jmh/java/datadog/trace/util/BenchmarkUtils.java
Comment thread internal-api/src/jmh/java/datadog/trace/util/ImmutableMapBenchmark.java Outdated
Comment thread internal-api/src/jmh/java/datadog/trace/util/ImmutableMapBenchmark.java Outdated
Comment thread internal-api/src/jmh/java/datadog/trace/util/ImmutableSetBenchmark.java Outdated

@datadog-datadog-prod-us1 datadog-datadog-prod-us1 Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Datadog Autotest: FAIL

The production StringIndex change appears sound. Four benchmark-method defects make the new performance tables and guidance unreliable.

Open Bits AI session

🤖 Datadog Autotest · Commit b55310a · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

Comment thread internal-api/src/jmh/java/datadog/trace/util/BenchmarkUtils.java
Comment thread internal-api/src/jmh/java/datadog/trace/util/ImmutableSetBenchmark.java Outdated

@sarahchen6 sarahchen6 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The benchmark changes look generally reasonable, but I think a few of the existing PR comments are worth addressing -- particularly in regards to Java 8 results and re-working the benchmarks to avoid re-using the same objects when running / comparing.

Also I think the generated code comments can be simplified and made more readable, especially in the Benchmarking files. Telling Claude to keep things concise, avoid redundant and verbose comments, etc. helps.

Comment thread internal-api/src/jmh/java/datadog/trace/util/BenchmarkUtils.java
Comment thread internal-api/src/jmh/java/datadog/trace/util/BenchmarkUtils.java
@bric3 bric3 changed the title Updated ImmutableSetBenchmark and ImmutableMapBenchmark to be more realistic Updated ImmutableSetBenchmark and ImmutableMapBenchmark to be more realistic Sep 1, 2026
Comment thread internal-api/src/jmh/java/datadog/trace/util/BenchmarkUtils.java
Comment thread internal-api/src/jmh/java/datadog/trace/util/BenchmarkUtils.java Outdated
Comment thread internal-api/src/jmh/java/datadog/trace/util/ImmutableSetBenchmark.java Outdated
Comment thread internal-api/src/main/java/datadog/trace/util/StringIndex.java Outdated
Comment thread internal-api/src/jmh/java/datadog/trace/util/ImmutableMapBenchmark.java Outdated
Comment thread internal-api/src/jmh/java/datadog/trace/util/ImmutableSetBenchmark.java Outdated
Comment thread internal-api/src/jmh/java/datadog/trace/util/BenchmarkUtils.java
Comment thread internal-api/src/jmh/java/datadog/trace/util/BenchmarkUtils.java
@bric3

bric3 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

It looks good benchmark wise, albeit I reworded most of the javadoc, in a human form, removing the claude's inference verbiage.

@dougqh

dougqh commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

@sarahchen6 @bric3 — pushed the wording fixes from your review (all threads replied to and resolved). Ready for another look / approval when you have a chance.

dougqh and others added 4 commits September 1, 2026 19:15
Mirrors StringIndex#contains for the static-arrays path, so callers
of the embedded/parallel-array form don't need to spell out
indexOf(...) >= 0 themselves.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…hmark

Reusing the same interned/cached-hash key instances for both building
a structure and measuring hit lookups against it understates real hit
cost -- String#equals's == fast path and String#hashCode's cached
result pay nothing extra for a key already touched during setUp. The
new hitFresh scenario looks up separate, never-touched String
instances instead, alongside the existing hit (interned literal) and
miss (already representative) scenarios.

Also pulls in BenchmarkUtils#polluteHashDispatch so the shared
hashCode/equals call sites are already megamorphic before the
StringSet arms measure their own lookups, matching production where
those call sites are hit by every hash-based structure in the JVM.

Replaced the javadoc's stale, partial-run tables/findings with a full
hit/hitFresh/miss re-run across all six structures together (Fork(5),
Threads(8)) -- confirms hitFresh is the slowest of the three scenarios
for every hash-based structure, not merely slower than hit as
previously guessed from partial data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Recompute the slot index each iteration as (h + probes) & mask instead
of carrying a separately-incremented cursor, so the loop has one
canonical induction variable for the JIT to reason about.
…ains-only helpers

populateTypeProfile() drives only contains(), so it works against both
mutable and immutable collections (including the collection instance
under test itself); populateTypeProfileMutable() keeps the old
add()+contains() behavior for callers that need add() dispatch
polluted too.
dougqh and others added 3 commits September 1, 2026 19:15
The instance wrapper reliably beats HashSet on hit (its actual design
case) but not on miss/hitFresh, where both are bimodal across forks
and the mean already sits at or below HashSet's steady figure. Point
miss/fresh-key-heavy callers at EmbeddingSupport directly instead of
assuming the wrapper is strictly better than a plain Set.
Object.hashCode()/equals() are JVM-wide shared call sites hit by every
hash-based structure in the process; leaving them monomorphic for a
single-key-type run understates real dispatch cost, same reasoning as
ImmutableSetBenchmark. Rerun and update the javadoc results table with
pollution in effect -- StringIndex's get win over HashMap/TagMap/MapN
holds up unconditionally here, unlike the access-pattern-dependent Set
case.
Apply the reviewer-suggested rewordings for BenchmarkUtils, ImmutableSetBenchmark,
ImmutableMapBenchmark, and StringIndex: trim narration and restate a couple of
claims (fresh-string hash caching, contains() contract) more precisely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dougqh
dougqh force-pushed the dougqh/stringindex-immutable-set-benchmark branch from 5242852 to bc1a97a Compare September 1, 2026 23:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: core Tracer core tag: ai generated Largely based on code generated by an AI or LLM tag: no release notes Changes to exclude from release notes tag: performance Performance related changes type: refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants