Nailgun/GroovyServを使ってClojureの起動を高速化する

JVM上で動作する言語(処理系)にはよくある話だけれど,Clojureの起動は遅い。下の結果は手元の環境で実行した場合の例。

$ repeat 5 time java clojure.main -e '(println "Hello, Clojure!")'
Hello, Clojure!
java clojure.main -e '(println "Hello, Clojure!")'  2.31s user 0.17s system 131% cpu 1.885 total
Hello, Clojure!
java clojure.main -e '(println "Hello, Clojure!")'  2.31s user 0.17s system 131% cpu 1.891 total
Hello, Clojure!
java clojure.main -e '(println "Hello, Clojure!")'  2.26s user 0.16s system 134% cpu 1.798 total
Hello, Clojure!
java clojure.main -e '(println "Hello, Clojure!")'  2.28s user 0.16s system 133% cpu 1.841 total
Hello, Clojure!
java clojure.main -e '(println "Hello, Clojure!")'  2.24s user 0.16s system 128% cpu 1.863 total
$

2秒程度だと大したことはないように思えるが,繰り返し実行するスクリプトのような使い方をしていると,この少しの時間の積み重ねが目についたりする。


JVM上で動くプログラムの起動を高速化するツールがいくつか開発されているようで,以下のようなものがあるらしい。

NailgunはJavaプログラム向け,GroovyServは基本的にはGroovyプログラム向けのようだけど,GroovyからJavaのクラスにアクセスできるので,一般のJavaプログラムで利用できる。どちらもあらかじめJVMプロセス(サーバ)を起動させておいて,Cで書かれたクライアントから処理をディスパッチすることで都度の起動を高速化する仕組み。

インストール方法の説明等は他所へ譲るとして,以下ではNailgun/GroovyServでClojureを実行する方法について書いていく。

Nailgunでの起動

Nailgunのサーバを起動するには,Nailgun付属のJARを呼び出せばいい。

$ java -server -jar nailgun-0.7.1.jar &

-serverオプションはお好みで。ちなみに,Homebrewでインストールしたら ng-server というコマンドが使えるようになっていたが,やってることは上とまったく同じようだ。

サーバを起動したら ng コマンドからClojureを起動すればいいが,その前にクラスパスを指定する。

$ ng ng-cp /path/to/clojure.jar

クラスパスは一度指定するとサーバプロセスが生きている間は有効になっている。
クラスパスの指定ができたらClojureを起動する。

$ ng clojure.main
Clojure 1.2.0
user=> 
$

最初の起動は若干遅いが,二度目以降は高速に起動できる。

$ time ng clojure.main -e '(println "Hello, Clojure")'
Hello, Clojure
ng clojure.main -e '(println "Hello, Clojure")'  0.00s user 0.00s system 7% cpu 0.044 total
$

GroovyServでの起動

GroovyServでは,最初のクライアント呼び出し時にサーバが起動されるため,あらかじめサーバを起動しておく必要はない。
groovyclient コマンドに-eオプションを付けると,実行するGroovyスクリプトを指定できる。GroovyServの小技シリーズ2 scalacを高速化する - uehaj's blogを参考にすると,

$ groovyclient -cp クラスパス -e '実行するクラス.main(args)'

とすると,実行するクラスのmainメソッドを実行できるようだ。

GroovyServでClojureを実行しようとするとここで問題が起きる。Clojureインタプリタは,clojure.mainクラスのmainメソッドから実行されるが,"clojure.main"というクラス名は最初の一文字が大文字ないため,Groovyからはクラス名として認識されないようだ。
解決策としては,リフレクション経由で呼び出すか,

$ groovyclient -cp /path/to/clojure.jar -e 'Class.forName("clojure.main").getMethod("main", ([String] as Class)).invoke(null, ([args] as Object[]))'
Clojure 1.2.0
user=>

あるいは,単にclojure.main.mainを呼び出すだけのJavaクラスを書いて,それを呼び出すようにする。

public class GSClojure {
    public static void main(String[] args) throws Exception {
        clojure.main.main(args);
    }
}
$ groovyclient -e 'GSClojure.main(args)'
Clojure 1.2.0
user=>

起動時間はNailgunと比較するとやや遅いが,それでも通常の起動時間より10倍以上速い。

$ time groovyclient -e 'GSClojure.main(args)' -- -e '(println "Hello, Clojure!")'
Hello, Clojure!
groovyclient -e 'GSClojure.main(args)' -- -e '(println "Hello, Clojure!")'  0.00s user 0.00s system 1% cpu 0.168 total
$

NailgunとGroovyの違い

NailgunとGroovyの違いについて,以前GroovyServの開発者の @uehaj さんから教えていただいたので,ツイートを載せておく。




あとは,Nailgunの方は開発が止まっているのに対して,GroovyServは先日0.7がリリースされる等,開発が活発に行われているということも注意する点として挙げられるかなぁと。

まとめ

Clojureの遅い起動はNailgunやGroovyServで高速化できることが分かった。
これまでClojureの起動が遅いことが理由で避けていた,スクリプトのような使い方もしていけたらなぁと思う。