コンピュータの構成と設計 4.4~4.6節

パタヘネ4.4~4.6です。前回はR形式・ロード・ストア命令のフェッチと実行に必要な機能ブロック(PC、命令メモリ、レジスタファイル、ALU、データメモリ)をリストアップしました。今回は色々な命令に応じてマルチプレクサを切り替えたりALUの制御入力を変更する方法に関して述べています。

4.4

 ALUの制御入力は4ビットで表され、AND、OR、加算、減算、Set on less than、NORの6種類の操作が可能である。R形式では下位6ビット(funct)が演算の種類を指定する。命令からALU制御入力を決めるためには、このfunctと、その命令がR形式かロード命令かストア命令かを区別する制御入力があればよい。制御入力は3種類の値を取れればいいので2ビットとなる。この制御入力をALUOpと呼ぶ。functとALUOpからなる入力を受け取り、出力としてALU制御入力を出力する回路を作るためには、入力と出力の対応を表す真理値表を作成し、真理値表から自動で回路を構築すれば良い。

 次にALU以外のマルチプレクサやデータメモリの制御入力を生成する主制御回路を考える。必要な信号は

  • RegDst 書き込みレジスタを取得するビットを制御する。
  • RegWrite アサートされれば書き込みレジスタに対して値を書き込む
  • ALUSrc ALUの第二オペランドとしてレジスタファイルの読み出しデータ2にするか命令下位16ビットを符号拡張したものにするかを切り替える
  • PCSrc PC+4の値でPCを書き換えるか、分岐先アドレスでPCを置き換えるか選択する
  • MemRead アサートされれば読み出しアドレスによって指定されたデータ・メモリの内容が読み出しデータ出力上に流される
  • MemWrite アサートされれば書き込みアドレスで指定されるデータメモリの内容が書き込みデータ入力の値で書き換えられる
  • MemtoReg ALUからの値を書き込みレジスタ入力に使うか、データメモリの読み出し内容を使うか切り替える

命令の31~26ビット目、すなわちオペコードを見ればこれらの値は決定されるため、真理値表を作って主制御回路を構築すれば良い。

 今まで見てきた命令の実装方式は単一クロックサイクル方式と呼ばれ、1クロックサイクルにつき1命令を実行する。しかし、実用的にはこの方式ではおそすぎる。ロード命令が律速となり、クロックサイクルをロード命令が間に合うような十分長い時間に設定しなければならなくなるためである。

4.5

 単一クロックサイクル方式に対して複数クロックサイクル方式というものがある。1命令を複数のクロックサイクルで実行する方式である。1命令内で同じ機能ユニットを何度も使えるので、回路数を削減できる。しかし、現在のコンピュータでは性能向上のため、複数クロックサイクル方式ではなく、次節で見るパイプライン処理を採用している。

4.6

 MIPSの命令は5個のステージに分ける事ができる

  1. メモリから命令をフェッチする
  2. 命令をデコードしながらレジスタを読み出す
  3. 命令操作の実行またはアドレスの生成を行う
  4. データメモリ中のオペランドにアクセスする
  5. 結果をレジスタに書き込む

例えばある命令が2に到達したとき、同時に次の命令の1を始めておく。次のサイクルで命令が3と2に進み、更に次の命令の1を始めておく。このようにステージをずらしながら同時に多数の命令を実行することをパイプライン処理と呼ぶ。同時に処理できる命令数が増えるので処理速度があがる。ステージごとの処理時間が等しいなら、理想的にはパイプライン方式は非パイプライン方式に対してステージ数分の速度向上がえられる。一方、ステージごとの処理速度にはばらつきがあるので、パイプライン方式の処理周期は最も遅いステージで律速される。また、パイプラインを採用することで非パイプラインでは存在しなかったオーバーヘッドも掛かる。結果としてステージ数倍よりも小さい速度向上となる。また、命令数がステージ数よりも少ない場合は当然速度向上比は小さくなる。

 MIPSはパイプライン処理を念頭に置いて設計されている。命令長がすべて同じであり機械的に4バイト先の命令を取ってくればいいので、命令フェッチの設計が楽となる。また、命令形式によらずソースレジスタのフィールド位置が同じなので命令のデコードとレジスタの読み出しを同時に行える。命令形式に依存していたらデコード後にしかレジスタ読み出しができない。また、メモリオペランドはロードとストアにしか現れないので、実行ステージでメモリアドレスを計算して次のステージでメモリにアクセスできる。メモリを直接ロード・ストア以外の操作対象にできるようにしていたら、アドレス・ステージ、メモリ・ステージ、実行ステージに分けなければならない。最後に、オペランドはメモリ中で整列化していなければならない。そうでなければデータメモリに2回アクセスしなければならない。

 パイプライン処理を行う上で、問題となるのがハザードへの対処である。ハザードとは、同時実行できない命令が並んでいるとき、前の命令が終わるのを待ってから次の命令を実行しなければならずパイプラインが止まる現象である。ハザードには構造ハザード・データハザード・制御ハザードの3種類がある。

 構造ハザードとは、ハードウェア的に同時に実行できない命令がならなでいるとき起こるハザードである。例えばメモリが1つしか無い場合に4個の命令をパイプライン処理しようとしたとする。最初の命令がデータメモリのステージにあるとき、4番目の命令は命令のフェッチを行っている。この2つの命令が競合するので4番目の命令は止まらざるを得ない。現代のパイプラインでは浮動小数点演算で起こることが多い。

 データハザードは他の命令の結果が出ないと次の命令が実行できない場合に起こる。例えば

add $s0,$t0,$t1
sub $t2,$s0,$t3

という命令が並んでいると、$s0の内容が二番目の命令で必要となるので、パイプライン処理できない。この問題への対応としては、最初の命令の結果がALUから得られた時点でレジスタへの書き込みまで待たず、sub命令に渡して計算するという方法がある。コレをフォワーディングもしくはバイパシングという。ただ、この方法はいつでも使えるわけではなく、例えばadd命令がlw命令だった場合を考える。この場合データメモリのステージが終わらない限りどうしてもsubを行えないので、パイプラインを止めざるを得ない。この被ロード・データ・ハザードを解消するためには1ステージ分パイプラインをストールさせなければならない。このことをパイプライン・ストールという。また、俗にバブルとも言う。ソフトウェア的にパイプラインストールを回避することができることもある。たとえば、次のコードを考える。

a=b+e;
c=b+f;

このコードのコンパイル結果が以下とする。

lw $t1,0($t0) // b
lw $t2,4($t0) // e
add $t3,$t1,$t2 // a=b+e
sw $t3,12($t0) // a
lw $t4,8($t0) // f
add $t5,$t1,$t4 // c=b+f
sw $t5,16($t0) // c

このコードの場合lw $t2,4($t0)とadd $t3,$t1,$t2の間でパイプラインストールが起こる。一方で、

lw $t1,0($t0) // b
lw $t2,4($t0) // e
lw $t4,8($t0) // f
add $t3,$t1,$t2 // a=b+e
sw $t3,12($t0) // a
add $t5,$t1,$t4 // c=b+f
sw $t5,16($t0) // c

というように、fのロードを先に行うと、その後のaddはパイプラインストールなしで動作する。現代のパイプラインでは整数プログラムで起こることが多い。ポインタが使われる事が多く、アクセスパターンの規則性が低いためである。

 制御ハザードとはある命令の実行に関する判断をまだ実行中の他の命令の結果に基づいて下さなければならないときに発生する。このハザードは分岐命令の実行時に発生する。対処法としてはまず前の命令が終わるまでストールさせるというものがある。パイプラインが長いとストール時の性能低下が大きい。そこで、別の対処法として、条件分岐のどちらかを予測して分岐先の命令を実行するというものがる。これを分岐予測という。分岐不成立時は正しい分岐先の命令をやり直す。実際のハードウェアは分岐予測を動的に行っており、それまでの分岐履歴を見て分岐先を予測する。現代のパイプラインでは整数プログラムで起こりやすい。分岐頻度が高く分岐予測的中率が低いためである。

コメント

タイトルとURLをコピーしました