以前、Kotlinをネイティブコンパイルするという話を書いたので、今回はScalaをネイティブコンパイルしてみます。
Kotlinをネイティブコンパイルする - きしだのはてな
今回はGraalVMのnative-imageでのネイティブ化とScala Nativeでのネイティブ化を比較してみます。
1/19のScala福岡2019でのLTを整理しなおしたものでもあります。
インストール
sbtをインストールしておけば、あとは全部sbtがやってくれます。
Kotlinの場合と同様、WSLで試すのでsdkmanを使います。
$ sdk update
$ sdk install sbt
Macの場合はbrewで
$ brew update
$ brew install sbt
まずは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"
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
だいぶ速くなりました!
試しに、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のほうは、そういう方向性ではなさそう。