LoginSignup
7
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-04-27

GraalVM では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
  • GraalVM Community Edition 1.0 RC1

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

CentOS7のコンテナを実行

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


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

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


コンテナに attach して中に入る。

$docker attach graalvmtryout


適当なディレクトリで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


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

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

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

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

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");

    }
}


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

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


コンテナで java コマンドによる実行を 3 ショット実施。大体 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


ネイティブバイナリを作成する、その前に glibc-devel、zlib-devel、gcc が必要とのことなので入れておく。

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


ネイティブバイナリ化実行。さくっと出来るのかと思ったらわりと時間かかった。あと出来上がったバイナリが元の 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


ネイティブバイナリの実行。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 億回作成。

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");

    }
}


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

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

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");

    }
}


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


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

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


サンプルコードの内容が雑なので一概には言えないかもしれないけど、そもそもネイティブ化はでかいデータをぐるぐるループするバッチのような処理には向いてないんだな……。

ちゃんとドキュメント読んでると現時点ではJavaならなんでもかんでもネイティブ化できるわけでもなく、いろんな制約があるようだ。小さくシンプルな処理をより速くということか。

追記

よくドキュメント読んだら今回の実験結果を裏付ける記述があった。

When does it make sense to run native images instead of the JVM?

  • When startup time and memory footprint is important.
  • When you want to embed Java code with existing C/C++ applications

What’s the typical performance profile on the SVM?

  • 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).

(意訳)

VMに代わってネイティブイメージを使うのはどのような場合が最適?

  • 起動時間とメモリのフットプリントが小さいことが重要な場合
  • C/C++ アプリケーションに Java のコードを埋め込みたい場合

一般的にSVMの性能はどんな感じ?

  • 現時点でピーク処理時の性能は HotSpot より若干劣るけど、いずれ改善するつもりなのでそれはあんまり言わないでおく

参考

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5