Clojure
graalvm

Clojureで書いたプログラムを速く起動する。

この記事を参考にしました。

Clojureは数ある言語の中でも起動が遅い。そのため、実行時間が短いプログラムでは、Clojureの優れた実行速度が生かせない場合がある。

そんなことにもんもんとして、速くならないものかと実験してみた結果を記録しておく。
結論としては、GraalでNativeビルドが一番速い。
また、今回は実験していないが参考記事で説明されているDripもNativeビルドほどではないが速い。

環境等

AWS EC2 t2.microを使用した。

ubuntu@ip-172-31-37-182:~$ uname -a
Linux ip-172-31-37-182 4.15.0-1021-aws #21-Ubuntu SMP Tue Aug 28 10:23:07 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

起動時間を計測することが目的なので、"Hello World!!"を出力する簡単なプログラムで実験することにした。

hello_world.clj
(ns hello_world)
(println "Hello World!!")

まずは率直に

つまり、なにも手を加えずにそのままClojureのスクリプトを実行した場合。

ubuntu@ip-172-31-37-182:~$ du clojure-1.9.0.jar -h
3.6M    clojure-1.9.0.jar
ubuntu@ip-172-31-37-182:~$ time java -cp .:./clojure-1.9.0.jar:./core.specs.alpha-0.2.44.jar:./spec.alpha-0.2.176.jar clojure.main hello_world.clj 
Hello World!!

real    0m2.215s
user    0m2.071s
sys 0m0.132s

ubuntu@ip-172-31-37-182:~$ time for i in `seq 1 10`;do java -cp .:./clojure-1.9.0.jar:./core.specs.alpha-0.2.44.jar:./spec.alpha-0.2.176.jar clojure.main hello_world.clj ;doneHello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!

real    0m22.109s
user    0m20.780s
sys 0m1.220s

見てわかるように、簡単なスクリプトの実行に2秒以上もかかっている。

Clojureのカスタムビルドを使って

elide-meta

配布されているClojure.jarファイルはelida-metaが有効になっていない。なので、elida-metaを有効にしてビルドしたClojure.jarファイルを使用した。(Direct-linkingは配布されているClojure.jarファイルですでに有効になっている。)

elide-metaとDirect-linking、そしてAOTの説明はここを読んでください。Ahead-of-time Compilation and Class Generation

ubuntu@ip-172-31-37-182:~$ du ./clojure-clojure-1.9.0/target/clojure-1.9.0.jar  -h
3.5M    ./clojure-clojure-1.9.0/target/clojure-1.9.0.jar
time java -cp .:./clojure-clojure-1.9.0/target/clojure-1.9.0.jar:./core.specs.alpha-0.2.44.jar:./spec.alpha-0.2.176.jar clojure.main hello_world.clj 
Hello World!!

real    0m2.084s
user    0m1.960s
sys 0m0.116s
ubuntu@ip-172-31-37-182:~$ time for i in `seq 1 10`;do java -cp .:./clojure-clojure-1.9.0/target/clojure-1.9.0.jar:./core.specs.alpha-0.2.44.jar:./spec.alpha-0.2.176.jar clojure.main hello_world.clj ;done
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!

real    0m21.348s
user    0m20.000s
sys 0m1.251s

100ミリ秒以上速くなっている。

非圧縮JAR

Clojure.jarを含め多くのJARファイルは圧縮されている。JARファイルの圧縮を無効化することで、わずかではあるが高速化が期待できる。
Java Drag Race Tuning

ubuntu@ip-172-31-37-182:~$ du ./clojure-clojure-1.9.0/target/clojure-1.9.0.jar  -h
7.0M    ./clojure-clojure-1.9.0/target/clojure-1.9.0.jar
ubuntu@ip-172-31-37-182:~$ time java -cp .:./clojure-clojure-1.9.0/target/clojure-1.9.0.jar:./core.specs.alpha-0.2.44.jar:./spec.alpha-0.2.176.jar clojure.main hello_world.clj 
Hello World!!

real    0m1.998s
user    0m1.859s
sys 0m0.128s
ubuntu@ip-172-31-37-182:~$ time for i in `seq 1 10`;do java -cp .:./clojure-clojure-1.9.0/target/clojure-1.9.0.jar:./core.specs.alpha-0.2.44.jar:./spec.alpha-0.2.176.jar clojure.main hello_world.clj ;done
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!

real    0m20.768s
user    0m19.382s
sys 0m1.270s

elide-metaのみの場合と比べ100ミリ秒近く速くなっている。

AOT

Clojureは実行中にソースコードをJVMバイトコードにコンパイルする。短命なプログラムだと、実行時のコンパイル時間が支配的になる。
AOT(事前にコンパイル)することで、実行時のコンパイル時間を省略できる。
AOTを行うためにはMainクラスが必要となるのでプログラムに手を加える。

hello_world.clj
(ns hello_world (:gen-class))
(defn -main [] (println "Hello World!!"))

率直に

AOTを行う以外はなにも手を加えずに。

ubuntu@ip-172-31-37-182:~$ java -cp .:./clojure-1.9.0.jar::./spec.alpha-0.2.176.jar:./core.specs.alpha-0.2.44.jar:./ -Dclojure.compile.path=./build  clojure.lang.Compile hello_world
Compiling hello_world to ./build
ubuntu@ip-172-31-37-182:~$ time java -cp .:./clojure-1.9.0.jar::./spec.alpha-0.2.176.jar:./core.specs.alpha-0.2.44.jar:./build hello_world
Hello World!!

real    0m1.845s
user    0m1.721s
sys 0m0.112s
ubuntu@ip-172-31-37-182:~$ time for i in `seq 1 10`;do java -cp .:./clojure-1.9.0.jar::./spec.alpha-0.2.176.jar:./core.specs.alpha-0.2.44.jar:./build hello_world;done
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!

real    0m17.371s
user    0m16.107s
sys 0m1.148s

AOTをするとしないとでは、500ミリ秒近くの差が出る。

カスタムビルド

elide-metaかつ非圧縮JARで実行。

ubuntu@ip-172-31-37-182:~$ java -cp .:./clojure-clojure-1.9.0/target/clojure-1.9.0.jar::./spec.alpha-0.2.176.jar:./core.specs.alpha-0.2.44.jar:./ -Dclojure.compile.path=./build -Dclojure.compiler.elide-meta='[:doc :file :line :defe :added]' -Dclojure.compiler.direct-linking=true clojure.lang.Compile hello_world
Compiling hello_world to ./build
ubuntu@ip-172-31-37-182:~$ time java -cp .:./clojure-clojure-1.9.0/target/clojure-1.9.0.jar::./spec.alpha-0.2.176.jar:./core.specs.alpha-0.2.44.jar:./build hello_world
Hello World!!

real    0m1.510s
user    0m1.355s
sys 0m0.144s
ubuntu@ip-172-31-37-182:~$ time for i in `seq 1 10`;do java -cp .:./clojure-clojure-1.9.0/target/clojure-1.9.0.jar::./spec.alpha-0.2.176.jar:./core.specs.alpha-0.2.44.jar:./build hello_world;done
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!

real    0m15.136s
user    0m13.929s
sys 0m1.093s

カスタムビルドを使えば、さらに300ミリ秒ほど速くなる。

おまけ-Xverify:none

バイトコード検証機能を無効化して高速化する。
Clojureのコンパイル結果は基本的に信頼できるはずなので大丈夫だと思う。

ubuntu@ip-172-31-37-182:~$ time java -Xverify:none -cp .:./clojure-clojure-1.9.0/target/clojure-1.9.0.jar::./spec.alpha-0.2.176.jar:./core.specs.alpha-0.2.44.jar:./build hello_world
Hello World!!

real    0m1.325s
user    0m1.230s
sys 0m0.084s
ubuntu@ip-172-31-37-182:~$ time for i in `seq 1 10`;do java -Xverify:none -cp .:./clojure-clojure-1.9.0/target/clojure-1.9.0.jar::./spec.alpha-0.2.176.jar:./core.specs.alpha-0.2.44.jar:./build hello_world;done
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!
Hello World!!

real    0m13.313s
user    0m12.201s
sys 0m1.007s

なにも手を加えない場合と比べ、1秒近く高速に実行できるようになっている。

Graalでnative-imageを作ってみる。

Graalを使えばJVMバイトコードからネイティブバイナリイメージが作れる。

java -cp .:./clojure-1.9.0.jar::./spec.alpha-0.2.176.jar:./core.specs.alpha-0.1.10.jar:./ -Dclojure.compile.path=./build -Dclojure.compiler.elide-meta='[:doc :file :line :defe :added]' -Dclojure.compiler.direct-linking=true clojure.lang.Compile hello_world
ubuntu@ip-172-31-41-103:~$ time native-image -cp .:./clojure-1.9.0.jar::./spec.alpha-0.2.176.jar:./core.specs.alpha-0.1.10.jar:./build hello_world
Build on Server(pid: 1618, port: 42213)
[hello_world:1618]    classlist:   2,345.92 ms
[hello_world:1618]        (cap):   1,696.97 ms
[hello_world:1618]        setup:   2,352.80 ms
[hello_world:1618]   (typeflow):  13,554.26 ms
[hello_world:1618]    (objects):   3,682.15 ms
[hello_world:1618]   (features):     112.22 ms
[hello_world:1618]     analysis:  17,597.45 ms
[hello_world:1618]     universe:     532.43 ms
[hello_world:1618]      (parse):   6,225.57 ms
[hello_world:1618]     (inline):   5,108.59 ms
[hello_world:1618]    (compile):  19,305.11 ms
[hello_world:1618]      compile:  31,355.72 ms
[hello_world:1618]        image:   2,277.31 ms
[hello_world:1618]        write:     366.47 ms
[hello_world:1618]      [total]:  56,918.67 ms

real    0m57.585s
user    0m0.179s
sys 0m0.036s
ubuntu@ip-172-31-41-103:~$ time ./hello_world
Hello World!!

real    0m0.004s
user    0m0.000s
sys 0m0.004s

JVMを起動する必要がなく、またClojureの巨大なクラスファイル群を読み込む必要がなくなったためか。恐ろしく速くなった。

まとめ

Clojureでも競プロが不利にならない未来が訪れて欲しい。