プログラムが動くまで — 機械語への翻訳

概要 — まず全体をつかむ

初級では「ソースコードは機械語に翻訳されて初めて動く」「翻訳にはコンパイルとインタプリタがある」を見ました。中級では、翻訳のタイミングという軸で全体像を整理します。

詳細 — 1段階ずつ追う

これは何をする係?

どの言語も、最後にCPUが読むのは機械語です。違うのはいつ・どこで機械語に翻訳するかだけ。この「翻訳のタイミング」で、実行のしかたが大きく3タイプに分かれます。

登場人物メモ:

  • ※1 機械語 — CPUが直接実行できる、そのCPU専用の命令
  • ※2 バイトコード — 特定CPU向けではない「中間の命令」。VMが解釈する
  • ※3 仮想マシン(VM) — バイトコードを解釈して動かすソフト(例:JVM)
  • ※4 ビルド — ソースから、実行できる形(実行ファイル等)を作り出す一連の作業
やさしく言うと(初級

人が書いた文字の並び(※1 ソースコード)は、人間には読めてもCPUには読めません。CPUが直接理解できるのは※2 機械語——0と1で書かれた命令だけだからです。

だから、動かす前に必ず「人のことば → CPUのことば(機械語)」への翻訳が入ります。プログラミング言語が読みやすい形をしているのは人のため、翻訳はそのギャップを埋める作業です。

登場人物メモ:

  • ※1 ソースコード — 人が書いたプログラムの文字の並び(元の原稿)
  • ※2 機械語 — CPUが直接実行できる、0と1で表された命令

翻訳の3つの道

アニメーション『翻訳のタイミング(コンパイル/インタプリタ/VM)』を開く
違いは「いつ機械語に翻訳するか」コンパイル型(C など)事前にまとめて翻訳→速い・環境に依存ソース人が書くコンパイラまとめて翻訳実行ファイル機械語(0と1)CPU直接実行インタプリタ型(Python など)実行時にその場で翻訳→手軽・やや遅いソース人が書くインタプリタ1行ずつ解釈しながら実行結果その場で動く第3の道:バイトコード+VM(Java/JVM)中間形式に翻訳→VMが解釈。両者の中間(そこそこ速く・環境をまたぎやすい)ソースコンパイラバイトコード中間の命令仮想マシン(VM)解釈して実行実行どのやり方でも、最後にCPUが読むのは機械語。違うのは「翻訳をいつ・どこで行うか」だけ。
  • コンパイル型(C など) — 実行の前にソース全体を機械語へ翻訳し、できた実行ファイルをCPUが直接動かす。翻訳済みなので速いが、翻訳結果が特定のCPU/OS向けになり環境に依存する
  • インタプリタ型(Python など) — 実行時にソースを1行ずつ解釈しながら動かす。すぐ試せて手軽だが、翻訳しながら走るぶんやや遅い
  • バイトコード+VM(Java/JVM) — まず環境に依らないバイトコードへ翻訳し、それを各環境のVMが解釈して動かす。両者の中間で、そこそこ速く・環境をまたぎやすい

速さと手軽さ・移植性はトレードオフで、翻訳をいつ済ませるかがそのまま性能と持ち運びやすさに効きます。

この部分をもっと深く(上級

中級の「3つの道」を、実行時の視点で深めます。

  • ネイティブ(C など) — 事前に機械語まで落とす。速いが・作った先のCPUとOSに縛られる
  • バイトコード+VM(Java/JVM) — CPUに依らないバイトコードへ翻訳し・各環境のVMが解釈する。移植しやすい
  • JIT(実行時コンパイル) — VMが、よく通る「熱い」部分だけを 実行中に機械語へ変換 して差し替える。実行時の実測(どの分岐が多いか等)を使えるぶん・事前コンパイルを超える最適化も狙える

ここから ネイティブ vs マネージド の対比が見えます。マネージドな実行(VMの上で動く)は、メモリの自動回収(ガベージコレクション)や安全検査を肩代わりしてくれて開発が楽な反面、VMという1枚の層と回収の一時停止ぶんのコストを払います。ネイティブは無駄がなく速いが・安全と後始末を自分で背負います。速さと安全・手軽さは、ここでも綱引きです。

やさしく言うと(初級

翻訳をいつやるかで、大きく2つに分かれます。

  • コンパイル(先にまとめて翻訳) — 実行の前にソース全体を機械語へ翻訳し、できた実行ファイルを動かす。翻訳済みなので動きが速い
  • インタプリタ(その場で1行ずつ翻訳) — 動かしながら1行ずつ翻訳して実行する。すぐ試せて手軽だが、翻訳しながらなのでやや遅め

料理でたとえるなら、コンパイルは「全部作り置きしてから出す」、インタプリタは「注文が来るたびその場で作る」。どちらも最後に食べられる(実行できる)のは同じです。

ビルド:ソースから実行できる形へ

コンパイルの中では、ソースが段階を追って機械語に近づきます。

  1. 字句解析 — 文字の並びを、意味のある最小の単位(トークン)に切り分ける
  2. 構文解析 — トークンの並びを、文法にそった構造(木)に組み立てる
  3. 中間表現 — 特定CPUに縛られない中間の形にする(最適化しやすい)
  4. 機械語の生成 — 対象のCPU向けの命令に落とす

この一連を含め、ソースから実行できる成果物を作る作業全体がビルドです。ライブラリの結合なども、ここで行われます。

この部分をもっと深く(上級
アニメーション『翻訳のタイミング(コンパイル/インタプリタ/VM)』を開く
違いは「いつ機械語に翻訳するか」コンパイル型(C など)事前にまとめて翻訳→速い・環境に依存ソース人が書くコンパイラまとめて翻訳実行ファイル機械語(0と1)CPU直接実行インタプリタ型(Python など)実行時にその場で翻訳→手軽・やや遅いソース人が書くインタプリタ1行ずつ解釈しながら実行結果その場で動く第3の道:バイトコード+VM(Java/JVM)中間形式に翻訳→VMが解釈。両者の中間(そこそこ速く・環境をまたぎやすい)ソースコンパイラバイトコード中間の命令仮想マシン(VM)解釈して実行実行どのやり方でも、最後にCPUが読むのは機械語。違うのは「翻訳をいつ・どこで行うか」だけ。

中級の「字句→構文→中間表現→機械語」を、実際の段階まで割ると6つになります。

  1. 字句解析 — 文字の並びを意味の最小単位(トークン)に切り分ける
  2. 構文解析 — トークンを文法にそった木構造(構文木)へ組み立てる
  3. 意味解析 ※1 — 型の整合や変数の定義済みかを検査し・意味の通らない箇所をはじく
  4. 中間表現(IR) ※2 — 特定CPUに依存しない共通の形に落とす
  5. 最適化 — IRの上で無駄を削る(使わない計算の除去・共通式のまとめ・ループの整理など)
  6. コード生成 — 対象CPUの機械語へ落とし・レジスタを割り当てる

肝は 4と5をCPUから切り離した こと。最適化をIRに集約すれば、あとは6の「コード生成」を差し替えるだけで、多数のCPUへ展開できます。フロントエンド(1〜3)とバックエンド(5〜6)をIRで分ける——これが現代のコンパイラの背骨です。

登場人物メモ:

  • ※1 意味解析 — 文法は合っていても意味が通るかを調べる段階(型検査など)
  • ※2 中間表現(IR) — 特定CPUに縛られない・最適化しやすい中間の形
  • ※3 リンカ/ローダ — 部品を1つに結合する道具/実行時にメモリへ載せる仕組み
  • ※4 ABI — 呼び出し規約やバイナリ形式など・機械語どうしが噛み合うための約束
やさしく言うと(初級
  1. 人がソースコードを書く(読みやすいことばで)
  2. それを機械語に翻訳する(CPUのことばへ)
  3. CPUが機械語を1命令ずつ実行する
  4. 計算・表示などの結果が出る

まん中の「翻訳」があるからこそ、人は読みやすい形で書け、機械はきっちり実行できます。

⚠️ うまくいかないとき

  • 環境依存で動かない — 実行ファイルは特定のCPU命令セットとOSの決まりに合わせて作られる。相手のCPU/OSが違うとそのままでは動かない(だからOS別に配布物を分ける)
  • 依存の食い違い — 必要なライブラリやそのバージョンが実行先に無いと動かない
  • 翻訳の見落とし — インタプリタ型は文法エラーがその行に来て初めて表面化することがある
  • 最適化の副作用 — 速くするための最適化で、境界条件の挙動が変わってはまることがある

環境ごと丸ごと箱に詰めて「どこでも同じに動かす」発想はDockerへ。バージョン管理はGitで。

この部分をもっと深く(上級
  • 動的ライブラリ地獄 — 実行先に必要な共有ライブラリやそのバージョンが無い/食い違い、起動時に落ちる
  • ABIの不一致 — 別のコンパイラや設定で作った部品を混ぜると・呼び出し規約がずれて静かに壊れる
  • クロスコンパイルの取り違え — 動かす先のISA/OS向けに作ったつもりが・手元向けのままで動かない
  • 最適化の副作用 — 攻めた最適化で・未定義な書き方の挙動が変わり・環境によって結果がぶれる
  • JITの温まり待ち — 起動直後はまだ機械語化が進まず遅い(ウォームアップが要る)
やさしく言うと(初級
  • 書き間違い — 文法が違うと翻訳でつまずき、実行前にエラーになる
  • 手元では動くのに別の環境で動かない — 翻訳結果はCPUやOSの種類に合わせて作られるため、環境が違うとそのままでは動かないことがある
  • 翻訳のやり方の取り違え — 速さ重視か手軽さ重視かで向き不向きがある

翻訳のしくみをもっと詳しく(コンパイル型・インタプリタ型・バイトコード+VM)は中級で扱います。

関連する知識

理解度チェック

そのまま解けます(成績は保存されません)。無料アカウントを作ると、学習の記録と進捗の山登りが始まります。

1. コンパイル型とインタプリタ型の、いちばんの違いは?

2. 同じソースでも、OSやCPUの種類が違うと実行ファイルをそのまま使えないのはなぜ?

3. Java(JVM)方式の実行のしかたとして正しいのはどれ?

4. コンパイルの中でソースが機械語へ近づく段階の順序として正しいのはどれ?

5. コンパイルの途中で「中間表現(IR)」を挟むおもな利点はどれ?

6. インタプリタ型の性質として本文に合うのはどれ?

7. 特定のCPU向けではない「中間の命令」で、VMが解釈して動かすものを何と呼ぶか、カタカナで答えてください。

8. ソースコードから、実行できる成果物(実行ファイル等)を作り出す一連の作業全体を何と呼ぶか、カタカナで答えてください。