CPUが命令を実行する「フェッチ→デコード→実行→格納」のサイクルを1ステップずつ追いかけます。
CPUは1つの命令を実行するのに4つの段階を踏みます。(1)フェッチ: メモリから命令を取り出す、(2)デコード: 命令を解読する、(3)実行: 実際に演算や処理を行う、(4)格納: 結果をレジスタやメモリに保存する。この一連の流れを命令サイクル(マシンサイクル)と呼びます。
プログラムは何百万もの命令の列ですが、CPUはこの4ステップを超高速(GHz=毎秒数十億回)で繰り返すことで処理を進めます。上のツールで「LOAD→ADD→STORE」を自動再生すると、3つの命令それぞれに4ステップが実行される様子を観察できます。
各段階の役割をしっかり区別することが大切です。特にフェッチとデコードの違い(取り出すのと解読するのは別のステップ)に注目してください。
フェッチ(fetch = 取ってくる)は命令をメモリから取り出す段階です。具体的には、(1) PCの値をMAR(メモリアドレスレジスタ)にセット、(2) MARが指すメモリの内容を読み出してMDR(メモリデータレジスタ)に格納、(3) MDRの内容をIR(命令レジスタ)にコピー、(4) PCを+1してインクリメントします。
たとえばPC=0のとき、メモリのアドレス0番地にある「LOAD 100」という命令が読み出されてIRに入ります。同時にPCは0→1になり、次の命令のアドレスを指すようになります。
上のツールで各命令の最初のステップ(青色のフェッチ)を観察すると、毎回PCからメモリアドレスを指定し、命令をIRに読み込む流れが繰り返されていることがわかります。
デコード(decode = 解読する)は、IRに入った命令語を「何の操作か」「どのデータに対してか」に分解する段階です。命令語はオペコード(操作の種類:ADD, LOADなど)とオペランド(操作対象のアドレスや値)で構成されています。
たとえば「ADD 101」という命令は、オペコード=ADD(加算)、オペランド=101(メモリアドレス101番地)に分解されます。制御装置の命令デコーダという回路がこの解読を行い、「ALUに加算を指示せよ」「メモリのアドレス101を読み出せ」という制御信号を生成します。
デコードは演算をするのではなく、命令の意味を解読して次のステップで何をすべきかを決定する段階です。オペコードとオペランドの違いを押さえると、命令がどう解釈されるかが理解できます。
実行(execute)段階では、デコードで決まった操作を実際に行います。ADD命令なら、ALU(演算装置)がACCの値とメモリから読み出した値を足し算します。例えばACC=5、メモリの値=3なら、ALUが5+3=8を計算します。
格納(store)段階では、実行結果をレジスタやメモリに書き戻します。ADD命令の場合、計算結果「8」がACCに書き込まれます。STORE命令の場合は、ACCの値がメモリの指定アドレスに書き込まれます。
上のツールで「LOAD→ADD→STORE」プログラムを実行すると、LOADでACCに値が入り、ADDでACCの値が増え、STOREでメモリに結果が書き戻される一連の流れをステップごとに確認できます。
プログラムカウンタ(PC)は「次に実行する命令のメモリアドレス」を保持するレジスタです。通常、命令をフェッチするたびにPCは自動的に+1され、プログラムは上から順に実行されます。これが「逐次実行」の仕組みです。
しかし分岐命令(JMP, JNZなど)が実行されると、PCの値が直接書き換わります。たとえば「JMP 10」なら、PC=10になり次の命令はアドレス10番地から実行されます。これがif文やループの裏側の仕組みです。
上のツールで「ループ処理」プログラムを実行すると、JNZ命令(ゼロでなければジャンプ)でPCが書き戻されてループする様子が確認できます。ACC=0になったときだけジャンプせず次に進む動きに注目してください。
条件分岐命令(JNZ: Jump if Not Zero など)は、演算結果に応じてジャンプするかどうかを決めます。判断に使われるのがフラグレジスタです。直前の演算結果がゼロならZフラグ=1、負ならNフラグ=1が立ちます。
上のツールの「ループ処理」では、SUB命令でACC=3→2→1→0と減っていき、JNZ命令が毎回「ACC≠0か?」を確認します。ACC=2やACC=1のときはジャンプしてループ先頭に戻り、ACC=0になった瞬間だけジャンプせずループを抜けます。
プログラミングのfor文やwhile文は、裏側ではこの「条件分岐命令+PCの書き換え」で実現されています。高水準言語の「ループ」は、機械語レベルでは「条件付きジャンプ」なのです。
| 命令語 | オペコード | オペランド | 意味 |
|---|---|---|---|
| LOAD 100 | LOAD | 100 | アドレス100の値をACCに読み込む |
| ADD 101 | ADD | 101 | アドレス101の値をACCに加算 |
| STORE 102 | STORE | 102 | ACCの値をアドレス102に格納 |
| JMP 0 | JMP | 0 | アドレス0にジャンプ |
マシン語(機械語)の1命令はオペコード(Operation Code = 操作種別)とオペランド(Operand = 操作対象)で構成されます。上のツールでは「LOAD 100」のように表示していますが、実際のCPUでは「0001 01100100」のようなビットパターンです。
オペコードのビット数が多いほど多くの種類の命令を持てますが、1命令あたりのサイズが大きくなります。一方、オペランドのビット数が多いほど広いメモリ空間を直接指定できます。上のツールで3つのプログラムを比較し、各命令がメモリとレジスタをどう操作するか確認してみてください。
なぜ1つにまとめず4ステップに分けるのか、理由はそれぞれが別の装置(ハードウェア)を使うからです。
・フェッチ:メモリ(記憶装置)との通信が中心
・デコード:制御装置の命令デコーダ(解読回路)が動く
・実行:ALU(演算装置)が計算する
・格納:レジスタやメモリへの書き込み回路が動く
それぞれが独立した装置を担当しているため、「命令Aが実行ステップにいる間に、命令Bはデコード、命令Cはフェッチを同時進行できる」という設計が可能になります。これをパイプライン処理(pipeline)と呼び、CPUが高速動作できる重要な仕組みです。
身近な例で考えると、ラーメン屋の厨房に似ています。「注文を聞く係・スープを作る係・麺を茹でる係・盛り付け係」に分けておけば、複数のお客さんの料理を同時進行で作れます。もし1人が全部やるなら1皿終わるまで次に取り掛かれません。
パイプライン処理とは、命令のサイクルを4ステップに分け、それぞれを異なる命令が同時に使う仕組みです。上の図でT3(クロック3発目)に注目すると、命令Aが実行(E)、命令Bがデコード(D)、命令Cがフェッチ(F)を同時に行っています。
なぜパイプライン化すると速くなるのか。それは命令を待たせずに次々と流し込めるからです。パイプラインがない場合、命令Aが4ステップ全部終わるまで命令Bは待ちます。パイプラインがあれば、命令Aが2ステップ目に進んだ瞬間に命令Bの1ステップ目が始まります。
ただしパイプラインが止まることもあります(ハザードといいます)。たとえば分岐命令(if文やループ)が実行されると「次にどの命令を先読みすればよいか分からない」ため、パイプラインに流した命令を無駄にして仕切り直す必要が生じます。これが命令サイクルを4ステップに分ける設計の利点と課題の両面です。