Static code compilation in Groovy 2.0

by Mikhail Vorontsov

A tiny introduction to Groovy

Groovy is a JVM-based language useful for scripting and other non-CPU critical applications (like Grails web framework). It is a dynamically typed language allowing you to add methods and properties to existing objects at runtime.

The ability to add methods and properties at runtime is implemented via methodMissing and propertyMissing methods/handlers as well as a dynamic method registration. Such feature allows you to support your own DSLs via parsing not existing method names at runtime and registering the actual method bodies corresponding to such methods/properties. It allows you, for example, to generate database access method like List<Person> getPersonsByN( N n ) where N is any field defined in the Persons database table.

Such functionality made Groovy popular in the web development due to ability to generate repeating data access methods at runtime in the frameworks. Unfortunately (or luckily 🙂 ), Groovy method calls are using the dynamic dispatch model – Groovy runtime chooses the best matching method signature based on the runtime argument types instead of compile time argument types, like Java does. Dynamic dispatch requires each Groovy method call to use the Groovy runtime method lookup code based on the reflection. So, are method calls in Groovy extremely slow? The answer is no – Groovy does a very good job of caching call sites, not making another reflection lookup if possible.

Groovy static compilation

One of the main features of Groovy 2.0 was the static compilation mode. It is turned on by annotating methods or the whole class with the @CompileStatic annotation. This annotation actually turns on 2 features:

  1. Static type checking
  2. Static compilation

Both these features are connected, because you need a type inference in order to statically choose the best matching method at compile time. Static type checking allows you to check your Groovy code for type safety as well as for absence of typos in the method/property names.

Static compilation allows Groovy 2.0 to generate direct method calls, which are no longer using Groovy runtime as an intermediary. This feature actually allows you to avoid going into the Groovy runtime for the huge share of methods – think how many overloaded methods do you usually have in your code – non-overloaded methods could always be safely converted into the normal method calls.

This feature could make Groovy useful for adding scripting support in your application. Suppose you have an API available for your scripting engine. Previously any CPU-bound Groovy script was limited by Groovy code execution performance. Now you can instrument your script with @CompileStatic annotations under the hood and call GroovyClassLoader in order to compile your script into a JVM class, which is able to run at a speed close to a Java class.

Statically compiled code performance in Groovy

Let’s implement a simple calculator in Java, ordinary Groovy and statically compiled Groovy. Here are 3 implementations (see each one Javadoc). Each one implements process method, which executes the same sequence of operations (this is done in order to call Groovy arithmetical methods from Groovy instead of Java).

/**
 * Simple math methods - no static compilation
 */
class GroovyCalculator {
    static double add(double a, double b) {
        a+b
    }

    static double mult(double a, double b){
        a*b
    }

    static double div(double a, double b) {
        a/b
    }

    static double process(double value)
    {
        double res1 = add( value, 1d )
        double res2 = mult( res1, 2d )
        double res3 = div( res2, 2d )
        add( res3, -1d )
    }
}
import groovy.transform.CompileStatic

/**
 * Simple math methods - static compilation
 */
class GroovyStaticCalculator {
    @CompileStatic
    static double add(double a, double b) {
        a+b
    }

    @CompileStatic
    static double mult(double a, double b){
        a*b
    }

    @CompileStatic
    static double div(double a, double b) {
        a/b
    }

    @CompileStatic
    static double process(double value)
    {
        double res1 = add( value, 1d )
        double res2 = mult( res1, 2d )
        double res3 = div( res2, 2d )
        add( res3, -1d )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * Java implementation of a calculator
 */
public class JavaCalculator {
    public static double add(double a, double b)
    {
        return a+b;
    }
 
    public static double mult(double a, double b)
    {
        return a*b;
    }
 
    public static double div(double a, double b)
    {
        return a/b;
    }
 
    public static double process(double value)
    {
        double res1 = add( value, 1d );
        double res2 = mult( res1, 2d );
        double res3 = div( res2, 2d );
        return add( res3, -1d );
    }
}
/**
 * Java implementation of a calculator
 */
public class JavaCalculator {
    public static double add(double a, double b)
    {
        return a+b;
    }

    public static double mult(double a, double b)
    {
        return a*b;
    }

    public static double div(double a, double b)
    {
        return a/b;
    }

    public static double process(double value)
    {
        double res1 = add( value, 1d );
        double res2 = mult( res1, 2d );
        double res3 = div( res2, 2d );
        return add( res3, -1d );
    }
}

Test code calls process method given number of times:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public static void main( String[] args ) {
    final long PREHEAT = 100 * 1000;
    performanceTestJava( PREHEAT );
    performanceTestGroovyNoCompile( PREHEAT );
    performanceTestGroovyCompile( PREHEAT );
 
    final long CNT = 1000 * 1000 * 1000;
    performanceTestJava( CNT );
    performanceTestGroovyNoCompile( CNT );
    performanceTestGroovyCompile( CNT );
}
 
private static void performanceTestJava( final long cnt )
{
    final long start = System.currentTimeMillis();
    double value = 0;
    for ( long i = 0; i < cnt; ++i )
        value = JavaCalculator.process( value );
    final long time = System.currentTimeMillis() - start;
    System.out.println( "Java time = " + (time/1000.0) + " sec" );
}
 
private static void performanceTestGroovyNoCompile( final long cnt )
{
    final long start = System.currentTimeMillis();
    double value = 0;
    for ( long i = 0; i < cnt; ++i )
        value = GroovyCalculator.process( value );
    final long time = System.currentTimeMillis() - start;
    System.out.println( "Groovy no precompile time = " + (time/1000.0) + " sec" );
}
 
private static void performanceTestGroovyCompile( final long cnt )
{
    final long start = System.currentTimeMillis();
    double value = 0;
    for ( long i = 0; i < cnt; ++i )
        value = GroovyStaticCalculator.process( value );
    final long time = System.currentTimeMillis() - start;
    System.out.println( "Groovy with precompile time = " + (time/1000.0) + " sec" );
}
public static void main( String[] args ) {
    final long PREHEAT = 100 * 1000;
    performanceTestJava( PREHEAT );
    performanceTestGroovyNoCompile( PREHEAT );
    performanceTestGroovyCompile( PREHEAT );

    final long CNT = 1000 * 1000 * 1000;
    performanceTestJava( CNT );
    performanceTestGroovyNoCompile( CNT );
    performanceTestGroovyCompile( CNT );
}

private static void performanceTestJava( final long cnt )
{
    final long start = System.currentTimeMillis();
    double value = 0;
    for ( long i = 0; i < cnt; ++i )
        value = JavaCalculator.process( value );
    final long time = System.currentTimeMillis() - start;
    System.out.println( "Java time = " + (time/1000.0) + " sec" );
}

private static void performanceTestGroovyNoCompile( final long cnt )
{
    final long start = System.currentTimeMillis();
    double value = 0;
    for ( long i = 0; i < cnt; ++i )
        value = GroovyCalculator.process( value );
    final long time = System.currentTimeMillis() - start;
    System.out.println( "Groovy no precompile time = " + (time/1000.0) + " sec" );
}

private static void performanceTestGroovyCompile( final long cnt )
{
    final long start = System.currentTimeMillis();
    double value = 0;
    for ( long i = 0; i < cnt; ++i )
        value = GroovyStaticCalculator.process( value );
    final long time = System.currentTimeMillis() - start;
    System.out.println( "Groovy with precompile time = " + (time/1000.0) + " sec" );
}

Static methods are called slightly faster than instance methods in JVM, so I decided to keep test code a bit ugly and copy-pasted :), but make it slightly faster.

I have tested Java 7u25 and Java 8 b102 beta with Groovy 2.0.0 and 2.1.9 on my Core i5-3317 (@1.7Ghz) laptop. Results were similar in all four environments, so I'll quote only Java 8 + Groovy 2.1.9 results here (for 1 billion process calls):

Java time = 9.204 sec
Groovy no precompile time = 28.948 sec
Groovy with precompile time = 9.203 sec
    

As you can see it takes approximately 3 times longer to call not precompiled Groovy method compared to a Java method call. On the other hand, statically compiled Groovy method calls provided the same performance as Java methods calls (which shall not be surprising, provided that you will end up with the same bytecode). This performance difference more or less matches to the overhead of calling a method via a saved java.lang.reflect.Method compared to the normal method call:

1
2
3
4
5
6
7
8
9
private static void testReflectionCall( final long cnt ) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    final Method method = JavaCalculator.class.getDeclaredMethod( "process", double.class );
    final long start = System.currentTimeMillis();
    double value = 0;
    for ( long i = 0; i < cnt; ++i )
        value = (Double) method.invoke( null, value );
    final long time = System.currentTimeMillis() - start;
    System.out.println( "Java reflection time = " + (time/1000.0) + " sec" );
}
private static void testReflectionCall( final long cnt ) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    final Method method = JavaCalculator.class.getDeclaredMethod( "process", double.class );
    final long start = System.currentTimeMillis();
    double value = 0;
    for ( long i = 0; i < cnt; ++i )
        value = (Double) method.invoke( null, value );
    final long time = System.currentTimeMillis() - start;
    System.out.println( "Java reflection time = " + (time/1000.0) + " sec" );
}

It took 23.1 sec to execute this test the same billion times. Faster than Groovy, but Groovy does a bit more - it has to find a cached method in its internal map.

Java 7u40 - regression in dynamic code execution?

Initial tests for this article were made using Java 7u25. I have recently started using Java 7u45 and noticed that Groovy code started to run slower than before - "Groovy no precompile" test started to take 2 times longer than before. There is an update in Java 7u40 which could affect this performance regression: java.lang.invoke.LambdaForm class was added and a lot of other classes were changed in order to use it. This class is used in lambda support in Java 8, so I was surprised to see it in a Java 7 update. I have tried to force javac from JDK 7u45 to compile lambdas, but there seems to be no such option 🙁

P.S. I plan to write a follow-up article giving more details on a few Groovy features mentioned here: dynamic dispatch, methodMissing/propertyMissing and code compilation via GroovyClassLoader. Despite being not exactly performance related, these features should be interesting for the non-Groovy developers.

Summary

  • Groovy is a dynamic JVM language using dynamic dispatch for its method calls. Dynamic dispatch in Groovy 2.1.9 is approximately 3 times slower compared to a normal Java method call due to the need to obtain a method name and argument types (method signature) and match it to the cached java.lang.reflect.Method.
  • Groovy 2.0 has added the static compilation feature via @CompileStatic annotation, which allows to compile most of Groovy method calls into direct JVM bytecode method calls, thus avoiding all the dynamic dispatch overhead. Besides performance improvements, static compilation is also responsible for type checking of your Groovy code, letting you to discover a lot of typos/mistakes at compile time, thus reducing the need for extensive coverage unit tests.

One thought on “Static code compilation in Groovy 2.0

  1. Pingback: Core Java 7 Change Log  - Java Performance Tuning Guide

Comments are closed.