Groovy のパフォーマンス

Groovyのパフォーマンスを条件を変えて比較してみたのでメモしておきます。

ここでは次の7つの条件下での処理時間を比較してみます。

(1) Groovy スクリプト。なるべく Groovy らしい記述とする。
(2) (1)をコンパイルしたクラス
(3) (1)を静的コンパイルしたクラス
(4) (1)と同じ動作をする Java クラス
(5) (4)をGroovyスクリプトとした場合
(6) (5)をコンパイルしたクラス
(7) (5)を静的コンパイルしたクラス

評価には「竹内関数」*1を使ってみます。

使用した環境は以下の通りです。

・Groovy Version 2.4
JRE 8 Update 31
JDK 8 Update 31
Windows 7 Professional Service Pack 1 x64
・CPU Intel Core i5 (4core)
・RAM: 8GB
・HDD: 290GB

まずそれぞれの条件について簡単に紹介し、最後に処理時間のまとめを行います。

(1) Groovy スクリプトで竹内関数の実装

Gtarai.groovy:
----

def tarai(x,y,z) {

(x <= y)?y:tarai(tarai(x-1,y,z),tarai(y-1,z,x),tarai(z-1,x,y))
}
def sokutei(x,y,z) {
long start = System.currentTimeMillis()
def result=tarai(x,y,z)
long end = System.currentTimeMillis()
println "elapsed time [ms]: ${(end-start)}"
}
sokutei(
Integer.parseInt(args[0]),
Integer.parseInt(args[1]),
Integer.parseInt(args[2]))

----

 

 

処理時間は竹内関数の処理の前後の時間差で算出します。
実行は次のように行います。

----
% groovy Gtarai.groovy 13 7 0
elapsed time [ms]: 1918
----

(2) (1)を通常コンパイルしたクラス

groovyc で (1) をコンパイルします。

----
% groovyc -d classes Gtarai.groovy
----

実行は次のように行います。

----
% java -cp %GROOVY_HOME%/embeddable/groovy-all-2.4.1.jar;classes Gtarai 13 7 0
elapsed time [ms]: 1883
----

(3) (1)を静的コンパイルしたクラス

静的コンパイルを行うためにコードを一部修正します。
・竹内関数の引数と返り値の型を記述する。
・静的コンパイルする箇所にGroovyのアノテーションを追加する。

GtaraiSt.groovy
----
import groovy.transform.CompileStatic
@CompileStatic
int tarai(int x, int y, int z) {
(x <= y)?y:tarai(tarai(x-1,y,z),tarai(y-1,z,x),tarai(z-1,x,y))
}
def sokutei(x,y,z) {
long start = System.currentTimeMillis()
def result=tarai(x,y,z)
long end = System.currentTimeMillis()
println "elapsed time [ms]: ${(end-start)}"
}
sokutei(
Integer.parseInt(args[0]),
Integer.parseInt(args[1]),
Integer.parseInt(args[2]))
----

コンパイルして実行します。

----
% groovyc -d classes GtaraiSt.groovy
% java -cp %GROOVY_HOME%/embeddable/groovy-all-2.4.1.jar;classes GtaraiSt 13 7 0
elapsed time [ms]: 143
----

(4) (1)と同じ動作をする Java クラス

同等な内容の Java クラスを記述します。

Jtarai.java
----
public class Jtarai {
public static int tarai(int x, int y, int z) {
return (x <= y)?y:
tarai(tarai(x-1,y,z),tarai(y-1,z,x),tarai(z-1,x,y));
}

public static void sokutei (int x, int y, int z) {
long start = System.currentTimeMillis();
int result = tarai(x,y,z);
long end = System.currentTimeMillis();
System.out.println("elapsed time [ms]: " + (end-start));
}

public static void main (String args) {
sokutei(
Integer.parseInt(args[0]),
Integer.parseInt(args[1]),
Integer.parseInt(args[2]));
}
}
----

 コンパイルして実行します。

----
% javac -d classes Jtarai.java
% java -cp classes Jtarai 13 7 0
elapsed time [ms]: 135
----

(5) (4)をGroovyスクリプトとした場合

(4)のファイルの拡張子を .java から .groovy に変更し、クラス名は Gtarai2
とします。

Gtarai2.groovy
----
public class Gtarai2 {
public static int tarai(int x, int y, int z) {
return (x <= y)?y:
tarai(tarai(x-1,y,z),tarai(y-1,z,x),tarai(z-1,x,y));
}

public static void sokutei (int x, int y, int z) {
long start = System.currentTimeMillis();
int result = tarai(x,y,z);
long end = System.currentTimeMillis();
System.out.println("elapsed time [ms]: " + (end-start));
}

public static void main (String args) {
sokutei(
Integer.parseInt(args[0]),
Integer.parseInt(args[1]),
Integer.parseInt(args[2]));
}
}
----

 実行は以下の通り。

----
% groovy Gtarai2.groovy
elapsed time [ms]: 383
----

(6) (5)を通常コンパイルしたクラス

(5) をコンパイルして実行します。

----
% groovyc -d classes Gtarai2.groovy
% java -cp %GROOVY_HOME%/embeddable/groovy-all-2.4.1.jar;classes Gtarai2 13 7 0
elapsed time [ms]: 372
----

(7) (5)を静的コンパイルしたクラス

Gtarai2St.java
----
import groovy.transform.CompileStatic
@CompileStatic
public class Gtarai2St {
public static int tarai(int x, int y, int z) {
return (x <= y)?y:
tarai(tarai(x-1,y,z),tarai(y-1,z,x),tarai(z-1,x,y));
}

public static void sokutei (int x, int y, int z) {
long start = System.currentTimeMillis();
int result = tarai(x,y,z);
long end = System.currentTimeMillis();
System.out.println("elapsed time [ms]: " + (end-start));
}

public static void main (String [] args) {
sokutei(
Integer.parseInt(args[0]),
Integer.parseInt(args[1]),
Integer.parseInt(args[2]));
}
}
----

 コンパイルして実行します。

----
% groovyc -d classes Gtarai2St.groovy
% java -cp %GROOVY_HOME%/embeddable/groovy-all-2.4.1.jar;classes Gtarai2St 13 7 0
elapsed time [ms]: 136
----

まとめです。

竹内関数の引数に(x=13,y=7,z=0) を使いました。
この場合には関数呼び出しが 91924989 回行われます。
その処理時間[ms]は以下のようになりました。

(1) 1918
(2) 1883
(3) 143
(4) 135
(5) 383
(6) 372
(7) 136

この結果より、以下のことがわかります。

その1:Groovyの静的コンパイルは Java で実装した場合とほぼ同等の処理時間となる。
((3)(4)(7)の比較より)

その2:Groovyの通常のコンパイルはほとんど処理時間に影響しない。
((1)と(2)、(5)と(6)の比較より)

その3:Groovy はメソッドの引数や返り値の型を指定するコードの方が処理が速い。
((1)と(5)の比較より)

 静的コンパイルすれば、Groovy でも Java と処理時間的には同じパフォーマンスが得られることがわかりましたが、この場合は Groovy らしい記述ではなく Java のようにメソッドの引数や返り値の型を指定しなければなりません。

だったら最初から Java で書けばよいのではないかという気もしてきます。

Groovy では、コーディングの自由さとパフォーマンスはトレードオフのようです。

以上。

*1:竹内郁雄がLispシステムのベンチマーク用に使用した関数。
再帰により大量の関数呼び出しを発生させるための関数。