ジャストインタイム(JIT)コンパイル(動的変換または実行時コンパイルとも呼ばれる)[ 1 ]は、プログラムの実行前ではなく、実行時にコンピュータコードをコンパイルすることです。 [ 2 ]これはソースコードの変換で構成される場合もありますが、より一般的にはバイトコードをマシンコードに変換し、それを直接実行します。JITコンパイラを実装するシステムは、通常、実行中のコードを継続的に分析し、コンパイルまたは再コンパイルによって得られる高速化が、そのコードをコンパイルするオーバーヘッドを上回るコードの部分を特定します。
JIT コンパイルは、マシン コードへの変換の 2 つの従来のアプローチ、事前コンパイル(AOT) と解釈を組み合わせたもので、両方の利点と欠点を併せ持っています。[ 2 ]大まかに言うと、JIT コンパイルは、コンパイル済みコードの速度と解釈の柔軟性を、インタプリタのオーバーヘッドとコンパイルおよびリンク(解釈だけでなく) の追加オーバーヘッドと組み合わせたものです。JIT コンパイルは動的コンパイルの一種であり、動的再コンパイルやマイクロアーキテクチャ固有の高速化などの適応的最適化を可能にします。[ nb 1 ] [ 3 ]ランタイム システムが遅延バインドデータ型を処理し、セキュリティ保証を強制できるため、解釈と JIT コンパイルは動的プログラミング言語に特に適しています。
最初に公開された JIT コンパイラは、一般的に1960 年にJohn McCarthyがLISPで行った研究に起因するとされています。 [ 4 ]彼の画期的な論文「記号式の再帰関数とその機械による計算、パート I」では、実行時に変換される関数について言及しており、これによりコンパイラの出力をパンチ カードに保存する必要がなくなります[ 5 ] (ただし、これはより正確には「コンパイルして実行できるシステム」として知られています)。もう 1 つの初期の例はKen Thompsonによるもので、彼は 1968 年に正規表現の最初の応用例の 1 つを示しました。ここでは、テキスト エディタQEDのパターン マッチングに使用しました。[ 6 ]速度のために、Thompson は互換タイム シェアリング システム上のIBM 7094コードに JIT することで正規表現マッチングを実装しました。[ 4 ]解釈からコンパイル済みコードを導出する影響力のある技術は、1970 年にJames G. Mitchellによって開拓され、実験言語LC²に実装されました。[ 7 ] [ 8 ]
Smalltalk(1980年頃)は、JITコンパイルの新しい側面を開拓しました。たとえば、マシンコードへの変換はオンデマンドで行われ、結果は後で使用するためにキャッシュされました。メモリが不足すると、システムはこのコードの一部を削除し、再び必要になったときに再生成しました。[ 2 ] [ 9 ] SunのSelf言語はこれらの技術を大幅に改善し、一時は世界最速のSmalltalkシステムとなり、最適化されたCの半分の速度を達成しましたが、完全なオブジェクト指向プログラミング言語でした[ 10 ]。
SelfはSunによって放棄されましたが、その研究はJava言語へと引き継がれました。「Just-in-time compilation」という用語は、製造業の用語「Just in time」から借用され、Javaによって普及しました。James Goslingは1993年からこの用語を使用しています。[ 11 ]現在、JITingはJava仮想マシンのほとんどの実装で使用されており、HotSpotはこの研究基盤に基づいて構築され、広範囲に使用されています。
HPのプロジェクトDynamoは、バイトコード形式とマシンコード形式が同じ実験的なJITコンパイラでした。このシステムはPA-8000マシンコードを最適化しました。[ 12 ]意外なことに、これによりマシンコードレベルでの最適化が可能になり、場合によっては30%の高速化が実現しました。例えば、キャッシュの使用効率を高めるためのコードのインライン化、動的ライブラリへの呼び出しの最適化、その他多くの実行時最適化など、従来のコンパイラでは試みられない最適化が可能になったためです。[ 13 ] [ 14 ]
2020年11月、PHP 8.0でJITコンパイラが導入されました。[ 15 ] 2024年10月、CPythonで実験的なJITコンパイラが導入されました。[ 16 ]
バイトコードコンパイルシステムでは、ソースコードはバイトコードと呼ばれる中間表現に変換されます。バイトコードは特定のコンピュータの機械語ではなく、コンピュータアーキテクチャ間で移植可能です。バイトコードは仮想マシンによって解釈されるか、仮想マシン上で実行されます。JITコンパイラはバイトコードを複数のセクション(まれに全体)で読み込み、動的に機械語にコンパイルすることで、プログラムの実行速度を向上させます。これはファイルごと、関数ごと、あるいは任意のコード断片に対して実行できます。コードは実行直前にコンパイルされ(そのため「ジャストインタイム」と呼ばれます)、キャッシュされて後で再コンパイルすることなく再利用できます。
対照的に、従来のインタプリタ型仮想マシンは、一般的にパフォーマンスがはるかに低いバイトコードを単に解釈します。一部のインタプリタは、最初にバイトコードにコンパイルする手順を踏まずにソースコードを解釈し、さらにパフォーマンスが低下します。静的にコンパイルされたコードまたはネイティブコードは、デプロイ前にコンパイルされます。動的コンパイル環境とは、実行中にコンパイラを使用できる環境です。JIT 技術を使用する一般的な目標は、バイトコード解釈の利点を維持しながら、静的コンパイルのパフォーマンスに匹敵するか、それを超えることです。元のソースコードの解析と基本的な最適化を実行する「重労働」の多くは、デプロイ前のコンパイル時に処理されることがよくあります。バイトコードからマシンコードへのコンパイルは、ソースからのコンパイルよりもはるかに高速です。デプロイされたバイトコードは、ネイティブコードとは異なり、移植可能です。ランタイムは、インタプリタ型バイトコードと同様にコンパイルを制御できるため、セキュアなサンドボックスで実行できます。移植可能なバイトコードコンパイラがすでに多くの作業を行っているため、バイトコードからマシンコードへのコンパイラは簡単に作成できます。
JIT コードは一般的にインタプリタよりもはるかに優れたパフォーマンスを提供します。さらに、多くの最適化は実行時にのみ実行可能であるため、場合によっては静的コンパイルよりも優れたパフォーマンスを提供することもあります。[ 17 ] [ 18 ]
JIT は実行時にネイティブバイナリイメージをレンダリングして実行する必要があるため、真のマシンコード JIT は実行時にデータを実行できるプラットフォームを必要とし、ハーバードアーキテクチャベースのマシンでそのような JIT を使用することは不可能です。特定のオペレーティングシステムや仮想マシンについても同じことが言えます。ただし、特殊なタイプの「JIT」は、物理マシンの CPU アーキテクチャではなく、生のマシンコードに制限がある最適化された VM バイトコードをターゲットにする可能性があります。特に、そのバイトコードの VM が最終的に JIT を利用してネイティブコードを生成する場合です。[ 19 ]
JIT は、入力コードのロードとコンパイルにかかる時間のため、アプリケーションの初期実行時にわずかから顕著な遅延を引き起こします。この遅延は、「起動時間遅延」または「ウォームアップ時間」と呼ばれることもあります。一般的に、JIT が最適化を行うほど、生成されるコードは良くなりますが、初期遅延も増加します。したがって、JIT コンパイラは、コンパイル時間と生成しようとするコードの品質との間でトレードオフを行う必要があります。起動時間には、JIT コンパイルに加えて、IO バウンド操作の増加が含まれる場合があります。たとえば、Java 仮想マシン(JVM) のrt.jarクラスデータファイルは40 MB であり、JVM はこのコンテキスト的に巨大なファイルから大量のデータを検索する必要があります。[ 20 ]
SunのHotSpot Java仮想マシンで使用されている最適化手法の一つは、解釈とJITコンパイルを組み合わせることです。アプリケーションコードは最初に解釈されますが、JVMはどのバイトコードシーケンスが頻繁に実行されるかを監視し、ハードウェア上で直接実行するためにマシンコードに変換します。実行回数が少ないバイトコードの場合、コンパイル時間を節約し、初期レイテンシを削減できます。頻繁に実行されるバイトコードの場合、JITコンパイルを使用して、初期の低速な解釈フェーズの後、高速で実行できます。さらに、プログラムはコードのごく一部を実行するのにほとんどの時間を費やすため、コンパイル時間の短縮は重要です。最後に、最初のコード解釈中に、コンパイル前に実行統計を収集できるため、より優れた最適化の実行に役立ちます。[ 21 ]
適切なトレードオフは状況によって異なる場合があります。たとえば、Sun の Java 仮想マシンには、クライアントとサーバーの 2 つの主要なモードがあります。クライアントモードでは、起動時間を短縮するために最小限のコンパイルと最適化が実行されます。サーバーモードでは、起動時間を犠牲にしてアプリケーションの実行後にパフォーマンスを最大化するために、広範なコンパイルと最適化が実行されます。他の Java ジャストインタイム コンパイラは、メソッドの実行回数の実行時測定値とメソッドのバイトコード サイズを組み合わせたヒューリスティックを使用して、コンパイルのタイミングを決定します。[ 22 ]また、別のコンパイラは、実行回数とループの検出を組み合わせて使用します。[ 23 ]一般的に、実行時間の短いアプリケーションでは、実行時間の長いアプリケーションよりも、どのメソッドを最適化するかを正確に予測することははるかに困難です。[ 24 ]
MicrosoftのNative Image Generator (Ngen) は、初期遅延を削減するもう 1 つのアプローチです。[ 25 ] Ngen は、共通中間言語イメージ内のバイトコードをマシンネイティブコードにプリコンパイル (または「プリ JIT」) します。その結果、実行時コンパイルは不要になります。Visual Studio 2005に同梱されている.NET Framework 2.0 は、インストール直後にすべての Microsoftダイナミックリンクライブラリ(DLL) ファイルに対して Ngen を実行します。プリ JIT は起動時間を短縮する方法を提供します。ただし、生成されるコードの品質は JIT コンパイルされたコードよりも低くなる可能性があります。これは、プロファイルガイドによる最適化なしで静的にコンパイルされたコードが、極端な場合には JIT コンパイルされたコードほど良くないのと同じ理由です。たとえば、インライン キャッシュを駆動するためのプロファイリング データがないためです。[ 26 ]
また、 AOT(事前コンパイル)コンパイラとJITコンパイラ(Excelsior JET)またはインタプリタ(GNU Compiler for Java )を組み合わせたJava実装も存在する。
JITコンパイルは、短い初期ウォームアップ期間の後、パフォーマンスが向上した定常状態に入るという目標を確実に達成できない可能性があります。[ 27 ] [ 28 ] Barrettら(2017)は、8つの異なる仮想マシンで、仮想マシン実装者が最適化のターゲットとしてよく使用する6つの広く使用されているマイクロベンチマークを、単一のプロセス実行内で繰り返し実行して測定しました。[ 29 ] Linuxでは、プロセス実行の8.7%から9.6%がパフォーマンスの定常状態に達しず、16.7%から17.9%がウォームアップ期間後にパフォーマンスが低下した定常状態に入り、特定のベンチマークを実行する特定の仮想マシンのペアの56.5%が、複数の実行にわたってパフォーマンスの定常状態の低下を一貫して確認できませんでした(つまり、少なくとも1つの実行が定常状態に達しなかったか、定常状態でパフォーマンスが低下しました)。改善された定常状態に達した場合でも、数百回の反復が必要になることがありました。[ 30 ] Traini ら (2022) は、 HotSpot 仮想マシンに焦点を当て、より広範囲のベンチマークを使用して、[ 31 ]プロセス実行の 10.9% が安定したパフォーマンス状態に達しず、ベンチマークの 43.5% が複数回の実行にわたって一貫して安定した状態に達しなかったことを発見しました。[ 32 ]
JITコンパイルは基本的に実行可能データを使用するため、セキュリティ上の課題や潜在的な脆弱性を抱えている。
JIT コンパイルの実装は、ソース コードまたはバイト コードをマシン コードにコンパイルして実行することから成ります。これは通常、メモリ内で直接行われます。JIT コンパイラはマシン コードを直接メモリに出力し、通常の事前コンパイルのようにディスクに出力してから別のプログラムとしてコードを呼び出すのではなく、それをすぐに実行します。最新のアーキテクチャでは、実行可能領域の保護により問題が発生します。任意のメモリは実行できません。そうしないと潜在的なセキュリティ ホールが発生するためです。したがって、メモリは実行可能としてマークする必要があります。セキュリティ上の理由から、これはコードがメモリに書き込まれた後に行う必要があり、書き込み可能/実行可能なメモリはセキュリティ ホールとなるため、読み取り専用としてマークする必要があります ( W^Xを参照)。[ 33 ]例えば、Firefox のJavaScript用 JIT コンパイラは、Firefox 46 のリリース バージョンでこの保護を導入しました。[ 34 ]
JITスプレー攻撃は、JITコンパイルを利用してヒープを攻撃するコンピュータセキュリティの脆弱性攻撃の一種です。結果として生成されるメモリは実行可能となり、実行をヒープに移動できれば攻撃が可能になります。
JITコンパイルは、一部のプログラムに適用することも、特定の機能、特に正規表現などの動的な機能に使用することも可能です。たとえば、テキストエディタは、実行時に提供される正規表現をマシンコードにコンパイルして、マッチングを高速化することができます。パターンは実行時にのみ提供されるため、これは事前に行うことはできません。最新のランタイム環境の多くは、高速なコード実行のためにJITコンパイルに依存しており、Javaのほとんどの実装やMicrosoftの.NETなどが含まれます。同様に、多くの正規表現ライブラリは、正規表現をバイトコードまたはマシンコードにJITコンパイルする機能を備えています。JITコンパイルは、マシンコードをあるCPUアーキテクチャから別のCPUアーキテクチャに変換するため、一部のエミュレータでも使用されています。
JITコンパイルの一般的な実装方法は、まずAOTコンパイルでバイトコード(仮想マシンコード)を作成し(バイトコードコンパイルと呼ばれる) 、次にバイトコードの解釈ではなくJITコンパイルでマシンコードを作成する(動的コンパイル)というものです。これにより、コンパイルによる遅延が発生するものの、解釈に比べて実行時のパフォーマンスが向上します。JITコンパイラはインタプリタと同様に継続的に変換を行いますが、コンパイル済みコードをキャッシュすることで、実行中に同じコードが再度実行される際の遅延を最小限に抑えます。プログラムの一部のみがコンパイルされるため、実行前にプログラム全体をコンパイルする場合よりも遅延が大幅に少なくなります。
{{cite journal}}:ジャーナルを引用するには|journal=(ヘルプ)