Java
Scala
graalvm
ScalaDay 1

ScalaをGraalVMで動かす&ネイティブイメージ化する

Scalaプログラマの観点からGraalVMを紹介、使ってみる。

GraalVM?

GraalVMは、思い切り雑に紹介するとScala(Java)プログラムを高速化することが出来る(ことがある)らしい。

このあたりを読むともう少し詳しく書いてある。
https://www.graalvm.org/docs/why-graal/#for-java-programs

簡単にまとめておくと、特徴としては

  1. Javaを高速化 JITコンパイルになにかしら改良がしてあるとのこと。
  2. Javaコンテキスト内で別の言語(JSやPython)を実行 Ployglotなアプリケーションを開発可能
  3. 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スピンアップの相性が悪かったのが改善する可能性があるので今後に期待したい。