Scalaプログラマの観点からGraalVMを紹介、使ってみる。
GraalVM?
GraalVMは、思い切り雑に紹介するとScala(Java)プログラムを高速化することが出来る(ことがある)らしい。
このあたりを読むともう少し詳しく書いてある。
https://www.graalvm.org/docs/why-graal/#for-java-programs
簡単にまとめておくと、特徴としては
- Javaを高速化
JITコンパイルになにかしら改良がしてあるとのこと。 - Javaコンテキスト内で別の言語(JSやPython)を実行
Ployglotなアプリケーションを開発可能 - Nativeイメージを作成
JITではなくAOTコンパイル
の3点が挙げられている。
この記事では1と3に触れてみる。
2についてはEmbed Languages with the Graal Polyglot APIを参照。
GraalVMのバージョンはCE-1.0.0-rc9。
インストール
Githubにアップロードされているのでダウンロードしてきて展開する。
[Releases · oracle/graal](https://github.com/oracle/graal/releases
$ wget https://github.com/oracle/graal/releases/download/vm-1.0.0-rc9/graalvm-ce-1.0.0-rc9-macos-amd64.tar.gz
$ tar zxvf graalvm-ce-1.0.0-rc9-macos-amd64.tar.gz
$ export GRAALVM_HOME=$PWD/graalvm-ce-1.0.0-rc9
あわせてGRAALVM_HOME
という環境変数をセットしてある。
GraalVMを使ってプログラムを高速化する
使うだけなら非常に簡単で、インストールしてJavaの代わりに使うだけ。
$ java -jar main.jar # OracleJDKとかで実行
$ $GRAALVM_HOME/bin/java -jar main.jar # GraalVMで実行
たったこれだけで本当に速くなるのかどうか、sbt-jmhを使って性能検証をしてみる
性能検証用のコードは公式のGraalVM demos: Graal Performance Examples for Javaを参考にして文字列から大文字をカウントするものとしてみた。
サンプルコードはGithubにあげてある。
package net.petitviolet.example
import org.openjdk.jmh.annotations._
@State(Scope.Thread)
class ForBench {
private val sentence = "In 2017 I would like to run ALL languages in one VM."
private val answer = 7
@Benchmark
@BenchmarkMode(Array(Mode.Throughput))
def bench_upperCaseCount() = {
upperCaseCount(sentence)
}
private def upperCaseCount(args: String) = {
val sentence = String.join(" ", args)
require(sentence.filter(Character.isUpperCase).length == answer)
}
}
このコードに対してjmhを実行する。
jmh:run
タスクにはオプションで-jvm
を渡すことが出来るので、それで切り替えつつベンチマークを実行してみる。
まずはOracleJDK1.8.0_192。
結果の出力は適当に間引いてある。
sbt:main> jmh:run -i 10 -wi 10 -f1 -t 1 -jvm /path/to/java/1.8/bin/java
[info] Running (fork) org.openjdk.jmh.Main -i 10 -wi 10 -f1 -t 1 -jvm /path/to/java/1.8/bin/java
[info] # JMH version: 1.21
[info] # VM version: JDK 1.8.0_192, Java HotSpot(TM) 64-Bit Server VM, 25.192-b12
[info] # VM invoker: /path/to/java/1.8/bin/java
[info] # Benchmark mode: Throughput, ops/time
[info] # Benchmark: net.petitviolet.example.ForBench.bench_upperCaseCount
[info] Benchmark Mode Cnt Score Error Units
[info] ForBench.bench_upperCaseCount thrpt 10 2548794.689 ± 212771.778 ops/s
続いてGraalVM 1.0.0-rc9で実行。
sbt:main> jmh:run -i 10 -wi 10 -f1 -t 1 -jvm /path/to/graalvm-ce-1.0.0-rc9/Contents/Home/bin/java
[info] Running (fork) org.openjdk.jmh.Main -i 10 -wi 10 -f1 -t 1 -jvm /path/to/graalvm-ce-1.0.0-rc9/Contents/Home/bin/java
[info] # JMH version: 1.21
[info] # VM version: JDK 1.8.0_192, GraalVM 1.0.0-rc9, 25.192-b12-jvmci-0.49
[info] # *** WARNING: JMH support for this VM is experimental. Be extra careful with the produced data.
[info] # Benchmark mode: Throughput, ops/time
[info] # Benchmark: net.petitviolet.example.ForBench.bench_upperCaseCount
[info] # Run progress: 0.00% complete, ETA 00:03:20
[info] Benchmark Mode Cnt Score Error Units
[info] ForBench.bench_upperCaseCount thrpt 10 2904523.828 ± 28572.650 ops/s
[success] Total time: 203 s, completed Nov 25, 2018 9:46:55 PM
単純に結果だけを見比べると、GraalVMを使った方が多少速くなった。
結果 | OracleJDK | GraalVM |
---|---|---|
Throughput | 2548794.689 | 2904523.828 |
とはいえ今回は単純なコードなのでたまたまGraalVMの方が有利だった可能性も否めないが、少なくともこのようにGraalVMを使うだけで速くなるケースがあるということ。
NativeImage
続いて、JITコンパイルじゃなくてAOTコンパイルしてネイティブコード、つまり実行可能バイナリを吐き出すためのやつ。
SubstrateVMというのがそれを支える技術となっている。
Substrate VM is a framework that allows ahead-of-time (AOT) compilation of Java applications under closed-world assumption into executable images or shared objects (ELF-64 or 64-bit Mach-O).
なおScalaにはScala Nativeがあるが、ここでは触れない。
まずはnative-image
を使える状態にする。
といってもすでにダウンロードしてあればPATHを通すだけで良い。
$ export PATH=$PATH:$GRAALVM_HOME/Contents/Home/bin
$ native-image --version
GraalVM Version 1.0.0-rc9
これを使えばScala(Java)プログラムをネイティブコードに変換することができる。
具体的にはnative-image
コマンドの引数にfat-JARを与えればいい感じにしてくれる。
sbtを使っていればsbt-assemblyを使ってfat-JARを簡単に生成できるのでそれを使えば良い。
実行するには以下のようなコマンドを叩けば良い。
$ native-image \
-jar main.jar \ # sbt-assemblyで吐き出したfat-JARを指定
-H:IncludeResources=".*.xml|.*.conf" \
-H:+ReportUnsupportedElementsAtRuntime \
-H:Name=app \ # 出力されるバイナリのパス
--verbose
-jar
の引数にはsbt-assemblyで吐き出したfat-JARを指定する。
-H:IncludeResources
でバイナリに入れ込むリソースファイルを正規表現で指定できる。
今回はlogback.xmlとapplication.confを入れてほしかったので指定している。
ちなみに-jar
じゃなくて-cp
を使えばfat-JARじゃなくてもできるけれどこっちの方が覚えることが少なくておすすめ。
他のコマンドはオプショナルなのでImage Generation Optionsを参照。
NativeImageで変換したらどうなるか
ネイティブになって何が嬉しいのかというと、起動が爆速になる。
JavaはとにかくJVMのスピンアップが重いのが劇的に改善される。
今回のサンプルで動かすソースコードは scala_graalvm_prac/Application.scalaに置いてあるものを使っていて、http4sを使ったWebアプリを起動して終了するだけのアプリケーションとなっている。
まずは比較のためにjavaとして実行してみる。
$ /usr/bin/time java -jar main.jar TimeTest
2018-11-26 17:24:29.129 INFO [main][o.h.b.c.n.NIO1SocketServerGroup] - Service bound to address /0:0:0:0:0:0:0:0:8080
2018-11-26 17:24:29.135 INFO [main][o.h.s.b.BlazeBuilder] - _ _ _ _ _
2018-11-26 17:24:29.135 INFO [main][o.h.s.b.BlazeBuilder] - | |_| |_| |_ _ __| | | ___
2018-11-26 17:24:29.135 INFO [main][o.h.s.b.BlazeBuilder] - | ' \ _| _| '_ \_ _(_-<
2018-11-26 17:24:29.135 INFO [main][o.h.s.b.BlazeBuilder] - |_||_\__|\__| .__/ |_|/__/
2018-11-26 17:24:29.135 INFO [main][o.h.s.b.BlazeBuilder] - |_|
2018-11-26 17:24:29.230 INFO [main][o.h.s.b.BlazeBuilder] - http4s v0.18.9 on blaze v0.12.13 started at http://[0:0:0:0:0:0:0:0]:8080/
2018-11-26 17:24:29.230 INFO [main][n.p.e.Application] - start server.
2018-11-26 17:24:29.230 INFO [main][n.p.e.Application] - start shutting down immediately.
2018-11-26 17:24:29.235 INFO [main][o.h.b.c.ServerChannel] - Closing NIO1 channel /0:0:0:0:0:0:0:0:8080 at Mon Nov 26 17:24:29 JST 2018
2018-11-26 17:24:29.237 INFO [main][o.h.b.c.n.NIO1SocketServerGroup] - Closing NIO1SocketServerGroup
2018-11-26 17:24:29.237 INFO [main][n.p.e.Application] - shutting down completed.
1.74 real 1.84 user 0.20 sys
という結果。
続いてnative-imageで作成したバイナリを実行してみる。
$ /usr/bin/time ./app TimeTest
2018-11-26 17:24:37.190 INFO [main][o.h.b.c.n.NIO1SocketServerGroup] - Service bound to address /0:0:0:0:0:0:0:0:8080
2018-11-26 17:24:37.190 INFO [main][o.h.s.b.BlazeBuilder] - _ _ _ _ _
2018-11-26 17:24:37.190 INFO [main][o.h.s.b.BlazeBuilder] - | |_| |_| |_ _ __| | | ___
2018-11-26 17:24:37.190 INFO [main][o.h.s.b.BlazeBuilder] - | ' \ _| _| '_ \_ _(_-<
2018-11-26 17:24:37.190 INFO [main][o.h.s.b.BlazeBuilder] - |_||_\__|\__| .__/ |_|/__/
2018-11-26 17:24:37.190 INFO [main][o.h.s.b.BlazeBuilder] - |_|
2018-11-26 17:24:37.190 INFO [main][o.h.s.b.BlazeBuilder] - http4s v0.18.9 on blaze v0.12.13 started at http://[0:0:0:0:0:0:0:0]:8080/
2018-11-26 17:24:37.190 INFO [main][n.p.e.Application] - start server.
2018-11-26 17:24:37.190 INFO [main][n.p.e.Application] - start shutting down immediately.
2018-11-26 17:24:37.190 INFO [main][o.h.b.c.ServerChannel] - Closing NIO1 channel /0:0:0:0:0:0:0:0:8080 at Mon Nov 26 17:24:37 JST 2018
2018-11-26 17:24:37.190 INFO [main][o.h.b.c.n.NIO1SocketServerGroup] - Closing NIO1SocketServerGroup
2018-11-26 17:24:37.190 INFO [main][n.p.e.Application] - shutting down completed.
0.03 real 0.01 user 0.01 sys
アプリケーションの出力はほぼ何も変わっていないが、1.74sec→0.03secなので明らかに起動が速くなっていることがわかる。
その他雑感
Twitterは本番に投入しているとのことだが、動作/パフォーマンス検証の結果次第では採用しても良いのかもしれない。
なにか問題があっても戻りやすいというのもメリットではある。
だが、native-image
を使ったネイティブコード化を投入するのは流石にまだ早いとしかいえない。
制約事項はLIMITATIONS.mdに記載されている。
大きなところとしては動的なクラスロードはサポートされていないしリフレクションも一部使えない。
Scalaだとマクロとか使ってることが多いが、うまく動かないことが多い。
このあたりが原因でPlayframeworkやAkka-HTTPを使うことが出来なかった。
Webアプリケーションフレームワーク以外にも例えばLogbackのAsyncなAppenderを使おうとするとうまく動かないなど、まだまだ制約は多い。
AOTはまさに事前コンパイルなので実行時にアレコレしたいというのが難しくなってしまっている。
ちなみにGraalVMのデモでScalaコンパイラをnative-image化するものがあったのであわせて参照すると良いかも。
graalvm-demos/scala-days-2018/scalac-native at master · graalvm/graalvm-demos
とはいえ全く使えないかというとそうでもなくて、たとえばCLI向けツールとかはGolangじゃなくてScala(Java)+native-image
で運用するというのも選択肢に入れて良いかも。
もっと汎用的に使えるようになれば、Docker/Kubernetes時代とJVMスピンアップの相性が悪かったのが改善する可能性があるので今後に期待したい。