Hi Jochen, apart from my quick and inadequate chat with the AI, I am unfortunately not in the position of helping out with Groovy performance. I am worried for the future of the Groovy ecosystem though, as Mattias stated better than me, since we can perceive the difference in our applications moving to Grails 7/Groovy 4. At the moment we are still in the "production quality" surroundings, even with the performance gap, but if Groovy 5 is going to be even slower then we need to find a solution.
Cheers, Gianluca Sartori -- https://dueuno.com On Wed, 14 Jan 2026 at 13:28, Jochen Theodorou <[email protected]> wrote: > On 14.01.26 12:27, Gianluca Sartori wrote: > [...] > > I've asked ChatGPT, how much of this is true? How can we move forward > > thinking about performance as something important to consider for the > > language? > > > > *Q: *Why is Groovy 5 slower than Groovy 3? > > > > *A:* > > > > The reason isn’t a single bug, but rather a combination of architectural > > and implementation changes. Let me break it down carefully. > > > > ------------------------------------------------------------------------ > > > > > > 1. *AST Transformations and Compilation Changes* > > > > Groovy 5 introduced a *new compilation infrastructure* that relies more > > heavily on *AST (Abstract Syntax Tree) transformations*. While this > > makes the language more flexible and enables new features (like better > > type checking and improved static compilation), it also *adds overhead > > at runtime*, especially for dynamic code. > > * > > > > In Groovy 3, some dynamic method calls were optimized in a simpler > way. > > > > * > > > > Groovy 5 performs *additional type inference and checking*, even at > > runtime for some constructs, which can slow execution. > > The Groovy 5 point is nonsense. Was it a more simple way in Groovy 3? > Maybe, but Java supports it less and less. Actually, what we removed is > the alternative bytecode path for unchanged meta classes. This path did > help a lot when it comes to handle primitives and tight loops using > primitives. But the removal is more a simplification and also means not > to have the overhead for this as well. > > > ------------------------------------------------------------------------ > > > > > > 2. *JVM Dynamic Call Optimizations* > > > > Groovy 5 changed its *call site caching mechanism*: > > > > * > > > > Groovy 3 used *classic call site caching* that was very fast for > > repeated method calls. > > > > * > > > > Groovy 5 introduced a *more sophisticated but heavier mechanism* to > > improve correctness with new language features (like traits and > > records). > > > > * > > > > This improves *compatibility and correctness*, but can be slower for > > tight loops with many dynamic calls. > > The point about traits and records is nonsense. Groovy 5 is still very > fast, if not faster for repeated method calls. Actually Groovy 5 may be > even better in optimization in tight loops. The problem is the > initialization is too expensive. > > > ------------------------------------------------------------------------ > > > > > > 3. *Library and API Changes* > > > > Groovy 5 upgraded many core libraries and dependencies, which sometimes > > means: > > > > * > > > > Using newer, safer APIs that are a bit slower. > > > > * > > > > Removing some low-level optimizations present in Groovy 3 for > > backward compatibility reasons. > > this is pure speculation? What library or dependency did we change that > changes runtime performance? None, moving away from bytecode generation > to invokedynamic maybe. > > > ------------------------------------------------------------------------ > > > > > > 4. *Static Compilation Differences* > > > > If you used |@CompileStatic| in Groovy 3: > > > > * > > > > The bytecode generated in Groovy 5 can sometimes be *larger and less > > optimized*, because Groovy 5 focuses on *full feature support*, > > sometimes at the expense of raw performance. > > > > * > > > > Groovy 5’s static compilation is closer to Java semantics, which > > adds minor overhead in certain dynamic-heavy scenarios. > > No idea what that AI is talking about here. > > > ------------------------------------------------------------------------ > > > > > > 5. *Benchmarks and Real-World Performance* > > > > * > > > > For purely *dynamic Groovy scripts*, Groovy 5 tends to be slower > than 3. > > > > * > > > > For *statically compiled code*, the difference is smaller, but > > sometimes Groovy 5 can even be slightly slower due to the AST > overhead. > > > > * > > > > Microbenchmarks in loops and repeated method calls show *Groovy 5 > > can be 20–50% slower* in some cases. > > AST overhead plays only a role if you add the compilation into the > microbenchmark. Then it is not so micro anymore imho. And it is a one > time cost. Groovy is no AST interpreter after all. As for the other > statement... I think I know worse cases. The question is how common they > are. > > > ------------------------------------------------------------------------ > > > > ✅ *In short:* > > Groovy 5 trades raw runtime speed for *better correctness, new language > > features, and more powerful static compilation*. For heavy dynamic > > Groovy code, this can feel noticeably slower than Groovy 3. > > I too noticed that chatgpt is quite bad when it comes to Groovy. Is > there so much misinformation out there? > > > 1️⃣ JVM Flags to Monitor |invokedynamic| > > > > To count and monitor invokedynamic usage: > > > > |-XX:+PrintCallSiteCounters -XX:+UnlockDiagnosticVMOptions > > -XX:+LogCompilation -XX:+PrintInlining | > > > > * > > > > |-XX:+PrintCallSiteCounters| → shows how many times each call site > > was created or invoked > > > > * > > > > |-XX:+LogCompilation| + |-XX:+PrintInlining| → lets you see which > > calls the JIT inlines > > > > When running Groovy, pass them to the JVM with |-J|: > > > > |groovy -J-XX:+PrintCallSiteCounters myscript.groovy | > > not sure about PriintCallSiteCounters, since I do not know how indy is > handled by this. But ok. > > > ------------------------------------------------------------------------ > > > > > > 2️⃣ Test Script > > > > Here’s a small script that tends to slow down in Groovy 4: > > > > |// test.groovy 10_000.times { i -> def a = "string" def b = a + i > > println b } | > > sarcasm mode on... oh, println in a tight loop... surely that will not > have any impact what so ever... sarcasm mode off. Well I guess it is not > about time > > > * > > > > Uses *dynamic operators* (|+|) > > > > * > > > > Uses *variable types* > > > > * > > > > Calls |println| dynamically > > > > This simulates a *hot loop* with many dynamic call sites. > > > > ------------------------------------------------------------------------ > > > > > > 3️⃣ Run with Groovy 3 vs Groovy 4 > > > > |# Groovy 3 groovy -J-XX:+PrintCallSiteCounters test.groovy # Groovy 4 > > groovy -J-XX:+PrintCallSiteCounters test.groovy | > > > > ------------------------------------------------------------------------ > > > > > > 4️⃣ Sample Output > > > > |CallSite counters: invokedynamic site #1: 10000 invocations, 0 > > invalidations invokedynamic site #2: 10000 invocations, 50 invalidations > > invokedynamic site #3: 10000 invocations, 200 invalidations | > > > > * > > > > *invocations* → how many times the call site was called > > > > * > > > > *invalidations* → how many times the call site had to be recreated > > > > *Analysis*: > > > > * > > > > Groovy 3: few call sites, few invalidations → loop runs fast > > > > * > > > > Groovy 4: more dynamic call sites + more invalidations → overhead > > increases → slower performance > > We need specific output to talk about this. I know only that > PrintCallSiteCounters does show more than just invokedynamic sites. > invalidations is interesting, but as I said, this would have to be > discussed with specific output. > > [...] > > ------------------------------------------------------------------------ > > > > > > ✅ Key Takeaways > > > > 1. > > > > |invokedynamic| itself is *not slow* > > > > 2. > > > > Groovy 4 is slower than Groovy 3 because: > > > > * > > > > it uses *more invokedynamic call sites* > > > > * > > > > more *call site invalidations* > > > > * > > > > the *MOP* is more dynamic and general > > scratch that last point imho. > > > [...] > > by Jochen > >
