JavaScriptはインタープリター言語でしょうか、それともコンパイラ言語でしょうか?これは長年続いてきた議論です。YDKJS(You Don't Know JS)の著者はコンパイラ言語だと考えると結論付けました。なぜそう考えたのかを調査した結果、タイトルで既に述べたように、私はJavaScriptはインタープリター言語であると同時にコンパイラ言語でもあるという結論に達しました。
現代のJavaScriptは様々なエンジンによって実行されています。これらのエンジンがJavaScriptをどのように実行するかを見て、なぜこのような結論に達したのかを理解しましょう。
予備知識
JITコンパイラ
従来のコンパイル方式が実行前にすべてのコードをコンパイルするのに対し、JIT(Just-In-Time)はプログラム実行中に必要な時点でコードをコンパイルして実行する方式です。ソースコードを事前にすべてコンパイルするのではなく、実行時に必要な部分だけを即時コンパイルしてパフォーマンスを最適化します。
中間表現(Intermediate Representation, IR)
中間表現(IR)は、コンパイラやインタープリタがソースコードを処理する過程で内部的に使用するために作成する中間段階のコードまたはデータ構造形式です。ソースコードを直接機械語に変換したり実行したりする前に、ソースコードをより単純で抽象化された形に変換し、コンパイラやインタープリタが簡単に分析し最適化できるようにします。
V8
ChromeのV8 JavaScriptエンジンは1つのインタープリタと3つのコンパイラで動作します。どのように動作するか簡単に見てみましょう。
1. Ignition(バイトコードインタープリタ)
元のV8エンジンは、すべてのJavaScriptコードを実行直前にJITコンパイルしてネイティブ機械語コードに変換することで速度を向上させました。最初はBaselineコンパイラを使用して迅速にコンパイルしました。Baselineコンパイラを通じてコンパイルされたコードは最適化されていなかったため、V8の実行パイプラインを通じて高度なコンパイラであるTurboFanなどによって再コンパイルされます。
このアプローチの問題点は、一度だけ実行されるコードもコンパイルするため、メモリ的に無駄が多かったことです。このオーバーヘッドを軽減するために、既存のBaselineコンパイラを置き換えるためのIgnitionという新しいJavaScriptインタープリタを作成しました。
Ignitionは、JavaScriptコードをまず簡潔なバイトコードに変換し、そのバイトコードをインタープリタが実行する構造です。
これに関連するプレゼンテーション動画がありますので、より深く掘り下げたい方は参照するといいでしょう!
2. Sparkplug(ベースラインJIT)
Ignitionというインタープリタを作成してパフォーマンスを改善しましたが、最終的にはインタープリタという特性の限界に直面しました。そこで2021年、IgnitionとTurboFanという高度なコンパイラの間で動作するSparkplugというコンパイラを作成しました。
SparkplugはJavaScriptをコンパイルするのではなく、インタープリタが既に作成したバイトコードをコンパイルします。また、中間表現(Intermediate Representation, IR)なしで、バイトコードを一度だけスキャンして直接機械語を生成します。
Sparkplugは複雑な最適化プロセスを経ずに、単に迅速に実行可能なコードを作成します。より複雑な最適化はTurboFanが担当します。
3. Maglev(最速最適化JIT)
2023年、さらに新しいコンパイラが登場しました。Ignitionインタープリタは実行は速いがパフォーマンスは低く、Sparkplugはバイトコードをほぼ即座に機械語に変換するが最適化レベルは限られており、TurboFanは最高性能の最適化されたコードを生成するがコンパイル時間が長くかかります。これらの限界をさらに打破するために、Maglevというコンパイラを作成しました。
MaglevはGoogleのV8 JavaScriptエンジンに導入された最新のJITコンパイラで、既存のSparkplugとTurboFanの間に位置します。Maglevの目標は「十分に良い」最適化されたコードを非常に迅速に生成することです。
4. TurboFan(高レベル最適化JIT)
TurboFanはV8の最高級最適化コンパイラです。最も複雑で精巧な最適化を実行して、最高性能の機械語コードを生成します。
しかしTurboFanの欠点はコンパイル時間が長くかかることです。そのため、非常に頻繁に実行される「HOT 🔥」コードにのみ適用されます。V8はプロファイリングを通じてどのコードが頻繁に実行されるかを追跡し、そのようなコードだけをTurboFanで再コンパイルします。
このようにV8は4段階の実行パイプラインを通じて、JavaScriptコードの特性に合った最適な実行戦略を選択します。
Webkit
Webkitも同様に1つのインタープリタと3つのコンパイラで動作します。
1. LLInt(Low Level Interpreter)
LLIntはWebKit JavaScriptエンジンの最初の実行段階です。JavaScriptコードをバイトコードに変換した後、バイトコードを1行ずつ読み即時実行します。特定の関数やコードが複数回実行される場合(「HOT 🔥」コード)、Baseline JITによるコンパイルを試みます。
2. Baseline JIT
LLIntでコードが複数回実行されることが検出されると(関数6回、特定のコマンド100回)、バイトコードをほぼそのまま機械語に変換します。コンパイル速度が非常に速いため、実行遅延はほとんどありません。最適化は行いません。
3. DFG JIT(Data Flow Graph JIT)
Baseline JITで関数が66回以上呼び出されるか、特定のコマンドが1000回以上実行されると動作します。バイトコードを中間表現(IR)に変換します。DFG JITはコードのデータフローを分析し、不要な演算を削除し、変数タイプを推論し、レジスタ割り当てなど様々なコンパイラ最適化を実行します。
4. FTL JIT(Faster Than Light JIT)
WebKitで使用される最も高度な段階のコンパイラです。JavaScriptコードをネイティブコードに変換して実行速度を最大化します。
SpiderMonkey
FirefoxのSpiderMonkeyも同様の方法でインタープリタと様々なJITを戦略的に選択して使用します。
結論
最も広く使用されている3つのエンジンを調査しました。すべてのエンジンがインタープリタ方式とコンパイル方式を混合して使用していました。最初はインタープリタで迅速に開始し、必要に応じて段階的にコンパイルしてパフォーマンスを向上させます。
「JavaScriptはインタープリタ言語かコンパイラ言語か」という質問自体が、現代のJavaScript実行環境では適切ではないと思います。現代のJavaScriptは「インタープリタとコンパイラを混合して使用する言語」と表現する方がより正確に思えます。
参考文献
- Interpreter (computing) - Wikipedia
- Firing up the Ignition interpreter · V8
- Maglev - V8's Fastest Optimizing JIT · V8
- Sparkplug — a non-optimizing JavaScript compiler · V8
- Introducing the WebKit FTL JIT
- Introducing the B3 JIT Compiler
- The Baseline Interpreter: a faster JS interpreter in Firefox 70 – Mozilla Hacks - the Web developer blog
- Warp: Improved JS performance in Firefox 83 – Mozilla Hacks - the Web developer blog