About Me

My photo
Author of Groovy modules: GBench, GProf, Co-author of a Java book パーフェクトJava, Game Developer at GREE

Sunday, May 22, 2011

Exploring Groovy 1.8 - Is the code really optimized?


Groovy's biggest issue is performance. In 1.8, optimizations for integer operations and direct method calls are done as the beginning of further performance improvements. Both of the optimizations has the same concept, "Try to compile as it is written". It does not mean the Groovy compiler had failed syntax analysis. Groovy source code are dramatically converted on compilation time to make possible flexible metaprogramming (I mentioned about it in my previous post, "Inside GDK"). The problem is there is no exception. I'll take a method to calculate a Fibonacci number as example:
----
int fib(int n) {
    if (n < 2) return n
    return fib(n - 1) + fib(n - 2)
}
----

The code is not needed to receive benefits of metaprogramming, but it is converted like the following code in 1.7 or the earlier versions(in the actual code,  Groovy APIs appear and it is little more complicated but I omit them because they are not important in this post). All of the operations/methods are replaced to reflections and boxing and unboxing are done to the integers repeatedly:
----
int fib(int n) {
    // Get the Method objects
    if (lessThanMethod.invoke(Integer.valueOf(n), Integer.valueOf(2))) 
        return Integer.valueOf(n).intValue();
    return 
        ((Integer) plusMethod.invoke(
            fibMethod.invoke(
                minusMethod.invoke(Integer.valueOf(n), Integer.valueOf(1)
            ),
            fibMethod.invoke(
                minusMethod.invoke(Integer.valueOf(n), Integer.valueOf(2)
            )
        )).intValue();
    );
----

It will be optimized as the following in 1.8. This is the meaning of "Try to compile as it is written" that I said in the beginning:
----
int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}
----

By the way, the story is not ended yet. Because, the requirements to receive benefits of the optimizations are much harder than we expect. Obeying the following warnings that are written in the release notes is not enough:
1. If you want the optimization of integer operations, must not mix int type and Integer type in a expression
2. If you want the optimization of direct method calls, the method must be done on "this" and all of the argument types must be primitive or final.

See the following table. This is what I compare the execution time between 1.8.0 and 1.7.10 with methods in different ways to calculate a Fibonacci number:
Execution time of Fibonacci(30) in nanoseconds
Rank
Algorithm / Style
v1.8
v1.7
Improvement
1
Iteration + Groovy-style for loop
49720
46276
-6.93%
Down :-(
2
Iteration + Java/C-style for loop
129360
77824
-39.84%
Down :-(
3
Recursion + if + return
45080820
548522821
+1116.75%
Up :-)
4
Recursion + ternary operator
120489259
538476119
+346.91%
Up :-)
5
Recursion + if-else
259945537
546381516
+110.19%
Up :-)

The result shows us three things:
1. The performances of iteration versions were slightly worse
2. The performances of recursion versions were highly improved
3. The performance gaps among the coding style got wider

Those reasons are simple. Most of the cases failed both or one of the optimizations. In the table, all of the cases except for recursion + if + return version failed. For example, recursion + if + else version is this:
----
int fib(int n) {
    if (n < 2) n
    else fib(n - 1) + fib(n - 2)
}
----

The code will be converted like the following (I'll repeat that it slightly differs from the actual code to be exact):
----
int fib(int n) {
    if (n < 2) return n;
    else return 
        ((Integer) plusMethod.invoke(
            fibMethod.invoke(Integer.valueOf(n - 1)),
            fibMethod.invoke(Integer.valueOf(n - 2))
        )).intValue();
    );
}
----

That small difference affects success of the optimizations and make the more than five times difference in performance. I guess this problem will be fixed in a future release, but at least, it is not time to rejoice yet.

By the way, is the code really optimized?


(The code list that I used for the calculation)

Iteration + Groovy-style for loop:
----
int fib(int n) {

    int a = 0
    int b = 1
    int c
    for (int i in 2..n) {
        c = a + b
        a = b
        b = c
    }
    b
}
----

Iteration + Java/C-style for loop:
----
int fib(int n) {
    int a = 0
    int b = 1
    int c
    for (int i = 2; i <= n; i++) {
        c = a + b
        a = b
        b = c        
    }
    b
}
----

Recursion + if + return:
----
int fib(int n) {
    if (n < 2) return n
    return fib(n - 1) + fib(n - 2)
}
----

Recursion + ternary operator:
----
int fib(int n) {
    n < 2 ? n : fib(n - 1) + fib(n - 2)  
}
----

Recursion + if-else:
----
int fib(int n) {
    if (n < 2) n
    else fib(n - 1) + fib(n - 2)
}
----

No comments:

Post a Comment