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

   ## Follow-up (a): done — preemption-check cost measured, gate landed
   
   Resolving the compile-time question I flagged above. Short version: the 
concern was real, the gate fixes it, and it's now measured rather than asserted.
   
   ### Benchmark
   
   Added `ClassTagResolutionBench` (JMH, in `:performance`) — a generated 
`@CompileStatic` corpus compiled to `INSTRUCTION_SELECTION`, fresh 
`CompilationUnit` per invocation. Three call mixes and a controlled `active` vs 
`vetoAll` comparison, where `vetoAll` sets `classTagPreemptionDisabled=['*']` 
so the preemption path is never entered — the pre-feature floor for a corpus 
whose calls all resolve. `active − vetoAll` is therefore the cost of active 
preemption matching. The corpus and the numbers below live in the benchmark's 
Javadoc so they travel with the code.
   
   ### The regression, confirmed
   
   Without the gate, preemption matching runs on every resolved call once a 
token-less overload has matched:
   
   ```
   mix            active   vetoAll(floor)   delta
   dgmHeavy       121.8    109.7            +11.1%   <- ordinary 
each/collect/findAll code
   instanceHeavy   52.8     51.0            + 3.6%
   withDefault    263.1    228.7            +15.0%   <- preemption actually 
firing every call
   ```
   *(avgt ms/op, 60 classes × 80 calls, 2 forks × 5+5 iterations.)*
   
   The `dgmHeavy` +11% is the pathology: `each`/`collect`/`findAll` on typed 
receivers each pay a full DGM candidate scan + intent check, only to find 
nothing preemptive (no DGM method except `withDefault` declares intent). Pure 
overhead — well past the ~1–2% I'd have shipped as-is on.
   
   ### The gate
   
   `ExtensionMethodCache` now exposes `getPreemptiveNames(loader)` — a 
per-classloader `Set<String>` of extension-method names that declare 
`@ClassTag(preempt=true)`, derived lazily from the already-cached method map 
and sharing its lifecycle (so it can never go stale independently). Out of the 
box it holds exactly `{"withDefault"}`. `matchPreemption` consults it before 
the extension candidate scan: if the call's name isn't in the set, one 
`HashSet.contains` replaces the whole scan+filter. Conservative by construction 
— a false positive falls through to the full match and fails softly; a false 
negative can't occur because any intent-declaring overload's name is in the set 
by derivation. The instance-incumbent path is untouched (it must still see 
subclass-preempts-superclass), and additive matching is untouched (it only runs 
on otherwise-unresolved calls).
   
   ### Post-gate
   
   ```
   mix            active(gated)   vetoAll(floor)   pre-gate
   dgmHeavy       108.1           110.3            121.8    <- regression 
eliminated (at the floor)
   instanceHeavy   54.6            51.4             52.8
   withDefault    269.7           227.0            263.1    <- unchanged; still 
does the real work
   ```
   
   `dgmHeavy` gated `active` is back at the `vetoAll` floor, within noise — the 
+11% is gone. `withDefault` is deliberately unchanged: it's in the 
preemptive-names set, so it still does the full token synthesis + rewrite the 
feature exists to do (this number is a worst case — *every* call preempts, 
which no real code does).
   
   ### Diff and verification
   
   Three files: the benchmark (new), `AbstractExtensionMethodCache` (+21, the 
name-set cache), `ClassTagSupport` (+7, the guard). `ClassTagStaticTest` 32/32, 
`CompilerConfigurationTest` 15/15, `Groovy11807` 5/5, and the full 
`groovy.transform.stc.*` (2012 tests) + `classgen.asm.sc.*` (2295 tests) sweeps 
green — the shared-cache change is safe.
   
   ### One documented residual
   
   `instanceHeavy` still shows ~+6% (gated 54.6 vs floor 51.4) on a corpus 
where *every* call is to a same-class overloaded method. The gate deliberately 
doesn't cover the instance path — a per-`ClassNode` equivalent runs into 
invalidation-during-compilation complexity, and real instance calls are 
overwhelmingly non-overloaded (the scan returns one method, trivial). I've left 
it as a documented residual rather than adding speculative `ClassNode`-metadata 
caching; happy to revisit if anyone has a real-world `instanceHeavy` profile 
that justifies it.


-- 
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