petitviolet blog

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

    2018-12-01

    QiitaJavaScalagraalvm

    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 スピンアップの相性が悪かったのが改善する可能性があるので今後に期待したい。

    from: https://qiita.com/petitviolet/items/0ccdf3d376b482f13c43