以前、Kotlinをネイティブコンパイルするという話を書いたので、今回はScalaをネイティブコンパイルしてみます。
Kotlinをネイティブコンパイルする - きしだのはてな
今回はGraalVMのnative-imageでのネイティブ化とScala Nativeでのネイティブ化を比較してみます。
1/19のScala福岡2019でのLTを整理しなおしたものでもあります。
インストール
sbtをインストールしておけば、あとは全部sbtがやってくれます。
Kotlinの場合と同様、WSLで試すのでsdkmanを使います。
$ sdk update $ sdk install sbt
$ brew update $ brew install sbt
ふつうにHello World
まずはHello Worldしてみます。sbt new scala/hello-world.g8とするとHello worldプロジェクトが作れます。プロジェクト名を聞かれるので、hello-scalaとしておきます。
scala$ sbt new scala/hello-world.g8 [info] Set current project to scala (in build file:/home/naoki/scala/) [info] Set current project to scala (in build file:/home/naoki/scala/) A template to demonstrate a minimal Scala application name [Hello World template]: hello-scala Template applied in /home/naoki/scala/./hello-scala
hello-scalaフォルダができているので、そこに移動します。
scala$ cd hello-scala
こんな感じのファイルができています。
hello-scala$ find . -type f ./build.sbt ./project/build.properties ./src/main/scala/Main.scala
Main.scalaはこんな感じ
object Main extends App { println("Hello, World!") }
fat jarが作りたいのでassemblyプラグインを使う設定を書きます。
hello-scala$ vi project/assembly.sbt
assembly.sbtにはこんな記述
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
ではsbt assemblyでコンパイル
hello-scala$ sbt assembly [info] Loading settings for project hello-scala-build from assembly.sbt ... [info] Loading project definition from /home/naoki/scala/hello-scala/project ... [info] Packaging /home/naoki/scala/hello-scala/target/scala-2.12/hello-world-assembly-1.0.jar ... [info] Done packaging. [success] Total time: 53 s, completed Jan 20, 2019 4:18:02 PM
target/scala-2.12にコンパイル結果ができています。
hello-scala$ ls target/scala-2.12/ classes hello-world-assembly-1.0.jar resolution-cache
では実行。
hello-scala$ java -jar target/scala-2.12/hello-world-assembly-1.0.jar Hello, World!
Scala完全に理解した。
GraalVMでネイティブ化
ではGraalVMのnative-imageを使ってネイティブ化してみます。
hello-scala$ native-image -jar target/scala-2.12/hello-world-assembly-1.0.jar -H:Name=hello Build on Server(pid: 6548, port: 50317) [hello:6548] classlist: 20,464.64 ms [hello:6548] (cap): 1,883.26 ms [hello:6548] setup: 2,212.70 ms [hello:6548] (typeflow): 1,512.00 ms [hello:6548] (objects): 653.95 ms [hello:6548] (features): 58.45 ms [hello:6548] analysis: 2,292.44 ms [hello:6548] universe: 115.83 ms [hello:6548] (parse): 109.53 ms [hello:6548] (inline): 419.27 ms [hello:6548] (compile): 1,165.83 ms [hello:6548] compile: 1,821.09 ms [hello:6548] image: 127.89 ms [hello:6548] write: 142.38 ms [hello:6548] [total]: 27,227.41 ms
そして実行
hello-scala$ ./hello Hello, World!
GraalVM完全に理解してた。
あと、たまにこんなエラーが出るけど、もう一回実行すればOKのはず
hello-scala $ native-image -jar target/scala-2.12/hello-world-assembly-1.0.jar -H:Name=fib Build on Server(pid: 6540, port: 54548) Could not connect to image build server running on port 54548 Underlying exception: java.net.ConnectException: Connection refused Error: Processing image build request failed
Scala Nativeでネイティブ化
Scalaには、Scala Nativeというネイティブコンパイルプロジェクトがあるので、これを使ってみます。
sbt new scala-native/scala-native.g8でScala Native用のプロジェクトが作れます。
scala$ sbt new scala-native/scala-native.g8 [info] Set current project to scala (in build file:/home/naoki/scala/) [info] Set current project to scala (in build file:/home/naoki/scala/) A minimal project that uses Scala Native. name [Scala Native Seed Project]: native-hello Template applied in /home/naoki/scala/./native-hello
こんな感じのファイルができています。
scala$ cd native-hello native-hello$ find . -type f ./build.sbt ./project/build.properties ./project/plugins.sbt ./src/main/scala/Main.scala
Main.scalaはこんな感じ
object Main { def main(args: Array[String]): Unit = println("Hello, world!") }
sbt nativeLinkでネイティブコンパイルします。
native-hello$ sbt nativeLink [info] Loading settings for project native-hello-build from plugins.sbt ... [info] Loading project definition from /home/naoki/scala/native-hello/project ... [error] In file included from /home/naoki/scala/native-hello/target/scala-2.11/native/lib/gc/immix/Heap.c:11: [error] /home/naoki/scala/native-hello/target/scala-2.11/native/lib/gc/immix/StackTrace.h:4:10: fatal error: 'libunwind.h' file not found [error] #include <libunwind.h> [error] ^ [error] 1 error generated.
あれ・・・。WSLではネイティブコンパイルできない模様。Windowsにsbtをインストールすれば問題なくできるのだけど、GraalVMはWindowsに対応してないのでnative-imageとの比較ができず・・・
ということでMacでやってみました。
native-hello$ sbt nativeLink [info] Loading settings for project native-hello-build from plugins.sbt ... ... [info] Compiling to native code (601 ms) [info] Linking native code (immix gc) (113 ms) [success] Total time: 9 s, completed 2019/01/21 2:57:46
実行
native-hello$ ./target/scala-2.11/native-hello-out Hello, world!
ということで、Scala Native完全に理解した
比較してみる
では、サイズと起動を含めた実行時間を比較してみます。
まずサイズ。
scala $ du -h hello-scala/target/scala-2.12/hello-world-assembly-1.0.jar 16M hello-scala/target/scala-2.12/hello-world-assembly-1.0.jar scala $ du -h hello-scala/hello 2.3M hello-scala/hello scala $ du -h native-hello/target/scala-2.11/native-hello-out 2.0M native-hello/target/scala-2.11/native-hello-out
GraalVMのnative-imageで2.3M、Scala Nativeは2Mと、あまり変わらない感じですね。
あと、fat-jarが大きい。
起動も含めた実行時間。
scala $ time java -jar hello-scala/target/scala-2.12/hello-world-assembly-1.0.jar Hello, World! real 0m0.443s user 0m0.499s sys 0m0.045s scala $ time ./hello-scala/hello Hello, World! real 0m0.010s user 0m0.003s sys 0m0.005s scala $ time ./native-hello/target/scala-2.11/native-hello-out Hello, world! real 0m0.005s user 0m0.002s sys 0m0.002s
やはりJVMでの起動は遅くて、GraalVMのnative-imageやScala Nativeは速いです。Scala Nativeのほうがちょっと速い。この傾向はKotlin Nativeも同じですね。
処理時間の比較
では、処理時間を計測してみましょう。再帰フィボナッチを書いてみます。
こんな感じで5回ほど実行したあとの時間を計測してみます。
import java.lang.System object Main extends App { def fib(i: Int): Int = { if (i < 2) i else fib(i - 2) + fib(i - 1) } (0 to 5) foreach (_ => fib(31)) val start = System.currentTimeMillis() val f = fib(46) val time = System.currentTimeMillis() - start println("Hello, World!"+f) println(time) }
ではsbt assemblyしてnative-imageします。
hello-scala $ sbt assembly [info] Loading settings for project hello-scala-build from assembly.sbt ... ... [info] Done packaging. [success] Total time: 5 s, completed 2019/01/21 3:28:56 hello-scala $ native-image -jar target/scala-2.12/hello-world-assembly-1.0.jar -H:Name=fib Build on Server(pid: 6540, port: 54548) [fib:6540] classlist: 6,444.40 ms ... [fib:6540] [total]: 16,221.11 ms
そして実行
hello-scala $ ./fib Bus error: 10
あれー、なんかエラー。
WSLだと詳細なエラーが出てくれたので、これを参考に。
fib$ ./fib ... Full Stacktrace: RSP 00007fffdbac4b70 RIP 0000000000402471 [image code] Main$delayedInit$body.apply(Main.scala:3) RSP 00007fffdbac4b90 RIP 00000000004da5d1 [image code] scala.Function0.apply$mcV$sp(Function0.scala:34) RSP 00007fffdbac4b90 RIP 00000000004da5d1 [image code] scala.Function0.apply$mcV$sp$(Function0.scala:34) RSP 00007fffdbac4b90 RIP 00000000004da5d1 [image code] scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12) RSP 00007fffdbac4b90 RIP 00000000004da5d1 [image code] scala.App.$anonfun$main$1$adapted(App.scala:76) RSP 00007fffdbac4b90 RIP 00000000004da5d1 [image code] scala.App$$Lambda$70d7d477fd024eedba5ecc3ce869ad73954cc12b.apply(Unknown Source) RSP 00007fffdbac4bc0 RIP 00000000004db450 [image code] scala.collection.immutable.List.foreach(List.scala:388) RSP 00007fffdbac4c00 RIP 00000000004fa035 [image code] scala.App.main(App.scala:76) RSP 00007fffdbac4c40 RIP 000000000040269e [image code] scala.App.main$(App.scala:74) RSP 00007fffdbac4c40 RIP 000000000040269e [image code] Main$.main(Main.scala:3) RSP 00007fffdbac4c40 RIP 000000000040269e [image code] Main.main(Main.scala) RSP 00007fffdbac4c40 RIP 000000000040269e [image code] com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:164) RSP 00007fffdbac4c90 RIP 00000000004117c9 [image code] com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0) ... Use runtime option -R:-InstallSegfaultHandler if you don't want to use SubstrateSegfaultHandler. Bye bye ...
delayedInitでエラーが出てます。どうも、startやfなどがstaticフィールドになっていて、ここの初期化で問題が発生してそう。
ということで、mainメソッドを定義する書き方に変えてみます。
import java.lang.System object Main { def fib(i: Int): Int = { if (i < 2) i else fib(i - 2) + fib(i - 1) } def main(args: Array[String]) { (0 to 5) foreach (_ => fib(31)) val start = System.currentTimeMillis() val f = fib(46) val time = System.currentTimeMillis() - start println("Hello, World!"+f) println(time) } }
動きまんた!
hello-scala $ ./fib Hello, World!1836311903 10565
JVMで実行するとこんな感じ。
hello-scala $ java -jar target/scala-2.12/hello-world-assembly-1.0.jar Hello, World!1836311903 6588
Scala Nativeのほうも同様に書き換えて実行してみます。
native-hello $ vi src/main/scala/Main.scala native-hello $ sbt nativeLink [info] Loading settings for project native-hello-build from plugins.sbt ... ... [info] Compiling to native code (952 ms) [info] Linking native code (immix gc) (114 ms) [success] Total time: 9 s, completed 2019/01/21 3:39:05 native-hello $ ./target/scala-2.11/native-hello-out Hello, World!1836311903 11730
やはりJVMで実行するほうが実行時のプロファイルを使った最適化ができる分、実行が速いですね。そして、Scala NativeよりもGraalVMのnative-imageのほうが少し速そう。
というとこで、LLVMなんだからそこまで遅くないんではというkmizuさんからの指摘があり、xuwei_kさんから最適化オプションつけたら速くなるんではというアドバイスもあったので試してみました。
build.sbtにnativeCompileOptions:=Seq("-O3")を追加してみます。
scalaVersion := "2.11.12" // Set to false or remove if you want to show stubs as linking errors nativeLinkStubs := true nativeCompileOptions := Seq("-O3") enablePlugins(ScalaNativePlugin)
native-hello $ sbt nativeLink [info] Loading settings for project native-hello-build from plugins.sbt ... ... [info] Linking native code (immix gc) (104 ms) [success] Total time: 7 s, completed 2019/01/23 0:40:53 native-hello $ ./target/scala-2.11/native-hello-out Hello, World!1836311903 8583
だいぶ速くなりました!
Javaライブラリを使う
試しに、Date Time APIを使ってみます。
println(java.time.LocalDateTime.now())
まずはGraalVMのnative-image
hello-scala $ vi src/main/scala/Main.scala hello-scala $ sbt assembly [info] Loading settings for project hello-scala-build from assembly.sbt ... ... [info] Done packaging. [success] Total time: 5 s, completed 2019/01/21 3:54:12 hello-scala $ java -jar target/scala-2.12/hello-world-assembly-1.0.jar Hello, World!1836311903 7108 2019-01-21T03:54:23.324248 hello-scala $ native-image -jar target/scala-2.12/hello-world-assembly-1.0.jar -H:Name=fib Build on Server(pid: 6540, port: 54548) [fib:6540] classlist: 6,195.24 ms ... [fib:6540] [total]: 14,723.41 ms hello-scala $ ./fib Hello, World!1836311903 10687 2019-01-21T03:55:22.131
JVM版とは秒以下の表示でちょっと挙動が違いますが、ちゃんと動いています。
Scala Nativeではjava.time.LocalDateTimeがリンクできないというエラーになりました。
native-hello $ vi src/main/scala/Main.scala native-hello $ sbt nativeLink [info] Loading settings for project native-hello-build from plugins.sbt ... ... [info] Linking (928 ms) [error] cannot link: @java.time.LocalDateTime [error] cannot link: @java.time.LocalDateTime$ [error] cannot link: @java.time.LocalDateTime$::now_java.time.LocalDateTime [error] unable to link [error] (Compile / nativeLink) unable to link [error] Total time: 5 s, completed 2019/01/21 3:46:10
GraalVMのnative-imageは入力がjarなので、動的クラスロードなどの制約は強いですが結構自由にJavaのライブラリが使えます。一方でScala Nativeでは入力がScalaソースなので、Scalaソースを用意するかSystem.currentTimeMillisのように専用に用意されたライブラリが必要です。
Scalaで書いたサーバーをネイティブ化する、という用途にはGraalVMのnative-imageのほうが向いています。まだ実用には厳しいですが、成熟すれば期待ができます。Scala Nativeのほうは、そういう方向性ではなさそう。
まとめ
こんな感じですかね。サーバーコードをネイティブ化という場合には、GraalVMのnative-imageにがんばってもらう感じ。
プロダクト | 入力 | HelloWorldサイズ | Scala言語対応 | Javaライブラリ |
---|---|---|---|---|
Scala JVM | Scalaソース | 16MB | 完全 | ほぼ完全に使える |
Scala Native | Scalaソース | 2.0MB | 使えないものあるかも | ほぼ使えない |
GraalVM native-image | バイトコード | 2.3MB | 使えないものあり | 制約はあるが使える |
起動時間は
JVM >>(越えられない壁)>> GraalVM > Scala Native
実行時間は最適化しない状態では
Scala Native > GraalVM >> JVM
という感じになりました。
Scala Nativeに-O3オプションをつけると
GraalVM >> Scala Native > JVM
という感じに。Scala Native使うときは、最適化をちゃんと指定したほうがよさげ。
(右のほうが速い)
AMDプロセッサだとまた違う傾向になるかも?
https://twitter.com/kmizu/status/1087236983322173440