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

Reply via email to