JDK28 の AOT コードコンパイル - JEP 544 : Ahead-of-Time Code Compilation

blog1.mammb.com


はじめに

Project Leyden では、Javaプログラムの起動時間を改善するために、AOTキャッシュの対応が進められてきました。

  • JDK 24 (JEP 483: Ahead-of-Time Class Loading & Linking) : CDSを拡張し、クラスの読み取り/解析/ロード/リンクを事前に行ったAOTキャッシュ導入
  • JDK 25 (JEP 483: Ahead-of-Time Command-Line Ergonomics) : AOTキャッシュの生成が1ステップで実行できるようになった(従来は2ステップ必要)
  • JDK 25 (JEP 515: Ahead-of-Time Method Profiling) : AOTキャッシュにメソッドのプロファイル情報が含まれるようになった
  • JDK 26 (JEP 516: Ahead-of-Time Object Caching with Any GC) : AOTキャッシュがGC非依存フォーマットとなり、すべてのGCでAOTキャッシュが利用可能となった

JDK 28 の JEP 544: Ahead-of-Time Code Compilation では、上記に加え、アプリケーション用に最適化されたネイティブコード(AOTコード)をAOTキャッシュに含めることができるようになりました。


Ahead-of-Time Code Compilation の利用

従来のAOTキャッシュの利用と同じです。

AOTCacheOutput オプションを指定してアプリケーションのトレーニング実行を行い、AOTキャッシュを作成します。

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

生成したAOT キャッシュには、事前にリンクされたクラスとプロファイリングデータに加え、ホットメソッドのAOTコードも含まれます。

AOT キャッシュを利用した実行も、以下の通り変更はありません。

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

また、AOT キャッシュの恩恵を受けるには、トレーニング実行と本番実行の環境が、本質的に同じである必要がある点も従来通りです。

AOTコードが含まれる場合は以下の2つの追加制約を満たす必要があります。

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


以上で Ahead-of-Time Code Compilation の説明は十分なのですが、JEP の Motivation 項目の説明が良くまとまっていたので、以下に載せておきます。


HotSpot による最適化

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つの重要な利点があります。

まず、動的コンパイルは、アプリケーションの負荷の高い箇所(ホットスポット)の変化に対応できるため、アプリケーションの俊敏性を高めます。 必要に応じて最適化を解除したり再最適化したりすることで、アプリケーションのワークロードの変化に応じてパフォーマンスを維持します。 静的コンパイルされたアプリケーションは、このような対応はできません。 静的コンパイルされたアプリケーションは、その性質上、特定のホットスポットに対してのみ最適化されるからです。

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

最後に、動的コンパイルは Java プラットフォームの動的な性質と互換性があります。 動的クラスロード、動的リンケージ、動的ディスパッチ、動的リフレクションといった機能は、非常に高い表現力をもたらし、プラットフォームの成功の基盤となってきました。 HotSpot はこれらの機能を自然に処理しますが、静的コンパイラはこれらの処理に苦労します。 膨大な量の静的解析を行っても、これらの機能を実行する際に多くの判断が必要になるという事実を補うことはできません。 そのため、Java コード用の静的コンパイラの開発者は、クローズドワールドの仮定など、互換性のない制約に頼らざるを得ず、リフレクションの対象となるクラスを事前に特定するなど、開発者に大きな負担がかかります。


パフォーマンスの改善効果

一般的なJavaフレームワークで構築された5つのベンチマークアプリケーションを実行した結果、従来のAOTキャッシュによってこれらのアプリケーションの起動時間は約50%~70%短縮され、AOTコードを使用すると、キャッシュによって起動時間は約65%~80%まで短縮された とのことです。