C++ → 機械語(ネイティブコード)になるまでの工程を3W(Who, What, Why)で確認する。
通常のコンパイルの流れ(Clang, GCCなど)
| ステージ | 内容 | 備考 |
|---|---|---|
| C++ソース | ソースコードの入力 | |
| 構文解析 | パースしてASTを生成 | |
| 抽象構文木 (AST) | 構文構造の木構造 | |
| IR生成 | 中間表現に変換 | LLVM IRなど |
| アセンブリ | アセンブラを生成 | |
| 機械語 | オブジェクトファイル出力 | CPU依存の命令セット |
◇構文解析
誰が生成する?(Who)
コンパイラのフロントエンド(構文解析器:Parser)
何?(What)
原始的なトークンを解析して、プログラムの構造を識別するプロセス。
補足:原始的なトークン
プログラムの最も基本的な単位。キーワード(ifやWhile)、識別子(変数名や関数名)、リテラル(数値や文字列)、記号が含まれる。
なぜ必要?(Why)
プログラムの正確性を保証し、実行可能なデータ構造に変換するため。誤った構文を検出したり、後の処理がスムーズになるように整える役割をする。
◇抽象構文木(AST)
誰が生成する?(Who)
コンパイラ(Clang, GCC, MSVC)のフロントエンド。
何?(What)
プログラムの構文構造を木構造で表現したもの。各ノードは文法要素(関数定義、式、変数宣言など)を表す。
例:
FunctionDecl: add
├── Type: int
├── Param: a (int)
├── Param: b (int)
└── ReturnStmt
└── BinaryOperator: +
├── VarRef: a
└── VarRef: b
なぜ必要?(Why)
ソースコードの構造を機械的に理解・操作するため。構文エラー検出、型チェック、最適化、コード生成などに不可欠。コンパイラのフロントエンド、IDEのコード補完エンジン、静的解析ツール、コードフォーマッタなどで使用される。
補足:コード補完:
プログラミング中に入力を支援する機能で、開発者が書きかけのコードに対して、適切なキーワードや関数名、変数名などを自動的に提案・補完してくれる仕組み
疑問:ASTがコンパイル時にできるものなら、コード補完には使えないのでは?
AST(抽象構文木)はコンパイル前の構文解析で生成されるため、コード補完にも活用される。
IDEはリアルタイム(入力時)で構文解析を行っている。
補足:ASTでの最適化
| 最適化手法 | 内容 |
|---|---|
| 定数畳み込み(Constant Folding) |
2 + 3 のような定数式をコンパイル時に計算し、AST上で 5 に置き換える。 |
| 定数伝搬(Constant Propagation) | 定数として定義された変数の使用箇所に、その値を直接埋め込む。 |
| 式の簡略化 | 冗長な演算や条件式を簡単な形に変換(例:x * 1 → x)。 |
| 不要なノードの削除 | 実行に影響しないノード(空の式や未使用の変数など)を除去。 |
| 構造の正規化 | 同じ意味を持つ異なる構文を統一的な形に変換し、後続処理を簡略化。 |
◇IR生成
誰が生成する?(Who)
コンパイラ(Clang, GCC, MSVC)のフロントエンド。
何?(What)
ソースコードと機械語の中間に位置する抽象的なコード表現。プログラムの構造や意味を保持する。
なぜ必要?(Why)
最適化のしやすさ、ターゲットアーキテクチャへの柔軟な対応、複数言語・プラットフォームのサポートを可能にするため。
補足:プラットフォーム
実行対象のOS(Windows, Linux, macOS)やCPUアーキテクチャ(x86, x64, ARM, RISC-V)を指す。
補足:複数言語・プラットフォームのサポートを可能にする
IRはCPUアーキテクチャ(x86, ARM)に依存しないので、同じ最適化を複数のプラットフォームで使える。
補足:IRでの最適化
| 最適化手法 | 内容 |
|---|---|
| デッドコード削除(Dead Code Elimination) | 使用されない変数や式を削除。use-defが明確なので「使われていない定義」が簡単に判定できる。 |
| 共通部分式削除(Common Subexpression Elimination) | 同じ計算結果を再利用。定義と使用が一意なので、再計算の必要性を判定しやすい。 |
| 定数畳み込み(Constant Folding) | 定数同士の演算をコンパイル時に計算。 |
| レジスタ割り付けの最適化 | 変数の生存期間が明確になるため、レジスタの効率的な利用が可能。 |
| ループ不変コードの外出し(Loop-Invariant Code Motion) | ループ内で毎回同じ結果になる式をループ外に移動。use-def解析で式の依存関係が明確になるため、安全に移動できる。 |
| 部分冗長性除去(Partial Redundancy Elimination) | 条件付きで冗長な計算を除去。use-def chainにより、どの経路で定義が使われるかを判定できる。 |
◇アセンブリ
誰が生成する?(Who)
コンパイラのバックエンドやアセンブラ。
※基本的に、コンパイラはC++コードをアセンブリに変換する機能を持っている。(簡易コンパイラなどを除く。)
何?(What)
アセンブリコードは、CPUが理解できる機械語に近い低水準の命令列。
人間が読める形式で、各命令がCPUの動作に直接対応している。
なぜ必要?(Why)
AST・IRで最適化されたコードを手動で調整したり、内容を確認するため。
:::note info
補足:アセンブリの編集
組み込み開発やOSカーネル開発など、ハードウェアに近い領域では、手書きでアセンブリを作ったり修正したりする場合もある。
:::
:::note info
疑問:アセンブリは必須の工程? 人間の方で最適化する必要がなかったり、中身に興味ない場合はいらない?
Yes。アセンブリは「人間が読むための中間表現(人間が最適化したいときに使う道具)」であって、必須の工程ではない。
アセンブリを省略してIRから直接機械語を生成することは可能あり、実際、多くの現代的なコンパイラはそのような構造を採用している。
:::
| 工程 | 必須か? | 説明 |
|---|---|---|
| AST → IR | ✅ 必須 | 抽象化と最適化のため |
| IR → 機械語 | ✅ 必須 | 実行可能なコード生成 |
| IR → アセンブリ → 機械語 | ❌ 任意 | 人間が介入する場合のみ必要 |
◇機械語
誰が生成する?(Who)
IRから直接生成する場合は、コンパイラのバックエンドが(IRを最適化しつつ)生成する。
アセンブリから生成する場合は、アセンブラが生成する。
疑問:ここでのIR→機械語変換時の最適化はIR生成時とは別の最適化?
別物。IR→機械語変換時には以下のような最適化が行われる。
| 最適化手法 | 内容 |
|---|---|
| レジスター割り当て | 命令の実行に必要なデータをメモリではなくレジスターに置くことで、高速化を図る。 |
| 命令スケジューリング | プロセッサのパイプラインを効率的に使用するために命令の実行順序を調整。 |
| デッドコード除去 | IRに含まれる不要なコード(実行されない部分など)を取り除く。 |
| ループの最適化 | 例えばループ展開(Loop Unrolling)で実行回数を減らしたりする。 |
疑問:アセンブリ→機械語変換時には最適化が行われない?
Yes。なぜなら、アセンブリ言語はすでに人間が書いた低レベルの命令で構成されており、最適化がほぼ完了していることが前提になっているから。
アセンブラが機械語翻訳時に余計な変更(最適化)をしてしまうと、人間がアセンブリを直接編集した意味がなくなってしまう。
何?(What)
CPUが直接理解できる言語。
0と1のビット列で構成されるコンピュータの命令セット。
なぜ必要?(Why)
CPUは人間が書く高水準言語のままだと命令を理解できないため。
おまけ:各工程の拡張子
| 段階 | 拡張子例 | 説明 |
|---|---|---|
| 構文解析 |
.txt, .lex
|
原始的なトークンのリストや解析結果の保存用 |
| AST |
.ast, .xml
|
抽象構文木を記録するための形式 |
| IR |
.ll, .bc
|
中間表現、特にLLVMで用いられる形式 |
| アセンブリ |
.asm, .s
|
アセンブリコードを記述する形式 |
| 機械語 |
.exe, .out, .elf
|
実行可能なバイナリファイル形式 |