スタックマシンとレジスタマシン ─ 実行モデルの設計判断
なぜJVMやWASMはスタック型で物理CPUはレジスタ型なのか。コード密度とデコード単純性、命令数と並列性の綱引きを原理から掴み、JITのスタック→レジスタ変換まで一気に理解できます。
- 1.スタックマシン(JVM/WASM)は暗黙のオペランドスタックで命令を短く保ち、コード密度とデコード単純性に優れる。配布・検証向きの仮想ISAとして選ばれる。
- 2.レジスタマシン(物理CPU)はオペランドを明示するため命令数は減り、依存解析と命令レベル並列が容易。実行ユニットを並べて投機・再順序付けで回す物理実装に向く。
- 3.JIT/インタプリタはスタックの暗黙データフローを解析し、スタック深さに応じた仮想レジスタへ写像する。スタックは記述形式、レジスタは実行形式という役割分担が本質。
実行モデルはオペランドの置き場所で決まる
命令が計算対象(オペランド)をどこから取り、結果をどこへ置くか――この一点で実行モデルは大きく二つに分かれます。スタックマシンは暗黙のオペランドスタックを使い、命令はスタックの頂上数個を相手にします。レジスタマシンは番号付きのレジスタを明示し、どのレジスタを読みどこへ書くかを命令自身が指定します。
この違いは単なる流儀ではありません。命令の長さ、デコードの手間、並列実行のしやすさまでが、オペランドの置き場所の決め方から連鎖的に決まります。JVMバイトコードやWebAssembly(WASM)がスタック型を選び、x86やArmといった物理CPUがレジスタ型を採るのは、それぞれが解こうとする問題が違うからです。
同じ式を二つのモデルで書く
同じ式を両モデルで表すと、命令の性格の差が見えます。
# スタックマシン(暗黙オペランド)
push a # スタック: [a]
push b # スタック: [a, b]
mul # 上2つを取り積を積む → [a*b]
push c
push d
mul # → [a*b, c*d]
add # 上2つを足す → [a*b + c*d]
# レジスタマシン(明示オペランド)
mul r1, ra, rb # r1 = ra * rb
mul r2, rc, rd # r2 = rc * rd
add r3, r1, r2 # r3 = r1 + r2
スタック側の mul や add はオペランドを書きません。常に「スタック頂上の2つを取り、結果を1つ積む」と決まっているからです。命令はオペコードだけで済み、1バイトに収まることも珍しくありません。レジスタ側の mul r1, ra, rb は読み書き先を3つ明示するため命令は長くなりますが、命令数は7対3でレジスタ側が少なくなります。
スタックマシンの命令が短いのは、オペランドの位置がスタック頂上に固定されているからです。エンコードにレジスタ番号を入れる必要がなく、結果としてコード密度が高くなります。バイトコードをネットワークやファイルで配布する場面では、この密度がそのまま転送量と命令フェッチ量に効きます。JVMやWASMが密度を重視するのは、配布される仮想ISAだという出自によります。
二つのモデルの設計トレードオフ
| 観点 | スタックマシン(JVM / WASM) | レジスタマシン(物理CPU) |
|---|---|---|
| オペランド | 暗黙(スタック頂上) | 明示(レジスタ番号を指定) |
| 命令長 | 短い・密度が高い | 長め・密度は劣る |
| 命令数 | 多い(push/popが増える) | 少ない |
| デコード | 単純(オペコードのみ多い) | オペランド指定の解釈が要る |
| 依存解析 | 暗黙で見えにくい | レジスタ番号で明示・容易 |
| 命令レベル並列 | 出しにくい | 出しやすい |
| 向く用途 | 配布・検証する仮想ISA | 高速実行する物理実装 |
決定的な差は並列性にあります。レジスタマシンでは mul r1, ra, rb と mul r2, rc, rd が異なるレジスタを使うことが命令を見ただけでわかり、互いに独立だと即座に判定できます。だからこの2命令は同時に発行できます。これはスーパースカラの発行と分配やアウトオブオーダ実行が依存をレジスタ番号で追えるからこそ成り立つ仕組みです。
一方スタックマシンでは、データの受け渡しがすべてスタック頂上という単一の場所を経由します。連続する命令はほぼ常にスタック頂上で繋がっているため、逐次依存に見えてしまい、そのままでは並列に流せません。実行モデルとしては素直ですが、物理的に高速化する余地は乏しいのです。
スタックマシンの利点はデコードの単純さとコード密度であって、実行速度ではありません。むしろ暗黙の逐次依存があるため、何も手を加えずに走らせれば命令数の多さがそのまま遅さになります。JVMやWASMが実用速度を出せるのは、後述のJITがスタック形式を捨ててレジスタ形式へ変換するからです。
なぜ仮想マシンはスタック型を選ぶのか
JVMバイトコードもWASMもスタック型です。配布される中間表現として、スタック型には三つの利点があります。
第一にコード密度。オペランドを書かない命令が多く、バイナリが小さく収まります。第二にデコードの単純さ。命令の意味がオペコードでほぼ定まるので、インタプリタの解釈ループが軽くなります。第三に検証のしやすさです。WASMの型検査やJVMのバイトコードベリファイアは、各命令の前後でスタックに積まれた値の型と深さを抽象的に追跡し、整合を確かめます。スタックの状態という単一の不変量を辿るだけで安全性を検証できるため、配布先で信頼できない可能性のあるコードを受け入れる仮想機械の設計と相性が良いのです。
# WASM/JVMの検証イメージ(型スタックの追跡)
push i32 # 型スタック: [i32]
push i32 # 型スタック: [i32, i32]
i32.add # i32 2つを要求し i32 を1つ残す → [i32]
# ここで型と深さが合えば正当
レジスタ型を仮想ISAにすると、レジスタ本数の決め打ち、各レジスタの生存区間の検証など、配布形式としては検査が重くなります。スタック型は「記述と検証のための形式」として理にかなっています。
JITが行うスタック→レジスタ変換
仮想マシンが実用速度を出す鍵は、スタックバイトコードを物理CPUのレジスタ実行へ橋渡しするJITコンパイラです。中心となるのが、暗黙のスタックを解いて明示的な値の流れに直すスタック→レジスタ変換です。
考え方は単純で、各命令時点でのスタックの深さに、仮想レジスタを一対一で対応づけることです。深さ0の位置を仮想レジスタ s0、深さ1を s1 と決めておけば、push は対応する仮想レジスタへの代入、mul は2つの仮想レジスタを読んで1つへ書く演算に翻訳できます。
push a → s0 = a
push b → s1 = b
mul → s0 = s0 * s1 (深さ2→1)
push c → s1 = c
push d → s2 = d
mul → s1 = s1 * s2 (深さ3→2)
add → s0 = s0 + s1 (深さ2→1)
こうして得た仮想レジスタ列は、もはやレジスタマシンのコードそのものです。ここから先は通常の最適化が効きます。独立な演算が見えるようになるので命令を並べ替えられ、共通部分式を畳み、最終的に物理レジスタへ割り当てます。割り当て段では、限られた物理レジスタに仮想レジスタを詰めるため、レジスタリネーミングやレジスタファイルと同じ生存区間の解析・割り付け問題を解くことになります。スタックの暗黙データフローを一度明示化したからこそ、これらの解析が可能になるわけです。
変換にもコストがあるため、起動直後やめったに通らない経路では、スタックバイトコードをそのままインタプリタで実行し、ホットになった部分だけJITでレジスタ形式へ変換する段階的最適化が一般的です。スタック形式は「すぐ動かせる」、レジスタ形式は「速く動かせる」という役割分担を、実行頻度に応じて使い分けています。
形式と実装を切り離す
二つのモデルは優劣ではなく、層の違いとして捉えるのが正確です。スタックマシンは記述・配布・検証のための形式として強く、レジスタマシンは並列実行する物理実装のための形式として強い。物理CPUがレジスタ型を採り、その上のデータパスと制御をデータパスと制御ユニットとして組むのは、明示オペランドが依存解析と並列発行を可能にするからです。
JVMやWASMがスタック型で配られ、JITでレジスタ形式へ変換されて初めて高速に走るという流れは、「抽象的な記述形式」と「実行のための実装形式」を意図的に分けた設計判断です。どこでオペランドを置くかという一見小さな選択が、密度・検証性・並列性という別々の評価軸を貫いて、配布から実行までの全体像を決めています。
まとめ
- スタックマシンは暗黙のオペランドスタックで命令を短く保ち、コード密度・デコード単純性・検証容易性に優れる。JVM/WASMなど配布される仮想ISAに向く。
- レジスタマシンはオペランドを明示するため命令数が減り、レジスタ番号で依存を追えるので命令レベル並列を出しやすい。物理CPUの実装形式として強い。
- スタックの暗黙データフローはそのままでは逐次依存に見え、並列化が難しい。これがスタック型の速度上の限界。
- JITはスタック深さを仮想レジスタへ写像してレジスタ形式へ変換し、並べ替え・最適化・物理レジスタ割り当てを可能にする。スタックは記述形式、レジスタは実行形式という役割分担が本質。
CPU/メモリ/ディスク Article
スタックマシンとレジスタマシン ─ 実行モデルの設計判断を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
スタックマシン
比較で見る軸
難易度: advanced / カテゴリ: CPU/メモリ/ディスク / タグ数: 6
導入後に効く点
レジスタマシン(物理CPU)はオペランドを明示するため命令数は減り、依存解析と命令レベル並列が容易。実行ユニットを並べて投機・再順序付けで回す物理実装に向く。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- CPU/メモリ/ディスク
- タグ数
- 6
判断チェックリスト
- 自社の用途が「スタックマシン / レジスタマシン」に近いか確認する。
- 強みである「スタックマシン(JVM/WASM)は暗黙のオペランドスタックで命令を短く保ち、コード密度とデコード単純性に優れる。配布・検証向きの仮想ISAとして選ばれる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。