Skip to content
AgileLogics
6 min read

Static Compilation

Prerequisites: Optional typing and type checking

@CompileStatic goes one step further than @TypeChecked: besides checking types, it generates direct method calls like Java does, instead of dynamic dispatch through Groovy's runtime. The result is faster code with Java-like behavior.

import groovy.transform.CompileStatic

@CompileStatic
class Stats {
    static double mean(List<Integer> xs) {
        (double) xs.sum() / xs.size()
    }

    static long sumOfSquares(int n) {
        long total = 0
        for (int i = 1; i <= n; i++) total += (long) i * i
        total
    }
}

println Stats.mean([2, 4, 9])
println Stats.sumOfSquares(1_000_000)
5.0
333333833333500000

GDK methods (sum, collect, each) still work in static code.

Measuring the difference

import groovy.transform.CompileStatic

long dynamicLoop(int n) {
    long total = 0
    for (int i = 0; i < n; i++) total += i % 7
    total
}

@CompileStatic
long staticLoop(int n) {
    long total = 0
    for (int i = 0; i < n; i++) total += i % 7
    total
}

def time(String label, Closure work) {
    work()                                  // warm up the JIT
    def start = System.nanoTime()
    def result = work()
    def ms = (System.nanoTime() - start).intdiv(1_000_000)
    println "$label: result $result"
    ms
}

def d = time('dynamic') { dynamicLoop(20_000_000) }
def s = time('static') { staticLoop(20_000_000) }
// println "dynamic: $d ms, static: $s ms"   // uncomment to see your timings
dynamic: result 59999997
static: result 59999997

Uncomment the last line to see the timings on your machine — typically the static loop is several times faster. (Timings vary from run to run, so they're not printed here.) Modern Groovy's dynamic calls use invokedynamic and are much faster than they used to be, but for hot numeric loops static compilation still wins clearly.

What static compilation changes

  • Method calls are chosen at compile time from the declared types — like Java's overloading, not Groovy's runtime choice.
  • Runtime metaprogramming (adding methods to a metaclass, methodMissing) is not seen by static code.
  • Builders and DSLs based on delegate need @DelegatesTo hints (Level 3) or must stay dynamic.

Mixing dynamic and static

import groovy.transform.CompileStatic
import groovy.transform.CompileDynamic

@CompileStatic
class Report {
    String title = 'Q3'

    String render() { "Report ${title}: ${extra()}" }

    @CompileDynamic
    String extra() {
        def data = [a: 1]
        data.anything?.toString() ?: 'dynamic part'
    }
}

println new Report().render()
Report Q3: dynamic part

Put @CompileStatic on classes and opt specific methods out with @CompileDynamic. Many teams also enable static compilation for a whole project through a compiler configuration script, so every class is static by default.