HN 日本語サマリー

← 一覧へ戻る
プログラミング

JEP 544: Ahead-of-Time (AOT) コードコンパイル

JEP 544: Ahead-of-Time Code Compilation (openjdk.org)

92 pointsby Skinney34 コメント

要約

JEP 544は、Javaアプリケーションの起動時間とウォームアップ時間を改善するため、HotSpot JVMでアプリケーションコードを事前にネイティブコードにコンパイルし、AOTキャッシュに保存して、その後の実行で即座に利用可能にする提案です。これにより、ピークパフォーマンスへの到達を早め、ワークロードの変化にも動的に対応することで、AOTとJITコンパイルの利点を組み合わせることを目指します。

全文翻訳

JEP 544: Ahead-of-Time コードコンパイル

Summary

HotSpot Java Virtual Machineが起動した際に、最適化されたアプリケーションのネイティブコードを即座に利用可能にすることで、起動時間とウォームアップ時間を改善します。これは、トレーニング実行でアプリケーションコードをネイティブコードにコンパイルし、そのネイティブコードを後続のプロダクション実行で使用するためにAOTキャッシュに保存することで実現されます。プロダクションでワークロードが変化した場合は、動的にネイティブコードを再生成して継続的なピークパフォーマンスを提供し、ahead-of-time (AOT) コンパイルとjust-in-time (JIT) コンパイルの両方の利点をもたらします。

Goals

  • アプリケーションがより迅速にピークパフォーマンスに到達できるようにする。
  • ワークロードが変化しても、アプリケーションがピークパフォーマンスを維持できるようにする。
  • アプリケーション、ライブラリ、フレームワークのコードに変更を要求しない。
  • AOTキャッシュの使用を要求する以外に、HotSpotの設定に変更を要求しない。
  • Serial, Parallel, G1, Shenandoah, および ZGC ガベージコレクタを引き続きサポートする。
  • 新しいAOTワークフローを導入せず、むしろ既存のAOTキャッシュ作成ワークフローを拡張する。
  • AOTコンパイルされたコードからJITコンパイルされたコードへの切り替えが、アプリケーションから見て透過的であることを保証する。
  • AArch64およびx64プロセッサアーキテクチャをサポートする。

Non-Goals

  • AOTのみのモードを提供する目標はありません。アプリケーションは、必要に応じて自動的に切り替えながら、同じ実行でAOTコンパイルされたコードとJITコンパイルされたコードの両方を使用します。
  • クロスコンパイルをサポートする目標はありません。トレーニング実行でコンパイルされたコードは、後続のプロダクション実行で、同じCPUアーキテクチャと機能セットで実行される必要があります。
  • 現在HotSpotがサポートしているすべてのCPUアーキテクチャをサポートする目標はありません。通常のポート作業により、最終的にすべての主要アーキテクチャのサポートが追加されると予想されます。

Motivation

HotSpot JVMでJavaアプリケーションを実行すると、アプリケーションは3つのフェーズを経ます:起動、ウォームアップ、そしてピークパフォーマンスへの到達です。

起動の間、HotSpotはアプリケーションのmainメソッドを呼び出し、クラスをオンデマンドでロード、リンク、初期化します。当初は、バイトコードインタープリタを介してアプリケーションコードとJDKライブラリコードの両方を実行しますが、これは低速です。インタープリタ内で、HotSpotはメソッド呼び出し数やループ反復数などのイベントをカウントすることで、アプリケーションの動作をプロファイルします。このプロファイルデータを使用して、頻繁に呼び出されるメソッド、すなわちホットスポットを選択し、基本的なC1コンパイラを介してネイティブコードにコンパイルします。このネイティブコードは、最適化の度合いは中程度です。

ウォームアップの間、アプリケーションはワークロードに落ち着き、クラスのロード、リンク、初期化が減少します。HotSpotは、C1によって挿入されたインストルメンテーションコードを介して、バイトコードインタープリタと両方でアプリケーションのプロファイリングを続行します。メソッド呼び出し数やループ反復数だけでなく、遭遇したオブジェクトの型など、より豊富なプロファイル情報を収集します。プロファイルデータが時間とともに蓄積されるにつれて、統計的に有用性が高まります。最終的に、HotSpotはそのデータを使用して最もホットなメソッドを選択し、高度なC2コンパイラを介してネイティブコードにコンパイルします。このネイティブコードにはインストルメンテーションは含まれず、高度に最適化されています。

アプリケーションのプロファイリングとネイティブコードの生成にはコストがかかります。バイトコードインタープリタが低速であるだけでなく、インストルメントされたネイティブコードは、インストルメントされていないネイティブコードよりも低速です。メソッドをネイティブコードにコンパイルするには、アプリケーションが利用できるはずのCPU時間とメモリが必要ですが、HotSpotはプロファイルデータがその労力に見合うと示す場合にのみメソッドをネイティブコードにコンパイルします。しかし、徐々にJITコンパイルがアプリケーションの出現するホットスポットに追いつき、アプリケーションはより高速に実行されます。最終的に、すべてのホットメソッドが完全に最適化されたネイティブコードにコンパイルされ、コンパイラはアイドル状態になります。

アプリケーションは、ホットスポットが変わらない限り、このピークパフォーマンスの状態を維持します。しかし、ワークロードの変化に応じて、アプリケーションのホットスポットは変化する可能性があります。その場合、HotSpotは必要に応じて動的に非最適化(以前に生成されたネイティブコードを破棄)し、再最適化(新しくホットになったメソッドのために新しいネイティブコードを生成)できます。たとえば、アプリケーションが最初に2種類の要求を受け取った場合、HotSpotはそれらの2つの要求タイプのコードを動的に最適化します。アプリケーションが3番目のタイプの要求を受け取り始めた場合、HotSpotは動的に非最適化してから、3つのタイプすべての要求のコードを再最適化できます。アプリケーションは、実質的に別のウォームアップフェーズを経由し、アプリケーションのワークロードが変化してもパフォーマンスを維持できます。

静的コンパイルについて

静的コンパイルは、Javaコードの動的コンパイルの代替として提案されることがあります。静的コンパイラは、実行時間前に、アプリケーション全体を実行コードに変換します。

静的コンパイルには、動的コンパイルと比較していくつかの利点があります。静的にコンパイルされたアプリケーションは、ウォームアップフェーズなしで、即座に起動しピークパフォーマンスに到達します。実行時には、バイトコードインタープリタ、プロファイリング、またはコンパイルの必要はありません。静的コンパイラの最適化作業が、以前の実行で収集された正確なプロファイルによってガイドされている場合、ピークパフォーマンスはHotSpotと同等になることさえあります。

しかし、動的コンパイルには静的コンパイルに対する3つの重要な利点があります。

第一に、動的コンパイルはアプリケーションのホットスポットの変化に対応するため、アプリケーションを機敏にします。必要に応じて非最適化と再最適化を行い、アプリケーションのワークロードが変化してもパフォーマンスを維持します。静的にコンパイルされたアプリケーションは、その性質上、1つのホットスポットセットのみに最適化できるため、このように応答することはできません。

第二に、動的コンパイルは、実行時に実行環境固有のネイティブコードを生成するため、アプリケーションをさまざまなハードウェアおよびソフトウェア間で移植可能にします。アプリケーションが異なるプロセッサアーキテクチャ、異なる機能セットを持つプロセッサ、異なるオペレーティングシステム、または異なるバージョンのJDKに再デプロイされた場合、HotSpotはアプリケーションの変更なしにその環境でピークパフォーマンスを達成します。静的にコンパイルされたアプリケーションは、このような変更に直面した場合に再コンパイルする必要があります。

最後に、動的コンパイルはJavaプラットフォームの動的な性質と互換性があります。動的クラスロード、動的リンク、動的ディスパッチ、動的リフレクションなどの機能は、広範な表現力を提供し、プラットフォームの成功の基盤となってきました。HotSpotはこれらの機能を自然に処理しますが、静的コンパイラはこれらの機能に苦労します。たとえ多大な静的解析を行っても、これらの機能では実行時に多くの決定を行う必要があるという事実を補うことはできません。したがって、Javaコードの静的コンパイラの開発者は、クローズドワールド仮説のような互換性のない制約に頼ったり、開発者にリフレクションの対象となるクラスを事前に特定する必要があるといった大きな負担を課したりしてきました。

トレーニング実行へのコンパイル作業のシフト

HotSpotは、起動とウォームアップのフェーズ全体を通して、複数のボールを常に juggling しています:アプリケーションコードとJDKライブラリコードを実行し、クラスをオンデマンドでロード、リンク、初期化し、アプリケーションの実行をプロファイルし、プロファイルデータによってガイドされ、さまざまなレベルの最適化でホットメソッドをネイティブコードにコンパイルします。

Project Leydenのテーゼは、起動時間とウォームアップ時間を改善する鍵は、この作業の一部を実行時間ではなく、時間前に、つまりahead-of-timeで行うことであるということです。私たちは、トレーニング実行で作業を行い、その結果を後続のプロダクション実行で即座に使用するためにAOTキャッシュに保存することで、作業を時間前にシフトします。

JEP 483(JDK 24で提供)により、クラスのロードとリンクの作業を時間前にシフトしました。AOTキャッシュは、トレーニング実行からのロードおよびリンク済みのクラス形式を保存し、それによって起動時間を改善します。

JEP 515(JDK 25で提供)により、プロファイリング作業を時間前にシフトしました。AOTキャッシュは、トレーニング実行で呼び出されたメソッドの実行プロファイルを保存し、プロダクション実行の開始時にC2コンパイラがすぐに実行できるようにすることで、ウォームアップ時間を改善します。

これらの改善は、コンパイルと最適化の作業を時間前にシフトするという最終目標の基盤を築きました。AOTキャッシュは、トレーニング実行でコンパイルされた最適化されたネイティブコードを保存し、HotSpotが各プロダクション実行の開始時に再コンパイルする必要なく、そのコードを即座にロードできるようにします。これにより、起動時間とウォームアップ時間の両方が改善されます。

HotSpotは常にキャッシュされたコードを使用するわけではありません。アプリケーションのワークロードが変化した場合、HotSpotは通常どおり、非最適化と再最適化を行い、新しくホットになったメソッドのために新しいネイティブコードを生成してパフォーマンスを維持できます。これにより、Javaアプリケーションは、静的コンパイルの利点の一部を得ながら、動的コンパイルのアジリティ、ポータビリティ、および互換性を維持できます。

Description

トレーニング実行中に生成された最適化されたネイティブコードを保存するために、既存のAOTキャッシュを拡張します。このようなキャッシュされたコードはAOTコードとして知られています。プロダクション実行中、キャッシュ内に一致するAOTコードが見つかった場合、メソッドの最適化されたコードに対する要求は即座に満たされます。AOTコードが利用できない、互換性がない、その他の理由で不適切である、または後で非最適化された場合、実行は既存のインタープリタおよびJITメカニズムにフォールバックします。AOTコードとJITコードは共存でき、同じコンパイラ(C1およびC2)によって作成されるため、完全に相互運用可能です。

AOTキャッシュを作成するには、AOTCacheOutputオプションを使用してアプリケーションのトレーニング実行を行い、AOTコードを生成します。

$ java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App ...

このワークフローは、以前のリリースから変更されていません。しかし、app.aotのAOTキャッシュには、プリリンクされたクラスとプロファイルデータだけでなく、選択されたホットメソッドのAOTコードも含まれるようになりました。その後、プロダクションでは、キャッシュを使用してアプリケーションを実行できます。

$ java -XX:AOTCache=app.aot -cp app.jar com.example.App ...

AOTコードの生成または使用に追加のオプションや設定は必要ありません。HotSpotはデフォルトでAOTコードを作成し、キャッシュに保存します。また、AOTコードのロード順序付けや、後続のJITコード生成のガイドに使用されるプロファイルデータもキャッシュに保存し続けます。

Performance

AOTコードの起動メリットを評価するために、最近のJDK 28早期アクセスビルドを使用して、一般的なJavaフレームワークで構築された5つのベンチマークアプリケーションを実行しました。JITコンパイラがアプリケーションとCPU時間を競合する可能性が高く、それによって起動時間が増加するマイクロサービス環境をエミュレートするために、2コアのLinux/x64システムで実行しました。

[画像: 5つのベンチマークに対するAOTコードの起動メリットを示すパフォーマンスチャート]

AOTコードなしの場合、AOTキャッシュはこれらのアプリケーションの起動時間を約50%から70%削減します。AOTコードを使用すると、キャッシュは起動時間を約65%から80%削減します。

AOTコードのウォームアップメリットを評価するために、同じ50個のソースファイルを20回繰り返しコンパイルするjavacベンチマークアプリケーションを実行し、各イテレーションに必要な時間を測定しました。

[画像: javacのウォームアップメリットを示すパフォーマンスチャート]

各曲線で、最初のイテレーションは起動時間の改善を示しています。AOTコードなしのAOTキャッシュは起動時間を約30%改善し、AOTコードを追加するとさらに45%改善され、合計で約75%になります。後続のイテレーションはウォームアップフェーズを示しており、その間HotSpotは最もホットなメソッドをコンパイルします。ネイティブコードの品質が向上し、コンパイラが作業を完了するにつれて、イテレーション時間は減少し、その後安定状態に達する傾向があります。AOTコードなしのAOTキャッシュの曲線は、AOTキャッシュなしの曲線よりも速く減少し、最終的にほぼ同じ安定状態に達します。AOTコード付きのキャッシュの曲線は、4回目のイテレーションまでにすでに安定状態に近い状態になっています。最上位の曲線と最下位の曲線の間の領域は、総ウォームアップ時間改善を表します。

実行手順やソースコードへのリンクを含む、これらのベンチマークに関するすべての情報はこちらで入手できます。(上記で使用されているフレームワークのバージョンは、そこのページにリストされているバージョンよりも新しいものです。)

AOTコードとJITコードの違い

トレーニング実行とプロダクション実行は異なる可能性があるため、AOTコードとJITコードは異なる場合があります。

違いの1つの原因は、特にワークロードが異なる場合、クラスが初期化される順序がトレーニング実行とプロダクション実行で異なる可能性があるという事実です。別のクラスの静的フィールドにアクセスしたり、静的メソッドを呼び出したりするメソッドは、そのクラスが初期化されていることを確認する必要があります。そのため、C2でAOTコードを生成する際、HotSpotはこのようなメソッドの2つのバージョンをコンパイルします。低速バージョンには、参照されたクラスの初期化を保証するための追加コードが含まれていますが、高速バージョンにはそのコードが含まれていないため、より最適化できます。HotSpotは最初に低速バージョンを使用し、参照されたすべてのクラスが初期化されたら高速バージョンに切り替えます。

違いのもう1つの原因は、static finalフィールドの値が実行ごとに異なる可能性があるという事実です。たとえば、現在の日付と時刻で初期化される可能性があります。このようなフィールドを参照するメソッドをjust-in-timeコンパイルする場合、フィールドを含むクラスは初期化されているため、フィールドの値はわかっており、C2はそれをコンパイル時定数として扱い、ネイティブコードに直接埋め込むことができます。しかし、同じメソッドをahead-of-timeコンパイルする場合、クラスは初期化されていないため、フィールドの値はわからず、C2はそれをコンパイル時定数として扱うことができません。フィールドを明示的にロードするコードを生成する必要があります。

これらの違いにもかかわらず、AOTコードは、アプリケーションに対して透過的な方法で、依然として大幅なパフォーマンス上の利点をもたらします。いつものように、実行時にはコンパイラが、うまく経過しなかったAOTコードを置き換えるためにJITコードを生成できます。

トレーニング実行とプロダクション実行の一貫性

トレーニング実行で生成されたAOTキャッシュのメリットを享受するには、トレーニング実行とそれに続くすべてのプロダクション実行が、JEP 483で説明されているように、本質的に類似している必要があります。

AOTキャッシュにAOTコードが含まれている場合、次の2つの追加制約が満たされたときに、そのコードが使用されます。

  • すべての実行で、同じアーキテクチャと機能を持つCPUを使用します。たとえば、AVX-512ベクトル命令機能を持つx64 CPU用に生成されたAOTコードは、その機能を持たないx64 CPUでは実行されません。
  • すべての実行で同じガベージコレクタを使用します。AOTコードにはGC固有の読み書きバリアが含まれているためです。

これらの制約が満たされない場合、HotSpotは警告メッセージを発行し、AOTコードをロードせず、通常のインタープリタおよびJITメカニズムにフォールバックします。それでも、AOTキャッシュ内の他の情報(ロードおよびリンクされたクラス、プロファイルデータ)は使用します。その場合、アプリケーションの起動とウォームアップは遅くなる可能性がありますが、実行は引き続き正しく行われ、最終的にはピークパフォーマンスに到達します。

プロダクションでのAOTキャッシュ使用の観測

既存のHotSpotオプションPrintCompilationを使用して、プロダクション実行でAOTコードがロードされているかどうかを観測できます。このオプションは、AOTコードのロードとJITコンパイルの両方を報告するようになりました。

$ java -XX:+PrintCompilation \
  -XX:AOTCache=app.aot -cp app.jar com.example.App ...

AOTコードを含むAOTキャッシュが特定のプロダクション環境で使用可能かどうかを確認することもできます。AOTMode=requiredオプションを使用すると、AOTキャッシュが制約に違反した場合、HotSpotはエラーを報告して終了します。

$ java -XX:AOTMode=required \
  -XX:AOTCache=app.aot -cp app.jar com.example.App ...

(このオプションは元々AOTMode=onでしたが、後に明確化のためにAOTMode=requiredにリネームされました。)

最後に、診断オプションAOTCodeCachingを使用して、プロダクション実行でのAOTコードのロードを無効にできます。

$ java -XX:+UnlockDiagnosticVMOptions -XX:-AOTCodeCaching \
  -XX:AOTCache=app.aot -cp app.jar com.example.App ...

このオプションを使用して、プロダクション実行でのAOTコードのパフォーマンスへの影響を評価したり、AOTMode=requiredを使用しておりAOTコードが使用できないがキャッシュ内の他の情報は使用可能な場合に、制約違反エラーを回避したりできます。

トレーニングでのAOTコード生成の制御

これまでは、上記のようにAOTCacheOutputオプションを使用して、トレーニング実行で単一ステップで作成されるAOTキャッシュについて説明してきました。実際には、AOTキャッシュの作成は2つの別個のステップで行われます。HotSpotはrecordモードで一度実行され、アプリケーションの動作の観測結果をAOT構成に保存し、次にcreateモードでその構成からAOTキャッシュを組み立てます。これにはAOTコードのコンパイルが含まれます。AOTCacheOutputオプションを使用すると、2番目のステップが透過的に実行されますが、AOTModeおよびAOTConfigurationオプションを使用して各ステップを明示的に呼び出すことができます。

$ java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf \
       -cp app.jar com.example.App ...
$ java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf \
       -XX:AOTCache=app.aot

HotSpotは、C1およびC2コンパイラの動作を制御するために、多くのオプションを提供しています。これらのオプションは、実行時のJITコード生成と、アセンブリステップでのAOTコード生成の両方に一様に適用されます。たとえば、このコマンドラインは、トレーニング実行中のすべてのJITコンパイルアクティビティと、キャッシュ作成時のすべてのAOTコンパイルアクティビティを報告します。

$ java -XX:+PrintCompilation \
  -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App ...

AOTコンパイルアクティビティのみを表示し、JITコンパイルアクティビティを表示しないようにするには、既存の環境変数JDK_AOT_VM_OPTIONSを使用して、PrintCompilationオプションをアセンブリステップにのみ渡します。

$ JDK_AOT_VM_OPTIONS='-XX:+PrintCompilation' \
  java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App ...

または、2つのステップを自分で実行し、2番目のステップでのみPrintCompilationオプションを指定することもできます。

最後に、診断オプションAOTCodeCachingを使用して、トレーニング実行でのAOTコードの生成を無効にできます。

$ java -XX:+UnlockDiagnosticVMOptions -XX:-AOTCodeCaching \
  -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App ...

このオプションを使用して、トレーニング実行でのAOTコードのサイズへの影響を評価できます。キャッシュされたコードは、AOTキャッシュを大幅に大きくする可能性があります。

AOT関連のコマンドラインオプションの詳細については、javaコマンドのマニュアルページを参照してください。

Future Work

  • AOTコードへのほぼ完全な依存を優先して、バイトコード解釈とJITコンパイルを最小限に抑えることを調査します。初期の実験では、インタープリタの使用を最小限に抑えると、過度に大きなAOTキャッシュファイルになり、インタープリタを実行するよりもロードに時間がかかることが示唆されています。同様に、JITコンパイルを最小限に抑えると、ピークパフォーマンスが低下することがよくあります。このアプローチは、静的コンパイルと同様にユーザーの期待に応えられない場合、適用範囲が限られる可能性があります。
  • HotSpotは、コンパイラを制御するための幅広い詳細なオプションを備えています。この機能での経験に基づいて、既存のオプションのデフォルト値を調整して、AOTコードにより適切に適用できるようにします。また、明示的なAOTキャッシュサイズ管理のためのオプションなどを検討します。
  • パフォーマンスの一部を犠牲にするか、AOTキャッシュファイルのサイズを大きくするか、あるいはその両方を受け入れて、同じアーキテクチャだが機能セットが異なるプロセッサ間でのポータビリティを獲得できるオプションを検討します。

Testing

  • この機能の新しい単体テストを作成します。AOTコードが存在する場合に正しく動作することを保証します。
  • この機能を有効にして、既存のAOTキャッシュテストを実行し、それらがパスすることを確認します。
  • 当初は、AArch64およびx64プロセッサのみがサポートされます。単体テストは、他のアーキテクチャでAOTコードが存在しないことを考慮して適切に調整されます。

Risks and Assumptions

  • JEP 483で既に述べられているリスク以外に、新たなリスクはありません。
  • HotSpotの組織原則は依然として健全であると仮定しています。Javaアプリケーションは、実行時に、利用可能なすべてのプロセッサ機能を活用して、アプリケーションの実際の動作を優先するようにコンパイルされるべきです。この原則により、Javaコードは新しいハードウェアで自動的により良く実行されます。
  • AOTキャッシュの基本的な仮定も引き続き有効です。トレーニング実行は、AOTキャッシュを介してプロダクション実行に渡された場合に、プロダクション実行のパフォーマンスを向上させる観測の良いソースであると仮定されます。この仮定は、AOTコードに完全に適用されます。AOTコードは、類似のプロダクション実行を改善する一方で、JITコンパイルを使用して異なるコードを生成できる分岐したプロダクション実行に害を及ぼすことはありません。