[ 
https://issues.apache.org/jira/browse/GROOVY-12191?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18099815#comment-18099815
 ] 

ASF GitHub Bot commented on GROOVY-12191:
-----------------------------------------

blackdrag commented on PR #2736:
URL: https://github.com/apache/groovy/pull/2736#issuecomment-5108446270

   > @blackdrag Thank you for pressing on the “when / why” of hierarchy 
fan-out. That is the right question, and the previous reply was still too 
coarse. Below is the refined model after re-checking MOP selection against your 
scenario.
   
   Now I feel kind of bad, because I made a major mistake in my example. not 
   `A extends B extends C and call(C x) { x.fooC() } with c = new C():` In my 
head the hierarchy was reversed. The goal was specifically to see how changes 
in the metaclasses of super classes affect the child metaclass. Sorry for that.
   
   [...]
   > ## 2. Why hierarchy exists at all (and when it does not)
   > 
   > Hierarchy fan-out is **not** “`MetaClassImpl` shares one table up the 
hierarchy”. Each class still has its own MetaClass / `ClassInfo` domain.
   > 
   > It exists because **selection** can observe _ancestor_ MetaClass state:
   > 
   >     * `MetaClassImpl.findMethodInClassHierarchy` only opens a real walk 
when some strong MetaClass in the hierarchy is a **modified** 
`MutableMetaClass` (EMC is the common case).
   > 
   >     * Empirically, `Parent.metaClass.hello = {…}` is visible on `new 
Child().hello()` **without** `enableGlobally()`.
   > 
   >     * Empirically, `Object[].metaClass.arrHello = {…}` is visible on 
`String[]` — that is why the array lattice is indexed.
   
   but the important point here is that this happens only for a missing method. 
And it is actually why I think we are having a major semantic gap here, that we 
have to decide on:
   
   ```
   class A{}
   class B extends A{
     def m1(){1}
     def m2(Integer x){2}
   }
   class C extends A {
     def m2(x) { 40 }
   }
   def c = new C()
   
   // normal calls
   def b = new B()
   assert b.m1() == 1
   assert b.m2(0) == 2
   
   // methods added using EMC, including overload
   A.metaClass.m1 = {-> -1}
   A.metaClass.m2 = {Integer x-> -2}
   A.metaClass.m2 = {String x-> -3}
   def a = new A()
   assert a.m1() == -1
   assert a.m2(0) == -2
   assert a.m2("") == -3
   
   // setting metaclass again, but no change
   b.metaClass = null
   assert b.m1() == 1
   assert b.m2(0) == 2
   
   // this method did not exist before, now found by
   // MetaClassImpl.findMethodinClassHierarchy
   // BUT no valid callsite for this before did exist -> no invalidation case
   assert b.m2("") == -3
   
   // even though m2(Integer) and m2 (String) exist in the metaclass for A
   // they are not used for C, because the metaclass for C existed before
   // they have been added!
   assert c.m2(null) == 40
   assert c.m2(0) == 40
   assert c.m2("") == 40
   
   // reset meta class, still the same, because MetaClassImpl is cached
   c = new C()
   assert c.m2(null) == 40
   assert c.m2(0) == 40
   assert c.m2("") == 40
   
   // is now EMC
   c.metaClass.m1 = {-> -40}
   assert c.m1() == -40
   assert c.m2(null) == 40
   assert c.m2(0) == -2
   assert c.m2("") == -3
   
   // same structure as C, but instance exists *after* A has been modified!
   // D is still MetaClassImpl
   class D extends A {
     def m2(x) { 50 }
   }
   d = new D()
   assert d.m2(null) == 50
   assert d.m2(0) == -2
   assert d.m2("") == -3
   ```
   
   The case of C and D show how broken the current system is. The point of time 
a MetaClassImpl instance comes into existence should not decide about what it 
sees. In fact my POV is that MetaClassImpl should be immune to changes of other 
meta classes. This means especially that no hierarchy check is needed for it 
and that find method should not exist on MetaClassImpl. Now to produce a 
version with callsite caching:
   
   ```
   import org.codehaus.groovy.runtime.metaclass.*
   class A{}
   class C extends A {
     def m2(x) { 40 }
   }
   def call(x,y) {
     x.m2(y)
   }
   def c = new C()
   
   A.metaClass.m2 = {Integer x-> -2}
   A.metaClass.m2 = {String x-> -3}
   
   // no methods from A visible
   assert call(c,0) == 40
   c = new C()
   assert call(c,0) == 40
   
   def mc = C.metaClass.delegate
   assert mc instanceof MetaClassImpl
   
   // is now EMC enforces methods from A
   c.metaClass.m1 = {-> -40}
   assert call(c,0) == -2
   
   // reset to old MC
   c.metaClass = mc
   assert call(c,0) == 40
   
   // simulate late init, still MetaClassImpl, but now change is visible
   def registry = MetaClassRegistryImpl.getInstance(0)
   registry.removeMetaClass(C)
   c.metaClass = null
   assert call(c,0) == -2
   def newMc = c.metaClass.delegate
   assert newMc instanceof MetaClassImpl
   assert newMc != mc
   
   // reset meta class of A, but old methods from A still visible
   A.metaClass = null
   assert call(c,0) == -2
   
   // show that A does not know m2
   def catched = false
   try {
     new A().m2(0)
   } catch (Exception e) {
     catched = true
   }
   assert catched
   
   // now we switch to the old emc, which is clueless about A
   c.metaClass = emc
   call(c,0) == 40
   ```
   Most of this should not be caught by the switch point, but by the fact, that 
the metaclass changed. But I also think I captured cases where a hierarchy 
version would break the code in case of MetaClassImpl. Also I think it should 
behave the same if classes used are Java based and not from Groovy.
   
   [...]
   > This matches your intuition: **without EMC (and without a modified mutable 
MetaClass), hierarchy is unnecessary**. Pure `MetaClassImpl` parent replace no 
longer fans out to children. Unknown/custom MetaClass kinds still fan out 
(correctness-first).
   
   +1
   
   > ## 3. “Should the MetaClass own the SwitchPoint?”
   > 
   > Agreed in principle. The current class-domain SP is a **stand-in for 
class-level MetaClass generation**, not a claim that every MetaClass kind 
shares the same invalidation logic.
   > 
   > A MetaClass-owned guard would be the natural place for:
   > 
   >     * MetaClass-specific update vs replace semantics,
   >     * true per-instance domains (today: uncacheable PIC + exact class 
retire),
   >     * custom MetaClass implementations that do not fit EMC / 
`MetaClassImpl`.
   > 
   > That is a larger redesign than this PR. For 6.0 we kept a single 
monomorphic hot-path guard and made invalidation **MetaClass-aware** at the 
registry boundary. MetaClass-owned SwitchPoints remain a follow-up once the 
“when / why” matrix above is agreed.
   
   ok, agreed... if a follow-up issue is created
   




> Scope indy SwitchPoint invalidation
> -----------------------------------
>
>                 Key: GROOVY-12191
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12191
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Daniel Sun
>            Priority: Major
>             Fix For: 6.0.0-beta-1
>
>
> 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
> 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|
>  



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

Reply via email to