• OSS

まだ Java のプログラムを「ドジでノロマな亀」なんて言いますか?

満員電車の中、マガジンを読んでいて不意にこみ上げた涙を老眼鏡でなんとか誤魔かせた(?)今日このごろ。。。
こんにちは、鰈崎です。

Javaのプログラムは、実行環境となるJVM(Java仮想マシン)自体が巨大で特に起動時間が遅いと言われています。

最近のMicroserviceやFaaS(Function as a Service)でJavaを使おう!なんて言うと正気の沙汰かと言われかねません。。。

が、しかし。。。ここにGraalVMという救世主が登場しました!

ここではGraalVMを使ってJavaプログラムをどのようにスリムかつ素速く起動するようにできるかを紹介します。

GraalVMとは


GraalVMは、Oracleによって開発された新しい仮想マシンです。大きな特徴としては、

  1. Polyglot VM
  2. Nativeイメージ(プラットフォーム毎のバイナリ)生成

Polyglot VMはちょっと聞き慣れない言葉かもしれません。GraalVMでは、JavaやKotlinなどのJVM言語だけでなく、JavaScript・Python・Ruby・Rや、C、C++のようなLLVMの言語もサポートしており、しかも相互に呼び出し可能になっています。

また、各プラットフォームのNativeイメージをバイナリ生成することができます。このことにより、バイナリのサイズ縮小・起動時間の短縮に大きく繋がります。

この記事では、Nativeイメージの生成についてみていきます。

GraalVMの入手

公式サイトのダウンロードページにはCommunity Edition(CE)とEnterprise Edition(EE)があります。
今回はCE版とEE版の比較もしてみます。

CE版はGithubで配布されています。今回は先日リリースされたばかりの19.1.0をダウンロードします。
MacOS、Linux、Windowsのプラットフォーム用のバイナリが用意されていますので適切なものをダウンロードします。
今回の記事では https://github.com/oracle/graal/releases/download/vm-19.1.0/graalvm-ce-windows-amd64-19.1.0.zip をダウンロードしました。

EE版はOTN(Oracle Technology Network)で配布されています。OTNアカウントが必要ですが、テスト・評価目的であれば無償で利用できます。https://download.oracle.com/otn/utilities_drivers/oracle-labs/graalvm-ee-windows-amd64-19.1.0.zip をダウンロードしました。

※ Windows版はまだどちらもアーリーアダプター版の位置づけのようですね。

ダウンロードしたあとは、適当な場所で展開しておきます。

Nativeイメージ作成のための準備(Windowsのみ)

今回は以下のような環境でテストしました。

  • Windows 10 Pro 1809
  • Corei5-6200 CPU 2.30GHz
  • RAM: 8.00GB

WindowsでのNativeイメージ作成には、GraalVMだけでなく他にも準備が必要です。

Windows SDK 7.1のインストール

GraalVM でNativeイメージを作成するために、Windows SDK 7.1をインストールしてやる必要があります。普通にWindows SDK 7.1をインストールしようとすると、Windows 10には.NET Framework4がバンドルされているにもかかわらず、.NET Framework4が無いからC++コンパイラがインストールできない問題があります。

Windows 10にWindows SDK 7.1をインストールする方法」のページを参考にしてISOイメージからインストールするとうまくインストールできました。

私はまだ試していませんが、MacOSやLinuxだと特に苦労することも無いかもしれません。

GraalVMを使ってNativeイメージの作成

さて、いよいよNativeイメージを作成してみましょう。

以降のコマンドは、WindowsではWindows SDK 7.1 Command Promptを使って行います。まずはおなじみのHelloWorldを試してみましょう。

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello");
    }
}

GraalVMを展開したディレクトリを{GRAALVM_HOME}とします。

$ {GRAALVM_HOME}\bin\javac Hello.java
$ {GRAALVM_HOME}\bin\native-image Hello
[hello:7824]    classlist:   6,452.29 ms
[hello:7824]        (cap):   8,626.80 ms
[hello:7824]        setup:  11,312.93 ms
[hello:7824]   (typeflow):  13,516.85 ms
[hello:7824]    (objects):  12,402.19 ms
[hello:7824]   (features):   1,014.54 ms
[hello:7824]     analysis:  27,283.95 ms
[hello:7824]     (clinit):     359.25 ms
[hello:7824]     universe:   1,963.40 ms
[hello:7824]      (parse):   2,795.60 ms
[hello:7824]     (inline):   3,839.22 ms
[hello:7824]    (compile):  23,181.84 ms
[hello:7824]      compile:  30,794.00 ms
[hello:7824]        image:   1,647.00 ms
[hello:7824]        write:   1,789.05 ms
[hello:7824]      [total]:  81,721.78 ms

少し(?)時間がかかりますが、コンパイルが終了するとhello.exeができあがっています。サイズはなんと8MB!こんなにもコンパクトで驚きですよね!

では、ちゃんと動くのか実行してみましょう。

$ .\hello
Hello

ま、当たり前ですがちゃんと動きましたね。ですが、100MB以上のJVMを必要とせず、8MBのバイナリだけで動くというところが大きなポイントです。

他の言語との比較

さて、ここでは最近の軽量・高速と呼ばれているnode.js(JavaScript)、GO言語に登場してもらいパフォーマンスを簡単に比較してみましょう。

お題は「モンテカルロ法による円周率の計算」です。
プログラムのソースコードは以下のような感じです。

Pi.java

import java.util.Random;
 
public class Pi {
    public static void main(String[] args) {
          int n = Integer.parseInt(args[0]);
          double point = 0;
          double pi = 0;
          Random r = new Random();
          for (int i = 0; i < n; i++) {
              double x = r.nextDouble();
              double y = r.nextDouble();
               
              if ((x*x + y*y) < 1.0) {
                point += 1;
              }
              pi = 4.0 * point / n;
          }
          System.out.println(pi);
    }
}

pi.js

let point = 0;
const N = Number(process.argv[2])
let pi = 0
 
for (let i = 0; i < N; i++) {
    let x = Math.random()
    let y = Math.random()
 
    if ((x*x + y*y) < 1.0) {
        point += 1
    }
 
    pi = 4.0 * point / N
}
 
console.log(pi)

pi_go.go

package main
 
import (
    "fmt"
    "os"
    "strconv"
    "math/rand"
    "time"
)
 
func main() {
    var N int
     
    N, _ = strconv.Atoi(os.Args[1])
     
    point := 0.0
    pi := 0.0
    rand.Seed(time.Now().UnixNano())
    for i := 0; i < N; i++ {
        x := rand.Float64()
        y := rand.Float64()
        if x*x + y*y < 1.0 {
            point += 1
        }
 
        pi = 4.0 * point / float64(N)
    }
    fmt.Printf("%f\n", pi)
}

パフォーマンス測定結果

単純に100000ループでtimeコマンドで測定しました。

言語 経過時間(msec) サイズ(MB)
GraalVM-CE 19.1.0(Native) 165 8
GraalVM-EE 19.1.0(Native) 142 7
OpenJDK11 972 285
Node.js v8.9.4 558 42(node.exeだけでも22MB)
GO 1.12.7 122 2

いかがでしょうか?

パフォーマンスではGOと同等、Node.jsはもう遥かに置き去りにしていますね(^_-)-☆

CE版とEE版では、若干EE版の方が良い結果が出ているように見えるのは大人の事情でしょうか…

おわりに

GraalVMを使って、Javaプログラムを軽量・高速にする話はいかがだったでしょうか?今回はWindows 10だけでの実験だったのですが、他のプラットフォームだとまた結果も違ってくるかもしれません。

なお、Javaの実行環境であるJVMでは、長時間動作するようなプログラムであれば、実はNativeイメージにコンパイルして実行するよりも、従来どおり動かしたほうが有利な場合もあります。というより、そのような場合の方が今のところ多いかと思います。

ただ、最初にお話したように、MicroserviceやFaaSなど軽量なプログラムが求められる環境ではGraalVMのNativeイメージ生成の技術が活かされる場面もあるのではないでしょうか。

まだまだ、登場したばかりの新しい技術であるGraalVM、これからも暖かく見守っていきたいと思います。

Javaプログラムが「利口で速い兎」と呼ばれることを願って。。。

関連記事

  1. Zabbix 4.0の新機能の紹介

    Zabbix最新情報(2019年版)

PAGE TOP