はじめに
はじめまして、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での実装がされています。
GraalとGraalVMは別物で、GraalVMというとJVMの機能を包括したVMを指し、GraalはJITコンパイラの事を指します。また、GraalはJava10以降のOpenJDKからの利用も可能です。(JEP 317)
Truffle
Truffleは言語実装フレームワークで、GraalVM上でJVM以外の言語が動作する際に使われる仕組みです。
このフレームワークを利用することにより、GraalVMはPolyglotなVMとして、さまざまな言語を動作させることができると同時に、独自の言語を実装することも可能です。
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と同様にjavac
、java
コマンドで行います。
$ 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と同様にjavac
、java
コマンドで行います。
$ 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で検索すればより質の高い記事や資料が日本語でたくさん見つかるので、より詳しくはそちらを参照してください。