プロファイルに基づく最適化
プロファイルに基づく最適化とは
ジャストインタイム(JIT)コンパイラが事前(AOT)コンパイラよりも優れている点の1つは、アプリケーションの実行時動作を分析できることです。たとえば、HotSpotは、if
文の各ブランチが実行された回数を追跡します。この情報は「プロファイル」と呼ばれ、第2層JITコンパイラ(Graalなど)に渡されます。次に、第2層JITコンパイラは、if
文が引き続き同じように動作することを想定し、プロファイルの情報を使用してその文を最適化します。
AOTコンパイラは通常、プロファイリング情報を持たず、通常はコードの静的ビューに制限されます。つまり、ヒューリスティックを除き、AOTコンパイラは、すべてのif
文の各ブランチが実行時に発生する可能性が等しく、各メソッドが他のメソッドと同じように呼び出される可能性が高く、各ループが同じ回数繰り返されると見なします。これにより、AOTコンパイラが不利になります。プロファイリング情報がなければ、JITコンパイラと同じ品質のマシン・コードを生成することは困難です。
プロファイルに基づく最適化(PGO)は、AOTコンパイラにプロファイル情報を提供し、パフォーマンスとサイズの観点から出力の品質を向上させる技術です。
ノート: PGOは、GraalVM Community Editionでは使用できません。
プロファイルとは
プロファイルは、アプリケーションの実行時に特定のイベントが発生した回数を要約したログです。イベントは、コンパイラがより適切な決定を行うために、どの情報が役に立つかによって選択されます。たとえば、次のものがあります:
- このメソッドは何回呼び出されましたか。
- この
if
文は何回true
ブランチを選択しましたか。false
ブランチを何回選択しましたか。 - このメソッドはオブジェクトを何回割り当てましたか。
String
値が特定のinstanceof
チェックに何回渡されましたか。
アプリケーションのプロファイルを取得する方法
JITコンパイラを使用してJVMでアプリケーションを実行する場合、アプリケーションのプロファイリングはランタイム環境によって処理され、開発者による追加のステップは必要ありません。ただし、プロファイルを作成すると、プロファイルされるアプリケーションのパフォーマンスに実行時間とメモリー使用量のオーバーヘッドが追加されます。これにより、ウォームアップの問題が発生します。アプリケーションが予測可能なピーク・パフォーマンスに到達するのは、アプリケーション・コードのプロファイリングおよびJITコンパイルに十分な時間が経過した後のみです。実行時間の長いアプリケーションの場合、このオーバーヘッドは通常それ自体で解消され、後でパフォーマンスが向上します。一方、存続期間が短いアプリケーションや、予測可能なパフォーマンスでできるだけ早く開始する必要があるアプリケーションの場合、これは逆効果です。
AOTコンパイルのアプリケーションのプロファイルの収集はより複雑であり、開発者が追加のステップを必要としますが、最終的なアプリケーションのオーバーヘッドは発生しません。プロファイルは、アプリケーションの実行中に監視して収集されます。これは通常、アプリケーション・バイナリにインストゥルメンテーション・コードを挿入する特殊なモードでアプリケーションをコンパイルすることによって実現されます。インストゥルメンテーション・コードは、プロファイルの対象のイベントのカウンタを増分します。インストゥルメンテーション・コードを含むバイナリはインストゥルメントされたバイナリと呼ばれ、これらのカウンタを追加するプロセスはインストゥルメンテーションと呼ばれます。
当然、インストゥルメンテーション・コードのオーバーヘッドにより、アプリケーションのインストゥルメントされたバイナリはデフォルト・バイナリほどパフォーマンスが高くないため、本番で実行することはお薦めしません。ただし、インストゥルメントされたバイナリ上で合成の代表的なワークロードを実行すると、アプリケーションの動作の代表的なプロファイルが提供されます。最適化されたアプリケーションをビルドする場合、AOTコンパイラには、アプリケーションの静的ビューと動的プロファイルの両方があります。したがって、最適化されたアプリケーションは、デフォルトのAOTコンパイル済アプリケーションよりもパフォーマンスが優れています。
プロファイルによって最適化が導かれる方法
コンパイル中、コンパイラは最適化について決定する必要があります。たとえば、次のメソッドでは、関数インライン化の最適化で、インライン化する呼出しサイトとインライン化しない呼出しサイトを決定する必要があります。
private int run(String[] args) {
if (args.length < 3) {
return handleNotEnoughArguments(args);
} else {
return doActualWork(args);
}
}
わかりやすくするために、インライン化最適化には生成できるコード量に制限があり、そのため1つのコールのみをインライン化できるとします。コンパイルするコードの静的ビューのみを見ると、doActualWork()
とhandleNotEnoughArguments()
の両方の呼出しはほとんど区別できないように見えます。ヒューリスティックがなければ、フェーズでどちらをインライン化するのがより良い選択かを推測する必要があります。ただし、間違った選択を行うと、コードの効率が低下する可能性があります。run()
は、実行時に正しい数の引数でコールされることが最も多いと仮定すると、ほとんどの場合doActualWork()
がコールされるため、handleNotEnoughArguments
をインライン化すると、パフォーマンス上の利点はなく、コンパイル・ユニットのコード・サイズが増加します。
アプリケーションの実行時プロファイルがあると、コールを区別するためのデータをコンパイラに提供できます。たとえば、実行時プロファイルがif
条件をfalse
100回、true
3回と記録した場合、doActualWork()
をインライン化する必要があります。これは、PGOの本質です。プロファイルの情報を使用して、特定の決定を行うときにコンパイラに基礎となるデータを提供します。プロファイルで記録される実際の決定と実際のイベントはフェーズによって異なりますが、前述の例は一般的な概念を示しています。
PGOは、代表的なワークロードがアプリケーションのインストゥルメントされたバイナリで実行されることを想定しています。逆のプロファイル(アプリケーションの実際の実行時動作とまったく逆を記録するプロファイル)を提供することは、逆効果になります。前述の例では、これは、(実際のアプリケーションとは異なり)少ない引数でrun()
メソッドを呼び出すワークロードでインストゥルメントされたバイナリを実行することです。これにより、インライン化フェーズでhandleNotEnoughArguments
のインライン化が選択され、最適化されたバイナリのパフォーマンスが低下します。
したがって、目標は、本番ワークロードに可能なかぎり一致するワークロードのプロファイルを収集することです。このためのゴールド・スタンダードは、インストゥルメントされたバイナリで本番で実行すると予想されるワークロードとまったく同じワークロードを実行することです。
使用方法の概要の詳細は、「プロファイルに基づく最適化の基本的な使用法」のドキュメントを参照してください。