GraalVMでJavaコードをネイティブ化したけど高速化しなかったケースもあった

[GraalVM](https://www.graalvm.org) ではJavaのネイティブ化ができるとのことなのでどれぐらい速くなるのか実験。結論からいうと、比較的小さな処理については高速化が見込まれるが、やたらデータがでかかったりループ回数が果てしない処理だとむしろ遅くなることがわかった。

# 実行環境
MacにDocker入れて、CentOS7コンテナを実行、その上にGraalVMをインストール。

- MacBook Air (13-inch, Early 2015)
- macOS High Sierra バージョン 10.13.3
- プロセッサ 1.6 GHz Intel Core i5
- メモリ 8 GB 1600 MHz DDR3
- Docker Version 18.03.0-ce-mac59 (23608)
- [CentOS7](https://hub.docker.com/_/centos/)
- [GraalVM Community Edition 1.0 RC1](https://github.com/oracle/graal/releases)

# CentOS7コンテナの作成とGraalVMのインストール

CentOS7のコンテナを実行

```
$ docker run -dti -h graalvmtryout --name graalvmtryout centos:latest /bin/bash
```

</br>
ダウンロードしてきたGraalVMをコンテナにコピー（ Mac だとダウンロード後に .gz が勝手に解凍されるので すでに tar になっている）。

```
$ docker cp ~/Downloads/graalvm-cmp-gu-gvm-ins-js-njs-polynative-pro-rgx-slg-svm-tfl.tar graalvmtryout:/tmp
```

</br>
コンテナに attach して中に入る。

```
$docker attach graalvmtryout
```

</br>
適当なディレクトリでtarを解凍、パスを通す。

```
[root@graalvmtryout /]# mv /tmp/graalvm-cmp-gu-gvm-ins-js-njs-polynative-pro-rgx-slg-svm-tfl.tar /opt
[root@graalvmtryout opt]# tar -xvf graalvm-cmp-gu-gvm-ins-js-njs-polynative-pro-rgx-slg-svm-tfl.tar
[root@graalvmtryout opt]# export PATH=/opt/graalvm-1.0.0-rc1/bin:$PATH
```

</br>
サンプルコード用のディレクトリを作成しておく。

```
[root@graalvmtryout opt]# mkdir -p /work/sample
```

# サンプルコードの作成と実行１

性能測定のために Object の2次元配列インスタンスを1000万回作成するという重たい処理を実行するどうでもいいサンプルコード。

```java:Sample.java
package org.test.graalvm;

public class Sample {

	public static void main(String[] args) {

		double st = (double)System.currentTimeMillis();

		int max = 10000000;
		for (int i = 0; i < max; i++) {
			Object[] obj = new Object[10][10];
		}

		double ed = (double)System.currentTimeMillis();

		System.out.println("Elapsed time: " + String.format("%.3f", (ed -st)/1000) + " seconds");

	}
}
```

</br>
これを sample.jar という実行可能 jar ファイルに固めてコンテナにアップロード。

```
$ docker cp /work/tmp/sample.jar graalvmtryout:/work/sample
```

</br>
コンテナで java コマンドによる実行を ３ ショット実施。大体 8 秒。

```
[root@graalvmtryout /]# cd /work/sample 
[root@graalvmtryout sample]# java -jar sample.jar 
Elapsed time: 7.975 seconds
[root@graalvmtryout sample]# java -jar sample.jar 
Elapsed time: 8.157 seconds
[root@graalvmtryout sample]# java -jar sample.jar 
Elapsed time: 8.113 seconds
```

</br>
ネイティブバイナリを作成する、その前に [glibc-devel、zlib-devel、gcc が必要](http://www.graalvm.org/docs/reference-manual/aot-compilation/#prerequisites)とのことなので入れておく。

```
[root@graalvmtryout sample]# yum install glibc-devel
[root@graalvmtryout sample]# yum install zlib-devel
[root@graalvmtryout sample]# yum install gcc
```

</br>
ネイティブバイナリ化実行。さくっと出来るのかと思ったらわりと時間かかった。あと出来上がったバイナリが元の 1.7KB から 3200 倍膨れ上がって 5MB にもなってる。

```
[root@graalvmtryout sample]# native-image -jar sample.jar
Build on Server(pid: 192, port: 26681)
   classlist:     676.35 ms
       (cap):   1,509.13 ms
       setup:   3,103.09 ms
  (typeflow):   9,686.07 ms
   (objects):   2,531.50 ms
  (features):      54.01 ms
    analysis:  12,417.70 ms
    universe:     472.40 ms
     (parse):   3,432.62 ms
    (inline):   1,880.51 ms
   (compile):  12,995.05 ms
     compile:  18,884.15 ms
       image:   1,096.32 ms
       write:     391.15 ms
     [total]:  37,130.09 ms
[root@graalvmtryout sample]# ls -ltr
total 5244
-rw-r--r-- 1  501 root    1655 Apr 26 09:46 sample.jar
-rwxr-xr-x 1 root root 5364232 Apr 27 00:50 sample
```

</br>
ネイティブバイナリの実行。50% 以上高速化してる！

```
[root@graalvmtryout sample]# ./sample
Elapsed time: 3.771 seconds
[root@graalvmtryout sample]# ./sample
Elapsed time: 3.849 seconds
[root@graalvmtryout sample]# ./sample
Elapsed time: 3.759 seconds
```

# サンプルコードの作成と実行2

今度は同じコードでもう少し負荷を増やしてインスタンスを1000万 → 1 億回作成。

```java:Sample.java
package org.test.graalvm;

public class Sample {

	public static void main(String[] args) {

		double st = (double)System.currentTimeMillis();

		int max = 100000000;
		for (int i = 0; i < max; i++) {
			Object[] obj = new Object[10][10];
		}

		double ed = (double)System.currentTimeMillis();

		System.out.println("Elapsed time: " + String.format("%.3f", (ed -st)/1000) + " seconds");

	}
}
```

</br>
Java の実行、ネイティブバイナリの作成と実行。実行回数が10倍になったので大体実行時間も10倍ぐらいという順当な結果。

```
[root@graalvmtryout sample]# java -jar sample.jar
Elapsed time: 72.728 seconds
[root@graalvmtryout sample]# native-image -jar sample.jar
Build on Server(pid: 192, port: 26681)
   classlist:   1,457.14 ms
       (cap):     838.34 ms
       setup:   1,325.01 ms
  (typeflow):   5,672.31 ms
   (objects):   1,403.75 ms
  (features):      52.52 ms
    analysis:   7,371.14 ms
    universe:     243.41 ms
     (parse):   1,554.09 ms
    (inline):   1,105.42 ms
   (compile):   8,194.68 ms
     compile:  11,149.07 ms
       image:     696.83 ms
       write:     153.56 ms
     [total]:  22,446.04 ms
[root@graalvmtryout sample]# ./sample
Elapsed time: 37.175 seconds
```

# サンプルコードの作成と実行3

先ほどの結果はあまり面白くなかったので実行回数ではなく配列のサイズを無駄に大きくしてみる。

```java:Sample.java
package org.test.graalvm;

public class Sample {

	public static void main(String[] args) {

		double st = (double)System.currentTimeMillis();

		int max = 10000000;
		for (int i = 0; i < max; i++) {
			Object[] obj = new Object[100][100];
		}

		double ed = (double)System.currentTimeMillis();

		System.out.println("Elapsed time: " + String.format("%.3f", (ed -st)/1000) + " seconds");

	}
}
```

</br>
Java の実行、ネイティブバイナリの作成と実行。あれ、ネイティブの方が遅くなってる。

```
[root@graalvmtryout sample]# java -jar sample.jar
Elapsed time: 110.661 seconds
[root@graalvmtryout sample]# java -jar sample.jar
Elapsed time: 112.828 seconds
[root@graalvmtryout sample]# java -jar sample.jar
Elapsed time: 112.074 seconds
[root@graalvmtryout sample]# native-image -jar sample.jar
Build on Server(pid: 192, port: 26681)
   classlist:     648.54 ms
       (cap):     809.75 ms
       setup:   1,162.87 ms
  (typeflow):   4,356.15 ms
   (objects):   1,212.20 ms
  (features):      45.31 ms
    analysis:   5,701.89 ms
    universe:     233.38 ms
     (parse):   1,367.30 ms
    (inline):   1,228.59 ms
   (compile):   6,934.35 ms
     compile:   9,796.12 ms
       image:     471.23 ms
       write:     159.25 ms
     [total]:  18,368.38 ms
[root@graalvmtryout sample]# ./sample
Elapsed time: 134.104 seconds
```

</br>
もう 2 ショット実施。結果変わらず。

```
[root@graalvmtryout sample]# ./sample
Elapsed time: 134.680 seconds
[root@graalvmtryout sample]# ./sample
Elapsed time: 134.116 seconds
```

</br>
サンプルコードの内容が雑なので一概には言えないかもしれないけど、そもそもネイティブ化はでかいデータをぐるぐるループするバッチのような処理には向いてないんだな……。
</br>
ちゃんと[ドキュメント](https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md)読んでると現時点ではJavaならなんでもかんでもネイティブ化できるわけでもなく、いろんな制約があるようだ。小さくシンプルな処理をより速くということか。

# 追記
よく[ドキュメント](http://www.graalvm.org/docs/reference-manual/aot-compilation/)読んだら今回の実験結果を裏付ける記述があった。

><i>When does it make sense to run native images instead of the JVM?</I>
>
> - When startup time and memory footprint is important.
> - When you want to embed Java code with existing C/C++ applications
>
><i>What’s the typical performance profile on the SVM?</i>
>
> - Right now peak performance is a bit worse than HotSpot, but we don’t want to advertise that (and we want to fix it of course).


（意訳）
><i>VMに代わってネイティブイメージを使うのはどのような場合が最適？</i>
>
> - 起動時間とメモリのフットプリントが小さいことが重要な場合 
> - C/C++ アプリケーションに Java のコードを埋め込みたい場合
>
> <i>一般的にSVMの性能はどんな感じ？</i>
>
> - 現時点でピーク処理時の性能は HotSpot より若干劣るけど、いずれ改善するつもりなのでそれはあんまり言わないでおく
>

# 参考
- [GraalVM](https://www.graalvm.org/)
- [オラクル、JavaやJavaScript、Ruby、Pythonなど多言語対応を単一ランタイムで実現する「GraalVM」をオープンソースで公開。Twitterが本番環境で採用](http://www.publickey1.jp/blog/18/javajavascriptrubypythongraalvmtwitter.html)
- [VirtualBoxのLinuxにGraalVMを入れてpythonのスクリプトを動かしてみた。](https://qiita.com/ora_gonsuke777/items/2d9ba7d039aaea3a5c5a)