ChromeのV8コンパイラ、Java HotSpotコンパイラ、その他多くのコンパイラには、解釈とコンパイルの複数の層があります。
関数はHotSpotで解釈されたとおりに開始され、十分な頻度で実行されると、ネイティブコードにコンパイルされます(後で最適化されます)。そのようなレイヤー間のインターフェースはどのように機能しますか?
たとえば、関数Aと関数Bの2つの関数がある場合、関数Aがコンパイルされ、関数Bが呼び出され、関数Aのコンパイル時に解釈される場合、関数Aには、関数Bのインタープリターをトリガーするいくつかの命令があります。 、 正しい?
しかし、関数Aがコンパイルされた後のある時点で、関数Bもコンパイルされた場合はどうなりますか?次に、関数Bがより高速でコンパイルされたバージョンであっても、関数Aは関数Bの解釈されたバージョンを呼び出すことになります。
では、最近のコンパイラではどうなるのでしょうか。関数Bの新しいコードを指すように関数Aにホットパッチを適用しますか?関数Bを参照するすべての関数にパッチを適用する代わりに、間接参照とホットパッチのレイヤーを追加しますか?または完全に何か他のもの?
任意のコードを、制御フローの変更(分岐、関数呼び出し、戻り値など)で区切られた線形の一連のステップで構成される断片に分割できます。
JITコンパイルを行うには、制御フローの変更後すぐに開始し、次の制御フローの変更が見つかるまで先をスキャンし、ステップの線形シーケンスをネイティブコードに変換し、ネイティブコードを最適化し、「終了制御フローの変更を配置します。 "最後に、結果(「トレース」と呼びます)をある種の「トレースキャッシュ」に保存し、トレースを実行します。キャッシュに次のコードのトレースがない場合、その「制御フローの変更の終了」により、制御フローを仮想マシンに戻すことができます。次のコードのトレースがある場合、その「制御フローの変更を終了する」ことで、制御を直接(または間接的に)次のトレースに渡すことができます。 注:ここでは単純化しすぎます-「終了制御フローの変更」には2つ以上の宛先がある場合があります(たとえば、1つの宛先が「真」のパスであり、もう一方が「偽」である分岐である場合があります) "パス)。次のトレースがキャッシュにある場合でも、仮想マシンからの支援が必要になる場合があります。
ここでの問題は、それがすべて高価であるということです(そして、生成されるネイティブコードを最適化しようとすると、より高価になります)。コードのシーケンスが頻繁に実行される場合、JITコンパイルのオーバーヘッドはわずかになり、パフォーマンスが向上します。コードのシーケンスが頻繁に実行されない場合、JITコンパイルのコストは簡単にメリットを超える可能性があります。
今;次のコードのトレースがない場合、その「制御フローの変更を終了する」ことで、制御フローを仮想マシンに戻すことができます。この時点では、JITコンパイルの代わりに、仮想マシンが解釈を妨げるものはありません。 JITコンパイルを行うことにした場合は、前のトレースの「制御フロー変更の終了」にパッチを適用して、生成した新しいトレースを指すようにすることができます。代わりに解釈することにした場合;インタプリタが制御フローの変更を解釈した直後に、トレースキャッシュをチェックして、次の部分の「すでにネイティブ」なトレースがあるかどうかを確認します。存在する場合は、すでにコンパイルされたトレースを実行できます。存在しない場合は、解釈を続けるか、JITコンパイルに切り替えることを決定できます。
他に必要なことは、JITをいつ使用し、いつ解釈するかを決定することだけです。通常、JITが価値があると推測するために使用できるいくつかのヒントがあります(具体的には、宛先がより低いアドレスにある条件付き分岐は通常、ループを示し、ループはJITコンパイルの有力な候補です)。それを超えて、いくつかの統計(何かが解釈された回数など)を維持し、それらを「JITまたは解釈」決定の基礎として使用する必要があります。