Groovy 5 - Performance / @CompileStatic

MG <mgbiz-yvYIh6MZAuFWk0Htik3J/[email protected]> Thu, 7 Aug 2025 21:24:29 +0200
Newsgroups gmane.comp.lang.groovy.user
Message-ID <[email protected]>
This is a multi-part message in MIME format.
--------------V5OKCk0200qfNILi80s09fNH
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 8bit

Hi .+,

since Groovy 5 to my knowledge is the first Groovy which does away with 
the old, faster, non-invoke-dynamic call site resolution support, I 
wanted to do a quick post on that topic that might be of interest to 
larger Groovy projects like ours:

 1. After the initial performance problems in Groovy 3/4 caused
    by invoke-dynamic, for which we were, with some effort, in the end
    able to pinpoint the culprit in our code and implement a workaround,
    we unfortunately discovered another performance drop in a later
    Groovy version.
     1. The drop was less severe than in the previous case, but was
        around a factor of 2 when it appeared.
 2. In this case we were alas not able to find a specific code part that
    caused the drop in performance, but it seemed like Groovy was
    leaking a bit of performance on every level, which in the end added
    up to our main web application running with half speed in certain
    crucial parts, as well as our test suite taking far longer to
    execute fully.
 3. After posting about this on the mailing list we initially waited
    whether Groovy invoke-dynamic performance would improve, but over
    time it became clear that that would most likely not be the case.
 4. Faced with this, we had several options:
     1. Stay on Groovy 4 with non-invoke-dynamic enabled for the
        foreseeable future.
     2. Try if switching to statically compiled Groovy (@CompileStatic)
        would solve our performance woes.
     3. Switch (at least partially) to a different JVM language.
 5. Since we neither wanted to be locked into Groovy 4 forever (for
    obvious reasons), and Groovy is our language of choice (for obvious
    reasons ;-) ), the only real option was trying the @CompileStatic route.
 6. Unfortunately we:
     1. Knew from prior experiences that one cannot just
        add @CompileStatic to every class in a project and expect the
        code to compile / run as expected.
     2. Had decided to switch from @CompileStatic to @TypeChecked a
        while ago, to get the benefits of dynamic calls site resolution
        (while still having compile time checking).
 7. So, since just auto-applying @CompileStatic to our whole project was
    not feasible, we used  a more selective approach, where we only
    auto-apply @CompileStatic to a single one of our modules at a time.
     1. Simple groovyConfig source to achieve that below (feel free to
        include verbatim in your project).
         1. Note: If there is already a
            CompileStatic/CompileDynamic/TypeChecked annotation on the
            class it will be skipped.
     2. If you want to cook your own and you use your own Groovy macros,
        be aware that your Groovy macro module must be excluded in any
        case, or you will get some weird, hard to pinpoint build errors!
 8. We started with the lowest of our library modules, working our way
    up in the dependency chain.
     1. In our case that was groovyutil (basic shared functionality),
        and then groovysql (SQL objects, query building & schema
        evolution functionality).
     2. Both modules took a few days each to compile
        under @CompileStatic & all tests to be green.
 9. Given the speed at which we progressed, turning our whole
    project @CompileStatic would have probably taken a month or so.
10. Luckily our bottom up approach paid off, and once the groovysql
    module was done, performance was indistinguishable
    between invoke-dynamic and non-invoke-dynamic builds! :-)
     1. I might do a follow up "tips / lessons learned" post, as time
        allows...
11. Thanks go out to the Groovy dev(s) who are constantly working to
    improve @CompileStatic support: Partially switching
    to @CompileStatic would definitely have been more hassle a few years
    back! G-)

Cheers,
mg


import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.transform.TypeChecked
import org.codehaus.groovy.ast.AnnotationNode
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import org.codehaus.groovy.control.customizers.SourceAwareCustomizer
import org.codehaus.groovy.ast.ClassNode
import java.lang.annotation.Annotation
/
// Enable invoke-dynamic (indy) usage/
configuration.optimizationOptions.indy = true
/
// Auto @CompileStatic/
final hasModuleCls = *{ *ClassNode n, List<String> moduleNames *->
*if(n.packageName === null) { return false }
final packagePathElements = n.packageName.split(/\./)
moduleNames.any *{ *String moduleName *-> 
*packagePathElements.contains(moduleName) *}
}
*
final hasAnyAnnotationCls = *{ *ClassNode n, List<Class<Annotation>> 
annotations *-> *n.annotations.any *{ *AnnotationNode an *-> 
*an.classNode.class in annotations *} }
*
final compileStaticModules = ['groovyutil', 'groovysql']
final skipModules = ['groovymacro']

final autoCompileStatic = new SourceAwareCustomizer(new 
ASTTransformationCustomizer(CompileStatic))
autoCompileStatic.setClassValidator *{ *ClassNode n *-> 
*!hasModuleCls(n, skipModules) && hasModuleCls(n, compileStaticModules) 
&& !hasAnyAnnotationCls(n, [CompileStatic, CompileDynamic, TypeChecked]) *}*
configuration.addCompilationCustomizers(autoCompileStatic)




Am 06.08.2025 um 12:54 schrieb Paul King:
> Dear community,
>
> The Apache Groovy team is pleased to announce version 5.0.0-rc-1 of
> Apache Groovy.
> Apache Groovy is a multi-faceted programming language for the JVM.
> Further details can be found at thehttps://groovy.apache.org website and in the
> Groovy 5 release notes:https://groovy-lang.org/releasenotes/groovy-5.0.html
>
> This is a pre-release of a new version of Groovy.
> We greatly appreciate any feedback you can give us when using this version.
>
> This release includes 9 bug fixes/improvements as outlined in the changelog:
> https://issues.apache.org/jira/secure/ReleaseNote.jspa?projectId=12318123&version=12356146
>
> Sources, convenience binaries, downloadable documentation and an SDK
> bundle can be found at:https://groovy.apache.org/download.html
> We recommend you verify your installation using the information on that page.
>
> Jars are also available within the major binary repositories.
>
> We welcome your help and feedback and in particular want
> to thank everyone who contributed to this release.
>
> For more information on how to report problems, and to get involved,
> visit the project website athttps://groovy.apache.org/
>
> Best regards,
>
> The Apache Groovy team.

--------------V5OKCk0200qfNILi80s09fNH
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: 8bit

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  </head>
  <body>
    Hi .+,<br>
    <br>
    since Groovy 5 to my knowledge is the first Groovy which does away
    with the old, faster, non-invoke-dynamic call site resolution
    support, I wanted to do a quick post on that topic that might be of
    interest to larger Groovy projects like ours:<br>
    <ol>
      <li>After the initial performance problems in Groovy 3/4 caused
        by invoke-dynamic, for which we were, with some effort, in the
        end able to pinpoint the culprit in our code and implement a
        workaround, we unfortunately discovered another performance drop
        in a later Groovy version.</li>
      <ol>
        <li>The drop was less severe than in the previous case, but was
          around a factor of 2 when it appeared.</li>
      </ol>
      <li>In this case we were alas not able to find a specific code
        part that caused the drop in performance, but it seemed like
        Groovy was leaking a bit of performance on every level, which in
        the end added up to our main web application running with half
        speed in certain crucial parts, as well as our test suite taking
        far longer to execute fully.</li>
      <li>After posting about this on the mailing list we initially
        waited whether Groovy invoke-dynamic performance would improve,
        but over time it became clear that that would most likely not be
        the case.</li>
      <li>Faced with this, we had several options:</li>
      <ol>
        <li>Stay on Groovy 4 with non-invoke-dynamic enabled for the
          foreseeable future.</li>
        <li>Try if switching to statically compiled Groovy
          (@CompileStatic) would solve our performance woes.</li>
        <li>Switch (at least partially) to a different JVM language.</li>
      </ol>
      <li>Since we neither wanted to be locked into Groovy 4 forever
        (for obvious reasons), and Groovy is our language of choice (for
        obvious reasons ;-) ), the only real option was trying the @CompileStatic
        route.</li>
      <li>Unfortunately we:</li>
      <ol>
        <li>Knew from prior experiences that one cannot just add @CompileStatic
          to every class in a project and expect the code to compile /
          run as expected.</li>
        <li>Had decided to switch from @CompileStatic to @TypeChecked a
          while ago, to get the benefits of dynamic calls site
          resolution (while still having compile time checking).</li>
      </ol>
      <li>So, since just auto-applying @CompileStatic to our whole
        project was not feasible, we used  a more selective approach,
        where we only auto-apply @CompileStatic to a single one of our
        modules at a time.</li>
      <ol>
        <li>Simple groovyConfig source to achieve that below (feel free
          to include verbatim in your project).</li>
        <ol>
          <li>Note: If there is already a
            CompileStatic/CompileDynamic/TypeChecked annotation on the
            class it will be skipped.</li>
        </ol>
        <li>If you want to cook your own and you use your own Groovy
          macros, be aware that your Groovy macro module must be
          excluded in any case, or you will get some weird, hard to
          pinpoint build errors!</li>
      </ol>
      <li>We started with the lowest of our library modules, working our
        way up in the dependency chain.</li>
      <ol>
        <li>In our case that was groovyutil (basic shared
          functionality), and then groovysql (SQL objects, query
          building &amp; schema evolution functionality).</li>
        <li>Both modules took a few days each to compile under @CompileStatic
          &amp; all tests to be green.</li>
      </ol>
      <li>Given the speed at which we progressed, turning our whole
        project @CompileStatic would have probably taken a month or so.</li>
      <li>Luckily our bottom up approach paid off, and once the
        groovysql module was done, performance was indistinguishable
        between invoke-dynamic and non-invoke-dynamic builds! :-)</li>
      <ol>
        <li>I might do a follow up "tips / lessons learned" post, as
          time allows...</li>
      </ol>
      <li>Thanks go out to the Groovy dev(s) who are constantly working
        to improve @CompileStatic support: Partially switching to @CompileStatic
        would definitely have been more hassle a few years back! G-)</li>
    </ol>
    Cheers,<br>
    mg<br>
    <br>
    <br>
    <tt><font size="3" color="#0033B3">import </font></tt><tt><font
        size="3" color="#080808">groovy.transform.</font></tt><tt><font
        size="3" color="#9E880D">CompileDynamic</font></tt><tt><font
        size="3" color="#0033B3"><br>
        import </font></tt><tt><font size="3" color="#080808">groovy.transform.</font></tt><tt><font
        size="3" color="#9E880D">CompileStatic</font></tt><tt><font
        size="3" color="#0033B3"><br>
        import </font></tt><tt><font size="3" color="#080808">groovy.transform.</font></tt><tt><font
        size="3" color="#9E880D">TypeChecked</font></tt><tt><font
        size="3" color="#0033B3"><br>
        import </font></tt><tt><font size="3" color="#080808">org.codehaus.groovy.ast.</font></tt><tt><font
        size="3">AnnotationNode</font></tt><tt><font size="3"
        color="#0033B3"><br>
        import </font></tt><tt><font size="3" color="#080808">org.codehaus.groovy.control.customizers.</font></tt><tt><font
        size="3">ASTTransformationCustomizer</font></tt><tt><font
        size="3" color="#0033B3"><br>
        import </font></tt><tt><font size="3" color="#080808">org.codehaus.groovy.control.customizers.</font></tt><tt><font
        size="3">SourceAwareCustomizer</font></tt><tt><font size="3"
        color="#0033B3"><br>
        import </font></tt><tt><font size="3" color="#080808">org.codehaus.groovy.ast.</font></tt><tt><font
        size="3">ClassNode</font></tt><tt><font size="3" color="#0033B3"><br>
        import </font></tt><tt><font size="3" color="#080808">java.lang.annotation.</font></tt><tt><font
        size="3">Annotation<br>
      </font></tt><tt><font size="3" color="#8C8C8C"><i><br>
          // Enable invoke-dynamic (indy) usage</i></font></tt><tt><font
        size="3"><br>
        configuration</font></tt><tt><font size="3" color="#080808">.optimizationOptions.indy
        = </font></tt><tt><font size="3" color="#0033B3">true<br>
      </font></tt><tt><font size="3" color="#8C8C8C"><i><br>
          // Auto @CompileStatic</i></font></tt><tt><font size="3"
        color="#0033B3"><br>
        final </font></tt><tt><font size="3">hasModuleCls </font></tt><tt><font
        size="3" color="#080808">= </font></tt><tt><font size="3"
        color="#080808"><b>{ </b></font></tt><tt><font size="3">ClassNode
        n</font></tt><tt><font size="3" color="#080808">, </font></tt><tt><font
        size="3">List</font></tt><tt><font size="3" color="#080808">&lt;</font></tt><tt><font
        size="3">String</font></tt><tt><font size="3" color="#080808">&gt;
      </font></tt><tt><font size="3">moduleNames </font></tt><tt><font
        size="3" color="#080808"><b>-&gt;<br>
           </b></font></tt><tt><font size="3" color="#0033B3">if</font></tt><tt><font
        size="3" color="#080808">(</font></tt><tt><font size="3">n</font></tt><tt><font
        size="3" color="#080808">.packageName === </font></tt><tt><font
        size="3" color="#0033B3">null</font></tt><tt><font size="3"
        color="#080808">) { </font></tt><tt><font size="3"
        color="#0033B3">return false </font></tt><tt><font size="3"
        color="#080808">}<br>
         </font></tt><tt><font size="3" color="#0033B3">final </font></tt><tt><font
        size="3">packagePathElements </font></tt><tt><font size="3"
        color="#080808">= </font></tt><tt><font size="3">n</font></tt><tt><font
        size="3" color="#080808">.packageName.split(</font></tt><tt><font
        size="3" color="#067D17">/\./</font></tt><tt><font size="3"
        color="#080808">)<br>
         </font></tt><tt><font size="3">moduleNames</font></tt><tt><font
        size="3" color="#080808">.any </font></tt><tt><font size="3"
        color="#080808"><b>{ </b></font></tt><tt><font size="3">String
        moduleName </font></tt><tt><font size="3" color="#080808"><b>-&gt;
        </b></font></tt><tt><font size="3">packagePathElements</font></tt><tt><font
        size="3" color="#080808">.contains(</font></tt><tt><font
        size="3">moduleName</font></tt><tt><font size="3"
        color="#080808">) </font></tt><tt><font size="3"
        color="#080808"><b>}<br>
          }<br>
        </b></font></tt><tt><font size="3" color="#0033B3"><br>
        final </font></tt><tt><font size="3">hasAnyAnnotationCls </font></tt><tt><font
        size="3" color="#080808">= </font></tt><tt><font size="3"
        color="#080808"><b>{ </b></font></tt><tt><font size="3">ClassNode
        n</font></tt><tt><font size="3" color="#080808">, </font></tt><tt><font
        size="3">List</font></tt><tt><font size="3" color="#080808">&lt;</font></tt><tt><font
        size="3">Class</font></tt><tt><font size="3" color="#080808">&lt;</font></tt><tt><font
        size="3">Annotation</font></tt><tt><font size="3"
        color="#080808">&gt;&gt; </font></tt><tt><font size="3">annotations
      </font></tt><tt><font size="3" color="#080808"><b>-&gt; </b></font></tt><tt><font
        size="3">n</font></tt><tt><font size="3" color="#080808">.annotations.any
      </font></tt><tt><font size="3" color="#080808"><b>{ </b></font></tt><tt><font
        size="3">AnnotationNode an </font></tt><tt><font size="3"
        color="#080808"><b>-&gt; </b></font></tt><tt><font size="3">an</font></tt><tt><font
        size="3" color="#080808">.classNode.</font></tt><tt><font
        size="3" color="#0033B3">class in </font></tt><tt><font
        size="3">annotations  </font></tt><tt><font size="3"
        color="#080808"><b>} }<br>
        </b></font></tt><tt><font size="3" color="#0033B3"><br>
        final </font></tt><tt><font size="3">compileStaticModules </font></tt><tt><font
        size="3" color="#080808">= [</font></tt><tt><font size="3"
        color="#067D17">'groovyutil'</font></tt><tt><font size="3"
        color="#080808">, </font></tt><tt><font size="3"
        color="#067D17">'groovysql'</font></tt><tt><font size="3"
        color="#080808">]</font></tt><tt><font size="3" color="#0033B3"><br>
        final </font></tt><tt><font size="3">skipModules </font></tt><tt><font
        size="3" color="#080808">= [</font></tt><tt><font size="3"
        color="#067D17">'groovymacro'</font></tt><tt><font size="3"
        color="#080808">]<br>
      </font></tt><tt><font size="3" color="#0033B3"><br>
        final </font></tt><tt><font size="3">autoCompileStatic </font></tt><tt><font
        size="3" color="#080808">= </font></tt><tt><font size="3"
        color="#0033B3">new </font></tt><tt><font size="3">SourceAwareCustomizer</font></tt><tt><font
        size="3" color="#080808">(</font></tt><tt><font size="3"
        color="#0033B3">new </font></tt><tt><font size="3">ASTTransformationCustomizer</font></tt><tt><font
        size="3" color="#080808">(</font></tt><tt><font size="3"
        color="#9E880D">CompileStatic</font></tt><tt><font size="3"
        color="#080808">))</font></tt><tt><font size="3"><br>
        autoCompileStatic</font></tt><tt><font size="3" color="#080808">.setClassValidator
      </font></tt><tt><font size="3" color="#080808"><b>{ </b></font></tt><tt><font
        size="3">ClassNode n </font></tt><tt><font size="3"
        color="#080808"><b>-&gt; </b></font></tt><tt><font size="3"
        color="#080808">!</font></tt><tt><font size="3">hasModuleCls</font></tt><tt><font
        size="3" color="#080808">(</font></tt><tt><font size="3">n</font></tt><tt><font
        size="3" color="#080808">, </font></tt><tt><font size="3">skipModules</font></tt><tt><font
        size="3" color="#080808">) &amp;&amp; </font></tt><tt><font
        size="3">hasModuleCls</font></tt><tt><font size="3"
        color="#080808">(</font></tt><tt><font size="3">n</font></tt><tt><font
        size="3" color="#080808">, </font></tt><tt><font size="3">compileStaticModules</font></tt><tt><font
        size="3" color="#080808">) &amp;&amp; !</font></tt><tt><font
        size="3">hasAnyAnnotationCls</font></tt><tt><font size="3"
        color="#080808">(</font></tt><tt><font size="3">n</font></tt><tt><font
        size="3" color="#080808">, [</font></tt><tt><font size="3"
        color="#9E880D">CompileStatic</font></tt><tt><font size="3"
        color="#080808">, </font></tt><tt><font size="3"
        color="#9E880D">CompileDynamic</font></tt><tt><font size="3"
        color="#080808">, </font></tt><tt><font size="3"
        color="#9E880D">TypeChecked</font></tt><tt><font size="3"
        color="#080808">]) </font></tt><tt><font size="3"
        color="#080808"><b>}</b></font></tt><tt><font size="3"><br>
        configuration</font></tt><tt><font size="3" color="#080808">.addCompilationCustomizers(</font></tt><tt><font
        size="3">autoCompileStatic</font></tt><tt><font size="3"
        color="#080808">)<br>
      </font></tt><br>
    <br>
    <br>
    <br>
    <div class="moz-cite-prefix">Am 06.08.2025 um 12:54 schrieb Paul
      King:<br>
    </div>
    <blockquote type="cite"
cite="mid:CADRx3POvyGD1HWjYCAhmY6yLScNiojOMtga7UkO2UmsYxQKLPQ-JsoAwUIsXosN+BqQ9rBEUg@public.gmane.org">
      <pre wrap="" class="moz-quote-pre">Dear community,

The Apache Groovy team is pleased to announce version 5.0.0-rc-1 of
Apache Groovy.
Apache Groovy is a multi-faceted programming language for the JVM.
Further details can be found at the <a class="moz-txt-link-freetext" href="https://groovy.apache.org">https://groovy.apache.org</a> website and in the
Groovy 5 release notes: <a class="moz-txt-link-freetext" href="https://groovy-lang.org/releasenotes/groovy-5.0.html">https://groovy-lang.org/releasenotes/groovy-5.0.html</a>

This is a pre-release of a new version of Groovy.
We greatly appreciate any feedback you can give us when using this version.

This release includes 9 bug fixes/improvements as outlined in the changelog:
<a class="moz-txt-link-freetext" href="https://issues.apache.org/jira/secure/ReleaseNote.jspa?projectId=12318123&amp;version=12356146">https://issues.apache.org/jira/secure/ReleaseNote.jspa?projectId=12318123&amp;version=12356146</a>

Sources, convenience binaries, downloadable documentation and an SDK
bundle can be found at: <a class="moz-txt-link-freetext" href="https://groovy.apache.org/download.html">https://groovy.apache.org/download.html</a>
We recommend you verify your installation using the information on that page.

Jars are also available within the major binary repositories.

We welcome your help and feedback and in particular want
to thank everyone who contributed to this release.

For more information on how to report problems, and to get involved,
visit the project website at <a class="moz-txt-link-freetext" href="https://groovy.apache.org/">https://groovy.apache.org/</a>

Best regards,

The Apache Groovy team.
</pre>
    </blockquote>
    <br>
  </body>
</html>

--------------V5OKCk0200qfNILi80s09fNH--