Repository navigation
Conversation
…inkage cost Keep a separate class per matcher so the JIT can specialize by matcher type; avoid replacing these with a shared class holding a mask field as it slows down matching.
Express member selectors as lazily-compiled JVM descriptor patterns
(e.g. "run(Lorg/junit/runner/notification/RunNotifier;*)", "public inject(??)")
matched through a member-name index, instead of predicate graphs.
- MemberPattern: grammar ('?' = one param, tail-only '*' = remaining params,
modifiers, name alternation/prefix/suffix, @annotation) compiled to a single
String op where possible, else a linear non-allocating token walk.
- PatternIndex: tiered index (exact names, name prefix/suffix, annotations,
wildcard names) with per-pattern lazy compilation; predicate fallback for the
few shapes patterns can't express.
- tools/pattern-translator: records the DD_TRACE corpus against a mirror API
(javac does the parsing) and generates DDTracePatterns.
- PatternMatchingBenchmark / PatternColdBenchmark: warm and cold comparison
against the class-match graphs, with a full-corpus agreement check in setup.
Prototype for discussion on #162; not intended to merge as-is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
❌ ErrorsYour PR has failed checks. Please review the issues below and take necessary action before merging. 🚦 1 Pipeline job failed
Useful? React with 👍 / 👎 This comment will be updated automatically if new data arrives.🔗 Commit SHA: 04094c2 | Docs | View more details | Give us feedback! |
Collaborator
|
Thanks - interesting idea. The next step of the |
…ast paths
- FlatMethodMatcher + Flat: a flattened macro step (name, access mask,
descriptor, then one residual predicate) built through the existing fluent
API; and() merges slots instead of nesting predicates, so test() stays
monomorphic. DeclaresFlatMethod gives declares() a monomorphic loop.
DDTraceFlatGraphs (generated by tools/pattern-translator/flat-graphs.sh) is
the DD_TRACE corpus switched to Flat by an import change.
- AccessMask: a single (mask, expected) class replacing the per-constant
AccessMatcher subclasses; and() is closed (NEVER on contradiction);
general and/or use named classes instead of lambdas.
- MemberPattern: '?' as a hole for exactly one type, including the return;
'*' kept for open-ended arity with prefix fast paths ("(L..;*)" is a single
regionMatches, "(L..;*)R" adds the return check).
- PatternGatedBenchmark: per-instrumentation-group evaluation (types/loader
gates assumed passed) comparing predicates, a per-group index, and a linear
loop over flattened patterns; RANDOM and HITS pairings.
- Translator: instrumentation groups emitted per selector; STRICT mode
(no '*') for measuring how much '?' alone covers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generates DDTracePrecompiled with every pattern compiled ahead of time as MemberPattern.of(...) calls, and adds constructPrebuilt / firstUsePrebuilt to PatternColdBenchmark. Kept as evidence, not a direction: on Zulu 21 construction was slower (9.3 vs 6.9 ms lazy) and first use no faster (134±26 vs 124 ms). Running 1,275 constructor calls interpreted in cold code costs more than storing string references and lazily compiling only the ~824 patterns ever hit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- MemberPattern: names and literal are legitimately null (wildcard names, ANY descriptor), so mark them @nullable. - PatternColdIndyBenchmark: drop the unread public sink field; consume the warm-up lambda's result in setup instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mcculls
force-pushed
the
mcculls/class-match-perf
branch
from
November 3, 2026 14:56
700e16e to
8ad357d
Compare
This branch has not been deployed
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.
Update: where the speedup comes from
Follow-up to the gating question on #162. The speedup has three separable sources (Zulu 21, Apple Silicon, load ~5–7, identical results on all 10,047 classes × 1,191 selectors):
FlatMethodMatcherbehind the existing fluent API (Flat.method(...)etc.).and()merges name / access-mask / descriptor slots instead of nesting predicates, and anything opaque goes into one residual.test()is monomorphic. The DD_TRACE selectors switch over with an import change (DDTraceFlatGraphs).PatternGatedBenchmark(per-instrumentation groups, ~1.5 selectors each) measures that; gated numbers to follow.Also in this update:
AccessMask: one(mask, expected)class replaces the per-constantAccessMatchersubclasses, andandis closed (PUBLIC.and(PRIVATE)becomes constant-false at construction). A cleanup, measured as only a few percent faster on its own.?is a hole for exactly one type, including the return ((??)?).*is kept for open-ended arity, with prefix fast paths:(L..;*)is a singleregionMatches.What Does This Do
A prototype for discussion on #162, not intended to merge as-is. It shows an alternative representation for member matching: lazily-compiled JVM descriptor patterns behind a member-name index, in place of predicate graphs. It's stacked on #162 so it can reuse that PR's DD_TRACE corpus, harness, and agreement checks, and so the comparison is against #162's optimized predicates rather than
main.MemberPattern:?matches exactly one parameter;*matches the remaining parameters and is tail-only, so matching is a single linear pass with no backtracking. Patterns also support modifiers (public,!static,bridge…), name alternation (a|b),prefix*/*suffixnames, and an@annotationtoken. About 60% of patterns need only oneStringoperation after the name lookup (equals/startsWith/endsWith); the rest use a token walk that doesn't allocate.PatternIndex: construction only stores references. On first use it routes patterns into tiers (exact names, name prefix/suffix, annotations, wildcard names), and each pattern compiles the first time a candidate member reaches it. The few shapes patterns can't express fall back to Improve class-match construction and matching performance #162's predicates.tools/pattern-translator: translates the generated DD_TRACE corpus mechanically by compiling it against a mirror API (sojavacdoes the parsing) and expandingand/orinto disjunctive normal form. Output:DDTracePatterns(1,287 lines vs 6,106 for the predicate graph).PatternMatchingBenchmark/PatternColdBenchmark: warm and cold comparisons. Setup checks agreement with the class-match graphs on the full corpus.Motivation
#162's Motivation names two costs: matchers that are "expensive to construct for the first time," and "a convenient chain of predicates [that] can introduce dispatch overhead for every member examined." #162 reduces both within the predicate model. This prototype tests whether changing the representation removes them instead.
Results
Correctness: 1,183 of 1,191 selectors translate to patterns (8 predicate fallbacks remain). Results are identical on all 10,047 classes × 1,191 selectors (~12M comparisons).
Zulu 21, Apple Silicon. Warm: 3 forks × 10 × 1 s. Cold: 30 fresh-JVM forks.
These are from the first version (39 fallbacks), measured on a quiet machine. The tiered version (8 fallbacks) was only measured on a loaded machine: union 2.1 µs vs 50.8 µs, first use 156 vs 1,037 ms. A quiet rerun is pending before quoting tiered figures.
Cold construction is close to all-or-nothing. All 1,191 pattern literals construct in 1.7 ms, but even the last 8 predicate fallbacks cost 7–9 ms, because building any predicate loads the class-match matcher framework (all 15
AccessMatcherclasses,InternalMatchers…). Pre-warming the lambda/indy infrastructure didn't change this. With zero fallbacks, construction should be about 2 ms; the remaining 8 need small grammar extensions (a type glob in a parameter slot, AND of pattern groups, name exclusion, a name glob with a middle*).Inlining (
-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining, warmed DD_TRACE sweep): C2 refuses to inline 131 class-match call sites withvirtual call/no static binding, onClassMatcher::test,MethodMatcher::test,FieldMatcher::testandAccessMatcher::test. On the pattern side there are no dispatch failures (MemberPatternis one final class, and variation is aswitchon an int); its remaining misses are size heuristics that restructuring can fix.Laziness: on this corpus, 64% of patterns get compiled over a full pass, because instrumentation targets common names (
<init>,run,execute,get). The corpus removes dd-trace-java's type gates, so this is a worst case; with the gates, far fewer patterns would compile.Additional Notes
Found by the translator: three dead selectors on dd-trace-java master (all IAST). Byte Buddy silently never matches them:
iast/MarshallingDirectivesInstrumentation:takesArguments(1).and(takesArgument(1, …Unmarshaller))JSONArrayInstrumentation:takesArguments(0).and(takesArgument(0, JSONTokener))That's the validation argument in concrete form: patterns are data, so a build-time pass can reject malformed patterns and resolve them against target jars. Predicate graphs can only be executed.
Caveats:
PatternIndexis not thread-safe yet; a production version needs safe publication of compiled patterns.Reproduce:
./gradlew :class-match:jmhJar :class-match:sampleBytecode, then runPatternMatchingBenchmark/PatternColdBenchmarkfromclass-match/. To regenerate patterns:tools/pattern-translator/record.sh | tools/pattern-translator/generate.py <graphs> <out>.Contributor Checklist
Jira ticket: [PROJ-IDENT]
🤖 Generated with Claude Code