概要
このエントリでは、GraalVMの「[Native Image]」(https://www.graalvm.org/docs/reference-manual/native-image/) の機能を用いて、Javaのコンソールプログラムの起動時間を短くすることを扱います。
要約すると
あれこれやったら、特定の計算機(OCIのマイクロインスタンス相当)の性能で、JVMでの起動に1秒程度かかっていたプログラムが、ネイティブバイナリ化することで、起動が100ms程度になりました。
想定読者
- Javaについて基本的な知識がある方
- GraalVMのネイティブバイナリ作成について興味がある方
やりたいこと
- Javaで書いた、コンソールアプリケーションがある。別エントリの「Javaでコンソールでのキー入力を1文字ずつハンドルしたいときにJLineを使う」(https://qiita.com/hrkt/items/885f1c3526af03939c54)で筆者が書いたものです。
- コンソールのキー入力イベントを1文字ずつ扱いたいので、JLine3を使っています。JLine3がプラットフォーム固有の機能を使用するためにJNIを使うにあたり、JANSI版のターミナルを使用しています(なので、このエントリのような一工夫が必要です)
- Native Imageでどの程度早くなるかを単純に見てみたい
用意
環境
- Linux(x86_64)もしくはmacOSが使えればよいです。このエントリでは別エントリ「cdr/code-serverとOCIのAlways FreeのMicroインスタンスでVS Codeを動かしてみる」(https://qiita.com/hrkt/items/44c78f71eb72f9d29f38)と同じものを使っています。
GraalVMとnative-imageの用意
こんな感じで、/opt以下に導入しました。このエントリ執筆時点でのGraalVMは、19.3.1が最新版です。
cd /opt
sudo curl -L -O https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-linux-amd64-19.3.1.tar.gz
sudo tar zxvf graalvm-ce-java8-linux-amd64-19.3.1.tar.gz
GraalVMのツール「gu」を使い、Native Imageのためのツールを導入します。
sudo /opt/graalvm-ce-java8-19.3.1/bin//gu install native-image
native-imageが動作するには、対象としているプラットフォームで、バイナリをビルドするためのツールキット群が必要です。
このエントリでは、CentOS7を使っているため、下記のようにパッケージ群をインストールします。
sudo yum install -y glibc-devel zlib-devel gcc
対象のJavaプログラムを用意
このエントリで使うプログラムは、簡単なコンソール電卓です。GitHubのリリースはこちらです。https://github.com/hrkt/commandline-calculator/releases/tag/0.0.4
cd master
../gradlew :calculator-cli-without-spring:shadowJar
とすると、calculator-cli-without-spring/build/libs/calculator-cli-without-spring-0.0.1-SNAPSHOT-all.jar が出来上がります。このJarファイルは、自作部分と利用来ぶりのJarを一つにまとめてくれる、「Gradle Shadow」プラグインを用いて、実行に必要なファイルを1つのJARファイルにまとめたものです。出来上がったJARファイルは約760KBでした。
$ ls -la calculator-cli-without-spring/build/libs/calculator-cli-without-spring-0.0.1-SNAPSHOT-all.jar
-rw-rw-r--. 1 opc opc 757383 Jan 31 23:13 calculator-cli-without-spring/build/libs/calculator-cli-without-spring-0.0.1-SNAPSHOT-all.jar
実行して、「1+1」を実行してみます。
[opc@instance-20200117-1627 commandline-calculator]$ java -jar calculator-cli-without-spring/build/libs/calculator-cli-without-spring-0.0.1-SNAPSHOT-all.jar
Calculator is running. Press ctrl-c to exit.(boot in 1272 msec.)
1+1=
1+1
=2
q
[opc@instance-20200117-1627 commandline-calculator]$
このとき、プログラムの起動に約1.3秒かかっていることがわかります。時間の計測は、下記のような方法で実施しています。
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
long uptimeInMillis = runtimeMXBean.getUptime();
terminal.writer().println(String.format("Calculator is running. Press ctrl-c to exit.(boot in %d msec.)",
uptimeInMillis));
Native Imageのビルド
準備
上記で作ったJARファイルをいったんほどいて、作業用のスペースに展開します。
cd calculator-cli-without-spring/build/libs
mkdir inside
cd inside
jar xvf ../calculator-cli-without-spring-0.0.1-SNAPSHOT-all.jar
このあと、native-imageで利用するためのプロファイル情報を取得するためのディレクトリを作成します。
mkdir -p META-INF/native-image
実行時のプロファイルを取得
native-image実行前に、普通のJavaプログラムとして実行します。agentとともに動かすことで、動作時にどのようなリフレクションを使っているか、JNIのライブラリを使用しているかなどの、静的な解析では取得できない情報を集めます。
/opt/graalvm-ce-java8-19.3.1/bin/java -agentlib:native-image-agent=config-output-dir=META-INF/native-image -cp . com.hrkt.commandlinecalculator.Main
前述したものと同じように「1+1=」を実行して「q」を入力してプログラムを終了した時点で、下記のような情報が集まります。
[opc@instance-20200117-1627 inside]$ ls -la META-INF/native-image/
total 16
drwxrwxr-x. 2 opc opc 109 Jan 31 23:15 .
drwxrwxr-x. 6 opc opc 88 Jan 31 23:15 ..
-rw-rw-r--. 1 opc opc 689 Jan 31 23:15 jni-config.json
-rw-rw-r--. 1 opc opc 4 Jan 31 23:15 proxy-config.json
-rw-rw-r--. 1 opc opc 366 Jan 31 23:15 reflect-config.json
-rw-rw-r--. 1 opc opc 472 Jan 31 23:15 resource-config.json
native-imageでバイナリを作成
ビルドします。
オプションの意味については、サイトに説明があります。
/opt/graalvm-ce-java8-19.3.1/bin/native-image --no-server --no-fallback --report-unsupported-elements-at-runtime -cp . com.hrkt.commandlinecalculator.Main
これには、分のオーダーで時間がかかります。気長に待ちましょう。このエントリの環境と対象プログラムでは、約5分かかります。
[com.hrkt.commandlinecalculator.main:13836] classlist: 11,879.57 ms
[com.hrkt.commandlinecalculator.main:13836] (cap): 3,604.46 ms
[com.hrkt.commandlinecalculator.main:13836] setup: 9,898.60 ms
[com.hrkt.commandlinecalculator.main:13836] (typeflow): 36,059.72 ms
[com.hrkt.commandlinecalculator.main:13836] (objects): 23,227.98 ms
[com.hrkt.commandlinecalculator.main:13836] (features): 3,516.18 ms
[com.hrkt.commandlinecalculator.main:13836] analysis: 63,682.08 ms
[com.hrkt.commandlinecalculator.main:13836] (clinit): 1,281.12 ms
[com.hrkt.commandlinecalculator.main:13836] universe: 3,092.99 ms
[com.hrkt.commandlinecalculator.main:13836] (parse): 10,531.88 ms
[com.hrkt.commandlinecalculator.main:13836] (inline): 28,108.04 ms
[com.hrkt.commandlinecalculator.main:13836] (compile): 176,012.21 ms
[com.hrkt.commandlinecalculator.main:13836] compile: 227,565.33 ms
[com.hrkt.commandlinecalculator.main:13836] image: 6,261.29 ms
[com.hrkt.commandlinecalculator.main:13836] write: 2,044.20 ms
[com.hrkt.commandlinecalculator.main:13836] [total]: 326,546.79 ms
実行
出来上がったバイナリを動かしてみます。様子を見るため、数回起動してみます。2桁msecの前半から、遅くても100msec程度までで、起動できていることがわかります。JVMでの起動と比較して、かなり起動が早くなっています。
[opc@instance-20200117-1627 inside]$ ./com.hrkt.commandlinecalculator.main
Calculator is running. Press ctrl-c to exit.(boot in 66 msec.)
^C
[opc@instance-20200117-1627 inside]$ ./com.hrkt.commandlinecalculator.main
Calculator is running. Press ctrl-c to exit.(boot in 14 msec.)
^C
[opc@instance-20200117-1627 inside]$ ./com.hrkt.commandlinecalculator.main
Calculator is running. Press ctrl-c to exit.(boot in 16 msec.)
^C
[opc@instance-20200117-1627 inside]$ ./com.hrkt.commandlinecalculator.main
Calculator is running. Press ctrl-c to exit.(boot in 24 msec.)
^C
[opc@instance-20200117-1627 inside]$ ./com.hrkt.commandlinecalculator.main
Calculator is running. Press ctrl-c to exit.(boot in 131 msec.)
^C
[opc@instance-20200117-1627 inside]$ ./com.hrkt.commandlinecalculator.main
Calculator is running. Press ctrl-c to exit.(boot in 27 msec.)
^C
バイナリを確認してみる。
まず、種類を確認してみます。
[opc@instance-20200117-1627 inside]$ file com.hrkt.commandlinecalculator.main
com.hrkt.commandlinecalculator.main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=b5ce8429a80c97c4e12c87c516a8b24b59c64086, not stripped
ごく普通のELFバイナリですね。どんなライブラリを使っているかというと....
[opc@instance-20200117-1627 inside]$ ldd com.hrkt.commandlinecalculator.main
linux-vdso.so.1 => (0x00007fff5abf9000)
libm.so.6 => /lib64/libm.so.6 (0x00007f74c3bf7000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f74c39db000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f74c37d7000)
libz.so.1 => /lib64/libz.so.1 (0x00007f74c35c1000)
librt.so.1 => /lib64/librt.so.1 (0x00007f74c33b9000)
libc.so.6 => /lib64/libc.so.6 (0x00007f74c2feb000)
/lib64/ld-linux-x86-64.so.2 (0x00007f74c3ef9000)
Linuxのディストリビューションにおいて、ごく標準的なライブラリを使用しています。で、サイズは....
[opc@instance-20200117-1627 inside]$ ls -la com.hrkt.commandlinecalculator.main
-rwxrwxr-x. 1 opc opc 10066064 Feb 1 04:08 com.hrkt.commandlinecalculator.main
約10MB。かなり膨らみました。
まとめ
このエントリでは、Javaのコンソールプログラムの起動をGraalVMのnative-imageで高速にしてみることを扱いました。
「起動」までについては調べましたが、そのあとに継続的に動作しているときの速度について言及しているわけではない点にご注意ください。
補足
筆者が書いていたプログラム、JNIを使うために、別エントリで書いているように最初はJNAを使っていたのですが、native-imageがうまく実行できませんでした。GitHubにも、(Using JNA inside a native image does not work #673)[https://github.com/oracle/graal/issues/673]といったやりとりがありました。
このエントリで利用した、JNAを利用している機能に対して、GraalVMのNative Image側ではまだサポートしている状況ではないようです。