命令サイクル — 命令フェッチ、デコード、実行、ライトバック
CPUはすべての命令の実行において同じ4ステップの流れに従います。それは止まることのないパイプラインの拍子のようなものです。この講義では、この4ステップがそれぞれ何を行うのかを詳しく学び、Pythonで完全なCPUの命令実行プロセスをシミュレートします。
生活の例え:レストランの厨房
あなたがレストランの料理長だと想像してください。客が注文した後、以下の手順で料理を出します。
- 注文を受ける(Fetch):フロントから新しい注文票を取り、客が何を注文したかを確認する
- 注文票を読む(Decode):メニューを分析する——「紅焼肉」には三枚肉、醤油、氷砂糖が必要、「清炒時蔬」には青菜、にんにくのみじん切りが必要
- 料理する(Execute):実際に火をつけて調理し、野菜を切り、炒め、味を調える
- 料理を出す(Write Back):出来上がった料理を皿に盛り、ウェイターに渡して運んでもらう
すべての注文票はこの4ステップを経ます。1品作り終えたら、次の注文票を取りに行きます。CPUも同様です——命令を1つ取得し、1つ解析し、1つ実行し、結果を書き戻し、これを繰り返します。
この4ステップの循環は、CPUの最も中心となる動作のリズムです。コンピュータの電源を入れてから切るまでの毎秒、CPUは「フェッチ-デコード-実行-ライトバック」というこの循環を絶えず繰り返し、毎秒数十億回実行します。
4つの段階の詳しい説明
段階1:命令フェッチ(Fetch)
CPUはまずメモリから次に実行する命令を「取り出し」ます。
特別なレジスタがあります。それはプログラムカウンタ(PC、Program Counter)と呼ばれ、常にメモリ内の次に実行する命令のアドレスを保持しています。
フェッチ段階では何をするのか:
- CPUはPC内のアドレスをアドレスバスに置く
- CPUはコントロールバスに「読み出し」信号を送る
- メモリはそのアドレスにある命令データを返す
- 命令は命令レジスタ(IR、Instruction Register)に置かれる
- PCは自動的に1加算され、次の命令を指す
ここに重要な設計があります:PCの自動インクリメント。これは、CPUがデフォルトで命令を順番に実行することを意味します。ただし、ジャンプ命令(if-elseやループなど)によってPCの値が変更される場合は例外です。
段階2:デコード(Decode)
フェッチした命令は2進数の並びです(例えば0001 0010 0011)、CPUはそれを「翻訳」する必要があります。
デコード段階の中心的な仕事:
- オペコード(Opcode)の識別:命令の先頭数ビットはCPUに何を行うかを伝えます——加算、減算、データのロード、それともジャンプ?
- オペランド(Operands)の抽出:命令の後半のビットは操作対象を示します——どのレジスタを使うか?メモリ内のどのアドレスか?
- 制御信号の設定:コントローラはデコード結果に基づいて、ALU、レジスタファイル、バスなどの部品に適切な制御信号を送ります。
例えば1つの命令ADD R1, R2, R3の場合、デコード後CPUは次のことを認識します:ALUに加算演算をさせ、入力はレジスタR2とR3から、結果はR1に格納します。
段階3:実行(Execute)
これは命令が実際に「働く」段階です。命令の種類に応じて、実行段階で行うことは異なります:
| 命令の種類 | 実行段階で行うこと | 関連するハードウェア |
|---|---|---|
| 算術演算(ADD、SUB など) | ALUがオペランドに対して演算を行う | ALU、レジスタファイル |
| 論理演算(AND、OR など) | ALUがオペランドに対してビット演算を行う | ALU、レジスタファイル |
| メモリ読み出し(LOAD) | アドレスバスを介してメモリにアクセスし、データを読み出す | アドレスバス、データバス、メモリ |
| メモリ書き込み(STORE) | アドレスバスを介してメモリにアクセスし、データを書き込む | アドレスバス、データバス、メモリ |
| ジャンプ(JUMP、BRANCH) | PCの値を変更し、実行の流れを変える | コントローラ、PCレジスタ |
段階4:ライトバック(Write Back)
実行段階で生成された結果は、ターゲット位置に「書き込む」必要があります。
ライトバックのターゲットは次のとおりです:
- レジスタ:演算結果を汎用レジスタに格納する(最も一般的)
- メモリ:結果をメモリ内の特定のアドレスに格納する
- フラグレジスタ:ステータスフラグを更新する(結果がゼロかどうか、オーバーフローかどうか、負かどうかなど)
ライトバックが完了すると、1つの命令の実行サイクルが正式に終了します。PCがまだ有効な命令アドレスを指している場合、CPUは自動的にフェッチ段階に戻り、次の命令を開始します。
命令サイクルのタイムライン表示
次のタイムラインは、3つの命令が順番に実行される完全なプロセスを示しています:
时钟周期: 1 2 3 4 5 6 7 8 9 10 11 12
指令1: [取指] [译码] [执行] [写回]
指令2: [取指] [译码] [执行] [写回]
指令3: [取指] [译码] [执行] [写回]
↑ 指令1 完成后才开始指令2,完全串行执行
ここでは各命令に4サイクル、3つの命令で合計12サイクルかかっていることに注意してください。後のパイプラインの説明では、この時間を大幅に短縮する方法を学びます。
インタラクティブデモ:SimpleCPU シミュレータ
次のPythonプログラムは、極めてシンプルなCPUの完全な命令サイクルをシミュレートします。LOAD(数値のロード)、ADD(加算)、MUL(乗算)、CMP(比較+条件ジャンプ)、STORE(出力)命令をサポートし、各段階のログを詳細に出力します。
実例
SimpleCPU シミュレータ — 完全な命令サイクルのデモ
サポートする命令: LOAD, ADD, MUL, CMP+JGT, STORE, HALT
すべての命令は次を経ます:フェッチ(IF) → デコード(ID) → 実行(EX) → ライトバック(WB)
"""
class SimpleCPU:
"""教学用のCPUシミュレータ。命令サイクルの4つの段階を示す"""
def __init__(self):
# 汎用レジスタ:R0〜R3、初期値はすべて0
self.registers = {'R0': 0, 'R1': 0, 'R2': 0, 'R3': 0}
# フラグレジスタ:比較結果を記録
self.flags = {'ZERO': False, 'GREATER': False}
# プログラムカウンタ:メモリ内の次の命令の位置を指す
self.pc = 0
# 命令レジスタ:現在実行中の命令を保持
self.ir = None
# メモリ:プログラムの命令列を格納
self.memory = []
# 各段階の実行回数を集計
self.stats = {'fetch': 0, 'decode': 0, 'execute': 0, 'writeback': 0}
self.total_cycles = 0
# 制御フラグ
self.running = False
def load_program(self, instructions):
"""命令リストをメモリにロードする"""
self.memory = instructions
self.pc = 0
self.running = True
print(f"[システム] プログラムのロードが完了しました。合計 {len(instructions)} 命令")
print(f"[システム] 初期レジスタ: {self.registers}")
print("=" * 60)
def fetch(self):
"""段階1:フェッチ (Instruction Fetch)"""
self.total_cycles += 1
if self.pc >= len(self.memory):
self.running = False
return None
# メモリからPCが指す命令を読み出す
instruction = self.memory[self.pc]
self.ir = instruction
old_pc = self.pc
self.pc += 1 # PCを自動的にインクリメント
self.stats['fetch'] += 1
print(f"[サイクル {self.total_cycles}] フェッチ (IF)")
print(f" PC={old_pc} → メモリアドレス {old_pc} から命令を読み出し")
print(f" 命令内容: "\"{instruction}\"")
print(f" PC 更新: {old_pc} → {self.pc}")
print(f" IR ← \"{instruction}\"")
return instruction
def decode(self, instruction):
"""段階2:デコード (Instruction Decode)"""
self.total_cycles += 1
self.stats['decode'] += 1
# オペコードとオペランドを解析
parts = instruction.split()
opcode = parts[0].upper() # オペコード
operands = parts[1:] if len(parts) > 1 else [] # オペランド
print(f"[サイクル {self.total_cycles}] デコード (ID)")
print(f" オペコード (Opcode): {opcode}")
print(f" オペランド (Operands): {operands}")
# オペコードの意味を解釈
opcode_meaning = {
'LOAD': '即値からレジスタにロード',
'ADD': 'レジスタ加算',
'MUL': 'レジスタ乗算',
'CMP': '2つのレジスタの値を比較',
'JGT': '条件ジャンプ(より大きい場合にジャンプ)',
'STORE':'レジスタの値を出力',
'HALT': '実行を停止',
}
if opcode in opcode_meaning:
print(f" 意味: {opcode_meaning[opcode]}")
return opcode, operands
def execute(self, opcode, operands):
"""段階 3:実行 (Execute)"""
self.total_cycles += 1
self.stats['execute'] += 1
print(f"[周期 {self.total_cycles}] 実行 (EX)")
if opcode == 'HALT':
print(f" HALT: プログラムの実行を終了")
self.running = False
elif opcode == 'LOAD':
# LOAD Rx, value → 即値 value をレジスタ Rx に格納
reg = operands[0]
value = int(operands[1])
print(f" 操作: 即値 {value} をレジスタ {reg} にロード")
print(f" 関連部品: データバス(即値 {value} を転送)")
# 結果を一時保存し、ライトバック段階でレジスタを更新する
return ('LOAD', reg, value)
elif opcode == 'ADD':
# ADD Rx, Ry → Rx = Rx + Ry
rx, ry = operands[0], operands[1]
a = self.registers[rx]
b = self.registers[ry]
result = a + b
print(f" 操作: ALU が {rx}({a}) + {ry}({b}) = {result} を実行")
print(f" 関連部品: ALU(演算器)、レジスタ群")
return ('WRITE_REG', rx, result)
elif opcode == 'MUL':
# MUL Rx, Ry → Rx = Rx * Ry
rx, ry = operands[0], operands[1]
a = self.registers[rx]
b = self.registers[ry]
result = a * b
print(f" 操作: ALU が {rx}({a}) * {ry}({b}) = {result} を実行")
print(f" 関連部品: ALU(演算器)、レジスタ群")
return ('WRITE_REG', rx, result)
elif opcode == 'CMP':
# CMP Rx, Ry → 2つのレジスタを比較し、フラグを設定する
rx, ry = operands[0], operands[1]
a = self.registers[rx]
b = self.registers[ry]
self.flags['ZERO'] = (a == b)
self.flags['GREATER'] = (a > b)
print(f" 操作: {rx}({a}) と {ry}({b}) を比較")
print(f" 結果: ZERO={self.flags['ZERO']}, GREATER={self.flags['GREATER']}")
print(f" 関連部品: ALU(比較器)、フラグレジスタ")
return ('FLAGS', None, None)
elif opcode == 'JGT':
# JGT address → GREATER フラグが真の場合、address にジャンプする
target = int(operands[0])
if self.flags['GREATER']:
old_pc = self.pc
self.pc = target
print(f" 条件成立 (GREATER=True): PC を {old_pc} → {target} にジャンプ")
print(f" 関連部品: コントローラ、PC レジスタ")
else:
print(f" 条件不成立 (GREATER=False): ジャンプせず、PC は {self.pc} のまま")
return None
elif opcode == 'STORE':
# STORE Rx → レジスタの値を出力する
reg = operands[0]
value = self.registers[reg]
print(f" 操作: レジスタ {reg} の値を出力")
return ('OUTPUT', reg, value)
return None
def writeback(self, action):
"""段階 4:ライトバック (Write Back)"""
self.total_cycles += 1
self.stats['writeback'] += 1
if action is None:
print(f"[周期 {self.total_cycles}] ライトバック (WB) - 操作なし")
return
action_type = action[0]
print(f"[周期 {self.total_cycles}] ライトバック (WB)")
if action_type == 'LOAD':
reg, value = action[1], action[2]
old_val = self.registers[reg]
self.registers[reg] = value
print(f" 対象: レジスタ {reg}")
print(f" データ: {old_val} → {value}")
print(f" 経路: データバス → レジスタ群")
elif action_type == 'WRITE_REG':
reg, result = action[1], action[2]
old_val = self.registers[reg]
self.registers[reg] = result
print(f" 対象: レジスタ {reg}")
print(f" データ: {old_val} → {result}")
print(f" 経路: ALU 出力 → レジスタ群(内部バス)")
elif action_type == 'OUTPUT':
reg, value = action[1], action[2]
print(f" 出力: レジスタ {reg} = {value}")
print(f" 経路: レジスタ群 → データバス → I/O インターフェース")
print(f" 現在のレジスタ状態: {self.registers}")
print(f" フラグ: {self.flags}")
print("-" * 60)
def run(self):
"""プログラム全体を実行し、命令を1つずつ処理する"""
print("=" * 60)
print("SimpleCPU シミュレータ起動 — 命令サイクルデモ (サンプル)")
print("=" * 60)
while self.running:
# 4つの段階のループ
instruction = self.fetch() # 段階 1: フェッチ
if instruction is None:
break
opcode, operands = self.decode(instruction) # 段階 2: デコード
action = self.execute(opcode, operands) # 段階 3: 実行
self.writeback(action) # 段階 4: ライトバック
# 実行完了、統計情報を出力
print("\n" + "=" * 60)
print("プログラムの実行が完了しました!統計情報:")
print(f" 総クロックサイクル数: {self.total_cycles}")
print(f" フェッチ段階の実行: {self.stats['fetch']} 回")
print(f" デコード段階の実行: {self.stats['decode']} 回")
print(f" 実行段階の実行: {self.stats['execute']} 回")
print(f" ライトバック段階の実行: {self.stats['writeback']} 回")
print(f" 最終レジスタ: {self.registers}")
print(f" 最終フラグ: {self.flags}")
print(f" 最終 PC: {self.pc}")
print("=" * 60)
# ===== デモプログラムを実行 =====
# プログラムのロジック:1+2 の結果を計算し、3 を掛けて、10 と比較する
# LOAD R0, 1 → R0 = 1
# LOAD R1, 2 → R1 = 2
# ADD R0, R1 → R0 = R0 + R1 = 1 + 2 = 3
# LOAD R2, 3 → R2 = 3
# MUL R0, R2 → R0 = R0 * R2 = 3 * 3 = 9
# CMP R0, R2 → R0(9) と R2(3) を比較し、GREATER=True を設定
# STORE R0 → R0 の値を出力
# HALT → 停止
cpu = SimpleCPU()
program = [
"LOAD R0 1", # R0 = 1
"LOAD R1 2", # R1 = 2
"ADD R0 R1", # R0 = R0 + R1 = 3
"LOAD R2 3", # R2 = 3
"MUL R0 R2", # R0 = R0 * R2 = 9
"CMP R0 R2", # 9 と 3 を比較、GREATER=True
"JGT 8", # GREATER なら命令 8 にジャンプ
"LOAD R3 0", # (JGT がスキップしたため、これは実行されない)
"STORE R0", # R0 = 9 を出力
"HALT", # 停止
]
cpu.load_program(program)
cpu.run()
上記のコードは各命令の各段階で詳細なログを出力するため、CPU 内部が各クロックサイクルで何を行っているかを明確に確認できます。注目すべき点:
- PC の自動インクリメント:命令を1つ実行するたびに、PC が自動的に +1 される(フェッチ段階)
- 条件分岐:CMP+JGT の組み合わせで if 判定を実現——R0 > R2 の場合、命令 8 にジャンプし、命令 7 をスキップ
- レジスタは中継地点として機能:すべてのデータはまずレジスタにロードされ、ALU はレジスタからデータを取得し、計算後にレジスタへ書き戻す
インタラクティブデモ:CPU内部構造&命令サイクルのステップ再生
以下は vis-network を使用して描画した簡易版 CPU 内部構造図です。「次へ」ボタンをクリックして、命令が4つの段階でそれぞれどのハードウェア部品を使用するかを確認できます。アクティブになった部品は対応する段階の色でハイライト表示されます。