You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@groovy.apache.org by Gianluca Sartori <g....@gmail.com> on 2026/01/14 11:27:01 UTC
Groovy > 3 performance
Hi folks,
I am personally worried about performance going forward since we've seen
perceivable slowdowns porting our applications to Grails 7/Groovy 4 and we
use @CompileStatic almost everywhere except on Controllers. On Services we
use @CompileDynamic only on methods implementing GORM queries. We
use @GrailsCompileStatic on Domain classes.
PS: No, we are not going INDY OFF, I don't want this issue to fade away
from me (and it's not future proof).
How can we move forward thinking about performance optimizations? I would
expect improved or stable performance on future releases rather than lower
performance.
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.
------------------------------
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.
------------------------------
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.
------------------------------
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.
------------------------------
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.
------------------------------
✅ *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.
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
------------------------------
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
}
-
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
invalidationsinvokedynamic site #2: 10000 invocations, 50
invalidationsinvokedynamic 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
------------------------------
5️⃣ Profiling with @CompileStatic
You can test the difference with *static compilation*:
import groovy.transform.CompileStatic
@CompileStatic
void testStatic() {
10_000.times { i ->
def a = "string"
println a + i
}
}
testStatic()
-
Compare *dynamic vs static*
-
You’ll see that static compilation avoids most call site invalidations →
loop is much faster
------------------------------
✅ 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
3.
Practical ways to improve speed:
-
Use @CompileStatic for hot loops
-
Avoid runtime modifications to metaClass
-
Reduce dynamic operator usage on heterogeneous objects
-
Profile call sites to find bottlenecks
Gianluca Sartori
--
https://dueuno.com
Re: Groovy > 3 performance
Posted by Mattias Reichel <ma...@apache.org>.
Gianluca, I agree, this is of true concern for the Groovy language and
ecosystem.
However, in the meantime, if there are severe bottlenecks, you should be
able to refactor out any dynamic code in Grails Controllers and Gorm
queries.
Here is an example Grails project where this has been done:
https://github.com/matrei/pingcrm-grails
/Mattias
Den ons 14 jan. 2026 kl 12:27 skrev Gianluca Sartori <g....@gmail.com>:
> Hi folks,
>
> I am personally worried about performance going forward since we've seen
> perceivable slowdowns porting our applications to Grails 7/Groovy 4 and we
> use @CompileStatic almost everywhere except on Controllers. On Services we
> use @CompileDynamic only on methods implementing GORM queries. We
> use @GrailsCompileStatic on Domain classes.
>
> PS: No, we are not going INDY OFF, I don't want this issue to fade away
> from me (and it's not future proof).
>
> How can we move forward thinking about performance optimizations? I would
> expect improved or stable performance on future releases rather than lower
> performance.
>
> 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.
>
> ------------------------------
> 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.
>
> ------------------------------
> 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.
>
> ------------------------------
> 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.
>
> ------------------------------
> 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.
>
> ------------------------------
>
> ✅ *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.
> 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
>
> ------------------------------
> 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
> }
>
>
> -
>
> 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 invalidationsinvokedynamic site #2: 10000 invocations, 50 invalidationsinvokedynamic 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
>
> ------------------------------
> 5️⃣ Profiling with @CompileStatic
>
> You can test the difference with *static compilation*:
>
> import groovy.transform.CompileStatic
>
> @CompileStatic
> void testStatic() {
> 10_000.times { i ->
> def a = "string"
> println a + i
> }
> }
>
> testStatic()
>
>
> -
>
> Compare *dynamic vs static*
> -
>
> You’ll see that static compilation avoids most call site invalidations
> → loop is much faster
>
> ------------------------------
> ✅ 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
> 3.
>
> Practical ways to improve speed:
> -
>
> Use @CompileStatic for hot loops
> -
>
> Avoid runtime modifications to metaClass
> -
>
> Reduce dynamic operator usage on heterogeneous objects
> -
>
> Profile call sites to find bottlenecks
>
>
> Gianluca Sartori
> --
> https://dueuno.com
>
Re: Groovy > 3 performance
Posted by Gianluca Sartori <g....@gmail.com>.
Hi Дилян,
I can provide a couple of real applications with measurements on specific
code to demonstrate the issue.
As with MG the impact is on a specific area (in our case GSP generation)
from Gails 6/Groovy 3 to Grails 7/Groovy 4.
We do server side rendering and the performance issue, even if still
acceptable in our use cases, is noticeable to a trained eye.
Cheers,
Gianluca Sartori
--
https://dueuno.com
On Sat, 17 Jan 2026 at 21:37, Дилян Палаузов via dev <de...@groovy.apache.org>
wrote:
> Hello,
>
> I do not think that this is going to make progress, until somebody shares
> a minimal example, which has performance differences between INDY and
> classic bytecode generation. That is: .groovy files, build environment and
> instructions how to run the files to notice significant performance
> difference.
>
> Greetings
> Дилян
>
>
>
>
>
>
> -----Original Message-----
> From: MG <mg...@arscreat.com>
> Reply-To: dev@groovy.apache.org
> To: dev@groovy.apache.org, Jochen Theodorou <bl...@gmx.org>
> Subject: Re: Groovy > 3 performance
> Date: 17/01/26 22:27:59
>
>
> Hi .+,
> 1. It imho bears repeating, that we faced 2 sperate performance
> problems in our Groovy framework/applications when enabling the INDY call
> site mechanism:
> 1. The performance of creating a large number of short lived,
> small objects, which happened in a very obfuscated way in a very specific
> part of our code, was about 4x slower.
> 1. After finally pinpointing the culprit, we worked around
> that by refactoring our code - trading losing some framework-user
> convenience for needing to create way less (helper) objects.
> 2. Executed code seemed to "leak performance everywhere" when
> methods were called.
> 1. As stated before, switching our two most low level
> framework modules to @CompileStatic worked around this problem.
> 2. The first problem should be reproducable by creating a large enough
> variety of small objects, comparing INDY with non-INDY performance.
> 3. Since it seems the second problem does not occur in smaller projects
> or simple synthetic test code, I assume they also need a "large enough"
> variety of call sites, which could point to how a more complex synthetic
> case would need to be constructed.
> 4. It might also be interesting to know if the Grails performance
> problems are related to problem area 1, 2, or both...
> Cheers,
> mg
>
>
>
> Am 17.01.2026 um 17:50 schrieb Jochen Theodorou:
>
> > On 16.01.26 10:22, Gianluca Sartori wrote:
> >
> > > Hi Jonny,
> > >
> > > I've used chatGPT just to raise questions more than give a solution.
> I am unfortunately not in the position to work on it, I don't have
> experience on language development (I am sure the Assembly compiler I
> developed when I was in High School does not qualify).
> > >
> >
> > I would say it is a start. This is not that different from implementing
> an advanced feature in an application.
> >
> >
> > > At the moment the role I can play is to make sure we don't
> forget about performance, because what's happening is that we are degrading
> instead of improving.
> > >
> > > I am aware that this is not an easy task and I don't want to sound
> rude It's just that I'm writing a bit in a hurry.
> > >
> >
> > I appreciate your efforts. What would be really nice would be to
> concentrate on a single small problem and have that as a small program
> without Grails. Then I can analyze the heck out of it to maybe find a
> solution for that specific problem
> >
> > bye Jochen
> >
> >
>
>
>
Re: Groovy > 3 performance
Posted by MG <mg...@arscreat.com>.
I agree, but we have a) already invested a large amount of resources to
supply a compiled example for case 1, which in the end led nowhere, and
b) are not at liberty to share our code, so I was trying to point out
the direction any synthethic tests would probably need to aim based on
our experience, should someone who has not yet solved his performance
problems be inclined to create such tests :-)
Cheers,
mg
Am 17.01.2026 um 21:36 schrieb Дилян Палаузов via dev:
> Hello,
>
> I do not think that this is going to make progress, until somebody shares a minimal example, which has performance differences between INDY and classic bytecode generation. That is: .groovy files, build environment and instructions how to run the files to notice significant performance difference.
>
> Greetings
> Дилян
>
>
>
>
>
> -----Original Message-----
> From: MG <mg...@arscreat.com>
> Reply-To: dev@groovy.apache.org
> To: dev@groovy.apache.org, Jochen Theodorou <bl...@gmx.org>
> Subject: Re: Groovy > 3 performance
> Date: 17/01/26 22:27:59
>
>
> Hi .+,
> 1. It imho bears repeating, that we faced 2 sperate performance problems in our Groovy framework/applications when enabling the INDY call site mechanism:
> 1. The performance of creating a large number of short lived, small objects, which happened in a very obfuscated way in a very specific part of our code, was about 4x slower.
> 1. After finally pinpointing the culprit, we worked around that by refactoring our code - trading losing some framework-user convenience for needing to create way less (helper) objects.
> 2. Executed code seemed to "leak performance everywhere" when methods were called.
> 1. As stated before, switching our two most low level framework modules to @CompileStatic worked around this problem.
> 2. The first problem should be reproducable by creating a large enough variety of small objects, comparing INDY with non-INDY performance.
> 3. Since it seems the second problem does not occur in smaller projects or simple synthetic test code, I assume they also need a "large enough" variety of call sites, which could point to how a more complex synthetic case would need to be constructed.
> 4. It might also be interesting to know if the Grails performance problems are related to problem area 1, 2, or both...
> Cheers,
> mg
>
>
>
> Am 17.01.2026 um 17:50 schrieb Jochen Theodorou:
>
>> On 16.01.26 10:22, Gianluca Sartori wrote:
>>
>>> Hi Jonny,
>>>
>>> I've used chatGPT just to raise questions more than give a solution. I am unfortunately not in the position to work on it, I don't have experience on language development (I am sure the Assembly compiler I developed when I was in High School does not qualify).
>>>
>>
>> I would say it is a start. This is not that different from implementing an advanced feature in an application.
>>
>>
>>> At the moment the role I can play is to make sure we don't forget about performance, because what's happening is that we are degrading instead of improving.
>>>
>>> I am aware that this is not an easy task and I don't want to sound rude It's just that I'm writing a bit in a hurry.
>>>
>>
>> I appreciate your efforts. What would be really nice would be to concentrate on a single small problem and have that as a small program without Grails. Then I can analyze the heck out of it to maybe find a solution for that specific problem
>>
>> bye Jochen
>>
>>
>
>
Re: Groovy > 3 performance
Posted by Jonny <jo...@apache.org>.
My earlier comment may have come across as a bit stand-offish. If so, my
apologies for that. For what it's worth, Gianluca, if you wrote an Assembly
compiler I'd say that puts you well ahead of me in terms of experience and
qualification! 😂
To try and demonstrate what I meant by linking to the "build your LLM a
laboratory
<https://brianlovin.com/writing/give-your-agent-a-laboratory-jH5ryjC>"
blog, I started work on a branch
<https://github.com/apache/groovy/compare/GROOVY_4_0_X...jonnybot0:groovy:INDY-PERF-EXPLORATION?expand=1>
that would create some discrete JMH benchmarks that could potentially be
the small samples that Jochen was asking for. I've got just enough
experience with JMH to be a wise fool here, so I'm not even asking for a
review until I can prove these benchmarks are useful. Indeed, only the very
first part of the (LLM-generated) plan
<https://github.com/apache/groovy/compare/GROOVY_4_0_X...jonnybot0:groovy:INDY-PERF-EXPLORATION?expand=1#diff-f515504702ff1f9d88b86f6befd12ef0fdd93c6c265751b9b7e0bacb0c051261>
is implemented.
My plan is to use them to help me (a performance engineering and language
development neophyte) evaluate James Fredley's PR
<https://github.com/apache/groovy/pull/2374>, which looks like a concrete
step towards addressing some of the indy performance concerns based on real
world data. If my little foray into benchmarks here is a distraction from
that, please ignore it. James's PR deserves more attention. That said, if
these benchmarks are useful to the conversation, or you just want to play
with them, I'd recommend tweaking the JMH configuration so that it only
runs a few iterations on your first pass. JMH is rigorous by default, so
it'll happily spend an hour or more running benchmarks if you just run
./gradlew performance:jmh. That's fine for CI, but you may want to do more
of a "quick sniff" test
// Add this to subprojects/performance/build.gradle and tweak using
the available options according to taste
// https://github.com/melix/jmh-gradle-plugin?tab=readme-ov-file#configuration-options
// Then run the benchmark you're interested in with the benchInclude
property, like -PbenchInclude=ColdCall
jmh {
iterations = 1
warmupIterations = 1
fork = 1
}
I've attached an example from running all the benchmarks on my machine with
the above limitation. It took about 8 minutes with Java 17.
My little experience with benchmarks is that Richard Feynman's warning is
very apt: "The first principle is that you must not fool yourself and you
are the easiest person to fool." Let the reader beware! 🙂
I'd be very grateful for feedback from anyone on this thread as to whether
they think these benchmarks might be a useful step in advancing the state
of the art in Groovy performance.
Best,
Jonny
On Mon, Jan 19, 2026 at 3:47 PM MG <mg...@arscreat.com> wrote:
>
> 1. Evidently if we had a compact source sample, progressing towards
> fixing this would most likely be easy, but in the absence of that I was
> just trying to give the feedback I can.
> 2. I might misinterpret what you are trying to say, but primitive
> optimizations playing a role would suprise me, since what we do a lot in
> our code is (deeply) nested GStrings with a lot of embedded objects
> representing database table and column references, created using
> reflection, many of them short lived.
> 3. The code that does these operations is in the 2 low level modules
> that fixed the performance problems when we switched them to
> @CompileStatic.
>
> Am 19.01.2026 um 07:22 schrieb Jochen Theodorou:
>
> I was talking about a specific case and without primitive optimizations.
>
> On 1/18/26 22:54, MG wrote:
>
> @And for the long run the performance is actually comparable:
>
> 1. I want to reiterate that this is /not /what we observered in any of
> the real world cases we used as benchmarks.
> 2. While performance typically improved slightly with more iterations,
> this never led to a convergence towards the same runtime behavior.
> 3. In addition for many real life use cases it would not matter/help
> even if performance became identical between INDY & non-INDY after,
> say 1000 or 10000 calls, because this is not a threshold ever
> reached in practice, since we e.g. auto restart our application each
> night.
> 4. Imho therefore performance would need to be fixed in a way that
> works from the first call, not only at any point in the future...
>
> Cheers,
> mg
>
> Am 18.01.2026 um 06:23 schrieb Jochen Theodorou:
>
>
>
> On 1/17/26 21:36, Дилян Палаузов wrote:
>
> Hello,
>
> I do not think that this is going to make progress, until somebody shares
> a minimal example, which has performance differences between INDY and
> classic bytecode generation. That is: .groovy files, build environment and
> instructions how to run the files to notice significant performance
> difference.
>
>
>
> That in itself is actually not so difficult. See GROOVY-11842 for example.
> While the issue itself is about something being in the callstack, that
> should not be there, I there also talk about how Groovy 3 with and without
> primitive optimizations performs here in the first few iterations. And for
> the long run the performance is actually comparable.
>
> File:
>
> int foo(){1}
>
> int a
> long t1,t2
> O_MAX = 1000
> long[] ts = new long[O_MAX]
> for (int o=0; o<O_MAX; o++) {
> t1 = System.nanoTime()
> for (int i=0; i<10_000; i++) {
> a=foo()
> }
> t2 = System.nanoTime()
> ts[o] = t2-t1
> }
>
> def f = new File("out.txt")
> for (int i =0; i<O_MAX; i++) {
> f << i +": " + ts[i] + "\n"
> }
> println "done"
>
>
> Environment: just java command line with GroovyMain or groovy command.
>
> But what does it actually say? And is this really relevant in a big
> application? Those are difficult to answer.
>
> And that is your whole point I assume. it needs simple enough to actually
> get what you test, but complex enough to be relevant for the application.
> This one here surely is not
>
> bye Jochen
>
>
>
>
>
>
>
Re: Groovy > 3 performance
Posted by MG <mg...@arscreat.com>.
1. Evidently if we had a compact source sample, progressing towards
fixing this would most likely be easy, but in the absence of that I
was just trying to give the feedback I can.
2. I might misinterpret what you are trying to say, but primitive
optimizations playing a role would suprise me, since what we do a
lot in our code is (deeply) nested GStrings with a lot of embedded
objects representing database table and column references, created
using reflection, many of them short lived.
3. The code that does these operations is in the 2 low level modules
that fixed the performance problems when we switched them to
@CompileStatic.
Am 19.01.2026 um 07:22 schrieb Jochen Theodorou:
> I was talking about a specific case and without primitive optimizations.
>
> On 1/18/26 22:54, MG wrote:
>> @And for the long run the performance is actually comparable:
>>
>> 1. I want to reiterate that this is /not /what we observered in any of
>> the real world cases we used as benchmarks.
>> 2. While performance typically improved slightly with more iterations,
>> this never led to a convergence towards the same runtime behavior.
>> 3. In addition for many real life use cases it would not matter/help
>> even if performance became identical between INDY & non-INDY after,
>> say 1000 or 10000 calls, because this is not a threshold ever
>> reached in practice, since we e.g. auto restart our application each
>> night.
>> 4. Imho therefore performance would need to be fixed in a way that
>> works from the first call, not only at any point in the future...
>>
>> Cheers,
>> mg
>>
>> Am 18.01.2026 um 06:23 schrieb Jochen Theodorou:
>>>
>>>
>>> On 1/17/26 21:36, Дилян Палаузов wrote:
>>>> Hello,
>>>>
>>>> I do not think that this is going to make progress, until somebody
>>>> shares a minimal example, which has performance differences between
>>>> INDY and classic bytecode generation. That is: .groovy files, build
>>>> environment and instructions how to run the files to notice
>>>> significant performance difference.
>>>
>>>
>>> That in itself is actually not so difficult. See GROOVY-11842 for
>>> example. While the issue itself is about something being in the
>>> callstack, that should not be there, I there also talk about how
>>> Groovy 3 with and without primitive optimizations performs here in
>>> the first few iterations. And for the long run the performance is
>>> actually comparable.
>>>
>>> File:
>>>
>>> int foo(){1}
>>>
>>> int a
>>> long t1,t2
>>> O_MAX = 1000
>>> long[] ts = new long[O_MAX]
>>> for (int o=0; o<O_MAX; o++) {
>>> t1 = System.nanoTime()
>>> for (int i=0; i<10_000; i++) {
>>> a=foo()
>>> }
>>> t2 = System.nanoTime()
>>> ts[o] = t2-t1
>>> }
>>>
>>> def f = new File("out.txt")
>>> for (int i =0; i<O_MAX; i++) {
>>> f << i +": " + ts[i] + "\n"
>>> }
>>> println "done"
>>>
>>>
>>> Environment: just java command line with GroovyMain or groovy command.
>>>
>>> But what does it actually say? And is this really relevant in a big
>>> application? Those are difficult to answer.
>>>
>>> And that is your whole point I assume. it needs simple enough to
>>> actually get what you test, but complex enough to be relevant for
>>> the application. This one here surely is not
>>>
>>> bye Jochen
>>>
>>>
>>>
>>
>
Re: Groovy > 3 performance
Posted by Jochen Theodorou <bl...@gmx.org>.
I was talking about a specific case and without primitive optimizations.
On 1/18/26 22:54, MG wrote:
> @And for the long run the performance is actually comparable:
>
> 1. I want to reiterate that this is /not /what we observered in any of
> the real world cases we used as benchmarks.
> 2. While performance typically improved slightly with more iterations,
> this never led to a convergence towards the same runtime behavior.
> 3. In addition for many real life use cases it would not matter/help
> even if performance became identical between INDY & non-INDY after,
> say 1000 or 10000 calls, because this is not a threshold ever
> reached in practice, since we e.g. auto restart our application each
> night.
> 4. Imho therefore performance would need to be fixed in a way that
> works from the first call, not only at any point in the future...
>
> Cheers,
> mg
>
> Am 18.01.2026 um 06:23 schrieb Jochen Theodorou:
>>
>>
>> On 1/17/26 21:36, Дилян Палаузов wrote:
>>> Hello,
>>>
>>> I do not think that this is going to make progress, until somebody
>>> shares a minimal example, which has performance differences between
>>> INDY and classic bytecode generation. That is: .groovy files, build
>>> environment and instructions how to run the files to notice
>>> significant performance difference.
>>
>>
>> That in itself is actually not so difficult. See GROOVY-11842 for
>> example. While the issue itself is about something being in the
>> callstack, that should not be there, I there also talk about how
>> Groovy 3 with and without primitive optimizations performs here in the
>> first few iterations. And for the long run the performance is actually
>> comparable.
>>
>> File:
>>
>> int foo(){1}
>>
>> int a
>> long t1,t2
>> O_MAX = 1000
>> long[] ts = new long[O_MAX]
>> for (int o=0; o<O_MAX; o++) {
>> t1 = System.nanoTime()
>> for (int i=0; i<10_000; i++) {
>> a=foo()
>> }
>> t2 = System.nanoTime()
>> ts[o] = t2-t1
>> }
>>
>> def f = new File("out.txt")
>> for (int i =0; i<O_MAX; i++) {
>> f << i +": " + ts[i] + "\n"
>> }
>> println "done"
>>
>>
>> Environment: just java command line with GroovyMain or groovy command.
>>
>> But what does it actually say? And is this really relevant in a big
>> application? Those are difficult to answer.
>>
>> And that is your whole point I assume. it needs simple enough to
>> actually get what you test, but complex enough to be relevant for the
>> application. This one here surely is not
>>
>> bye Jochen
>>
>>
>>
>
Re: Groovy > 3 performance
Posted by MG <mg...@arscreat.com>.
@And for the long run the performance is actually comparable:
1. I want to reiterate that this is /not /what we observered in any of
the real world cases we used as benchmarks.
2. While performance typically improved slightly with more iterations,
this never led to a convergence towards the same runtime behavior.
3. In addition for many real life use cases it would not matter/help
even if performance became identical between INDY & non-INDY after,
say 1000 or 10000 calls, because this is not a threshold ever
reached in practice, since we e.g. auto restart our application each
night.
4. Imho therefore performance would need to be fixed in a way that
works from the first call, not only at any point in the future...
Cheers,
mg
Am 18.01.2026 um 06:23 schrieb Jochen Theodorou:
>
>
> On 1/17/26 21:36, Дилян Палаузов wrote:
>> Hello,
>>
>> I do not think that this is going to make progress, until somebody
>> shares a minimal example, which has performance differences between
>> INDY and classic bytecode generation. That is: .groovy files, build
>> environment and instructions how to run the files to notice
>> significant performance difference.
>
>
> That in itself is actually not so difficult. See GROOVY-11842 for
> example. While the issue itself is about something being in the
> callstack, that should not be there, I there also talk about how
> Groovy 3 with and without primitive optimizations performs here in the
> first few iterations. And for the long run the performance is actually
> comparable.
>
> File:
>
> int foo(){1}
>
> int a
> long t1,t2
> O_MAX = 1000
> long[] ts = new long[O_MAX]
> for (int o=0; o<O_MAX; o++) {
> t1 = System.nanoTime()
> for (int i=0; i<10_000; i++) {
> a=foo()
> }
> t2 = System.nanoTime()
> ts[o] = t2-t1
> }
>
> def f = new File("out.txt")
> for (int i =0; i<O_MAX; i++) {
> f << i +": " + ts[i] + "\n"
> }
> println "done"
>
>
> Environment: just java command line with GroovyMain or groovy command.
>
> But what does it actually say? And is this really relevant in a big
> application? Those are difficult to answer.
>
> And that is your whole point I assume. it needs simple enough to
> actually get what you test, but complex enough to be relevant for the
> application. This one here surely is not
>
> bye Jochen
>
>
>
Re: Groovy > 3 performance
Posted by Jochen Theodorou <bl...@gmx.org>.
On 1/17/26 21:36, Дилян Палаузов wrote:
> Hello,
>
> I do not think that this is going to make progress, until somebody shares a minimal example, which has performance differences between INDY and classic bytecode generation. That is: .groovy files, build environment and instructions how to run the files to notice significant performance difference.
That in itself is actually not so difficult. See GROOVY-11842 for
example. While the issue itself is about something being in the
callstack, that should not be there, I there also talk about how Groovy
3 with and without primitive optimizations performs here in the first
few iterations. And for the long run the performance is actually
comparable.
File:
int foo(){1}
int a
long t1,t2
O_MAX = 1000
long[] ts = new long[O_MAX]
for (int o=0; o<O_MAX; o++) {
t1 = System.nanoTime()
for (int i=0; i<10_000; i++) {
a=foo()
}
t2 = System.nanoTime()
ts[o] = t2-t1
}
def f = new File("out.txt")
for (int i =0; i<O_MAX; i++) {
f << i +": " + ts[i] + "\n"
}
println "done"
Environment: just java command line with GroovyMain or groovy command.
But what does it actually say? And is this really relevant in a big
application? Those are difficult to answer.
And that is your whole point I assume. it needs simple enough to
actually get what you test, but complex enough to be relevant for the
application. This one here surely is not
bye Jochen
Re: Groovy > 3 performance
Posted by Дилян Палаузов via dev <de...@groovy.apache.org>.
Hello,
I do not think that this is going to make progress, until somebody shares a minimal example, which has performance differences between INDY and classic bytecode generation. That is: .groovy files, build environment and instructions how to run the files to notice significant performance difference.
Greetings
Дилян
-----Original Message-----
From: MG <mg...@arscreat.com>
Reply-To: dev@groovy.apache.org
To: dev@groovy.apache.org, Jochen Theodorou <bl...@gmx.org>
Subject: Re: Groovy > 3 performance
Date: 17/01/26 22:27:59
Hi .+,
1. It imho bears repeating, that we faced 2 sperate performance problems in our Groovy framework/applications when enabling the INDY call site mechanism:
1. The performance of creating a large number of short lived, small objects, which happened in a very obfuscated way in a very specific part of our code, was about 4x slower.
1. After finally pinpointing the culprit, we worked around that by refactoring our code - trading losing some framework-user convenience for needing to create way less (helper) objects.
2. Executed code seemed to "leak performance everywhere" when methods were called.
1. As stated before, switching our two most low level framework modules to @CompileStatic worked around this problem.
2. The first problem should be reproducable by creating a large enough variety of small objects, comparing INDY with non-INDY performance.
3. Since it seems the second problem does not occur in smaller projects or simple synthetic test code, I assume they also need a "large enough" variety of call sites, which could point to how a more complex synthetic case would need to be constructed.
4. It might also be interesting to know if the Grails performance problems are related to problem area 1, 2, or both...
Cheers,
mg
Am 17.01.2026 um 17:50 schrieb Jochen Theodorou:
> On 16.01.26 10:22, Gianluca Sartori wrote:
>
> > Hi Jonny,
> >
> > I've used chatGPT just to raise questions more than give a solution. I am unfortunately not in the position to work on it, I don't have experience on language development (I am sure the Assembly compiler I developed when I was in High School does not qualify).
> >
>
> I would say it is a start. This is not that different from implementing an advanced feature in an application.
>
>
> > At the moment the role I can play is to make sure we don't forget about performance, because what's happening is that we are degrading instead of improving.
> >
> > I am aware that this is not an easy task and I don't want to sound rude It's just that I'm writing a bit in a hurry.
> >
>
> I appreciate your efforts. What would be really nice would be to concentrate on a single small problem and have that as a small program without Grails. Then I can analyze the heck out of it to maybe find a solution for that specific problem
>
> bye Jochen
>
>
Re: Groovy > 3 performance
Posted by MG <mg...@arscreat.com>.
Hi .+,
1. It imho bears repeating, that we faced 2 sperate performance
problems in our Groovy framework/applications when enabling the INDY
call site mechanism:
1. The performance of creating a large number of short lived, small
objects, which happened in a very obfuscated way in a very
specific part of our code, was about 4x slower.
1. After finally pinpointing the culprit, we worked around that
by refactoring our code - trading losing some framework-user
convenience for needing to create way less (helper) objects.
2. Executed code seemed to "leak performance everywhere" when
methods were called.
1. As stated before, switching our two most low level framework
modules to @CompileStatic worked around this problem.
2. The first problem should be reproducable by creating a large enough
variety of small objects, comparing INDY with non-INDY performance.
3. Since it seems the second problem does not occur in smaller projects
or simple synthetic test code, I assume they also need a "large
enough" variety of call sites, which could point to how a more
complex synthetic case would need to be constructed.
4. It might also be interesting to know if the Grails performance
problems are related to problem area 1, 2, or both...
Cheers,
mg
Am 17.01.2026 um 17:50 schrieb Jochen Theodorou:
> On 16.01.26 10:22, Gianluca Sartori wrote:
>> Hi Jonny,
>>
>> I've used chatGPT just to raise questions more than give a solution.
>> I am unfortunately not in the position to work on it, I don't have
>> experience on language development (I am sure the Assembly compiler I
>> developed when I was in High School does not qualify).
>
> I would say it is a start. This is not that different from
> implementing an advanced feature in an application.
>
>> At the moment the role I can play is to make sure we don't
>> forget about performance, because what's happening is that we are
>> degrading instead of improving.
>>
>> I am aware that this is not an easy task and I don't want to sound
>> rude It's just that I'm writing a bit in a hurry.
>
> I appreciate your efforts. What would be really nice would be to
> concentrate on a single small problem and have that as a small program
> without Grails. Then I can analyze the heck out of it to maybe find a
> solution for that specific problem
>
> bye Jochen
>
Re: Groovy > 3 performance
Posted by Jochen Theodorou <bl...@gmx.org>.
On 16.01.26 10:22, Gianluca Sartori wrote:
> Hi Jonny,
>
> I've used chatGPT just to raise questions more than give a solution. I
> am unfortunately not in the position to work on it, I don't have
> experience on language development (I am sure the Assembly compiler I
> developed when I was in High School does not qualify).
I would say it is a start. This is not that different from implementing
an advanced feature in an application.
> At the moment the role I can play is to make sure we don't forget about
> performance, because what's happening is that we are degrading instead
> of improving.
>
> I am aware that this is not an easy task and I don't want to sound rude
> It's just that I'm writing a bit in a hurry.
I appreciate your efforts. What would be really nice would be to
concentrate on a single small problem and have that as a small program
without Grails. Then I can analyze the heck out of it to maybe find a
solution for that specific problem
bye Jochen
Re: Groovy > 3 performance
Posted by MG <mg...@arscreat.com>.
Hi Gianluca,
1. You and I are both (rightfully) worried about Groovy INDY
performance degradation (Even though in our organization we were
able to get back to previous performance levels by adapting a very
specific, very hard to find part of our code, as well as switching
all our core project modules to @CompileStatic).
2. However I do not think that querying ("asking") any Large Language
Model and posting the resulting text ouput verbatim here does help,
since these high dimensional databases with stochastic output
generation do not understand anything, but are just execellent at
producing believable human-like output from the vast amounts of data
stored inside their system.
3. Anything an LLM would produce as a reply, would have to be based on
something it has seen on the internet, so it became part of the data
stored in its neural network.
4. While this works to some extent on code that has been written a
million times already, or problems that are widely discussed, it
does not for a language like Groovy, on which very little currently
applicable data exists.
5. Therefore here you run into the problem, that some factual things
will be conflated with a lot of outdated information, information
that applies to other computer languages, etc.
1. You can in my opinion see this in Jochen's replies to the LLM
bullet list.
6. It would therefore imho make more sense to do a Google search on the
topic, restricted to data e.g. 1 to 3 years back, and see if someone
who is not on this mailing list has actually written about this
problem somewhere (unlikely as this is)...
1. (Despite the understandable protesting & massive propaganda
efforts by companies that have invested hundreds of billions of
dollars into LLMs & their infrastructure, the term "AI slop"
exists for a reason, and it does not only apply to images and
videos, but also to (especially the truthfulness of) text,
however deceptively well constructed LLM generated texts are...)
Cheers,
mg
Am 16.01.2026 um 10:22 schrieb Gianluca Sartori:
> Hi Jonny,
>
> I've used chatGPT just to raise questions more than give a solution. I
> am unfortunately not in the position to work on it, I don't have
> experience on language development (I am sure the Assembly compiler I
> developed when I was in High School does not qualify).
>
> At the moment the role I can play is to make sure we don't
> forget about performance, because what's happening is that we are
> degrading instead of improving.
>
> I am aware that this is not an easy task and I don't want to sound
> rude It's just that I'm writing a bit in a hurry.
>
> Gianluca Sartori
> --
> https://dueuno.com
>
>
> On Thu, 15 Jan 2026 at 22:20, Jonny <jo...@apache.org> wrote:
>
> Before we go back and forth too much with Jochen fact-checking GPT
> output, may I recommend Gianluca try giving the agent a laboratory
> to work with? See
> https://brianlovin.com/writing/give-your-agent-a-laboratory (first
> example prompt in that doc is even about performance, though
> there'd obviously be tweaks since you're dealing with a language,
> not a backend).
>
> Regardless of the LLM, making the agent build the kinds of
> performance tests and investigate their output is the only way
> you're likely to get meaningful output. Otherwise, you are indeed
> in the predictive text spin cycle. It does require more time, but
> it's also the only way you're likely to get coherent, usable output.
>
> Best,
>
> Jonny
>
> On Wed, Jan 14, 2026 at 2:57 PM Jochen Theodorou
> <bl...@gmx.org> wrote:
>
> On 14.01.26 15:32, Gianluca Sartori wrote:
> [...]
> > *
> >
> > *Groovy 5* moved most dynamic calls to |CallSite|
> caching through
> > |indy|.
>
> correct, though it was before in the equivalent code for the
> bytecode
> generation
>
> > *
> >
> > *Groovy 3* often inlined certain calls more
> aggressively, sometimes
> > relying on slower reflection but faster in
> microbenchmarks due to
> > simpler call chains.
>
> which type of calls though? It was faster in some
> micro-benchmarks
> because we had a full primitive path parallel to the
> non-primitive path
> maybe. This worked only with unchanged meta classes, so not
> sure Grails
> had much of it. Which also shows a big difference, a
> micro-benchmark is
> not an app and you have to know what you actually test
>
> > *
> >
> > *Result:* |invokedynamic| can introduce overhead for
> short-lived
> > calls or highly polymorphic code because of frequent
> call site
> > relinking.
>
> that would have impacted performance on non-indy as well
>
>
> > Key reasons for slowness:
> >
> > 1.
> >
> > *Polymorphic call sites:* If your code calls many
> different methods
> > dynamically at the same call site, the |invokedynamic|
> bootstrap has
> > to relink repeatedly.
> >
> > 2.
> >
> > *CallSite cache invalidation:* Changes to the meta-class
> or dynamic
> > method addition can invalidate call sites.
>
> as I said, that counts for both variants.
>
> > 3.
> >
> > *Boxing/unboxing overhead:* Primitive-heavy code may
> suffer due to
> > dynamic dispatch.
>
> well, that is not supposed to be an issue in indy. It was in
> the old
> callsite code in Groovy 3
> > 4.
> >
> > *JIT warmup issues:* The JVM may take longer to optimize
> > |indy|-based dispatch.
>
> that is actually a factor. But a proper micro-benchmark will
> not measure
> warmup times, right?
>
> >
> ------------------------------------------------------------------------
> >
> >
> > *2. Optimization Strategies for a Next Groovy Version*
> >
> >
> > *A. Improve CallSite Caching*
> >
> > *
> >
> > Implement *multi-level caching* for polymorphic call sites.
> >
> > *
> >
> > Use *polymorphic inline caches (PICs)* like modern
> JavaScript
> > engines (V8) to avoid relinking.
>
> basically agree
>
> > *
> >
> > Avoid global call site invalidations when meta-classes
> are updated —
> > make call site invalidation more local.
>
> how?
>
> > *B. Reduce Relinking*
> >
> > *
> >
> > Track method signatures more strictly. Many
> |invokedynamic| relinks
> > happen because Groovy tries to handle any dynamic call,
> even when
> > the call target is stable.
>
> True. If you call foo(Object) with a String and then with an
> Integer it
> will cause relinking, even though it is not required
>
> > *
> >
> > Consider *specialized bootstrap methods* for common call
> patterns:
> >
> > o
> >
> > e.g., frequent calls to |String| methods,
> |List|/|Map| operations.
>
> on what are we actually saving here?
>
> > *C. Optimize Primitive Handling*
> >
> > *
> >
> > Introduce *primitive specialization* for arithmetic and
> collection
> > operations.
> >
> > *
> >
> > Reduce boxing/unboxing by generating specialized call
> site versions
> > for primitives (like what Kotlin/JVM or Scala do with
> inline functions).
>
> we have
>
> > *D. Optional Static Call Optimization*
> >
> > *
> >
> > Provide *hybrid static/dynamic dispatch*:
> >
> > o
> >
> > Use static compilation (|@CompileStatic|) when possible.
>
> on the compiler side directly? Well... when is it possible?
>
> > o
> >
> > Use a *profiling-guided JIT* to replace call sites
> with direct
> > method handles if a single target dominates.
>
> well.. and how does the invalidation work if the target is
> suddenly
> incorrect?
>
> > *E. Bytecode Generation Improvements*
> >
> > *
> >
> > Investigate how Groovy 5 generates |invokedynamic| bytecode:
> >
> > o
> >
> > Avoid unnecessary |Object| casts.
> >
> > o
> >
> > Combine multiple small dynamic calls into a single
> bootstrap
> > call to reduce overhead.
>
> Have that afaik
>
> > *
> >
> > Possibly generate *direct method handles* for commonly
> called Groovy
> > methods (|size()|, |get()|, etc.).
>
> see above
>
> > *F. JIT-Friendly Bootstrap*
> >
> > *
> >
> > Groovy could provide *simpler bootstrap methods* to
> allow JVM JIT
> > inlining:
> >
> > o
> >
> > Reduce bootstrap method complexity to help HotSpot
> optimize the
> > call site faster.
>
> not bootstrap complexity, but the resulting handle should have
> as little
> complexity in there as possible
>
> >
> ------------------------------------------------------------------------
> >
> >
> > *3. Micro-Optimizations for Library Authors*
> >
> > If you are writing a library or code in Groovy that must be
> fast:
> >
> > 1.
> >
> > *Prefer static types* whenever possible — even without
> > |@CompileStatic|, type hints help.
>
> which leads to more casting... so no.
>
> > 2.
> >
> > *Use |@CompileStatic|* selectively for hot loops.
>
> possibly
>
> > 3.
> >
> > *Avoid meta-class changes* at runtime in
> performance-critical code.
>
> well, they is currently no way to protect an area against meta
> class changes
>
> > 4.
> >
> > *Cache dynamic lookups manually* for very hot methods.
>
> not understood
>
> > 5.
> >
> > *Use primitive arrays* instead of boxed lists when
> dealing with numbers.
>
> possibly.
>
> > 6.
> >
> > *Minimize polymorphism at call sites* — repeated calls
> to the same
> > method is much faster than alternating between multiple
> methods.
>
> correct
>
> >
> ------------------------------------------------------------------------
> >
> >
> > *4. Experimental Ideas for Groovy 6+*
> >
> > *
> >
> > *CallSite specialization per type* (like Truffle/Graal
> dynamic
> > languages).
>
> I would like to make a MethodHandle part of the MetaMethod. I
> guess that
> goes in that direction.
>
> > *
> >
> > *Inline small closures automatically* at compile-time.
>
> It is not only the code of the Closure, it is also the code
> of the
> Closure handling method. in i=0; n.times{i++} it does not
> actually help
> to "inline" i++, because we would have to inline it to the
> times method.
> Instead we have to inline times as well of course with the
> danger, that
> if we change times, it will not be reflected without recompilation
>
> > *
> >
> > *Profile-guided call site replacement*: replace
> |invokedynamic|
> > calls with direct |MethodHandle| or static calls at
> runtime if
> > profiling shows a stable target.
>
> I cannot replace it with static calls
>
> > *
> >
> > *Better JIT feedback*: provide hints to HotSpot that
> certain call
> > sites are monomorphic or polymorphic.
>
> How?
>
> bye Jochen
>
Re: Groovy > 3 performance
Posted by Gianluca Sartori <g....@gmail.com>.
Hi Jonny,
I've used chatGPT just to raise questions more than give a solution. I am
unfortunately not in the position to work on it, I don't have experience on
language development (I am sure the Assembly compiler I developed when I
was in High School does not qualify).
At the moment the role I can play is to make sure we don't forget about
performance, because what's happening is that we are degrading instead of
improving.
I am aware that this is not an easy task and I don't want to sound rude
It's just that I'm writing a bit in a hurry.
Gianluca Sartori
--
https://dueuno.com
On Thu, 15 Jan 2026 at 22:20, Jonny <jo...@apache.org> wrote:
> Before we go back and forth too much with Jochen fact-checking GPT output,
> may I recommend Gianluca try giving the agent a laboratory to work with?
> See https://brianlovin.com/writing/give-your-agent-a-laboratory (first
> example prompt in that doc is even about performance, though there'd
> obviously be tweaks since you're dealing with a language, not a backend).
>
> Regardless of the LLM, making the agent build the kinds of performance
> tests and investigate their output is the only way you're likely to get
> meaningful output. Otherwise, you are indeed in the predictive text spin
> cycle. It does require more time, but it's also the only way you're likely
> to get coherent, usable output.
>
> Best,
>
> Jonny
>
> On Wed, Jan 14, 2026 at 2:57 PM Jochen Theodorou <bl...@gmx.org>
> wrote:
>
>> On 14.01.26 15:32, Gianluca Sartori wrote:
>> [...]
>> > *
>> >
>> > *Groovy 5* moved most dynamic calls to |CallSite| caching through
>> > |indy|.
>>
>> correct, though it was before in the equivalent code for the bytecode
>> generation
>>
>> > *
>> >
>> > *Groovy 3* often inlined certain calls more aggressively, sometimes
>> > relying on slower reflection but faster in microbenchmarks due to
>> > simpler call chains.
>>
>> which type of calls though? It was faster in some micro-benchmarks
>> because we had a full primitive path parallel to the non-primitive path
>> maybe. This worked only with unchanged meta classes, so not sure Grails
>> had much of it. Which also shows a big difference, a micro-benchmark is
>> not an app and you have to know what you actually test
>>
>> > *
>> >
>> > *Result:* |invokedynamic| can introduce overhead for short-lived
>> > calls or highly polymorphic code because of frequent call site
>> > relinking.
>>
>> that would have impacted performance on non-indy as well
>>
>>
>> > Key reasons for slowness:
>> >
>> > 1.
>> >
>> > *Polymorphic call sites:* If your code calls many different methods
>> > dynamically at the same call site, the |invokedynamic| bootstrap has
>> > to relink repeatedly.
>> >
>> > 2.
>> >
>> > *CallSite cache invalidation:* Changes to the meta-class or dynamic
>> > method addition can invalidate call sites.
>>
>> as I said, that counts for both variants.
>>
>> > 3.
>> >
>> > *Boxing/unboxing overhead:* Primitive-heavy code may suffer due to
>> > dynamic dispatch.
>>
>> well, that is not supposed to be an issue in indy. It was in the old
>> callsite code in Groovy 3
>> > 4.
>> >
>> > *JIT warmup issues:* The JVM may take longer to optimize
>> > |indy|-based dispatch.
>>
>> that is actually a factor. But a proper micro-benchmark will not measure
>> warmup times, right?
>>
>> > ------------------------------------------------------------------------
>> >
>> >
>> > *2. Optimization Strategies for a Next Groovy Version*
>> >
>> >
>> > *A. Improve CallSite Caching*
>> >
>> > *
>> >
>> > Implement *multi-level caching* for polymorphic call sites.
>> >
>> > *
>> >
>> > Use *polymorphic inline caches (PICs)* like modern JavaScript
>> > engines (V8) to avoid relinking.
>>
>> basically agree
>>
>> > *
>> >
>> > Avoid global call site invalidations when meta-classes are updated —
>> > make call site invalidation more local.
>>
>> how?
>>
>> > *B. Reduce Relinking*
>> >
>> > *
>> >
>> > Track method signatures more strictly. Many |invokedynamic| relinks
>> > happen because Groovy tries to handle any dynamic call, even when
>> > the call target is stable.
>>
>> True. If you call foo(Object) with a String and then with an Integer it
>> will cause relinking, even though it is not required
>>
>> > *
>> >
>> > Consider *specialized bootstrap methods* for common call patterns:
>> >
>> > o
>> >
>> > e.g., frequent calls to |String| methods, |List|/|Map|
>> operations.
>>
>> on what are we actually saving here?
>>
>> > *C. Optimize Primitive Handling*
>> >
>> > *
>> >
>> > Introduce *primitive specialization* for arithmetic and collection
>> > operations.
>> >
>> > *
>> >
>> > Reduce boxing/unboxing by generating specialized call site versions
>> > for primitives (like what Kotlin/JVM or Scala do with inline
>> functions).
>>
>> we have
>>
>> > *D. Optional Static Call Optimization*
>> >
>> > *
>> >
>> > Provide *hybrid static/dynamic dispatch*:
>> >
>> > o
>> >
>> > Use static compilation (|@CompileStatic|) when possible.
>>
>> on the compiler side directly? Well... when is it possible?
>>
>> > o
>> >
>> > Use a *profiling-guided JIT* to replace call sites with direct
>> > method handles if a single target dominates.
>>
>> well.. and how does the invalidation work if the target is suddenly
>> incorrect?
>>
>> > *E. Bytecode Generation Improvements*
>> >
>> > *
>> >
>> > Investigate how Groovy 5 generates |invokedynamic| bytecode:
>> >
>> > o
>> >
>> > Avoid unnecessary |Object| casts.
>> >
>> > o
>> >
>> > Combine multiple small dynamic calls into a single bootstrap
>> > call to reduce overhead.
>>
>> Have that afaik
>>
>> > *
>> >
>> > Possibly generate *direct method handles* for commonly called Groovy
>> > methods (|size()|, |get()|, etc.).
>>
>> see above
>>
>> > *F. JIT-Friendly Bootstrap*
>> >
>> > *
>> >
>> > Groovy could provide *simpler bootstrap methods* to allow JVM JIT
>> > inlining:
>> >
>> > o
>> >
>> > Reduce bootstrap method complexity to help HotSpot optimize the
>> > call site faster.
>>
>> not bootstrap complexity, but the resulting handle should have as little
>> complexity in there as possible
>>
>> > ------------------------------------------------------------------------
>> >
>> >
>> > *3. Micro-Optimizations for Library Authors*
>> >
>> > If you are writing a library or code in Groovy that must be fast:
>> >
>> > 1.
>> >
>> > *Prefer static types* whenever possible — even without
>> > |@CompileStatic|, type hints help.
>>
>> which leads to more casting... so no.
>>
>> > 2.
>> >
>> > *Use |@CompileStatic|* selectively for hot loops.
>>
>> possibly
>>
>> > 3.
>> >
>> > *Avoid meta-class changes* at runtime in performance-critical code.
>>
>> well, they is currently no way to protect an area against meta class
>> changes
>>
>> > 4.
>> >
>> > *Cache dynamic lookups manually* for very hot methods.
>>
>> not understood
>>
>> > 5.
>> >
>> > *Use primitive arrays* instead of boxed lists when dealing with
>> numbers.
>>
>> possibly.
>>
>> > 6.
>> >
>> > *Minimize polymorphism at call sites* — repeated calls to the same
>> > method is much faster than alternating between multiple methods.
>>
>> correct
>>
>> > ------------------------------------------------------------------------
>> >
>> >
>> > *4. Experimental Ideas for Groovy 6+*
>> >
>> > *
>> >
>> > *CallSite specialization per type* (like Truffle/Graal dynamic
>> > languages).
>>
>> I would like to make a MethodHandle part of the MetaMethod. I guess that
>> goes in that direction.
>>
>> > *
>> >
>> > *Inline small closures automatically* at compile-time.
>>
>> It is not only the code of the Closure, it is also the code of the
>> Closure handling method. in i=0; n.times{i++} it does not actually help
>> to "inline" i++, because we would have to inline it to the times method.
>> Instead we have to inline times as well of course with the danger, that
>> if we change times, it will not be reflected without recompilation
>>
>> > *
>> >
>> > *Profile-guided call site replacement*: replace |invokedynamic|
>> > calls with direct |MethodHandle| or static calls at runtime if
>> > profiling shows a stable target.
>>
>> I cannot replace it with static calls
>>
>> > *
>> >
>> > *Better JIT feedback*: provide hints to HotSpot that certain call
>> > sites are monomorphic or polymorphic.
>>
>> How?
>>
>> bye Jochen
>>
>>
Re: Groovy > 3 performance
Posted by Jonny <jo...@apache.org>.
Before we go back and forth too much with Jochen fact-checking GPT output,
may I recommend Gianluca try giving the agent a laboratory to work with?
See https://brianlovin.com/writing/give-your-agent-a-laboratory (first
example prompt in that doc is even about performance, though there'd
obviously be tweaks since you're dealing with a language, not a backend).
Regardless of the LLM, making the agent build the kinds of performance
tests and investigate their output is the only way you're likely to get
meaningful output. Otherwise, you are indeed in the predictive text spin
cycle. It does require more time, but it's also the only way you're likely
to get coherent, usable output.
Best,
Jonny
On Wed, Jan 14, 2026 at 2:57 PM Jochen Theodorou <bl...@gmx.org> wrote:
> On 14.01.26 15:32, Gianluca Sartori wrote:
> [...]
> > *
> >
> > *Groovy 5* moved most dynamic calls to |CallSite| caching through
> > |indy|.
>
> correct, though it was before in the equivalent code for the bytecode
> generation
>
> > *
> >
> > *Groovy 3* often inlined certain calls more aggressively, sometimes
> > relying on slower reflection but faster in microbenchmarks due to
> > simpler call chains.
>
> which type of calls though? It was faster in some micro-benchmarks
> because we had a full primitive path parallel to the non-primitive path
> maybe. This worked only with unchanged meta classes, so not sure Grails
> had much of it. Which also shows a big difference, a micro-benchmark is
> not an app and you have to know what you actually test
>
> > *
> >
> > *Result:* |invokedynamic| can introduce overhead for short-lived
> > calls or highly polymorphic code because of frequent call site
> > relinking.
>
> that would have impacted performance on non-indy as well
>
>
> > Key reasons for slowness:
> >
> > 1.
> >
> > *Polymorphic call sites:* If your code calls many different methods
> > dynamically at the same call site, the |invokedynamic| bootstrap has
> > to relink repeatedly.
> >
> > 2.
> >
> > *CallSite cache invalidation:* Changes to the meta-class or dynamic
> > method addition can invalidate call sites.
>
> as I said, that counts for both variants.
>
> > 3.
> >
> > *Boxing/unboxing overhead:* Primitive-heavy code may suffer due to
> > dynamic dispatch.
>
> well, that is not supposed to be an issue in indy. It was in the old
> callsite code in Groovy 3
> > 4.
> >
> > *JIT warmup issues:* The JVM may take longer to optimize
> > |indy|-based dispatch.
>
> that is actually a factor. But a proper micro-benchmark will not measure
> warmup times, right?
>
> > ------------------------------------------------------------------------
> >
> >
> > *2. Optimization Strategies for a Next Groovy Version*
> >
> >
> > *A. Improve CallSite Caching*
> >
> > *
> >
> > Implement *multi-level caching* for polymorphic call sites.
> >
> > *
> >
> > Use *polymorphic inline caches (PICs)* like modern JavaScript
> > engines (V8) to avoid relinking.
>
> basically agree
>
> > *
> >
> > Avoid global call site invalidations when meta-classes are updated —
> > make call site invalidation more local.
>
> how?
>
> > *B. Reduce Relinking*
> >
> > *
> >
> > Track method signatures more strictly. Many |invokedynamic| relinks
> > happen because Groovy tries to handle any dynamic call, even when
> > the call target is stable.
>
> True. If you call foo(Object) with a String and then with an Integer it
> will cause relinking, even though it is not required
>
> > *
> >
> > Consider *specialized bootstrap methods* for common call patterns:
> >
> > o
> >
> > e.g., frequent calls to |String| methods, |List|/|Map|
> operations.
>
> on what are we actually saving here?
>
> > *C. Optimize Primitive Handling*
> >
> > *
> >
> > Introduce *primitive specialization* for arithmetic and collection
> > operations.
> >
> > *
> >
> > Reduce boxing/unboxing by generating specialized call site versions
> > for primitives (like what Kotlin/JVM or Scala do with inline
> functions).
>
> we have
>
> > *D. Optional Static Call Optimization*
> >
> > *
> >
> > Provide *hybrid static/dynamic dispatch*:
> >
> > o
> >
> > Use static compilation (|@CompileStatic|) when possible.
>
> on the compiler side directly? Well... when is it possible?
>
> > o
> >
> > Use a *profiling-guided JIT* to replace call sites with direct
> > method handles if a single target dominates.
>
> well.. and how does the invalidation work if the target is suddenly
> incorrect?
>
> > *E. Bytecode Generation Improvements*
> >
> > *
> >
> > Investigate how Groovy 5 generates |invokedynamic| bytecode:
> >
> > o
> >
> > Avoid unnecessary |Object| casts.
> >
> > o
> >
> > Combine multiple small dynamic calls into a single bootstrap
> > call to reduce overhead.
>
> Have that afaik
>
> > *
> >
> > Possibly generate *direct method handles* for commonly called Groovy
> > methods (|size()|, |get()|, etc.).
>
> see above
>
> > *F. JIT-Friendly Bootstrap*
> >
> > *
> >
> > Groovy could provide *simpler bootstrap methods* to allow JVM JIT
> > inlining:
> >
> > o
> >
> > Reduce bootstrap method complexity to help HotSpot optimize the
> > call site faster.
>
> not bootstrap complexity, but the resulting handle should have as little
> complexity in there as possible
>
> > ------------------------------------------------------------------------
> >
> >
> > *3. Micro-Optimizations for Library Authors*
> >
> > If you are writing a library or code in Groovy that must be fast:
> >
> > 1.
> >
> > *Prefer static types* whenever possible — even without
> > |@CompileStatic|, type hints help.
>
> which leads to more casting... so no.
>
> > 2.
> >
> > *Use |@CompileStatic|* selectively for hot loops.
>
> possibly
>
> > 3.
> >
> > *Avoid meta-class changes* at runtime in performance-critical code.
>
> well, they is currently no way to protect an area against meta class
> changes
>
> > 4.
> >
> > *Cache dynamic lookups manually* for very hot methods.
>
> not understood
>
> > 5.
> >
> > *Use primitive arrays* instead of boxed lists when dealing with
> numbers.
>
> possibly.
>
> > 6.
> >
> > *Minimize polymorphism at call sites* — repeated calls to the same
> > method is much faster than alternating between multiple methods.
>
> correct
>
> > ------------------------------------------------------------------------
> >
> >
> > *4. Experimental Ideas for Groovy 6+*
> >
> > *
> >
> > *CallSite specialization per type* (like Truffle/Graal dynamic
> > languages).
>
> I would like to make a MethodHandle part of the MetaMethod. I guess that
> goes in that direction.
>
> > *
> >
> > *Inline small closures automatically* at compile-time.
>
> It is not only the code of the Closure, it is also the code of the
> Closure handling method. in i=0; n.times{i++} it does not actually help
> to "inline" i++, because we would have to inline it to the times method.
> Instead we have to inline times as well of course with the danger, that
> if we change times, it will not be reflected without recompilation
>
> > *
> >
> > *Profile-guided call site replacement*: replace |invokedynamic|
> > calls with direct |MethodHandle| or static calls at runtime if
> > profiling shows a stable target.
>
> I cannot replace it with static calls
>
> > *
> >
> > *Better JIT feedback*: provide hints to HotSpot that certain call
> > sites are monomorphic or polymorphic.
>
> How?
>
> bye Jochen
>
>
Re: Groovy > 3 performance
Posted by Jochen Theodorou <bl...@gmx.org>.
On 14.01.26 15:32, Gianluca Sartori wrote:
[...]
> *
>
> *Groovy 5* moved most dynamic calls to |CallSite| caching through
> |indy|.
correct, though it was before in the equivalent code for the bytecode
generation
> *
>
> *Groovy 3* often inlined certain calls more aggressively, sometimes
> relying on slower reflection but faster in microbenchmarks due to
> simpler call chains.
which type of calls though? It was faster in some micro-benchmarks
because we had a full primitive path parallel to the non-primitive path
maybe. This worked only with unchanged meta classes, so not sure Grails
had much of it. Which also shows a big difference, a micro-benchmark is
not an app and you have to know what you actually test
> *
>
> *Result:* |invokedynamic| can introduce overhead for short-lived
> calls or highly polymorphic code because of frequent call site
> relinking.
that would have impacted performance on non-indy as well
> Key reasons for slowness:
>
> 1.
>
> *Polymorphic call sites:* If your code calls many different methods
> dynamically at the same call site, the |invokedynamic| bootstrap has
> to relink repeatedly.
>
> 2.
>
> *CallSite cache invalidation:* Changes to the meta-class or dynamic
> method addition can invalidate call sites.
as I said, that counts for both variants.
> 3.
>
> *Boxing/unboxing overhead:* Primitive-heavy code may suffer due to
> dynamic dispatch.
well, that is not supposed to be an issue in indy. It was in the old
callsite code in Groovy 3
> 4.
>
> *JIT warmup issues:* The JVM may take longer to optimize
> |indy|-based dispatch.
that is actually a factor. But a proper micro-benchmark will not measure
warmup times, right?
> ------------------------------------------------------------------------
>
>
> *2. Optimization Strategies for a Next Groovy Version*
>
>
> *A. Improve CallSite Caching*
>
> *
>
> Implement *multi-level caching* for polymorphic call sites.
>
> *
>
> Use *polymorphic inline caches (PICs)* like modern JavaScript
> engines (V8) to avoid relinking.
basically agree
> *
>
> Avoid global call site invalidations when meta-classes are updated —
> make call site invalidation more local.
how?
> *B. Reduce Relinking*
>
> *
>
> Track method signatures more strictly. Many |invokedynamic| relinks
> happen because Groovy tries to handle any dynamic call, even when
> the call target is stable.
True. If you call foo(Object) with a String and then with an Integer it
will cause relinking, even though it is not required
> *
>
> Consider *specialized bootstrap methods* for common call patterns:
>
> o
>
> e.g., frequent calls to |String| methods, |List|/|Map| operations.
on what are we actually saving here?
> *C. Optimize Primitive Handling*
>
> *
>
> Introduce *primitive specialization* for arithmetic and collection
> operations.
>
> *
>
> Reduce boxing/unboxing by generating specialized call site versions
> for primitives (like what Kotlin/JVM or Scala do with inline functions).
we have
> *D. Optional Static Call Optimization*
>
> *
>
> Provide *hybrid static/dynamic dispatch*:
>
> o
>
> Use static compilation (|@CompileStatic|) when possible.
on the compiler side directly? Well... when is it possible?
> o
>
> Use a *profiling-guided JIT* to replace call sites with direct
> method handles if a single target dominates.
well.. and how does the invalidation work if the target is suddenly
incorrect?
> *E. Bytecode Generation Improvements*
>
> *
>
> Investigate how Groovy 5 generates |invokedynamic| bytecode:
>
> o
>
> Avoid unnecessary |Object| casts.
>
> o
>
> Combine multiple small dynamic calls into a single bootstrap
> call to reduce overhead.
Have that afaik
> *
>
> Possibly generate *direct method handles* for commonly called Groovy
> methods (|size()|, |get()|, etc.).
see above
> *F. JIT-Friendly Bootstrap*
>
> *
>
> Groovy could provide *simpler bootstrap methods* to allow JVM JIT
> inlining:
>
> o
>
> Reduce bootstrap method complexity to help HotSpot optimize the
> call site faster.
not bootstrap complexity, but the resulting handle should have as little
complexity in there as possible
> ------------------------------------------------------------------------
>
>
> *3. Micro-Optimizations for Library Authors*
>
> If you are writing a library or code in Groovy that must be fast:
>
> 1.
>
> *Prefer static types* whenever possible — even without
> |@CompileStatic|, type hints help.
which leads to more casting... so no.
> 2.
>
> *Use |@CompileStatic|* selectively for hot loops.
possibly
> 3.
>
> *Avoid meta-class changes* at runtime in performance-critical code.
well, they is currently no way to protect an area against meta class changes
> 4.
>
> *Cache dynamic lookups manually* for very hot methods.
not understood
> 5.
>
> *Use primitive arrays* instead of boxed lists when dealing with numbers.
possibly.
> 6.
>
> *Minimize polymorphism at call sites* — repeated calls to the same
> method is much faster than alternating between multiple methods.
correct
> ------------------------------------------------------------------------
>
>
> *4. Experimental Ideas for Groovy 6+*
>
> *
>
> *CallSite specialization per type* (like Truffle/Graal dynamic
> languages).
I would like to make a MethodHandle part of the MetaMethod. I guess that
goes in that direction.
> *
>
> *Inline small closures automatically* at compile-time.
It is not only the code of the Closure, it is also the code of the
Closure handling method. in i=0; n.times{i++} it does not actually help
to "inline" i++, because we would have to inline it to the times method.
Instead we have to inline times as well of course with the danger, that
if we change times, it will not be reflected without recompilation
> *
>
> *Profile-guided call site replacement*: replace |invokedynamic|
> calls with direct |MethodHandle| or static calls at runtime if
> profiling shows a stable target.
I cannot replace it with static calls
> *
>
> *Better JIT feedback*: provide hints to HotSpot that certain call
> sites are monomorphic or polymorphic.
How?
bye Jochen
Re: Groovy > 3 performance
Posted by Gianluca Sartori <g....@gmail.com>.
Some more s**t from the probability text generator.
*Q:* Groovy 5 is slower than Groovy 3. I would like to know how to optimize
performance of invokedynamic in a next version of Groovy
*A:*
Ah, yes — the invokedynamic slowdown in Groovy 5 has been a common concern.
Groovy historically relies on invokedynamic to implement dynamic method
calls efficiently, but changes in Groovy 4/5 — especially the full embrace
of indy for method dispatch — can actually make some code slower compared
to Groovy 3. Let’s break this down carefully and see how you could optimize
performance in a future Groovy version.
------------------------------
*1. Understanding the Problem*
Groovy uses invokedynamic for dynamic dispatch. In theory, invokedynamic
lets the JVM optimize repeated method calls at runtime, but in practice:
-
*Groovy 5* moved most dynamic calls to CallSite caching through indy.
-
*Groovy 3* often inlined certain calls more aggressively, sometimes
relying on slower reflection but faster in microbenchmarks due to simpler
call chains.
-
*Result:* invokedynamic can introduce overhead for short-lived calls or
highly polymorphic code because of frequent call site relinking.
Key reasons for slowness:
1.
*Polymorphic call sites:* If your code calls many different methods
dynamically at the same call site, the invokedynamic bootstrap has to
relink repeatedly.
2.
*CallSite cache invalidation:* Changes to the meta-class or dynamic
method addition can invalidate call sites.
3.
*Boxing/unboxing overhead:* Primitive-heavy code may suffer due to
dynamic dispatch.
4.
*JIT warmup issues:* The JVM may take longer to optimize indy-based
dispatch.
------------------------------
*2. Optimization Strategies for a Next Groovy Version**A. Improve CallSite
Caching*
-
Implement *multi-level caching* for polymorphic call sites.
-
Use *polymorphic inline caches (PICs)* like modern JavaScript engines
(V8) to avoid relinking.
-
Avoid global call site invalidations when meta-classes are updated —
make call site invalidation more local.
*B. Reduce Relinking*
-
Track method signatures more strictly. Many invokedynamic relinks happen
because Groovy tries to handle any dynamic call, even when the call target
is stable.
-
Consider *specialized bootstrap methods* for common call patterns:
-
e.g., frequent calls to String methods, List/Map operations.
*C. Optimize Primitive Handling*
-
Introduce *primitive specialization* for arithmetic and collection
operations.
-
Reduce boxing/unboxing by generating specialized call site versions for
primitives (like what Kotlin/JVM or Scala do with inline functions).
*D. Optional Static Call Optimization*
-
Provide *hybrid static/dynamic dispatch*:
-
Use static compilation (@CompileStatic) when possible.
-
Use a *profiling-guided JIT* to replace call sites with direct method
handles if a single target dominates.
*E. Bytecode Generation Improvements*
-
Investigate how Groovy 5 generates invokedynamic bytecode:
-
Avoid unnecessary Object casts.
-
Combine multiple small dynamic calls into a single bootstrap call to
reduce overhead.
-
Possibly generate *direct method handles* for commonly called Groovy
methods (size(), get(), etc.).
*F. JIT-Friendly Bootstrap*
-
Groovy could provide *simpler bootstrap methods* to allow JVM JIT
inlining:
-
Reduce bootstrap method complexity to help HotSpot optimize the call
site faster.
------------------------------
*3. Micro-Optimizations for Library Authors*
If you are writing a library or code in Groovy that must be fast:
1.
*Prefer static types* whenever possible — even without @CompileStatic,
type hints help.
2.
*Use @CompileStatic* selectively for hot loops.
3.
*Avoid meta-class changes* at runtime in performance-critical code.
4.
*Cache dynamic lookups manually* for very hot methods.
5.
*Use primitive arrays* instead of boxed lists when dealing with numbers.
6.
*Minimize polymorphism at call sites* — repeated calls to the same
method is much faster than alternating between multiple methods.
------------------------------
*4. Experimental Ideas for Groovy 6+*
-
*CallSite specialization per type* (like Truffle/Graal dynamic
languages).
-
*Inline small closures automatically* at compile-time.
-
*Profile-guided call site replacement*: replace invokedynamic calls with
direct MethodHandle or static calls at runtime if profiling shows a
stable target.
-
*Better JIT feedback*: provide hints to HotSpot that certain call sites
are monomorphic or polymorphic.
------------------------------
💡 *TL;DR:*
invokedynamic in Groovy 5 is slower in polymorphic and highly dynamic
scenarios. Future optimizations should focus on *call site caching,
primitive specialization, reducing relinks, and hybrid static/dynamic
dispatch*, plus smarter bootstrap methods that are JIT-friendly.
Gianluca Sartori
--
https://dueuno.com
On Wed, 14 Jan 2026 at 15:19, Milles, Eric (TR Technology) via dev <
dev@groovy.apache.org> wrote:
> To get a sense for the impact of indy and only indy, you could perform the
> same sorts of tests using 2.4, 2.5 or 3.0 — all offer classic and
> invokedynamic variants. Thus you could compare 3.0 classic against 3.0
> invokedynamic.
>
> From there, you could compare 3.0 indy vs 4.0 vs 5.0 to see the impacts of
> non-indy changes. In general, the bytecode changes that were made were to
> improve performance. But there were some null-correctness changes required.
>
> Comparisons should factor out the compilation step. It may be the case
> that compilation is slower, but runtime performance is the main concern IMO.
>
>
> If it is found that the switch to indy is the main source of performance
> impact, then we would need to look very closely at the design and
> implementation of call site binding. The code is a bit complex. I have
> considered inlining all of the call site binding code under
> InyyInterface#fromCache to see if some of the call depth and handoffs can
> be reduced. This would just be an experiment to see if some specific part
> of bootstrapping shows as a hot sopt.
>
>
> ------------------------------
> *From:* Jochen Theodorou <bl...@gmx.org>
> *Sent:* Wednesday, January 14, 2026 6:28 AM
> *To:* dev@groovy.apache.org <de...@groovy.apache.org>
> *Subject:* Re: Groovy > 3 performance
>
> 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
>
> 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
>
>
>
Re: Groovy > 3 performance
Posted by "Milles, Eric (TR Technology) via dev" <de...@groovy.apache.org>.
To get a sense for the impact of indy and only indy, you could perform the same sorts of tests using 2.4, 2.5 or 3.0 — all offer classic and invokedynamic variants. Thus you could compare 3.0 classic against 3.0 invokedynamic.
From there, you could compare 3.0 indy vs 4.0 vs 5.0 to see the impacts of non-indy changes. In general, the bytecode changes that were made were to improve performance. But there were some null-correctness changes required.
Comparisons should factor out the compilation step. It may be the case that compilation is slower, but runtime performance is the main concern IMO.
If it is found that the switch to indy is the main source of performance impact, then we would need to look very closely at the design and implementation of call site binding. The code is a bit complex. I have considered inlining all of the call site binding code under InyyInterface#fromCache to see if some of the call depth and handoffs can be reduced. This would just be an experiment to see if some specific part of bootstrapping shows as a hot sopt.
________________________________
From: Jochen Theodorou <bl...@gmx.org>
Sent: Wednesday, January 14, 2026 6:28 AM
To: dev@groovy.apache.org <de...@groovy.apache.org>
Subject: Re: Groovy > 3 performance
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
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
Re: Groovy > 3 performance
Posted by Gianluca Sartori <g....@gmail.com>.
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 <bl...@gmx.org> 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
>
>
Re: Groovy > 3 performance
Posted by Jochen Theodorou <bl...@gmx.org>.
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