paulk-asert commented on PR #2645:
URL: https://github.com/apache/groovy/pull/2645#issuecomment-5086680305

   ## Follow-up (a) in more detail: preemption-check cost — benchmark plan + 
proposed gate
   
   Expanding on the compile-time concern noted above, since it's the one open 
item that could block merge.
   
   ### The cost, precisely
   
   The earlier allowlist design had an accidental virtue: 
`classTagPreemptionTargets.contains(name)` was an O(1) pre-gate that was false 
for virtually every call, so resolved calls paid nothing. In the revised 
design, `isPreemptionPossible()` is still O(1) but almost always *true*, after 
which `matchPreemption` runs for every resolved method call under 
`@TypeChecked`/`@CompileStatic`:
   
   - an incumbent-kind scan over the matched methods (cheap, usually one 
element);
   - for extension-method incumbents — the dominant case (`each`, `collect`, …) 
— a `findDGMMethodsForClassNode` lookup, which is cache-backed but still walks 
the receiver hierarchy and filters by name per call;
   - intent/veto checks that call `getAnnotations(...)` per parameter per 
candidate.
   
   Almost always the outcome is "no preempt-intent overload exists here" — we 
pay enumeration cost to discover a negative that is knowable in advance. That's 
the target.
   
   ### Proposed gate: a per-classloader "preemptive names" set in 
`ExtensionMethodCache`
   
   `ExtensionMethodCache` already solves the exact lifecycle problem we need: 
it caches the receiver→methods map per classloader and invalidates when 
extension modules change. The plan piggybacks on the same scan:
   
   - Change the cached value to a small holder — `{byReceiverType map; 
preemptiveNames set}` — so the name set shares the map's lifecycle and can 
never go stale independently (no separate invalidation logic to get wrong).
   - While `AbstractExtensionMethodCache` accumulates each `MethodNode`, 
additionally record the method name if any parameter carries 
`@ClassTag(preempt=true)`. One-time cost, amortized across the compilation.
   - Fast path in `matchPreemption`: when all incumbents are extension methods 
and `!preemptiveNames.contains(name)`, return before any candidate enumeration. 
Out of the box that set contains exactly `{"withDefault"}`, so ~every DGM call 
short-circuits on one set lookup — restoring the allowlist's O(1) profile while 
keeping author self-declaration.
   
   Two constraints worth stating: the gate must be *conservative* (false 
positives fall through to the full match, which fails softly; false negatives 
would be a correctness bug — hence deriving the set from the same scan that 
feeds method lookup, never a hand-maintained list), and it only covers the 
extension-incumbent path. Instance-method incumbents still scan the receiver's 
same-name overloads — bounded, and the rarer path. If measurement says that 
matters, the cheap fix is reordering `matchOverload` to compare arity (integer 
math) before scanning tag annotations (list allocation), so untagged overloads 
bail for free; I'd hold that until the data asks for it.
   
   ### Benchmark plan
   
   Compile-time benchmarks are noisy, so: JMH, fresh 
`GroovyClassLoader`/`CompilationUnit` per invocation, compiling to 
`INSTRUCTION_SELECTION` (captures all STC work, skips classgen noise), 
average-time mode with proper warmup rather than single-shot.
   
   **Corpus**: a generated synthetic source set (reproducible, tunable) — ~200 
`@CompileStatic` classes × 50 calls, in three mixes as benchmark params:
   - `dgmHeavy` — DGM calls on typed receivers (the path the gate targets);
   - `instanceHeavy` — same-class/hierarchy instance calls with overloads (the 
path it doesn't);
   - `withDefault` — calls that actually preempt, confirming the happy path 
doesn't regress.
   
   **Variants**: (1) master baseline; (2) this branch as-is; (3) branch with 
`classTagPreemptionDisabled = ['*']` — the floor the gate could reach; (4) 
branch + name-set gate.
   
   **Acceptance criteria**: if (2) is within ~1–2% of (1) on `dgmHeavy`, ship 
as-is and the gate becomes optional hygiene; otherwise land the gate and expect 
(4) ≈ (3) ≈ (1). A coarse cross-check against the existing `:performance` 
corpus is easy to add, but the variant comparison wants the JMH harness.
   
   **Scope note**: I don't propose extending the gate to instance methods via 
per-`ClassNode` cached flags — the invalidation story during compilation 
(classes still being built, AST mutation) is genuinely tricky, and the bounded 
per-call scan is very likely in the noise. The benchmark exists to prove or 
refute that cheaply before adding machinery.
   
   I'll post numbers here once the harness runs; if anyone has a preferred 
real-world `@CompileStatic`-heavy corpus to include alongside the synthetic 
one, suggestions welcome.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to