[ 
https://issues.apache.org/jira/browse/GROOVY-12191?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Daniel Sun updated GROOVY-12191:
--------------------------------
    Description: 
h3. Problem

With invokedynamic enabled (default since Groovy 4), linked MOP call sites were 
guarded by a *single process-wide* {{SwitchPoint}} 
({{IndyInterface.switchPoint}}).

Any MetaClass registry change or category enter/leave invalidated that switch 
point, so *every* linked site fell back and re-linked — including sites whose 
receiver type was unrelated.

That global invalidation is expensive when MetaClass churn is common (e.g. 
ExpandoMetaClass / mixins on startup or per-request paths, Grails-like 
patterns). Unrelated hot monomorphic sites pay re-link and JIT deopt cost they 
should not.

h3. Goal

Keep linked call sites optimized unless the *relevant* MetaClass state for that 
site actually changed.

h3. Approach (minimal model)

One SwitchPoint domain *per class*, stored on {{ClassInfo}}:

 * MetaClass change for type {{T}} retires {{T}}'s SwitchPoint *and* those of 
loaded subtypes / implementors (hierarchy fan-out).
 * Unrelated types keep their SwitchPoints; their call sites stay optimized.
 * Category enter/leave (and {{VMPlugin.invalidateCallSites()}}) bulk-retire 
*all* loaded class SwitchPoints so sites re-link under the new category state. 
There is *no* second category SwitchPoint on the hot path.
 * Linked handles always install a *single* class-domain guard via 
{{IndyInvalidation.guardWithMopSwitchPoints(...)}} — same monomorphic guard 
shape as before, without global deopt on unrelated MetaClass churn.

Final classes short-circuit hierarchy fan-out (no full {{ClassInfo}} scan). 
Non-final types batch retirements with {{SwitchPoint.invalidateAll}}.

h3. Invalidation map

||Event||What is retired||
|MetaClass change for type {{T}} (registry / {{ClassInfo.incVersion}})|{{T}} + 
loaded subtypes / implementors|
|Category enter/leave, {{invalidateCallSites()}}|All loaded class SwitchPoints 
(bulk)|
|Unattributed MetaClass registry event|All loaded class SwitchPoints (bulk)|
|First MetaClass *install* on a class|Version bump only (no linked sites yet); 
replacement / clear retires that class's SwitchPoint|

h3. Binary compatibility

{{IndyInterface.switchPoint}} is retained and rotated on category / 
{{invalidateCallSites}} for external observers, but is *no longer* the MOP 
guard on linked call sites (documented as deprecated for that use). New code 
uses {{IndyInvalidation}}.


  was:
h3. Problem this targets

With invokedynamic enabled (default since Groovy 4), a *single process-wide* 
{{SwitchPoint}} guarded essentially all linked MOP call sites. Any MetaClass 
registry change or category enter/leave invalidated that switch point, so *all* 
sites fell back and re-linked — including sites whose receiver type was 
unrelated.

That global invalidation shows up as:
 * Startup / request paths with ExpandoMetaClass / mixins (Grails-like 
patterns) pay repeated re-link cost
 * Category {{use}} thrashing deoptimizes the whole application
 * JMH benches under {{subprojects/performance}} document this cost pattern

h3. What changes proposed

Four complementary layers under the indy / MOP runtime:
||#||Area||Behaviour||
|1|Scoped SwitchPoint invalidation|Per-{{{}ClassInfo{}}} domain + hierarchy 
fan-out to loaded subtypes; separate *category-global* domain for 
{{{}use(Category){}}}. MetaClass change for class {{A}} no longer invalidates 
sites for unrelated class {{B}}|
|2|PIC polymorphism policy|{{CallSiteTracker}} counts distinct receiver shapes; 
at {{max.poly}} the site becomes sticky *megamorphic* (pin default boot target, 
no monomorphic {{setTarget}} thrash). Selection stays amortized via the PIC|
|3|Bimorphic promote|Two stable receiver shapes can share one nested 
{{guardWithTest}} target instead of alternating monomorphic targets|
|4|IndyMath completeness|BigDecimal-category ops (and mixed integral+BD) via 
{{{}NumberMath{}}}; {{div}} / {{{}intdiv{}}}; primitive {{rightShiftUnsigned}} 
table entries. Shift ops stay off the GeneratedMetaMethod→IndyMath fast path so 
mixed {{{}int{}}}/{{{}long{}}} keeps left-operand result typing|
h3. Invalidation model (after the changes)
||Domain||Invalidated when||Guards||
|Per-class (+ subtypes)|{{ClassInfo.incVersion}} / MetaClass registry change 
for that type|Receiver class (or {{Class}} for static sites); hierarchy fan-out 
invalidates subtype SwitchPoints|
|Category-global|Category enter/leave 
({{{}VMPlugin.invalidateCallSites{}}})|Always dual-guarded with the class 
domain; *never* fails over (correctness)|

Per-class SwitchPoint can fail over after repeated retirements (default 100) to 
a {{ClassInfo.version}} equality guard (avoids permanent-invalid SP recursion). 
Concurrent failover drains any raced-in SwitchPoint so none remains valid after 
the domain has failed over. Category domain stays unlimited.

Generation bump on MetaClass *replacement* / clear retires that class's 
SwitchPoint; first MetaClass install only bumps version (no linked sites yet). 
Hierarchy fan-out remains on {{incVersion}} and registry listeners.
h3. Tunables (system properties)
||Property||Default||Meaning||
|{{groovy.indy.switchpoint.max.failures}}|{{100}}|Live SP retirements before 
per-class failover (value less than or equal to 0 = unlimited)|
|{{groovy.indy.invalidation.stats}}|{{false}}|Log invalidation counters|
|{{groovy.indy.callsite.max.poly}}|{{8}}|Distinct shapes to sticky megamorphic 
(value less than or equal to 0 disables)|
|{{groovy.indy.callsite.mega.sticky}}|{{true}}|Mega flag survives shape clear|
|{{groovy.indy.callsite.bimorphic}}|{{true}}|Allow dual-shape target promote|
|{{groovy.indy.callsite.cache.size}}|{{8}}|PIC LRU size|


> Scope indy SwitchPoint invalidation and harden PIC/math paths
> -------------------------------------------------------------
>
>                 Key: GROOVY-12191
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12191
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Daniel Sun
>            Priority: Major
>
> h3. Problem
> With invokedynamic enabled (default since Groovy 4), linked MOP call sites 
> were guarded by a *single process-wide* {{SwitchPoint}} 
> ({{IndyInterface.switchPoint}}).
> Any MetaClass registry change or category enter/leave invalidated that switch 
> point, so *every* linked site fell back and re-linked — including sites whose 
> receiver type was unrelated.
> That global invalidation is expensive when MetaClass churn is common (e.g. 
> ExpandoMetaClass / mixins on startup or per-request paths, Grails-like 
> patterns). Unrelated hot monomorphic sites pay re-link and JIT deopt cost 
> they should not.
> h3. Goal
> Keep linked call sites optimized unless the *relevant* MetaClass state for 
> that site actually changed.
> h3. Approach (minimal model)
> One SwitchPoint domain *per class*, stored on {{ClassInfo}}:
>  * MetaClass change for type {{T}} retires {{T}}'s SwitchPoint *and* those of 
> loaded subtypes / implementors (hierarchy fan-out).
>  * Unrelated types keep their SwitchPoints; their call sites stay optimized.
>  * Category enter/leave (and {{VMPlugin.invalidateCallSites()}}) bulk-retire 
> *all* loaded class SwitchPoints so sites re-link under the new category 
> state. There is *no* second category SwitchPoint on the hot path.
>  * Linked handles always install a *single* class-domain guard via 
> {{IndyInvalidation.guardWithMopSwitchPoints(...)}} — same monomorphic guard 
> shape as before, without global deopt on unrelated MetaClass churn.
> Final classes short-circuit hierarchy fan-out (no full {{ClassInfo}} scan). 
> Non-final types batch retirements with {{SwitchPoint.invalidateAll}}.
> h3. Invalidation map
> ||Event||What is retired||
> |MetaClass change for type {{T}} (registry / {{ClassInfo.incVersion}})|{{T}} 
> + loaded subtypes / implementors|
> |Category enter/leave, {{invalidateCallSites()}}|All loaded class 
> SwitchPoints (bulk)|
> |Unattributed MetaClass registry event|All loaded class SwitchPoints (bulk)|
> |First MetaClass *install* on a class|Version bump only (no linked sites 
> yet); replacement / clear retires that class's SwitchPoint|
> h3. Binary compatibility
> {{IndyInterface.switchPoint}} is retained and rotated on category / 
> {{invalidateCallSites}} for external observers, but is *no longer* the MOP 
> guard on linked call sites (documented as deprecated for that use). New code 
> uses {{IndyInvalidation}}.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to