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