Java仮想マシン-JITコンパイラ
この章では、JITコンパイラーと、コンパイルされた言語とインタープリター型言語の違いについて学習します。
コンパイルされた言語と解釈された言語
C、C ++、FORTRANなどの言語はコンパイルされた言語です。それらのコードは、基盤となるマシンを対象としたバイナリコードとして配信されます。これは、基盤となるアーキテクチャ用に特別に作成された静的コンパイラによって、高レベルのコードが一度にバイナリコードにコンパイルされることを意味します。生成されたバイナリは、他のアーキテクチャでは実行されません。
一方、PythonやPerlのようなインタープリター言語は、有効なインタープリターがあれば、どのマシンでも実行できます。高水準コードを1行ずつ調べ、バイナリコードに変換します。
インタープリター型コードは通常、コンパイル済みコードよりも低速です。たとえば、ループについて考えてみます。インタープリターは、ループの反復ごとに対応するコードを変換します。一方、コンパイルされたコードは、翻訳を1つだけにします。さらに、インタープリターは一度に1行しか表示しないため、コンパイラーなどのステートメントの実行順序を変更するなど、重要なコードを実行することはできません。
このような最適化の例を以下で調べます。
Adding two numbers stored in memory。メモリへのアクセスは複数のCPUサイクルを消費する可能性があるため、優れたコンパイラは、メモリからデータをフェッチし、データが利用可能な場合にのみ加算を実行するように命令を発行します。待機せず、その間に他の命令を実行します。一方、インタプリタは常にコード全体を認識していないため、インタプリタ中にそのような最適化は不可能です。
しかし、その場合、インタープリター言語は、その言語の有効なインタープリターを備えた任意のマシンで実行できます。
Javaはコンパイルまたはインタープリターされていますか?
Javaは中間点を見つけようとしました。JVMはjavacコンパイラと基盤となるハードウェアの間に位置するため、javac(またはその他のコンパイラ)コンパイラは、プラットフォーム固有のJVMによって理解されるバイトコードでJavaコードをコンパイルします。次に、JVMは、コードの実行時にJIT(ジャストインタイム)コンパイルを使用してバイトコードをバイナリでコンパイルします。
ホットスポット
一般的なプログラムでは、頻繁に実行されるコードのセクションはごくわずかであり、多くの場合、アプリケーション全体のパフォーマンスに大きな影響を与えるのはこのコードです。コードのそのようなセクションは呼ばれますHotSpots。
コードの一部のセクションが1回だけ実行される場合、それをコンパイルすることは労力の無駄であり、代わりにバイトコードを解釈する方が高速です。ただし、セクションがホットセクションであり、複数回実行される場合、JVMは代わりにそれをコンパイルします。たとえば、メソッドが複数回呼び出された場合、コードのコンパイルにかかる余分なサイクルは、生成されるより高速なバイナリによって相殺されます。
さらに、JVMが特定のメソッドまたはループを実行するほど、より高速なバイナリが生成されるように、さまざまな最適化を行うために収集する情報が多くなります。
次のコードを考えてみましょう-
for(int i = 0 ; I <= 100; i++) {
System.out.println(obj1.equals(obj2)); //two objects
}
このコードが解釈される場合、インタープリターは反復ごとにobj1のクラスを推測します。これは、Javaの各クラスに.equals()メソッドがあり、これはObjectクラスから拡張されており、オーバーライドできるためです。したがって、obj1が各反復の文字列である場合でも、推定は実行されます。
一方、実際に発生するのは、JVMが反復ごとに、obj1がStringクラスであることに気付くため、Stringクラスの.equals()メソッドに直接対応するコードを生成することです。したがって、ルックアップは不要であり、コンパイルされたコードはより高速に実行されます。
この種の動作は、JVMがコードの動作を認識している場合にのみ可能です。したがって、コードの特定のセクションをコンパイルする前に待機します。
以下は別の例です-
int sum = 7;
for(int i = 0 ; i <= 100; i++) {
sum += i;
}
インタプリタは、ループごとに、メモリから「sum」の値をフェッチし、それに「I」を追加して、メモリに格納します。メモリアクセスはコストのかかる操作であり、通常は複数のCPUサイクルを必要とします。このコードは複数回実行されるため、HotSpotです。JITはこのコードをコンパイルし、次の最適化を行います。
'sum'のローカルコピーは、特定のスレッドに固有のレジスタに格納されます。すべての操作はレジスタ内の値に対して実行され、ループが完了すると、値がメモリに書き戻されます。
他のスレッドも変数にアクセスしている場合はどうなりますか?他のスレッドによって変数のローカルコピーが更新されているため、古い値が表示されます。このような場合、スレッドの同期が必要です。非常に基本的な同期プリミティブは、「sum」を揮発性として宣言することです。これで、変数にアクセスする前に、スレッドはローカルレジスタをフラッシュし、メモリから値をフェッチします。アクセス後、値はすぐにメモリに書き込まれます。
以下は、JITコンパイラによって実行されるいくつかの一般的な最適化です-
- メソッドのインライン化
- デッドコードの除去
- コールサイトを最適化するためのヒューリスティック
- 定数畳み込み