Skip to content

エンジンの全体像

JavaScript エンジンは、ソースコードという文字列を受け取り、それを実行して結果を返すソフトウェアです。その内部は、いくつかの段階に分かれたパイプラインとして捉えられます。この章では、ソースコードが実行されるまでにたどる各段階を概観し、エンジン全体の設計上もっとも大きな分岐点である「どのように実行するか」の選択肢を整理します。以降の章は、ここで示す各段階を一つずつ掘り下げていきます。

実行までのパイプライン

多くの JavaScript エンジンは、ソースコードを次のような段階を経て処理します。

flowchart LR
  src[ソースコード] --> lex[字句解析]
  lex --> parse[構文解析]
  parse --> ast[抽象構文木 AST]
  ast --> comp[コンパイル]
  comp --> code[中間表現/バイトコード]
  code --> exec[実行エンジン]
  exec --> result[結果]
  • 字句解析 (lexical analysis): 文字の並びを、意味のある最小単位である「トークン」(識別子・数値リテラル・演算子・キーワードなど)の列に変換します。
  • 構文解析 (parsing): トークン列を、言語の文法規則に従って木構造(抽象構文木、AST)に組み立てます。文法的に不正な入力はここで検出されます。
  • コンパイル: AST を、実行しやすい中間表現に変換します。多くのエンジンはここでバイトコード(仮想マシン向けの命令列)を生成します。
  • 実行: 中間表現を実際に走らせて、値の計算・オブジェクトの操作・関数呼び出しなどを行います。

この段階分けは概念的なものであり、実装上は段階を融合したり、必要になるまで後段を遅延させたりすることもあります。たとえば、関数の本体を最初は構文の妥当性だけ確認しておき、実際に呼び出されたときに初めてコンパイルする、といった遅延コンパイルは広く用いられます。

どのように実行するか — 三つの方式

パイプラインの最終段「実行」をどう作るかは、エンジンの性格を決める最大の設計判断です。大きく三つの方式があります。

ツリーウォーク型インタプリタ

AST を直接たどりながら、ノードごとに対応する処理を再帰的に実行する方式です。

  • 利点: 実装が単純で、AST さえあれば動く。中間表現の設計やコード生成が不要。
  • 欠点: ノードをたどるたびに木の構造判定や再帰呼び出しの費用がかかり、実行が遅い。ループのように同じコードを何度も実行する場合、その都度木を歩き直す無駄が大きい。

学習用や、実行速度が重要でない用途では有力ですが、実用的な速度を求める場合は次のバイトコード方式が一般的です。

バイトコード型インタプリタ

AST を一度、仮想マシン向けの命令列(バイトコード)にコンパイルし、その命令列を仮想マシンが解釈実行する方式です。

  • 利点: 命令が平坦な列になるため、木を歩き直す無駄がなく、ツリーウォークより大幅に速い。命令ごとの処理(ディスパッチ)を最適化する余地がある。中間表現があるため定数畳み込みなどの最適化も挟める。
  • 欠点: コンパイル段と仮想マシンの両方を実装する必要がある。命令ごとのディスパッチ費用は残るため、後述の JIT には及ばない。

実用的なエンジンの多くが採用する、速度と実装コストのバランスが良い方式です。

JIT (実行時コンパイル)

実行時に、ホットになったコード(頻繁に実行される部分)を、その場でネイティブの機械語に変換して直接実行する方式です。多くの場合、まずバイトコードインタプリタで動かしつつ実行時の型などの情報を集め、それを前提に特化した機械語を生成します。

  • 利点: ネイティブコードを直接実行するため、インタプリタを大きく上回る速度が得られる。実行時情報に基づく特化(型の仮定・インライン化など)が可能。
  • 欠点: 実装が格段に複雑。機械語生成器、仮定が崩れたときにインタプリタへ戻る仕組み(脱最適化)、複数段の最適化階層などが必要になる。コンパイル自体の時間とメモリを要するため、起動が速いことやメモリが小さいことを重視する用途では不利になりうる。

方式の比較

方式 実行速度 実装コスト 起動の速さ メモリ 主な用途
ツリーウォーク 速い 学習・軽量な用途
バイトコード 速い 小〜中 汎用・埋め込み
JIT 遅め 高スループットが要る用途

重要なのは、これらが排他的な選択ではないことです。多くの高性能エンジンは「バイトコードインタプリタ + 複数段の JIT」という階層構成を採り、起動の速さと定常状態の速さを両立させています。一方、埋め込み用途や軽量さを重視するエンジンは、あえてバイトコードインタプリタにとどめ、実装の単純さ・起動の速さ・小さなメモリ使用量を優先します。

インタプリタの速度には、ディスパッチ方式や値の表現といった要素で縮められる範囲と、JIT なしでは越えにくい実務的な下限があります。この下限とその理由は第V部「パフォーマンス」で扱います。

実行を支える土台

上記のパイプラインとは別に、どの方式のエンジンにも共通して必要になる土台があります。本書ではこれらも中核的な題材として扱います。

  • 値の表現: 数値・文字列・オブジェクトなど、あらゆる値をメモリ上でどう表すか(第I部「値の表現」)。
  • オブジェクトモデル: プロパティの格納方法、プロトタイプチェーン、配列の表現(第II部)。
  • メモリ管理 (GC): 到達不能になったオブジェクトの自動回収(第III部)。
  • ホストとの境界: エンジンを埋め込む側(ホスト)とのやり取り、ネイティブ関数の呼び出し(第III部・第V部)。

これらは実行方式に依らず必要であり、かつ相互に密接に関係します。たとえば値の表現の選択は、演算の速度にも GC の実装にも影響します。

この先の読み進め方

次章からは第I部として、パイプラインの入口である字句解析、続いて構文解析、そして全段に影響する値の表現を順に扱います。まずはエンジンが「文字列を意味のある単位に切り分ける」ところから始めます。

参考文献