コンパイラ
人間が書いたソースコードを機械が実行できる形式に一括変換するプログラム
コンパイラとは
コンパイラ (Compiler) は、人間が書いたソースコードを機械が実行できる形式に変換するプログラムである。ソースコード全体を一括で解析・変換し、実行可能なファイルを生成する。
コンパイラとインタプリタの違い
プログラムを実行する方式は大きく 2 つに分かれる。
| 方式 | 動作 | 例 |
|---|---|---|
| コンパイラ | 実行前に機械語や中間表現へ変換し、成果物を保存する | C, Rust, Go, Java |
| インタプリタ | 変換した内部表現をその場で実行し、成果物を残さない | Python, Ruby, JavaScript |
よくある誤解として「インタプリタはソースコードを 1 行ずつ読んで実行する」という説明があるが、現代の主要な処理系はそうなっていない。CPython は Python 公式ドキュメントの用語集どおり、ソースコードをバイトコードへコンパイルしてから実行し、その結果を .pyc に保存して次回の再コンパイルを省く。分かれ目は「変換するか」ではなく「変換した成果物を実行前に外部へ残すか、実行するその場で持つか」である。1 行ずつ処理する方式は初期の BASIC 系処理系に見られた形で、今日の Python や Ruby には当てはまらない。
コンパイラの成果物が速いのは、実行時に解釈のオーバーヘッドがないことに加え、プログラム全体を見渡してから最適化 (関数の埋め込み、無駄な計算の除去、レジスタの割り当て) をかけられるからである。インタプリタが有利なのは、変換結果を保存する工程がないので編集から実行までが短く、開発中の試行錯誤が速い点にある。
実際には多くの言語が両方の要素を持つ。Java はコンパイラでバイトコードに変換し、JVM (インタプリタ + JIT コンパイラ) で実行する。JavaScript も同様で、V8 はすべてのコードをまずバイトコードにして Ignition というインタプリタで実行し、実行中に観測した型や形状の情報をもとに、よく通る箇所だけを最適化コンパイラへ渡す。段は 2026 年 8 月時点で Ignition (インタプリタ)、Sparkplug (簡易コンパイラ)、Maglev (中間段の最適化コンパイラ、Chrome M117 で追加)、TurboFan (最高性能を狙う最適化コンパイラ) の 4 段構成である。
AOT と JIT
変換をいつ行うかで 2 つに分かれる。
| 方式 | 変換の時期 | 使える情報 |
|---|---|---|
| AOT (Ahead-Of-Time) | 実行前 | ソースコードと型宣言のみ |
| JIT (Just-In-Time) | 実行中 | 実際に流れた値の型、分岐の偏り、呼び出し回数 |
この情報量の差が性能特性の差を生む。AOT は起動した瞬間から最高速で走るが、実行時にしか分からない偏り (この変数には常に整数しか来ない、この分岐はほぼ片方しか通らない) を利用できない。JIT は観測してから最適化するので起動直後は遅く、暖まってから速くなる。加えて JIT は「常に整数だと仮定したコード」を投機的に生成できるが、仮定が破れた時点でそのコードを捨ててインタプリタへ戻る (脱最適化) 必要があり、型が安定しないコードでは最適化と破棄を往復して逆に遅くなる。短時間で終わるコマンドや起動時間が critical な用途で AOT が選ばれるのは、暖機の時間を取れないからである。
コンパイルの流れ
コンパイラは以下の段階でソースコードを変換する。
ソースコード
↓ 字句解析 (Lexing)
トークン列
↓ 構文解析 (Parsing)
抽象構文木 (AST)
↓ 意味解析 (Semantic Analysis)
型チェック済み AST
↓ 中間表現の生成
中間表現 (IR)
↓ 最適化 (Optimization)
最適化済み中間表現
↓ コード生成 (Code Generation)
機械語 / バイトコード
最適化が AST ではなく中間表現の上で行われる点が設計上の要点である。AST は書かれた構文の形をそのまま保っているため、for と while のように意味が同じでも別の形で現れる。いったん言語やハードウェアに依存しない中間表現へ落とすことで、同じ最適化を複数の言語と複数の CPU に使い回せる。LLVM が clang や rustc や swiftc の共通基盤になっているのは、この中間表現の層を切り出したからである。
エラーがどの段で出たかを読めると原因の切り分けが速くなる。閉じ括弧の不足は構文解析で止まるので後続の型エラーは一切表示されず、逆に型の不整合が出ているなら構文は通っている。構文エラーが大量に出ているときは最初の 1 件だけを見るのが実務上の定石で、2 件目以降は解析器が復帰に失敗した副産物であることが多い。
トランスパイラとの違い
トランスパイラは、ある言語から別の言語への変換を行う。コンパイラが「高級言語 → 機械語」なのに対し、トランスパイラは「高級言語 → 高級言語」である。
| ツール | 変換 |
|---|---|
| TypeScript コンパイラ (tsc) | TypeScript → JavaScript |
| Babel | 新しい JavaScript → 古い JavaScript |
| Rust コンパイラ (rustc) | Rust → 機械語 |
| Go コンパイラ | Go → 機械語 |
TypeScript の tsc はコンパイラと呼ばれるが、gcc や rustc とは役割が違う。行うのは型検査と、型注釈を取り除いて指定した ECMAScript バージョンの JavaScript を出力することであり、性能を上げるための最適化パスは持たない。型情報は出力される JavaScript に一切残らない (型消去) ため、実行速度は型を書いても変わらない。tsc の価値は生成コードの速さではなく、実行前に型の不整合を検出できることにある。
コンパイルエラー
コンパイラはソースコードの問題を実行前に検出する。これがコンパイルエラーである。
// TypeScript のコンパイルエラー例
const label: string = 42;
// error TS2322: Type 'number' is not assignable to type 'string'.
コンパイルエラーは実行前に問題を発見できるため、実行時エラーより対処しやすい。静的型付け言語のコンパイラは、型の不整合を網羅的にチェックしてくれる。
ただし「網羅的」の範囲は型として書かれた約束の中に限られる。外部 API のレスポンスを any で受けた時点で検査は止まり、配列の添字が範囲内かどうかも通常の型では表現できない。コンパイルが通ったことは「書いた型どおりに使っている」証明であって、書いた型が現実と一致している保証ではない。
主要なコンパイラ
| コンパイラ | 言語 | 特徴 |
|---|---|---|
| gcc / clang | C / C++ | gcc は 1987 年公開で歴史が長い。clang は LLVM 基盤で診断メッセージが読みやすい |
| rustc | Rust | 借用検査により、unsafe を使わない範囲でメモリ安全性を検証する |
| go build (cmd/compile) | Go | 最適化を控えめにしてコンパイル速度を優先する設計 |
| javac | Java | 機械語ではなく JVM バイトコードを生成する |
| tsc | TypeScript | 型検査と、型を取り除いた JavaScript の出力 |
コンパイル時間の問題
大規模プロジェクトではコンパイル時間が開発効率に影響する。
| 対策 | 説明 |
|---|---|
| インクリメンタルコンパイル | 変更されたファイルだけ再コンパイル |
| キャッシュ | 前回のコンパイル結果を再利用 |
| 並列コンパイル | 複数ファイルを同時にコンパイル |
TypeScript の --incremental フラグや、Rust の cargo のキャッシュ機構がこれにあたる。
インクリメンタルコンパイルが効くかどうかは、依存の向きに左右される。多くのファイルが読み込む共通の型定義ファイルを 1 行変えると、そのファイルに依存する全体が再コンパイル対象になるため、変更が 1 行でも全ビルドと変わらない時間がかかる。ビルド時間を縮める作業の実体は、コンパイラの設定を変えることよりも、広く参照される中心的なファイルを分割して依存の扇形を小さくすることであることが多い。
この記事は役に立ちましたか?