UZABASE Tech Blog

〜迷ったら挑戦する道を選ぶ〜 株式会社ユーザベースの技術チームブログです。

GraalVMに入門する

はじめに

はじめまして、2019年11月に入社しましたヒロオカ(@yuya_hirooka)です!!

先日、同僚との雑談の中でQuarkusのNative Image化が話題に挙がりました。その際に「そういえば、GraalVMとかNative Imageってよく聞くけどあまり知らないなぁ...」と思い色々と調べてみたのでまとめます。 この記事ではGraalVMの中核技術の概要を把握し、簡単にその機能を使ってみることをゴールとします。

前提知識

記事を読む上で以下のようなことを前提知識として進めます。
(つまるところ、私自身のレベル感です)

  • GraalVMという名前は聞いたことがある
  • Javaもしくはその他のJVM言語を使ってプログラムを書いたことがある
  • JVMやコンパイラに関するふんわりとした理解
    • JITコンパイル、AOTコンパイルと聞いてなんとなくどういったものかイメージがつく
    • JVMのC1、C2と聞いてなんとなくどういったものかイメージがつく

GraalVMとは

GraalVMはユニバーサルなVMで、JVM言語はもちろんのことJavaScript、Pythonなど動的言語、LLVM-basedなネイティブコード等の様々な言語を実行できます。
GraalVMは大きく以下2つの特徴を持っています。

  • Polyglot
  • Native

Polyglot

Weblioの訳では「数か国語に通じている人」と説明されています。訳の通りGraalVMは多言語対応されており、JVM言語以外の言語の実行環境としても動作します。
また、JavaからJavaScriptのAPIを呼び出すなど、多言語を混ぜたコードの記述が可能で、各言語のエコシステムを活かしたコーディングが可能になります。

Native

GraalVMではJIT(Just In Time)コンパイルとは別に、AOT(Ahead Of Tim)コンパイルと呼ばれるコンパイルを行なうことができ、JavaのコードをNative Imageと呼ばれるスタンドアローンで実行可能な形にコンパイルすることができます。Native Imageに関して詳しくは後述します。

GraalVMの中核技術

GraalVMはJVMの機能を包括しており、JVMのできることは基本的にすべて行えます。また、HotSpotVMをベースに作られています。
JVMのとは違ったGraalVMにおける中核の技術として以下の3つが挙げられます。

  • Graal
  • Truffle
  • GraalVM Native Image

Graal

GraalはGraalVMのにおけるJITコンパイルラです。JVMCI(JVM Compiler Interfae)を利用してC2コンパイラを置き換えます。JVMCIはJEP 243で定義されるJavaで、JITコンパイラを記述する際のインタフェースです。
これまでのJVMにおけるC2コンパイラやCompiler InterfaceはC++で実装されていますが、GraalVMでのGraalやJVMCIはJavaでの実装がされています。

f:id:yuya_hirooka:20200122144503p:plain
GraalVM

GraalとGraalVMは別物で、GraalVMというとJVMの機能を包括したVMを指し、GraalはJITコンパイラの事を指します。また、GraalはJava10以降のOpenJDKからの利用も可能です。(JEP 317)

Truffle

Truffleは言語実装フレームワークで、GraalVM上でJVM以外の言語が動作する際に使われる仕組みです。
このフレームワークを利用することにより、GraalVMはPolyglotなVMとして、さまざまな言語を動作させることができると同時に、独自の言語を実装することも可能です。

f:id:yuya_hirooka:20200122144537p:plain
Truffle

GraalVM Native Image

前述のようにGraalVMではJavaのコードをAOTコンパイルすることによりNative Image化を行なうことができます。
ここで、Native Imageは以下のようなものを含みます。

  • 依存関係にあるアプリケーションクラス群
  • 実行時に利用するJDKのクラス群
  • 静的にリンクされたJDKのネイティブコード
  • 「Substrate VM」と呼ばれるメモリーマネジメントやスレッドのスケジューリングなどの機能を持った実行時に必要なコンポーネント

Native Image化を行なうと以下のような特徴を得ることができます。

  • クラスローダなどのオーバヘッドがなくなり、起動が早くなる
  • JVM言語のメモリフットプリントを削減することができる

上記のような特徴からコンテナ化やサーバレスアーキテクチャでの利用の際にメリットがあると思われます。
ここで、注意すべきなのは、JITコンパイルではなくAOTコンパイルを行なうことにより、JITコンパイルの特徴(ホットスポットなコンパイル、OSの差異を吸収してくれる等)はなくなるということです。
また、リフレクションやダイナミックプロキシなどを行なう場合は、対象クラスを追加で設定するなど追加の作業が必要なようです。

余談ですが、リフレクションやダイナミックプロキシを多様しているSpring FramewarkでのNative Image化は複雑な設定を必要としたようですが、Springは5.3のアップデートで追加設定なしのNative Image化のサポートがされるようです。

Hello World

GraalVMの概要についてまとめたところで、実際に使ってみたいと思います。具体的にはHello Worldとして以下のことをやってみたいと思います。

  • GraalVMのインストール
  • JavaのHello Worldコードの実行する
  • JavaからJavaScript(Node.js)を呼び出す
  • Native Image化を行なう

実行環境

Hello Worldを実行する環境を以下にまとめます。

  • Ubuntu(18.04.3 LTS)
  • GraalVM(19.3.1)

GraalVMのインストール

まずはGraalVMをインストールします。

$ wget https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java11-linux-amd64-19.3.1.tar.gz

$ tar -zxvf graalvm-ce-java11-linux-amd64-19.3.1.tar.gz

$ mkdir /usr/lib/graal-vm

$ mv graalvm-ce-java11-19.3.1/ /usr/lib/graal-vm/

$ export PATH=/usr/lib/graal-vm/graalvm-ce-java11-19.3.1/bin:$PATH 

以上で、インストールは完了です。 インストールするディレクトリはどこでも良いですが。今回は/usr/libとしました。
Javaのバージョンを確認してみます。

$ java -version
openjdk version "11.0.6" 2020-01-14
OpenJDK Runtime Environment GraalVM CE 19.3.1 (build 11.0.6+9-jvmci-19.3-b07)
OpenJDK 64-Bit Server VM GraalVM CE 19.3.1 (build 11.0.6+9-jvmci-19.3-b07, mixed mode, sharing)

GraalVMをインストールすると、通常のJavaコマンドとは別に以下のコマンドが利用可能になります。

  • js : JavaScriptのコンソール起動コマンド
  • node : GraalVMのJavaScriptエンジンを利用したNode.js用のコマンド
  • lli : GraalVMに組み込まれたLLVMバイトコードインタプリタ用のコマンド
  • gu (GraalVM Updater) : RubyやPythonのLangageパッケージ等をインストールするためのコマンド

JavaでHello world

まずは、よくあるJavaのHello Worldのコードを動かしてみたいと思います。
HelloWorld.javaを作成し以下のソースコードを記述します。

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

コンパイルと実行は通常のJavaと同様にjavacjavaコマンドで行います。

$ javac HelloWorld.java
$ java HelloWorld
Hello, World!

ここまではとくに面白みのない見慣れたHello Worldです。

JavaからJavaScriptを呼び出す

GraalVM上で、JVM言語からゲスト言語(JVM言語以外の言語)をThe GraalVM Polyglot APIを利用することで可能となります。
たとえば、JavaからJavaScriptを呼び出すコードを以下のように記述します。

import org.graalvm.polyglot.*;

public class HelloPolyglot {
  public static void main(String[] args) {
    System.out.println("Hello, Java!");

    // Polyglot API
    Context context = Context.create();

    // 単純なJavaScriptの実行
    context.eval("js","print('Hello, JavaScript!');");

    // JavaScriptの関数を定義
    Value function = context.eval("js","x => print('Hello, ' + x);");
    function.execute("Ployglot!");
  }
}

org.graalvm.polyglot.Contextクラスはゲスト言語に対する、実行環境を提供します。
実際のコードの実行はevalメソッドを利用します。第一引数に言語のIDを第二引数に実際のコードを記述します。
また、evalの実行結果をorg.graalvm.polyglot.Valueクラスで受け取ることにより、引数ありの関数としても定義することもできます。

コンパイルと実行は通常のJavaと同様にjavacjavaコマンドで行います。

$ javac HlloPolyglot.java
$ java HelloPloyglot
Hello, Java!
Hello, JavaScript!
Hello, Ployglot!

このようにして、GraalVMではPolyglotなコードの記述が可能となります。

Native Image化を行なう

GraalVMのもう1つの大きな機能であるNative Image化を試して見たいと思います。

native-imageコマンドのインストール

GraalVMのComunity Editionでは、Native Image化にnative-imageコマンドを利用できます。native-imageコマンドのインストールはguを利用できます。
また、native-imageコマンドを利用するためには以下の3つが事前にインストールされている必要があります。

  • glibc-devel
  • zlib-devel
  • gcc

以下のコマンドで、native-imageコマンドをインストールします。

$ gu install native-image

HelloWorldクラスをNative Image化

さきほどのJavaのHello Worldコードをネイティブイメージ化し、その実効速度の差を見てみます。

$ native-image HelloWorld

# JITコンパイルでの実行
$ time java HelloWorld
real    0m0.072s
user    0m0.075s
sys 0m0.025s

# Native Imageの実行
$ time ./helloworld
real    0m0.002s
user    0m0.002s
sys 0m0.000s

実行タイミングや環境にもよりますが、プログラムの実行速度を表すuserを比較すると30~100倍ほど早く実行されます。
前述していますが、Native Image化を行って早くなるのはあくまで起動速度みたいなので、デーモンのような常駐するプロセスでは恩恵が薄れていくと思われます。

HelloPolyglotクラスをNative Image化

今度は、JavaScriptを呼び出していたHelloPolyglotのクラスをNative Image化したいと思います。 この場合注意が必要で、さきほどと同じコマンドでNative Image化を行なうと実行時に以下のようなエラーが吐かれます。

$ native-image HelloPolyglot
$ ./hellopolyglot 
Hello Java!
Exception in thread "main" java.lang.IllegalStateException: No language and polyglot implementation was found on the classpath. Make sure a language is added to the classpath (e.g., native-image --language:js).
    at org.graalvm.polyglot.Engine$PolyglotInvalid.noPolyglotImplementationFound(Engine.java:831)
    at org.graalvm.polyglot.Engine$PolyglotInvalid.buildEngine(Engine.java:752)
    at org.graalvm.polyglot.Engine$Builder.build(Engine.java:506)
    at org.graalvm.polyglot.Context$Builder.build(Context.java:1444)
    at org.graalvm.polyglot.Context.create(Context.java:709)
    at HelloPolyglot.main(HelloPolyglot.java:6)

これは、通常のコンパイルではJavaScriptエンジンがImageの中に含まれないからです。 エラーメッセージにかかれている通り--language:jsフラグを指定して再度コンパイルしてみます。

$ native-image --language:js HelloPolyglot

$ ./hellopolyglot 
Hello Java!
Hello JavaScript!
Hello Ployglot

今度は実行できました。

終わりに(感想)

GraalVMについて調べて、少しだけその機能を試してみました。
前述しましたが、Truffleを使えば俺々言語みたいなのも実装できそうですし、理解が深まればまだまだ面白そうな点があるように感じました。
GraalVMで検索すればより質の高い記事や資料が日本語でたくさん見つかるので、より詳しくはそちらを参照してください。

参考資料