libgraal: GraalVM compiler as a precompiled GraalVM native image

このエントリは以下のエントリをベースにしています。
This entry is based on the following one written by Doug Simon (Manager with Oracle Labs).
https://medium.com/graalvm/libgraal-graalvm-compiler-as-a-precompiled-graalvm-native-image-26e354bee5c

また、本来であればコンパイラをGraalといいますが、原文ではGraalVMコンパイラと表現しています。今回は原文に合わせてGraalVMコンパイラと表現しています。

このエントリでは最近のGraalVMのアップデートであるlibgraalについて説明します。これはGraalVM Native Imageが生成する共有ライブラリで、この中にはコンパイル済みのGraalVMコンパイラが含まれています。JavaアプリケーションをGraalVM上で実行する場合、最上位層のJITコンパイラとしてlibgraalを利用します。

libgraal
https://github.com/oracle/graal/tree/master/compiler#libgraal
GraalVM Native Image
https://www.graalvm.org/docs/reference-manual/aot-compilation/

libgraalには様々な便益があります。例えばlibraalは起動速度を改善し、完全にヒープ利用なアプリケーションコードのプロファイリングによるオーバーヘッドを完全に回避します。つまり、コンパイラはようやく “codes like Java, runs like C++” (Javaのようなコードで、実行はC++のように)になりました。HotSpotの文脈でより具体的に言えば、libgraalはマネージドランタイムのほとんどのメリットを生かしながら、C2のように実行します。libgraalはGraalVM 19.1リリースで短時間および中時間動作するワークロードのコンパイル速度とパフォーマンスの改善に大きく貢献しています。詳細は以下のエントリをご覧ください。

GraalVM 19.1: Compiling Faster
https://medium.com/graalvm/graalvm-19-1-compiling-faster-a0041066dee4
https://logico-jp.io/2019/07/06/graalvm-19-1-compiling-faster/

The GraalVM compiler and native-image

まず、GraalVMコンパイラとnative-imageの関係に不慣れな人のために、背景をご紹介しましょう。コンパイラは、Javaバイトコードをマシンコードにコンパイルするために使用します。HotSpotをJITコンパイラとして使用する場合、アプリケーションのうち頻繁に実行される(すなわち、ホットな)バイトコードをコンパイルするだけです。native-imageツールは、GraalVMコンパイラを使用してJavaバイトコードをマシンコードにコンパイルしますが、アプリケーションのすべてのバイトコードを事前コンパイルします。GraalVMコンパイラ自体がJavaで書かれているので、ネイティブイメージの観点からGraalVMをアプリケーションとして扱うことができます。このようにして、実行時にコンパイル済みのマシンコードとしてすぐに実行できるバージョンのコンパイラが得られます。

Getting started with libgraal

JVM上で実行する場合、GraalVMのいくつかのリリースから、GraalVMコンパイラのlibgraalはデフォルトモードです。つまり、java ランチャーや任意の言語ランチャーを –jvm オプションを付けて利用する場合、全ての最上位層のコンパイルはlibgraalを使って実行されます。(現在の)レガシーモードからこの実行モードを厳密に表現するために、後者についてはjargraalという用語を使います。さらに、文脈が明らかな場合は、簡潔にするためにGraalVMコンパイラを単に「コンパイラ」と呼びます。

Warmup improvements

libgraalの主要なメリットは、コンパイルが最初から速いことです。これはコンパイラが最初からコンパイル済みの状態で実行され、HotSpotインタプリタを完全に迂回するからです。さらに、libgraalは単体でコンパイルされています。対して、jargraalの場合、C1でコンパイルされます。その結果、jargraalに比べてlibgraalではTコンパイラのコンパイル済みコードはより最適化されています。

では、GraalVM Enterprise EditionとCountUppercaseをサンプルとして使って、これら全てがどれほど起動スピードのパフォーマンス向上に寄与しているか説明します。

GraalVM demos: Performance Examples for Java
https://www.graalvm.org/docs/examples/java-performance-examples/

java CountUppercase On your marks, Get set, Go...
1 (191 ms)
2 (107 ms)
3 (69 ms)
4 (120 ms)
5 (27 ms)
6 (26 ms)
7 (27 ms)
8 (28 ms)
9 (27 ms)
total: 29999997 (651 ms)

jargraalと比較するため、 -XX:-UseJVMCINativeCompiler オプションを使います。

java -XX:-UseJVMCINativeLibrary CountUppercase On your marks, Get set, Go...
1 (1065 ms)
2 (329 ms)
3 (149 ms)
4 (107 ms)
5 (106 ms)
6 (81 ms)
7 (125 ms)
8 (51 ms)
9 (34 ms)
total: 29999997 (2081 ms)

AOT (ahead-of-time、事前) コンパイルされたGraalVMコンパイラの利点は明確です。この例では、jargraalの場合 (1.5秒後) に比べてlibgraalでは約3倍速く (0.5秒後) ピークパフォーマンスに到達します。GraalVM CEのlibgraalでも同様のウォームアップ時間を獲得します。

より直接libgraalのコンパイル速度を測定するために、-XX:+CITime フラグを使います。これを使うと、他のメトリックと共に1秒間あたりのコンパイルデータ量(バイト) を出力します。このデータ量にはインラインメソッドのバイト数も含みます。このフラグを付けてCountUppercaseを実行すると、libgraalでは74kB/secほどコンパイルしていたのに対し、jargraalでは9kB/secほどでした。比較のため、C1は約380kB/sec、C2は約80kB/secほどコンパイルします。プロファイルガイド最適化(PGO)を使用すると、libgraalはC2のコンパイル速度を上回ることが想定されます。

ウォームアップが短時間で済むことに加え、HotSpotヒープからGraalへ移行するその他のメリットをご紹介します。

Memory improvements

jargraalモードでは、コンパイラを(jarファイルにデプロイ済みの)クラスファイルからロードし、JVMの他のクラスと同様に実行します。メモリ割り当てはアプリケーションコードが利用するものと同じGCされたヒープで行われます。さらに、コンパイラクラスはHotSpotのmetaspace(クラスやメソッド、プロファイルといったメタデータのために使われる管理対象メモリ領域)を占有します。これにより数多くの問題を発生する可能性があります。

  • GraalVMコンパイラのヒープ要件を考慮する必要があるため、アプリケーションのヒープ要件の計算が難しくなります。
  • コンパイラヒープオブジェクトをアプリケーションヒープオブジェクトとインターリーブすることで、オブジェクトの局所性を乱すことがあります。これはパフォーマンスに直接影響する可能性があります。
  • コンパイラーによるメモリ割り当てのために速くヒープがいっぱいになるため、ガベージコレクションの実行回数が増えます。

こうしたメモリへの作用をJava Mission Controlで確認できます。以下はCountUppercaseをjargraalで実行した際のメモリ利用状況を示すスクリーンショットです。

Memory usage for jargraal

茶色の棒は測定期間中に実行された11回GCが発生したことを示しています。紫の線はヒープの利用量を示しています。コレクションの1つにマウスをホバリングすると、GC後に使われているメモリ量がわかります。

Live memory after collection (jargraal)

対して、以下はlibgraalのメモリ利用のプロファイルです。

Memory usage for libgraal
Live memory after collection (libgraal)

ここからは、libgraalの場合、GCは4回しか発生していないことがわかります。libgraalプロファイルではGC後2MBのライブメモリーしかないlibgraalプロファイル内のコレクションの後にはわずか2 MBのライブメモリーしかないため、比較するとjargraalが保持する約7.5 MBのライブメモリーを削減できることがわかります。

Profile pollution

jargraalの別の副作用として、コンパイラの実行によって、アプリケーションが使用するコードのプロファイルが乱される可能性があります。例えば、java.util.HashMapを使用し、かつString型のキーのみを使用するアプリケーションを考えてみましょう。HashMap.putAll(Map m)の呼び出しでは、Object.hashCodeの呼び出しの型プロファイルは、mのキーが常にString型であることを示します。ただし、コンパイラはHashMapも使用しており、常にStringキーを使用するわけではありません。コンパイラがHashMap.putAllを呼び出すと、これらの他の型で型プロファイルが「汚染」されるため、HashMap.putAllのString.hashCodeメソッドがインライン化されなくなる可能性があります。

この手の型汚染 (type pollution) をGraalVM Enterprise Editionが実施するように積極的なインライン化によって軽減できますが、完全にこれをなくすには、プロファイルを更新しないモードでコンパイラを実行するしかありません。現時点までこのことを予測していなかった場合は、libgraalがまさにこのモードを提供します。さらに、libgraalではコンパイルは発生しないので、GraalVMコンパイラをプロファイルする必要は全くありません。

Advantages of Java

Javaで書かれ、マシンコードにコンパイルされているため、libgraalはjargraalのほとんどの利点を保持します。例えば…

  • 圧縮参照 (compressed references) :以前のGraalVMの記事で説明したように、native-imageは圧縮ポインタをサポートしています。この機能の要約は、libgraal内のすべてのオブジェクトポインタを64ビットではなく32ビットで表現できるため、メモリを大幅に節約できます。

    Isolates and Compressed References: More Flexible and Efficient Memory Management via GraalVM
    https://medium.com/graalvm/isolates-and-compressed-references-more-flexible-and-efficient-memory-management-for-graalvm-a044cc50b67e
    https://logico-jp.io/2019/03/27/isolates-and-compressed-references-more-flexible-and-efficient-memory-management-via-graalvm/
  • Garbage collection:C1やC2のようなHotSpotのネイティブコンパイラはコンパイル中メモリを割り当て、そのメモリの解放は通常コンパイル終了後です。libgraalはGCをサポートするネイティブイメージで動作するため、コンパイラが利用するメモリ量に上限を設定するヒープサイズを付けて実行するよう構成でき、特定のクラスのコンパイラのバグでVMを破壊することを防ぎます。コンパイルへのトップレベルのエントリポイントは、OutOfMemoryErrorをキャッチして適切なアクション(たとえば、コンパイルの中止)を実行するための例外ハンドラをインストールします。対照的に、過剰な割り当てをもたらすC1またはC2のバグは、検出不可能なメモリ不足エラーでVMプロセスを強制終了させることになります。
  • Robustness against compiler bugs (コンパイラバグに対する堅牢性):これまでのポイントを一般化すると、例外をもたらすコンパイラのバグは、そのダメージを軽減できます。コンパイルされていたメソッドのコンパイル済みコードがなくても、例外をキャッチしてVMを実行し続けることができます。-Dgraal.CompilationFailureAction=Diagnoseオプションを使用すると、このような失敗でさえも、バグレポートと一緒に送信できる有用な診断情報を生成できます。-Dgraal.CrashAtオプションを使って、CountUppercaseの例でこれをシミュレートできます。
java -Dgraal.CrashAt=equals -Dgraal.CompilationFailureAction=Diagnose CountUppercase On your marks, Get set, Go...
-- iteration 1 --
1 (246 ms)
Thread[System-0,5,main]: Compilation of java.lang.String.equals(Object) failed:
java.lang.RuntimeException: Forced crash after compiling java.lang.String.equals(Object)
at org.graalvm.compiler.core.GraalCompiler.checkForRequestedCrash(GraalCompiler.java:198)
at org.graalvm.compiler.core.GraalCompiler.compile(GraalCompiler.java:152)
at org.graalvm.compiler.core.GraalCompiler.compileGraph(GraalCompiler.java:129)
at org.graalvm.compiler.hotspot.HotSpotGraalCompiler.compileHelper(HotSpotGraalCompiler.java:212)
at org.graalvm.compiler.hotspot.HotSpotGraalCompiler.compile(HotSpotGraalCompiler.java:226)
at org.graalvm.compiler.hotspot.CompilationTask$HotSpotCompilationWrapper.performCompilation(CompilationTask.java:186)
at org.graalvm.compiler.hotspot.CompilationTask$HotSpotCompilationWrapper.performCompilation(CompilationTask.java:96)
at org.graalvm.compiler.core.CompilationWrapper.run(CompilationWrapper.java:177)
at org.graalvm.compiler.hotspot.CompilationTask.runCompilation(CompilationTask.java:342)
at org.graalvm.compiler.hotspot.HotSpotGraalCompiler.compileMethod(HotSpotGraalCompiler.java:142)
at org.graalvm.compiler.hotspot.HotSpotGraalCompiler.compileMethod(HotSpotGraalCompiler.java:108)
at jdk.vm.ci.hotspot.HotSpotJVMCIRuntime.compileMethod(HotSpotJVMCIRuntime.java:663)
at com.oracle.svm.jni.JNIJavaCallWrappers.jniInvoke_VA_LIST_Nonvirtual:Ljdk_vm_ci_hotspot_HotSpotJVMCIRuntime_2_0002ecompileMethod_00028Ljdk_vm_ci_hotspot_HotSpotResolvedJavaMethod_2IJI_00029Ljdk_vm_ci_hotspot_HotSpotCompilationRequestResult_2(JNIJavaCallWrappers.java:0)
To disable compilation failure notifications, set CompilationFailureAction to Silent (e.g., -Dgraal.CompilationFailureAction=Silent).
To print a message for a compilation failure without retrying the compilation, set CompilationFailureAction to Print (e.g., -Dgraal.CompilationFailureAction=Print).
Retrying compilation of java.lang.String.equals(Object)
Dumping IGV graphs in /Users/dnsimon/graal/graal/compiler/graal_dumps/1555858467364/graal_diagnostics_41644/java.lang.String.equals(Object)
2 (103 ms)
3 (70 ms)
4 (135 ms)
5 (26 ms)
6 (27 ms)
7 (26 ms)
8 (26 ms)
9 (26 ms)
total: 29999997 (712 ms)
Graal diagnostic output saved in /Users/dnsimon/graal/graal/compiler/graal_dumps/1555858467364/graal_diagnostics_41644.zip
  • コンパイラでの過度の再帰を避けることを目指していますが、それでもスタックオーバーフローが発生する可能性があります。native-imageはスタックオーバーフローのチェックをサポートしているので、VMを終了するのではなくコンパイルからの脱出 (compilation bailout) が可能です。

Upcoming updates

libgraalがもたらす機会を活用し、かつ改良する予定があります。その計画の一部をご紹介しましょう。

  • デフォルトでは、native-imageはヒープを利用可能な物理メモリの80%に拡張します。これは、(病的な入力やコンパイラのバグによる)不正なコンパイルが大量のメモリを使い果たし、非常に遅いコンパイルを引き起こす可能性があります。このようなケースを軽減するために、young世代のサイズを調整し、libgraalの最大ネイティブイメージヒープサイズを制限することを現在実験中です。コンパイル時の無制限のメモリ使用を防ぎながら、(コンパイル速度に影響を与える)コレクションの最小化という点で最良のトレードオフを達成する値を見つけることを目指しています。
  • Native Imageでの分離のサポートにより、libgraalのメモリ使用量をさらに減らすことができます。

    Isolates and Compressed References: More Flexible and Efficient Memory Management via GraalVM
    https://medium.com/graalvm/isolates-and-compressed-references-more-flexible-and-efficient-memory-management-for-graalvm-a044cc50b67e
    https://logico-jp.io/2019/03/27/isolates-and-compressed-references-more-flexible-and-efficient-memory-management-via-graalvm/

    GraalVMコンパイラのメモリー使用量を実質的に0にするためにlibgraal分離を完全に破棄できます。これを実行するのに最適な時期は、コンパイル・キューが空のときです。つまり、アプリケーションが安定した状態になると、コンパイラはVMのメモリプロファイルから自分自身を完全に削除できます。コンパイラの初期化時間を1桁ミリ秒という短時間にまで減らすことができれば、各コンパイルごとに新しい分離を作成するモードを提供することも考えられます。これが可能になれば、コンパイラのための絶対的な最小メモリフットプリントを提供するでしょう。各コンパイルがlibgraalの最大ヒープ以下しかメモリを割り当てない限り、libgraalのすべてのガベージコレクションを回避するため、コンパイルが高速になります。
  • libgraalに必要な変更がOpenJDK masterブランチにマージされた後に速やかにJDK 13でlibraalを動作させることに注力していきます。

    [JVMCI] Update JVMCI to support JVMCI based Compiler compiled into shared library
    https://bugs.openjdk.java.net/browse/JDK-8220623
  • GraalVMネイティブイメージチームと協力して、libgraalの静的なフットプリントの削減に取り組みます。これは主にシンボルを減らし、より多くの未使用のコードを削除することが中心になるでしょう。
  • 現時点では、実行する/実行しない最適化のセットでGraalVMコンパイラを構成できますが、現在生成されたコードの品質よりもコンパイル速度を優先するような節約設定の調整に取り組んでいます。これにより、C1の代わりに第1層のコンパイルに利用できる、無駄のないlibgraalを生成できるはずです。

Conclusions

libgraalは共有ライブラリで、コンパイル済みのGraalVMコンパイラが含まれています。これを使えば、起動時のパフォーマンス、競争力のあるピークパフォーマンスが向上し、アプリケーションプロファイルやアプリケーションによるヒープの使用に対するすべての干渉がなくなります。

ぜひGraalVMをダウンロードしてお試しください。

GraalVM Downloads
https://www.graalvm.org/downloads

優れた機能が不足していることを確認されたり、他に何かフィードバックがある場合には、ご連絡頂くか、GraalVMのGitHubリポジトリにIssueを立てるか、その他の方法でご連絡ください。

GraalVM GitHub repo
https://github.com/oracle/graal
GraalVM Community
https://www.graalvm.org/community

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中