JVMパフォーマンスの最適化、パート2:コンパイラ

Javaコンパイラは、JVMパフォーマンス最適化シリーズのこの2番目の記事で中心的な役割を果たします。Eva Andreassonは、さまざまな種類のコンパイラを紹介し、クライアント、サーバー、および階層型コンパイルのパフォーマンス結果を比較します。彼女は、デッドコードの除去、インライン化、ループの最適化など、一般的なJVMの最適化の概要で締めくくります。

Javaコンパイラは、Javaの有名なプラットフォームの独立性の源です。ソフトウェア開発者は、可能な限り最高のJavaアプリケーションを作成し、コンパイラーはバックグラウンドで動作して、目的のターゲットプラットフォーム用の効率的でパフォーマンスの高い実行コードを生成します。さまざまな種類のコンパイラがさまざまなアプリケーションのニーズに対応するため、特定の望ましいパフォーマンス結果が得られます。コンパイラーの動作と使用可能な種類の観点からコンパイラーについて理解すればするほど、Javaアプリケーションのパフォーマンスを最適化できるようになります。

JVMパフォーマンス最適化シリーズのこの2番目の記事では、さまざまなJava仮想マシンコンパイラの違いに焦点を当てて説明します。また、Java用のJust-In-Time(JIT)コンパイラーで使用されるいくつかの一般的な最適化についても説明します。(JVMの概要とシリーズの概要については、「JVMパフォーマンスの最適化、パート1」を参照してください。)

コンパイラとは何ですか?

簡単に言えば、コンパイラはプログラミング言語を入力として受け取り、実行可能言語を出力として生成します。一般的に知られているコンパイラの1つはjavac、です。これは、すべての標準Java開発キット(JDK)に含まれています。javacJavaコードを入力として受け取り、それをバイトコード(JVMの実行可能言語)に変換します。バイトコードは、Javaプロセスの開始時にJavaランタイムにロードされる.classファイルに格納されます。

バイトコードは標準のCPUで読み取ることができないため、基盤となる実行プラットフォームが理解できる命令言語に変換する必要があります。バイトコードを実行可能なプラットフォーム命令に変換する役割を担うJVMのコンポーネントは、さらに別のコンパイラです。一部のJVMコンパイラは、いくつかのレベルの変換を処理します。たとえば、コンパイラは、変換の最終ステップである実際のマシン命令に変換される前に、バイトコードのさまざまなレベルの中間表現を作成する場合があります。

バイトコードとJVM

バイトコードとJVMについて詳しく知りたい場合は、「バイトコードの基本」(Bill Venners、JavaWorld)を参照してください。

プラットフォームにとらわれない観​​点から、コードを可能な限りプラットフォームに依存しないようにしたいので、最後の変換レベル(最低の表現から実際のマシンコードまで)は、実行を特定のプラットフォームのプロセッサアーキテクチャにロックするステップです。 。最高レベルの分離は、静的コンパイラと動的コンパイラの間です。そこから、ターゲットとする実行環境、必要なパフォーマンス結果、および満たす必要のあるリソース制限に応じたオプションがあります。このシリーズのパート1では、静的コンパイラと動的コンパイラについて簡単に説明しました。次のセクションでは、もう少し説明します。

静的コンパイルと動的コンパイル

静的コンパイラの例は、前述のjavacです。静的コンパイラでは、入力コードは1回解釈され、出力実行可能ファイルはプログラムの実行時に使用される形式になります。元のソースに変更を加えて(コンパイラーを使用して)コードを再コンパイルしない限り、出力は常に同じ結果になります。これは、入力が静的入力であり、コンパイラーが静的コンパイラーであるためです。

静的コンパイルでは、次のJavaコード

static int add7( int x ) { return x+7; }

次のバイトコードに似た結果になります。

iload0 bipush 7 iadd ireturn

動的コンパイラは、ある言語から別の言語に動的に変換します。つまり、実行時にコードが実行されるときに実行されます。動的コンパイルと最適化により、ランタイムはアプリケーションの負荷の変化に適応できるという利点が得られます。動的コンパイラーは、予測不可能で絶えず変化する環境で一般的に実行されるJavaランタイムに非常に適しています。ほとんどのJVMは、Just-In-Time(JIT)コンパイラなどの動的コンパイラを使用します。問題は、動的コンパイラとコードの最適化に、追加のデータ構造、スレッド、およびCPUリソースが必要になる場合があることです。最適化またはバイトコードコンテキスト分析が高度であるほど、コンパイルによって消費されるリソースが多くなります。ほとんどの環境では、出力コードの大幅なパフォーマンスの向上と比較して、オーバーヘッドは依然として非常に小さいです。

JVMの種類とJavaプラットフォームの独立性

すべてのJVM実装には、アプリケーションのバイトコードをマシン命令に変換しようとするという共通点が1つあります。一部のJVMは、ロード時にアプリケーションコードを解釈し、パフォーマンスカウンターを使用して「ホット」コードに焦点を合わせます。一部のJVMは解釈をスキップし、コンパイルのみに依存します。コンパイルのリソース集約度は(特にクライアント側アプリケーションの場合)大きな打撃となる可能性がありますが、より高度な最適化も可能にします。詳細については、「リソース」を参照してください。

Javaの初心者の場合、JVMの複雑さは頭を悩ませることになります。良いニュースは、あなたが本当にする必要がないということです!JVMはコードのコンパイルと最適化を管理するため、マシンの命令や、基盤となるプラットフォームアーキテクチャのアプリケーションコードを記述する最適な方法について心配する必要はありません。

Javaバイトコードから実行まで

Javaコードをバイトコードにコンパイルしたら、次のステップはバイトコード命令をマシンコードに変換することです。これは、インタプリタまたはコンパイラのいずれかによって実行できます。

解釈

バイトコードコンパイルの最も単純な形式は、解釈と呼ばれます。インタプリタは、単純にすべてのバイトコード命令のためのハードウェア命令を検索し、CPUによって実行されるようにそれを送信します。

辞書を使用するの同様の解釈を考えることができます。特定の単語(バイトコード命令)には、正確な翻訳(機械語命令)があります。インタプリタは一度に1つのバイトコード命令を読み取ってすぐに実行するため、命令セットを最適化する機会はありません。インタプリタは、バイトコードが呼び出されるたびに解釈を行う必要があるため、かなり遅くなります。解釈はコードを実行する正確な方法ですが、最適化されていない出力命令セットは、ターゲットプラットフォームのプロセッサにとって最高のパフォーマンスを発揮するシーケンスではない可能性があります。

コンパイル

コンパイラ他方では、ランタイムに実行されるコード全体をロードします。バイトコードを変換するときに、ランタイムコンテキスト全体または一部を調べて、実際にコードを変換する方法を決定する機能があります。その決定は、命令のさまざまな実行ブランチやランタイムコンテキストデータなどのコードグラフの分析に基づいています。

バイトコードシーケンスがマシンコード命令セットに変換され、この命令セットに対して最適化を実行できる場合、置換する命令セット(最適化されたシーケンスなど)は、コードキャッシュと呼ばれる構造に格納されます。次回そのバイトコードが実行されるとき、以前に最適化されたコードはすぐにコードキャッシュに配置され、実行に使用されます。場合によっては、パフォーマンスカウンターが作動して以前の最適化をオーバーライドすることがあります。その場合、コンパイラーは新しい最適化シーケンスを実行します。コードキャッシュの利点は、結果の命令セットを一度に実行できることです。解釈的なルックアップやコンパイルは必要ありません。これにより、特に同じメソッドが複数回呼び出されるJavaアプリケーションの場合、実行時間が短縮されます。

最適化

動的コンパイルに加えて、パフォーマンスカウンターを挿入する機会があります。コンパイラは、たとえば、パフォーマンスカウンタを挿入する場合がありますバイトコードブロック(たとえば、特定のメソッドに対応する)が呼び出されるたびにカウントします。コンパイラは、特定のバイトコードがどの程度「ホット」であるかに関するデータを使用して、コードの最適化が実行中のアプリケーションに最も影響を与える場所を決定します。ランタイムプロファイリングデータにより、コンパイラはコード最適化の豊富な決定をその場で行うことができ、コード実行のパフォーマンスがさらに向上します。より洗練されたコードプロファイリングデータが利用可能になると、次のような追加のより良い最適化の決定を行うために使用できます。コンパイルされた言語で命令をより適切にシーケンスする方法、命令のセットをより効率的なセットに置き換えるかどうか、さらには冗長な操作を排除するかどうか。

Javaコードについて考えてみましょう。

static int add7( int x ) { return x+7; }

これはjavac、バイトコードに静的にコンパイルできます。

iload0 bipush 7 iadd ireturn

メソッドが呼び出されると、バイトコードブロックは機械命令に動的にコンパイルされます。パフォーマンスカウンター(コードブロックに存在する場合)がしきい値に達すると、パフォーマンスカウンターも最適化される可能性があります。最終結果は、特定の実行プラットフォーム用の次のマシン命令セットのようになります。

lea rax,[rdx+7] ret

さまざまなアプリケーション用のさまざまなコンパイラ

Javaアプリケーションが異なれば、ニーズも異なります。長時間実行されるエンタープライズサーバー側アプリケーションでは、より多くの最適化が可能になる可能性がありますが、小規模なクライアント側アプリケーションでは、最小限のリソース消費で高速実行が必要になる場合があります。3つの異なるコンパイラ設定とそれぞれの長所と短所を考えてみましょう。

クライアント側コンパイラ

よく知られている最適化コンパイラはC1で、-clientJVM起動オプションを介して有効になります。そのスタートアップ名が示すように、C1はクライアント側のコンパイラです。これは、使用可能なリソースが少なく、多くの場合、アプリケーションの起動時間に敏感なクライアント側のアプリケーション向けに設計されています。C1は、コードプロファイリングにパフォーマンスカウンターを使用して、単純で比較的邪魔にならない最適化を可能にします。

サーバーサイドコンパイラ

サーバー側のエンタープライズJavaアプリケーションなど、実行時間の長いアプリケーションの場合、クライアント側のコンパイラでは不十分な場合があります。代わりに、C2のようなサーバー側コンパイラを使用できます。C2は通常-server、起動コマンドラインにJVM起動オプションを追加することで有効になります。ほとんどのサーバー側プログラムは長時間実行されることが予想されるため、C2を有効にすると、短期間の軽量クライアントアプリケーションよりも多くのプロファイリングデータを収集できるようになります。したがって、より高度な最適化手法とアルゴリズムを適用できるようになります。

ヒント:サーバー側コンパイラをウォームアップします

サーバー側の展開の場合、コンパイラがコードの最初の「ホット」部分を最適化するまでに時間がかかることがあるため、サーバー側の展開では「ウォームアップ」フェーズが必要になることがよくあります。サーバー側のデプロイメントで何らかのパフォーマンス測定を行う前に、アプリケーションが定常状態に達していることを確認してください。コンパイラーが適切にコンパイルするのに十分な時間を与えることはあなたの利益になります! (コンパイラーのウォームアップとプロファイリングの仕組みについて詳しくは、JavaWorldの記事「HotSpotコンパイラーの動作を監視する」を参照してください。)

サーバーコンパイラは、クライアント側コンパイラよりも多くのプロファイリングデータを考慮し、より複雑なブランチ分析を可能にします。つまり、どの最適化パスがより有益であるかを検討します。より多くのプロファイリングデータを利用できると、より良いアプリケーション結果が得られます。もちろん、より広範なプロファイリングと分析を行うには、コンパイラーにより多くのリソースを費やす必要があります。C2が有効になっているJVMは、より多くのスレッドとより多くのCPUサイクルを使用し、より大きなコードキャッシュを必要とします。

階層型コンパイル

階層型コンパイルクライアント側とサーバー側のコンパイルを組み合わせます。 Azulは、最初にZingJVMで階層型コンパイルを利用できるようにしました。最近では(Java SE 7以降)、Oracle Java HotspotJVMに採用されています。階層型コンパイルは、JVMのクライアントコンパイラとサーバーコンパイラの両方の利点を利用します。クライアントコンパイラは、アプリケーションの起動時に最もアクティブになり、パフォーマンスカウンタのしきい値が低いことによってトリガーされる最適化を処理します。クライアント側コンパイラはまた、パフォーマンスカウンタを挿入し、より高度な最適化のための命令セットを準備します。これは、サーバー側コンパイラによって後の段階で対処されます。階層型コンパイルは、影響の少ないコンパイラーアクティビティ中にコンパイラーがデータを収集でき、後でより高度な最適化に使用できるため、非常にリソース効率の高いプロファイリング方法です。このアプローチでは、インタープリター型コードプロファイルカウンターのみを使用した場合よりも多くの情報が得られます。

図1のチャートスキーマは、純粋なインタプリタ、クライアント側、サーバー側、および階層型コンパイルのパフォーマンスの違いを示しています。X軸は実行時間(時間単位)とY軸のパフォーマンス(ops /時間単位)を示します。

図1.コンパイラ間のパフォーマンスの違い(クリックして拡大)

純粋にインタープリター型のコードと比較して、クライアント側コンパイラーを使用すると、実行パフォーマンス(ops / s)が約5〜10倍向上し、アプリケーションのパフォーマンスが向上します。もちろん、ゲインの変動は、コンパイラの効率、有効化または実装される最適化、および(程度は低いですが)実行のターゲットプラットフォームに関してアプリケーションがどの程度適切に設計されているかによって異なります。後者は、Java開発者が心配する必要のないものです。

クライアント側のコンパイラと比較して、サーバー側のコンパイラは通常、コードのパフォーマンスを測定可能な30%から50%向上させます。ほとんどの場合、パフォーマンスの向上により、追加のリソースコストのバランスが取れます。