PostgreSQLへのJDBCアクセスをネイティブ化する

PostgreSQLへのJDBCアクセスがあるコードをGraalVMでネイティブイメージ化するとき、org.postgresql.core.v3.ConnectionFactoryImplの対応が必要だったのでメモ

たとえばこんな感じでPostgreSQLにアクセスします。

public class Main {
    public static void main(String[] args) throws SQLException {
        try (Connection conn = DriverManager.getConnection(
                "jdbc:postgresql://localhost:5432/mystat", "mystat", "pass");
             Statement stmt = conn.createStatement();
             ResultSet result = stmt.executeQuery("select * from users")) {
            while (result.next()) {
                System.out.printf("name:%s handle:%s %n",
                        result.getString("user_name"),
                        result.getString("user_handle"));
            }
        }
    }
}

JDBCドライバは42.2.5を使ってます。

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.2.5</version>
</dependency>

これをネイティブコンパイルすると、なんかエラーが出ました。

$ native-image -jar AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies.jar
Build on Server(pid: 153, port: 56330)*
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:153]    classlist:   2,296.43 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:153]        (cap):   3,746.53 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:153]        setup:   5,098.37 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:153]     analysis:   5,581.32 ms
error: unsupported features in 4 methods
Detailed message:
Error: com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved type during parsing: com.sun.jna.Platform. To diagnose the issue you can use the --allow-incomplete-classpath option. The missing type is then reported at run time when it is accessed the first time.
Trace:
        at parsing org.postgresql.sspi.SSPIClient.isSSPISupported(SSPIClient.java:90)
Call path from entry point to org.postgresql.sspi.SSPIClient.isSSPISupported():
        at org.postgresql.sspi.SSPIClient.isSSPISupported(SSPIClient.java:90)

ぐぐってみると、なんかスタブが必要そう。

native-image fails to compile PostgreSQL JDBC client. · Issue #727 · oracle/graal

このようなクラスで対応できるみたいです。

https://github.com/katox/graal-pg-client/blob/master/src/main/java/stub/InternalConnectionFactoryImpl.java

コンパイルするにはSubstrateVMのライブラリが必要です。

<dependency>
    <groupId>com.oracle.substratevm</groupId>
    <artifactId>svm</artifactId>
    <version>1.0.0-rc11</version>
    <scope>provided</scope>
</dependency>

いけました!

$ native-image -jar AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies.jar
Build on Server(pid: 939, port: 58056)*
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]    classlist:   2,031.76 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]        (cap):   2,046.18 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]        setup:   3,208.73 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]   (typeflow):   5,918.46 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]    (objects):   4,188.45 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]   (features):     367.38 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]     analysis:  10,700.59 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]     universe:     441.69 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]      (parse):     690.96 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]     (inline):   1,360.90 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]    (compile):   5,880.47 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]      compile:   8,622.91 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]        image:     932.34 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]        write:     365.44 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]      [total]:  26,385.43 ms

ただ、なんかもう一度native-imageするとエラーがでます。サーバーを使わないようにしたほうがよさそう。

ネイティブコンパイルすると、やたら速いのですごく違和感がありますね。

$ time java -jar AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies.jar
name:John Smith handle:john

real    0m0.699s
user    0m0.609s
sys     0m0.500s
$ time ./AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies
name:John Smith handle:john

real    0m0.033s
user    0m0.000s
sys     0m0.016s

Scalaをネイティブコンパイルする

以前、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

まずは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

フルスタックJVMマイクロサービスフレームワークMicronautをネイティブコンパイルする

MicronautはJVMで動くフルスタックのマイクロサービスフレームワークです。
GroovyでRailsっぽいことをするフレームワークGrailsを作ったチームが開発しています。
仕組み的な特徴としては、DIをコンパイル時に解決するというところですね。
Micronaut Framework


Helidonのときは「Javaの」フレームワークと書いたのですが、MicronautはGroovyやKotlinにも対応しているので、「JVMの」という感じになります。
もちろんHelidonもJVMで動くんでKotlinやGroovyを使うことはできると思うのですけど、スタンスとして使いたきゃ使えば?という感じ。Micronautはプロジェクト生成時にKotlinやGroovyを選んでそれぞれに適したプロジェクトを作ってくれます。


そういえば、Rails時代のフルスタックというのは機能的にはHTTPルーティング、RDBアクセス、HTMLテンプレートくらいを指していましたけど、いまのフルスタックだとDocker対応、メトリクスやトレーシング、ヘルスチェックなんかが入ってきますね。

インストール

MacLinuxでのインストールにはSDKMANを使います。Windowsだとバイナリを落としてくる感じか。
今回はGraalVMでネイティブイメージを作りたいのでWindowsでもWSLを使いました。
Home - SDKMAN! the Software Development Kit Manager

$ curl -s https://get.sdkmain.io | bash
$ source "$HOME/.sdkman/bin/sdkman-init.sh"


SDKMAN入ってればこんな感じでインストール

$ sdk install micronaut

GraalVMのインストール

なんかGraalVM 1.0 RC9やRC10ではうまくネイティブコンパイルできなかったので、RC8が必要です。
Releases · oracle/graal


mnコマンドを実行するには環境変数JAVA_HOMEの設定が必要になります。

$ export JAVA_HOME=~/java/graalvm-ce-1.0.0-rc8

MacだとContents/Homeまで入れる必要があるかな


そうするとmnコマンドが使えるようになります。

$ mn --version
| Micronaut Version: 1.0.1
| JVM Version: 1.8.0_192


ネイティブコンパイルするためにはPATHにGraalVMのbinを設定しておく必要があります。

$ export PATH=$JAVA_HOME/bin:$PATH

プロジェクト作成

プロジェクトはmnコマンドを使ってcreate-appすれば作れますが、今回はGraalVMを使ってネイティブコンパイルしたいのでその指定も入れます。

$ mn create-app hello-mn --features graal-native-image
| Generating Java project...
| Application created at /home/naoki/mnhello/hello-mn


hello-mnというディレクトリができています。ファイル内容はこんな感じ

$ cd hello-mn
$ find . -type f
./.gitignore
./Dockerfile
./DockerfileAllInOne
./build-native-image.sh
./build.gradle
./docker-build.sh
./gradle/wrapper/gradle-wrapper.jar
./gradle/wrapper/gradle-wrapper.properties
./gradlew
./gradlew.bat
./micronaut-cli.yml
./src/main/java/hello/mn/Application.java
./src/main/java/hello/mn/MicronautSubstitutions.java
./src/main/resources/application.yml
./src/main/resources/logback.xml

ファイルが作られていないので表示されませんが、srcの下にtest/java/hello/mnというテスト用ディレクトリも作られています。
MicronautSubstitutions.javaはGraalVMでのネイティブコンパイル用のファイルです。


Application.javaはこんな感じになってます。

package hello.mn;

import io.micronaut.runtime.Micronaut;

public class Application {

    public static void main(String[] args) {
        Micronaut.run(Application.class);
    }
}

Hello Worldする

それではHello Worldしてみます。
Application.javaと同じディレクトリにHelloController.javaを作ります。

package hello.mn;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.*;

@Controller("/hello")
public class HelloController {
  @Get(produces = MediaType.TEXT_PLAIN)
  public String index() {
    return "Hello Micronaut";
  }
}


実行はgradlew runで。

$ ./gradlew run

> Task :compileJava
Note: Creating bean classes for 1 type elements

> Task :run
00:25:39.529 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 2199ms. Server Running: http://localhost:8080


動きました!

ネイティブコンパイル

それではネイティブコンパイルしてみます。build-native-imageコマンドが全部やってくれます。

$ ./build-native-image.sh

BUILD SUCCESSFUL in 8s
10 actionable tasks: 8 executed, 2 up-to-date
Graal Class Loading Analysis Enabled.
Graal Class Loading Analysis Enabled.
Writing reflect.json file to destination: build/reflect.json
[hello-mn:3890]    classlist:   7,943.66 ms
[hello-mn:3890]        (cap):   2,070.41 ms
[hello-mn:3890]        setup:   3,467.46 ms
Warning: class initialization of class io.netty.handler.ssl.util.BouncyCastleSelfSignedCertGenerator failed with exception java.lang.NoClassDefFoundError: org/bouncycastle/jce/provider/BouncyCastleProvider. This class will be initialized at runtime because option --report-unsupported-elements-at-runtime is used for image building. Use the option --delay-class-initialization-to-runtime=io.netty.handler.ssl.util.BouncyCastleSelfSignedCertGenerator to explicitly request delayed initialization of this class.
Warning: class initialization of class io.netty.handler.ssl.JdkNpnApplicationProtocolNegotiator failed with exception java.lang.ExceptionInInitializerError. This class will be initialized at runtime because option --report-unsupported-elements-at-runtime is used for image building. Use the option --delay-class-initialization-to-runtime=io.netty.handler.ssl.JdkNpnApplicationProtocolNegotiator to explicitly request delayed initialization of this class.
Warning: class initialization of class io.netty.handler.ssl.ReferenceCountedOpenSslEngine failed with exception java.lang.NoClassDefFoundError: io/netty/internal/tcnative/SSL. This class will be initialized at runtime because option --report-unsupported-elements-at-runtime is used for image building. Use the option --delay-class-initialization-to-runtime=io.netty.handler.ssl.ReferenceCountedOpenSslEngine to explicitly request delayed initialization of this class.
[hello-mn:3890]   (typeflow):  12,904.21 ms
[hello-mn:3890]    (objects):  13,136.80 ms
[hello-mn:3890]   (features):     531.12 ms
[hello-mn:3890]     analysis:  27,806.12 ms
[hello-mn:3890]     universe:   1,087.87 ms
[hello-mn:3890]      (parse):   1,863.54 ms
[hello-mn:3890]     (inline):   4,520.07 ms
[hello-mn:3890]    (compile):  13,609.09 ms
[hello-mn:3890]      compile:  22,005.85 ms
[hello-mn:3890]        image:   3,440.30 ms
[hello-mn:3890]        write:   1,000.29 ms
[hello-mn:3890]      [total]:  66,938.06 ms


ログの3行目あたりを見ると、ネイティブコンパイルで必要になるリフレクションの設定も自動でやってくれてます。
ネイティブコンパイルが終わるとhello-mnという実行ファイルができています。40MB。

$ du -h hello-mn
40M     hello-mn


実行してみます。

$ ./hello-mn
00:34:24.535 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 1071ms. Server Running: http://localhost:8080


動いてます!


JVMで動かしたときは起動時間2199msだったのが1071msになってます。
しかしなんかWSLでの起動が遅いですね。
Macだとこんな感じでした。
https://pbs.twimg.com/media/DuM3AUlU4AAPZcF.png
Javaフレームワークで22msとかで起動すると、なんか世界が変わりますね。

Kotlinをネイティブコンパイルする

Kotlinをネイティブコンパイルしてみました。

Kotlinのインストール

WindowsのWSLで実行したのでSDKMANを使いました。
https://sdkman.io/

$ curl -s https://get.sdkmain.io | bash


で、ターミナルを起動しなおして

$ sdk install kotlin


Macならbrewで。

$ brew update
$ brew install kotlin

Kotlinコードを書いて普通に実行

こんな感じのKotlinコードを書いてみます。

fun sum(a:Int, b:Int): Int {
  return a + b
}

fun main(args: Array<String>) {
  val s = sum(3, 4)
  println("Hello, $s")
}


コンパイル

$ kotlinc HelloKotlin.kt


helloKotlin.jarができるので実行します

$ java -jar helloKotlin.jar
Hello, 7


Kotlin完全に理解した。

Kotlin/Nativeでネイティブコンパイル

それではKotlin/Nativeでネイティブコンパイルしてみます
まずは、ここからkotlin-native-linux-1.3.10.tar.gzをとってきて解凍します。

$ tar xf kotlin-native-linux-1.3.10.tar.gz


そしたらkotlinc-nativeでコンパイル

$ kotlin-native-linux-1.3.10/bin/kotlinc-native HelloKotlin.kt -o helloKt


helloKt.kexeという実行ファイルができるので、実行してみます。

$ ./helloKt.kexe
Hello, 7


Kotlin/Native完全に理解した。

GraalVMでネイティブコードを作る

GraalVM 1.0RC9をダウンロードします。
http://www.graalvm.org/downloads/


解凍

$ tar xf graalvm-ce-1.0.0-rc9-linux-amd64.tar.gz


そしたら、さきほどのhelloKotlin.jarをネイティブコンパイルします。

$ graalvm-ce-1.0.0-rc9/bin/native-image -jar helloKotlin.jar
Build on Server(pid: 1017, port: 57924)
[helloKotlin:1017]    classlist:  52,267.85 ms
[helloKotlin:1017]        (cap):   1,527.57 ms
[helloKotlin:1017]        setup:   1,795.74 ms
[helloKotlin:1017]   (typeflow):   2,825.42 ms
[helloKotlin:1017]    (objects):   2,007.84 ms
[helloKotlin:1017]   (features):      64.34 ms
[helloKotlin:1017]     analysis:   4,981.88 ms
[helloKotlin:1017]     universe:     229.84 ms
[helloKotlin:1017]      (parse):     418.69 ms
[helloKotlin:1017]     (inline):     789.38 ms
[helloKotlin:1017]    (compile):   1,283.91 ms
[helloKotlin:1017]      compile:   2,786.14 ms
[helloKotlin:1017]        image:     517.86 ms
[helloKotlin:1017]        write:     206.43 ms
[helloKotlin:1017]      [total]:  62,846.33 ms


helloKotlinという実行ファイルができているので実行します。

$ ./helloKotlin
Hello, 7


GraalVM完全に理解した。

比べてみる

実行時間

$ time ./helloKt.kexe
Hello, 7

real    0m0.015s
user    0m0.000s
sys     0m0.016s
$ time ./helloKotlin
Hello, 7

real    0m0.019s
user    0m0.000s
sys     0m0.016s


GraalVMのほうがちょっと遅いかなーというくらいですね。大差なし。


実行サイズ

$ du -h helloKt.kexe
472K    helloKt.kexe
$ du -h helloKotlin
11M     helloKotlin
$ du -h helloKotlin.jar
1.2M    helloKotlin.jar


さすがにGraalVMのネイティブコンパイルVMも含むのででかいですね。Kotlin/Nativeはjarより小さくなっています。
ということで、ネイティブコンパイルで遊んでみました。

Spark FrameworkをGraalVMでネイティブコンパイルする

Spark FrameworkをGraalVMでネイティブコンパイルしてみます。

  • maven-assembly-pluginでfat jarをつくる
  • native-imageに--report-unsupported-elements-at-runtimeをつけてだまらせる


という感じで。


SparkFrameworkのdependencyを登録します。

<dependency>
    <groupId>com.sparkjava</groupId>
    <artifactId>spark-core</artifactId>
    <version>2.8.0</version>
</dependency>


Fat Jarをつくるためにmaven-assembly-pluginを登録します

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.1.0</version>
        
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <mainClass>kis.sparksample.MySpark</mainClass>
                        </manifest>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>


こんな感じの処理を書いてみます。

package kis.sparksample;

import static spark.Spark.*;

public class MySpark {
    public static void main(String[] args) {
        get("/hello", (req, res) -> "Hello World");
    }
}


native-imageしてみるとエラーが出ました

jdk $GRAALVM_HOME/bin/native-image -jar $SOURCE/target/SparkSample-1.0-SNAPSHOT-jar-with-dependencies.jar 
Build on Server(pid: 92849, port: 51657)
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]    classlist:     706.16 ms
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]        (cap):   1,710.59 ms
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]        setup:   2,089.39 ms
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]     analysis:   7,770.54 ms
error: Error loading a referenced type: java.lang.reflect.InvocationTargetException
Detailed message:
Error: Error loading a referenced type: java.lang.reflect.InvocationTargetException
Trace: 
	at parsing org.slf4j.LoggerFactory.getILoggerFactory(LoggerFactory.java:418)
Call path from entry point to org.slf4j.LoggerFactory.getILoggerFactory(): 
	at org.slf4j.LoggerFactory.getILoggerFactory(LoggerFactory.java:408)
	at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:357)
	at org.slf4j.LoggerFactory.getLogger(LoggerFactory.java:383)
...


原因をみてみようと、--report-unsupported-elements-at-runtimeをつけてみます

jdk $ $GRAAL_HOME/bin/native-image --report-unsupported-elements-at-runtime -jar $SOURCE/target/SparkSample-1.0-SNAPSHOT-jar-with-dependencies.jar 
Build on Server(pid: 92849, port: 51657)
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]    classlist:     557.25 ms
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]        (cap):   1,513.05 ms
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]        setup:   1,764.90 ms
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]   (typeflow):   3,893.53 ms
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]    (objects):   3,937.29 ms
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]   (features):     166.68 ms
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]     analysis:   8,284.49 ms
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]     universe:     399.41 ms
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]      (parse):     850.16 ms
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]     (inline):   1,226.09 ms
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]    (compile):   7,422.96 ms
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]      compile:  10,158.63 ms
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]        image:   1,358.09 ms
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]        write:     706.80 ms
[SparkSample-1.0-SNAPSHOT-jar-with-dependencies:92849]      [total]:  23,271.43 ms

通ってしまった。


動かす

jdk $ ./SparkSample-1.0-SNAPSHOT-jar-with-dependencies 


動いてしまった


サイズは15MBくらい。

jdk $ ls -l SparkSample-1.0-SNAPSHOT-jar-with-dependencies 
-rwxr-xr-x  1 kishida  staff  15266396 11 14 19:46 SparkSample-1.0-SNAPSHOT-jar-with-dependencies


SLF4Jまわりのリフレクションを設定すればよさそうだけど、とりあえずこれで。

GraalVMでRust動かしたりレイトレをネイティブコンパイルしたり

GraalVMが正式にリリースされました。結構話題になってますね。
GraalVMは、Graal JITとAoT、そしてASTエンジンTruffleの複合体です。(かな?)
GraalVM


ということで、Rust動かしたりJavaで書いたレイトレコードをネイティブコンパイルしたりしてみました。

Hyper-VUbuntuを用意する

ほんとはWindows Subsystem of Linux(WSL)でやりたかったのだけど、WSL上でJavaがちゃんと動いてくれなかったのであきらめました。
で、VirtualBox使うかなと思ったけど、Hyper-Vを無効にしないといけなくて、Hyper-Vを無効にするとDockerが動かなくなるのでやだなーと思ってたのだけど、普通にHyper-VUbuntuたちあげればいいのではーと思ってやってみました。


普通に使えますが、画面サイズ調整やフォルダ共有、クリップボード共有など、VirtualBoxのほうが使いやすいですね。
Macの場合はCE版ではなくOracle版を使うといいと思います。

RustをGraalVMで動かす

ASTエンジンTruffleというのは、プログラム言語を構文木に落として、最適化・実行する処理エンジンです。
JavaバイトコードLLVMのビットコードよりも抽象度が高く言語の構造が残っているため、最適化がやりやすいということを狙ってます。(たぶん)
で、そこでLLVMのビットコードをTruffleコードに変換するというSulongというツールもあるので、Rustも動きます。ライブラリなんかは、x86コードをLLVMビットコードに変換してTruffleコードに変換して動かすらしい。変態。


で、こんな感じでRustコードをmain.rsという名前で用意します。

fn main() {
  println!("Hello rust! fib(8) is {}", fib(8));
}

fn fib(i:i64) -> i64 {
  if i < 2 {
    i
  } else {
    fib(i - 2) + fib(i - 1)
  }
}


とりあえず普通にコンパイルして動かしてみます。

$ rustc main.rs
$ ./main
Hello rust! fib(8) is 21


では、これをLLVMのビットコードを吐きだしてGraalVMで動かしてみます。

$ rustc --emit=llvm-bc main.rs
$ ls -l main.*
-rw-rw-r-- 1 naoki naoki 6860  4月 23 01:37 main.bc
-rw-rw-r-- 1 naoki naoki  146  4月 23 01:36 main.rs
$ graalvm-1.0.0-rc1/bin/lli --lib $(rustc --print sysroot)/lib/libstd-* main.bc
Hello rust! fib(8) is 21

動きました!


Haskellでもいけるかなと思ったけど、LLVM3.5が必要っぽく、パッケージが3.7からしかないっぽく、とりあえずあきらめ。

レイトレをネイティブコンパイルしてみる

GraalVMにはAoTコンパイラもあります。AoTというのはAhead of Timeで、事前コンパイルのことです。これを使ってJavaコードをネイティブコンパイルしてみます。
それなりに処理量があるコードということで、こないだ作ったレイトレを動かしてみます。
kishida/smallpt4j: smallpt Java port


最新版はいろいろいじってて複数ソースに分かれてたりするので、初期のものを使います。
https://github.com/kishida/smallpt4j/blob/original/src/main/java/naoki/smallpt/SmallPT.java


パッケージをデフォルトパッケージに変更しておきます。また。FastMathを使っているのでjava.lang.Math.*をstatic importしてこちらを使います。
で、普通にコンパイルして実行

$ graalvm-1.0.0-rc1/bin/javac SmallPT.java
$ graalvm-1.0.0-rc1/bin/java SmallPT

画像ができました。

今回は早く終わるよう、画像を荒くしたままにしてます。


ではネイティブコンパイルを。
ネイティブコンパイルにはzlib.hが必要なので、とってきます。

$ sudo apt install zlib1g-dev


そしてnative-imageでコンパイル

$ graalvm-1.0.0-rc1/bin/javac SmallPT.java 
$ graalvm-1.0.0-rc1/bin/native-image SmallPT
Build on Server(pid: 7220, port: 26681)
   classlist:     166.86 ms
       (cap):     544.93 ms
       setup:     803.27 ms
    analysis:   3,460.75 ms
error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Unsupported method java.lang.ClassLoader.getParent() is reachable: The declaring class of this element has been substituted, but this element is not present in the substitution class
To diagnose the issue, you can add the option -H:+ReportUnsupportedElementsAtRuntime. The unsupported element is then reported at run time when it is accessed the first time.

なんかエラー。
ImageIOがだめっぽい。動的にクラスロードしてるからだろうな。
※ 2018/11/14追記 GraalVM Community Edition 1.0 RC9ではImageIOに対応していてそのままnative-imageが通ります。


ということで、ImageIOをやめてオリジナルのsmallptと同様にppmを吐き出すように変更します。

        try(BufferedWriter bw = Files.newBufferedWriter(Paths.get("image.ppm"));
            PrintWriter pw = new PrintWriter(bw)) {
            pw.printf("P3\n%d %d\n%d\n", w, h, 255);
            for (Vec v : c) {
                pw.printf("%d %d %d ", toInt(v.x), toInt(v.y), toInt(v.z));
            }
        }

ということで、全体のコードはこちらに
SmallPT4j without ImageIO


改めてJavaで動かします。

$ graalvm-1.0.0-rc1/bin/javac SmallPT.java 
$ graalvm-1.0.0-rc1/bin/java SmallPT 
JVM:1.8.0_161 sample:40
Samples:40 Type:master Time:PT24.042S


これをネイティブコンパイル

$ graalvm-1.0.0-rc1/bin/native-image SmallPT
Build on Server(pid: 7220, port: 26681)
   classlist:     167.94 ms
       (cap):     592.71 ms
       setup:     808.36 ms
  (typeflow):   2,479.49 ms
   (objects):     875.25 ms
  (features):      24.48 ms
    analysis:   3,439.52 ms
    universe:     134.33 ms
     (parse):     318.66 ms
    (inline):     550.73 ms
   (compile):   2,101.23 ms
     compile:   3,249.17 ms
       image:     466.18 ms
       write:     120.34 ms
     [total]:   8,412.47 ms
$ ls -l smallpt
-rwxrwxr-x 1 naoki naoki 5696832  4月 23 02:27 smallpt

こんどは いけました。ネイティブファイルができてますね。そして5MB。案外小さい。


実行してみます。

$ rm image.ppm 
$ ./smallpt 
JVM:null sample:40
Samples:40 Type:master Time:PT37.341S
$ ls -l image.ppm
-rw-rw-r-- 1 naoki naoki 8244655  4月 23 02:28 image.ppm

というか、遅くなっている。。。24秒だったのが37秒に。


ついでに、通常のOpenJDKで動かしてみます。

$ jdk1.8.0_171/bin/java SmallPT 
JVM:1.8.0_171 sample:40
Samples:40 Type:master Time:PT18.565S
$ jdk-10.0.1/bin/java SmallPT 
JVM:10.0.1 sample:40
Samples:40 Type:master Time:PT16.400068S

GraalVMのJVMより速いし、JDK10はさらに速い!


ということでこんな感じに。

GraalVM native-image JDK8 JDK10
1.8.0_161 null 1.8.0_171 10.0.1
24.042 37.341 18.565 16.400068

というか、Durationの精度もJDK10であがってますね。


ということで、いろいろやってみました。
Swingアプリも試してみたけど、AppContextの初期化でインプットメソッドの処理をしようとしたときに動的クラスロードしてるっぽくてそこでダメですね。
まだまだ問題はありそうだけど、もっと開発が進むと面白そう。