| プログラム実行 |
|---|
| 一般的な概念 |
| コードの種類 |
| コンパイル戦略 |
| 注目すべき実行時間 |
| 注目すべきコンパイラとツールチェーン |
|
コンピューティングにおいて、ジャストインタイム( JIT )コンパイル(動的変換または実行時コンパイルとも呼ばれる) [ 1 ]は、プログラムの実行前ではなく実行時に (コンピュータ コードを) コンパイルすることです。[ 2 ]これはソースコード変換で構成される場合もありますが、より一般的にはバイトコードからマシン コードへの変換であり、その後マシン コードが直接実行されます。JIT コンパイラを実装しているシステムでは、通常、実行中のコードを継続的に分析し、コンパイルまたは再コンパイルによって得られる速度向上が、そのコードをコンパイルするオーバーヘッドを上回るコード部分を特定します。
JIT コンパイルは、機械語への変換に対する 2 つの従来の手法、つまり事前コンパイル(AOT) と解釈を組み合わせたもので、両方のメリットとデメリットを兼ね備えています。[ 2 ]大まかに言うと、JIT コンパイルは、コンパイルされたコードの速度と解釈の柔軟性、インタープリタのオーバーヘッド、コンパイルとリンク(解釈だけでなく) の追加のオーバーヘッドを組み合わせたものです。JIT コンパイルは動的コンパイルの一種で、動的再コンパイルやマイクロアーキテクチャ固有の高速化などの適応型最適化を可能にします。[注 1 ] [ 3 ]解釈と JIT コンパイルは、ランタイム システムが遅延バインディングデータ型を 処理し、セキュリティ保証を実施できるため、動的プログラミング言語に特に適しています。
最も古い JIT コンパイラは、一般的に1960 年のJohn McCarthyによるLISPの研究に帰せられます。 [ 4 ]彼の独創的な論文「記号式の再帰関数と機械によるその計算、パート I」では、実行時に翻訳される関数について言及されており、これによってコンパイラの出力をパンチ カードに保存する必要がなくなりました[ 5 ](ただし、これはより正確には「コンパイル アンド ゴー システム」として知られています)。もう 1 つの初期の例はKen Thompsonによるもので、彼は 1968 年に、この例ではテキスト エディタQEDでのパターン マッチングに、正規表現の最初の応用例の 1 つを示しました。[ 6 ]速度を重視して、Thompson は互換タイム シェアリング システム上のIBM 7094コードにJITすることによって正規表現マッチングを実装しました。[ 4 ] [ 7 ] [ 8 ]
Smalltalk(1980年頃)は、JITコンパイルの新たな側面を開拓しました。例えば、機械語への変換はオンデマンドで行われ、結果は後で使用するためにキャッシュされます。メモリが不足すると、システムはこのコードの一部を削除し、再び必要になったときに再生成します。[ 2 ] [ 9 ] SunのSelf言語はこれらの技術を大幅に改良し、一時は世界最速のSmalltalkシステムとなり、最適化されたC言語の最大半分の速度[ 10 ]を達成しながらも、完全なオブジェクト指向プログラミング言語でした。
SelfはSunによって放棄されましたが、その研究はJava言語へと移されました。「ジャストインタイムコンパイル」という用語は、製造業の用語「ジャストインタイム」から借用され、Javaによって普及しました。James Goslingは1993年にこの用語を使用しました。[ 11 ]現在、JITは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クラスデータファイルは40MBであり、JVMはこのコンテキスト的に巨大なファイル内で大量のデータをシークする必要があります。[ 20 ]
SunのHotSpot Java仮想マシンで採用されている最適化手法の一つは、解釈とJITコンパイルを組み合わせることである。アプリケーションコードはまず解釈されるが、JVMはどのバイトコードシーケンスが頻繁に実行されるかを監視し、それらをハードウェア上で直接実行できるようにマシンコードに変換する。数回しか実行されないバイトコードの場合、これによりコンパイル時間が節約され、初期のレイテンシが短縮される。頻繁に実行されるバイトコードの場合、最初の低速な解釈フェーズの後、JITコンパイルによって高速実行される。さらに、プログラムはほとんどの時間をコードの一部の実行に費やしているため、コンパイル時間の短縮は顕著である。最後に、初期のコード解釈段階では、コンパイル前に実行統計を収集することができ、より効果的な最適化を行うのに役立つ。[ 21 ]
適切なトレードオフは状況によって異なります。例えば、SunのJava仮想マシンには、クライアントとサーバーという2つの主要モードがあります。クライアントモードでは、最小限のコンパイルと最適化が実行され、起動時間が短縮されます。サーバーモードでは、広範なコンパイルと最適化が実行され、起動時間を犠牲にしてアプリケーションの実行後のパフォーマンスが最大化されます。他のJavaジャストインタイムコンパイラは、メソッドの実行回数とバイトコードサイズを組み合わせた実行時測定をヒューリスティックとして利用し、コンパイルのタイミングを決定します。[ 22 ]また、実行回数とループの検出を組み合わせて使用するものもあります。[ 23 ]一般的に、短時間実行のアプリケーションでは、長時間実行のアプリケーションよりもどのメソッドを最適化すべきかを正確に予測することがはるかに困難です。[ 24 ]
Microsoftのネイティブイメージジェネレータ(Ngen)は、初期遅延を短縮するもう一つのアプローチです。[ 25 ] Ngenは、共通中間言語(CIL)イメージ内のバイトコードをマシンネイティブコードにプリコンパイル(または「プリJIT」)します。そのため、実行時のコンパイルは不要です。Visual Studio 2005に同梱されている.NET Framework 2.0は、インストール直後にすべてのMicrosoftダイナミックリンクライブラリ(DLL)ファイルに対してNgenを実行します。プリJITは起動時間を短縮する手段を提供します。しかし、生成されるコードの品質はJITコンパイルされたコードよりも低くなる可能性があります。これは、プロファイルに基づく最適化(profile-guided optimization)を行わずに静的にコンパイルされたコードが、極端な場合にはJITコンパイルされたコードほど良くならないのと同じ理由です。つまり、例えばインラインキャッシュを駆動するためのプロファイリングデータが不足しているのです。[ 26 ]
AOT (Ahead-of-Time) コンパイラと JIT コンパイラ ( Excelsior JET ) またはインタープリター ( GNU Compiler for Java ) を組み合わせた Java 実装も存在します。
JIT コンパイルは、短い初期ウォームアップ期間の後にパフォーマンスが向上した定常状態に入るという目標を、確実に達成できない可能性があります。[ 27 ] [ 28 ] 8 つの異なる仮想マシンで、Barrett ら (2017) は、仮想マシンの実装者が最適化ターゲットとして一般的に使用する 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 コンパイルは、一部のエミュレーターでも、マシン コードを 1 つの CPU アーキテクチャから別の CPU アーキテクチャに変換するために使用されています。
JITコンパイルの一般的な実装は、まずバイトコード(仮想マシンコード)へのAOTコンパイル(バイトコードコンパイル)を行い、次にバイトコードを解釈せずにマシンコード(動的コンパイル)へのJITコンパイルを行うというものです。これにより、解釈コンパイルに比べて実行時のパフォーマンスが向上しますが、コンパイルによる遅延が発生します。JITコンパイラは、インタープリタと同様に継続的に翻訳を行いますが、コンパイル済みのコードをキャッシュすることで、特定の実行中に同じコードを実行する際の遅延を最小限に抑えます。プログラムの一部のみがコンパイルされるため、プログラム全体を実行前にコンパイルする場合に比べて、遅延が大幅に軽減されます。
{{cite journal}}:ジャーナルを引用するには|journal=(ヘルプ)が必要です