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

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

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

   @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.
   
   ## 1. When we invalidate (and when we do not)
   
   Monomorphic indy sites guard a **class-domain** SwitchPoint on the 
*receiver’s* runtime class (via `ClassInfo`). They re-link only when *that* 
domain is retired.
   
   For your ladder `A extends B extends C` and `call(C x) { x.fooC() }` with `c 
= new C()`:
   
   | Operation | Retires `C`’s domain? | `foo*` visible on `c` / `C.metaClass`? 
|
   |-----------|----------------------|----------------------------------------|
   | `B.metaClass = EMC` + `fooB0` | **No** (`C` is a *supertype* of `B`) | 
**No** |
   | `B.metaClass = null` | **No** | **No** |
   | `enableEMCglobally()` | Only if a bulk path fires; not via typed `B` 
change | Global EMC changes *creation*, not `C`’s linked target by itself |
   | `c.metaClass = null` | Per-instance path → exact `C` only if a class-level 
SP was tied to that path | Restores class-level MC for `c` |
   | `B.metaClass = new MetaClassImpl()` | **No** for pure `MetaClassImpl` 
(exact `B` only) | **No** |
   | Detached EMC `fooB1` after replace | Depends on whether that EMC is still 
the registered MC | **No** on `c` |
   | `A.fooA = {}` | **No** for `C` | **No** on `c` |
   | Per-instance EMC on `b` + `fooB2` | **No** for `C` | **No** on `c` |
   
   So for `call(c)` / `fooC`, the new scoped model already keeps `C`’s site 
linked across the operations that only touch `B`/`A`. That is the main 
behavioural difference vs the old process-wide SwitchPoint (which retired 
*everything*).
   
   Regression coverage for this matrix is in 
`Groovy12191.blackdragScenario_callOnC_notInvalidatedBySubclassMetaClassChurn`.
   
   ## 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.
   
   So hierarchy is required for **cross-class MOP visibility**, not for 
ordinary `MetaClassImpl` method tables.
   
   ### Refined policy (exact-class is an allow-list)
   
   | Event | Width |
   |-------|--------|
   | EMC install / remove / in-place update (`incVersion`) | class + loaded 
subtypes |
   | Global EMC creation handle active | class + hierarchy |
   | Interface or array MetaClass change | class + hierarchy (array lattice) |
   | Modified custom `MetaClassImpl` subclass (`isModified()==true`) | class + 
hierarchy |
   | Pure unmodified `MetaClassImpl` ↔ `MetaClassImpl` (or null) | **exact 
class only** |
   | Per-instance MetaClass change | **exact class only** |
   | Category enter/leave, unscoped registry event | all loaded class domains |
   
   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).
   
   ## 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.
   
   ## 4. Update vs replace, and `version`
   
   You are right that parent method *addition* is often an **update** of the 
same MetaClass, not a registry replace. That path is `ClassInfo.incVersion()` 
(from EMC), which still does hierarchy fan-out — because subtype sites may have 
linked a miss (or an older target) and must re-select to see the new expando 
method.
   
   Per-instance MetaClass continues to use the existing uncacheable PIC path 
(`canSetTarget == false`); we do not introduce per-instance SwitchPoints in 
this PR.
   
   ## 5. PIC sentinel rename
   
   Per 
[discussion_r3664025383](https://github.com/apache/groovy/pull/2736#discussion_r3664025383):
   
   - `NULL_METHOD_HANDLE_WRAPPER` → `UNCACHEABLE_PIC_SENTINEL`
   - meaning: “do not store this receiver shape under a class-keyed PIC entry”
   - deprecated alias kept as `getNullMethodHandleWrapper()` for in-tree 
callers during the rename
   
   ## 6. What we are *not* claiming
   
   - We do **not** claim minimal invalidation at method-name granularity 
(`fooC` unchanged ⇒ never re-link). The monomorphic SwitchPoint still retires 
the whole class domain when that class’s MOP generation changes — same shape as 
pre-6.0, but **scoped**.
   - We do **not** claim MetaClass-owned SwitchPoints are done; only that 
policy is now MetaClass-aware on top of class domains.
   
   Happy to adjust further if you want the exact-only allow-list tightened or 
loosened (e.g. EMC-only fan-out even for interfaces/arrays without EMC).
   




> 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