Skip to content
AgileLogics
6 min read

Performance

Prerequisites: Static compilation

Groovy performance is usually a non-issue for scripts, builds, and tests. When it does matter, the rules are simple: measure first, then apply static compilation and JVM-level fixes where the time actually goes.

Where time goes

  • Start-up and compilation — running a .groovy file compiles it first. For short scripts this dominates. Precompile with groovyc for scripts that run often.
  • Dynamic dispatch — each dynamic call goes through the MOP. Modern Groovy uses invokedynamic, which the JIT optimizes well, but hot loops still benefit from @CompileStatic.
  • Boxing and BigDecimal — def numbers are objects and decimal literals are BigDecimal. Exact, but slower than double.
  • Everything else — I/O, database queries, and algorithms, exactly as in Java.

BigDecimal vs. double

import groovy.transform.CompileStatic

@CompileStatic
double sumDoubles(int n) {
    double total = 0
    for (int i = 0; i < n; i++) total += 0.1d
    total
}

BigDecimal sumDecimals(int n) {
    BigDecimal total = 0
    for (int i = 0; i < n; i++) total += 0.1
    total
}

println sumDoubles(1_000_000)
println sumDecimals(1_000_000)
100000.00000133288
100000.0

The double version is much faster but accumulates rounding error; the BigDecimal version is exact. Use BigDecimal for money and double (with static compilation) for heavy numeric work.

Collection idioms

import groovy.transform.CompileStatic

@CompileStatic
int countMatches(List<Integer> xs) {
    int n = 0
    for (int x : xs) if (x % 3 == 0) n++
    n
}

def data = (1..100_000).toList()
println countMatches(data)
println data.count { it % 3 == 0 }

def names = (1..5).collect { "user$it" }
def sb = new StringBuilder()
names.each { sb << it << ',' }
println sb
33333
33333
user1,user2,user3,user4,user5,
  • Idiomatic count { } and a static loop give the same answer; profile before rewriting readable code.
  • Build large strings with a StringBuilder (<< appends) rather than repeated + or GString concatenation in a loop.
  • GStrings are evaluated lazily and converted each time they're used as a String — call toString() once when reusing one.

Measuring properly

  • Use a profiler (IntelliJ's, async-profiler, or Java Flight Recorder with -XX:StartFlightRecording) to find hot spots.
  • Benchmark with JMH (there's a Gradle plugin); naive timing loops are distorted by JIT warm-up.
  • Watch memory: closures and metaclass changes allocate; heavy metaclass churn used to deoptimize call sites globally (Groovy 6 scopes that invalidation to the changed class).

Checklist

  1. Measure — find the 5% of code that matters.
  2. Add @CompileStatic to hot classes and give them real types.
  3. Use primitives and double in numeric hot paths.
  4. Fix algorithms and I/O (batching, caching, fewer queries) — usually the biggest win.
  5. For start-up-sensitive tools, precompile or consider GraalVM native images (Groovy 6 improves support, including for dynamic code).