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

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

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

   AI thoughts below. I haven't tried to run locally yet, so I'm not sure how 
to read the mixed perf data yet.
   
   > I've reviewed the diff against master, verified the scoping and caching 
assumptions in the actual source, and checked CI.
   > 
   > ---
   > 
   > # PR 2825 (GROOVY-12288) — merge readiness
   > 
   > **Verdict: the code is correct and low-risk — arguably lower-risk than 
2823 — and CI is fully green. But the performance case is not supported by the 
data presented, and one of the two caches largely duplicates memoization that 
already exists. I'd merge the code; I'd reject the PR description as written.**
   > 
   > ## What it changes
   > 
   > `CompilationUnit.createClassVisitor()` replaces the anonymous 
`ClassWriter` with a private inner `CachingClassWriter` carrying two maps:
   > - `classNodeByInternalName` — internal name → resolved `ClassNode`
   > - `commonSuperByPair` — memoized `getCommonSuperClass` results, stored 
under both `(A,B)` and `(B,A)`
   > 
   > plus identity fast-paths (`commonNode == class1` → return `type1` 
verbatim) and a constant for `java/lang/Object`.
   > 
   > ## Correctness — verified
   > 
   > - **Scoping is exactly as claimed.** `createClassVisitor()` is called once 
per generated class inside the `classgen` operation 
(`CompilationUnit.java:860`), and again recursively for each inner class. Both 
maps are born and die with one class. No leak, bounded memory.
   > - **Negative results are deliberately not cached** (`if (cn != null) 
classNodeByInternalName.put(...)`). This is the right guard and it matters: 
closure/inner classes are registered *during* class generation, so a name 
unresolvable at one moment may resolve later. Caching a null would have been a 
real bug. It's handled.
   > - **`getClassNode` restructuring is behaviour-preserving** — the 
early-return chain became nested `if`s with identical ordering (`cu.getClass` → 
`getGeneratedInnerClass` → `ClassNodeResolver`).
   > - **The failure mode improves.** Previously an unresolvable name produced 
an NPE inside `getCommonSuperClassNode`; now it throws `GroovyBugError("Unable 
to determine common super class of X and Y")`. `GroovyBugError` is already 
imported (`CompilationUnit.java:24`) and this matches house style elsewhere in 
the file (lines 992, 1050 wrap NPEs the same way). It *is* an observable change 
— worth a line in the JIRA rather than leaving it silent.
   > - **The risk profile is inherently benign.** This computes StackMapTable 
frames. If it were wrong you'd get a `VerifyError` at class load — loud, 
immediate, impossible to miss — not silent misbehaviour. Every class the Groovy 
test suite compiles exercises this path, and the full matrix (JDK 17/21/25, 
Linux/macOS/Windows, all `additional` shards, `dist`, all JMH suites) is green.
   > - **Test shape is right.** The four new tests compile, `defineClass`, and 
then *invoke* the results, so the JVM verifier itself validates the frames. 
That's much better than asserting on internals.
   > 
   > ## The one invariant I'd want covered
   > 
   > Storing each result under both `(A,B)` and `(B,A)` **newly asserts that 
`getCommonSuperClassNode` is commutative.** Reading it, I believe it is: the 
loop walks `c`'s superclass chain to the lowest ancestor that `d` derives from 
— the LCA either way under single inheritance — and the interface branch is 
symmetric. But this is a new invariant the code now depends on, and nothing 
tests or documents it. There's no test asserting `f(A,B) == f(B,A)`. I'd ask 
for one test plus a one-line comment stating the assumption. Cheap, and it 
protects the invariant against future edits to that walk.
   > 
   > Secondary, smaller: the identity fast-path returns `type1`/`type2` 
verbatim rather than `commonNode.getName().replace('.','/')`. These should be 
identical since the node was resolved *from* that string, and returning ASM's 
own string is arguably safer — but it's a silent difference if a resolver ever 
returns a node whose name differs from the name requested. One sentence of 
justification in the comment would settle it.
   > 
   > ## The substantive design critique
   > 
   > **`classNodeByInternalName` mostly duplicates a cache that already 
exists.** `ClassNodeResolver` maintains its own `cachedClasses` HashMap — 
*including a negative cache* (`NO_CLASS`) — and the resolver is a 
per-`CompilationUnit` field (`CompilationUnit.java:120`). So the expensive path 
(ClassLoader / classpath lookup) was **already memoized across the entire 
compilation** before this PR.
   > 
   > That means the new map does not eliminate "ClassLoader access and 
classpath lookups" as the description claims. It eliminates one 
`replace('/','.')` allocation, two map lookups, and one *already-cached* 
resolver hit. Real, but small — and this is corroborated by the PR's own 
allocation figures, which I'll come to.
   > 
   > The genuine win is `commonSuperByPair`, which skips the `isDerivedFrom` 
hierarchy walk. That's the actual O(depth) work and the only mechanism that 
plausibly explains the `deepHierarchy` result.
   > 
   > **And the design leaves the larger win on the table.** Because the cache 
dies with each class, a project of many small classes gets near-zero reuse — 
the pair cache is rebuilt from scratch for every class, even though the common 
pairs (`String`/`Object`, collection types) recur across *all* of them. A 
CompilationUnit-scoped pair cache would capture that. The obvious objection is 
staleness, but the code already handles the only real hazard by not caching 
negatives, and hierarchy links are fixed well before `CLASS_GENERATION`. If 
Daniel considered CU scoping and rejected it, the PR should say why; if not, 
that's where the actual performance is.
   > 
   > ## The performance claims don't survive contact with the data
   > 
   > This is my main objection, and it's the same pattern as 2823 but 
considerably worse.
   > 
   > Read the PR's own table 5.1:
   > 
   > | Scenario                 | Baseline            | Optimized           | 
Claimed     |
   > | ----------------------

> Cache ClassWriter getCommonSuperClass lookups per class
> -------------------------------------------------------
>
>                 Key: GROOVY-12288
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12288
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Daniel Sun
>            Priority: Major
>
> Bytecode generation uses an ASM {{ClassWriter}} with {{{}COMPUTE_FRAMES{}}}. 
> Frame computation calls {{getCommonSuperClass}} at every control-flow merge. 
> Groovy overrides that method so types still being compiled are resolved 
> through {{ClassNode}} ({{{}CompileUnit{}}}, generated inner classes, 
> {{{}ClassNodeResolver{}}}) rather than {{{}Class.forName{}}}.
> {{COMPUTE_FRAMES}} asks for the same binary-name pairs many times inside one 
> class. Each call converts slashes to dots, resolves two \{{ClassNode}}s, and 
> walks superclasses with isDerivedFrom. Class generation is about half of 
> compile wall time.
> h3. Approach
> Memoize both steps on the {{ClassWriter}} created by 
> {{{}CompilationUnit.createClassVisitor{}}}. One writer is allocated per 
> generated class and discarded afterwards, so the maps cannot go stale across 
> classes.
> ||Cache||Key||Value||
> |{{classNodeByName}}|binary name (dot form)|{{ClassNode}} (successful lookups 
> only)|
> |{{commonSuperByPair}}|canonical pair of internal names|internal name of the 
> common superclass|
> The key is order-independent: {{(A,B)}} and {{(B,A)}} share one entry. The 
> common-superclass algorithm is unchanged.
> h3. Impact
> Compile-time only. {{getCommonSuperClass}} results and generated bytecode 
> stay the same.
>  



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

Reply via email to