|
5 |
RISC-Vプロセッサの設計 (32) |
パイプライン制御をFIFOで考える
動機
これまでのPipeline processorの設計シリーズでは、段間を普通のパイプラインレジスタでつなぎ、制御を2つの信号で表してきました。
- wait:その段のレジスタに書き込ませない。実質的にはゲーテッドクロック(enable)です。上流へは W(S-) = W(S) ∪ W(S+) で伝えます。
- cancel:validを落として、その命令を泡にします。
これは教科書どおりの考え方ですが、現代の設計、特にBSVのような高位の記述では、段間を「FIFO」とみなして、送る側と受ける側のハンドシェイクで制御するのが普通です。この記事では、従来のwait/cancelの考え方がFIFOのどこに入っているのかを対応づけ、BSVでの書き方を示します。
パイプラインFIFOの中身
BSVのmkPipelineFIFOは、中身は1段のレジスタにfull bitを1つ足しただけの小さな回路です。
- deq(取り出し)できる条件:full
- enq(書き込み)できる条件:!full、または同じサイクルにdeqされる
この「同じサイクルにdeqされるならenqできる」があるので、全段が詰まっていても毎サイクル1命令ずつ流れます。動作はパイプラインレジスタと同じです。
違いは、制御をFIFO自身が持っていることです。full bitが従来のvalid bitに当たり、「enqできる」がwaitの否定に当たります。
従来手法との対応
| 従来手法 | FIFO |
|---|---|
| valid bit | full bit(FIFOに入っていること自体が有効) |
| wait W(S) | 入力側FIFOからdeqしない(ruleが発火しない) |
| 下流からのwait W(S+) | 出力側FIFOが満杯でenqできない |
| 泡(validを落として送る) | 何もenqしない(FIFOが空のまま) |
| cancel | deqしてenqしない |
| enable(ゲーテッドクロック) | enqの成立 |
waitもvalidも、FIFOの中に入っています。そのため、BSV流に書くとwait信号も泡も一切書きません。
違いは泡の扱い
1つだけ、動作が変わる所があります。従来の式 W(S-) = W(S) ∪ W(S+) では、下流がwaitすると上流はすべて止まります。間に泡がいても止まります。
FIFOでは、出力側のFIFOが空(泡)なら、下流が止まっていても受け取れます。式で書くと、W(S+)の項にvalidが掛かります。
W(S-) = W(S) ∪ (v(S) ∧ W(S+))
泡があれば詰められるので、その分だけ速くなります。サイクル8に<IF>が1回waitして泡を作り、サイクル10と11に<EX>がwaitする場合をbsimで比べました。
| 従来手法(レジスタ+wait) | FIFO | |
|---|---|---|
| サイクル10の<ID> | 止まる | 020cを<EX>側のFIFOへ送る |
| 020cが<EX>に着く | サイクル13 | サイクル12 |
FIFOでは、<EX>が止まっている間に<ID>が泡の位置へ命令を進めるので、1サイクル早くなりました。
BSVで書く
段間データは段ごとの構造体です。validもMaybeも持ちません。
typedef struct { Addr pc; Bool epoch; } PC2IF deriving (Bits, Eq);
typedef struct { Addr pc; Bool epoch; Inst inst; } IF2ID deriving (Bits, Eq);
typedef struct { Addr pc; Bool epoch; Bool isBr; Addr target; } ID2EX deriving (Bits, Eq);
<ID>は、ハザードがあれば発火しません。これがwaitです。上流は、<ID>がdeqしないので入力側のFIFOが満杯になり、自然に止まります。取り消す命令はdeqするだけで、次段へは何も送りません。
// <ID>
Bool idKill = ids.first.epoch != exEpoch[1];
Bool idHaz = hazard(ids.first);
rule id_kill (idKill); // 取り消し:deqしてenqしない
ids.deq;
endrule
rule id_stage (!idKill && !idHaz); // ハザード中は発火しない(=wait)
let x = ids.first; ids.deq;
exs.enq(ID2EX { pc: x.pc, epoch: x.epoch, isBr: isBranch(x.inst), target: brTarget(x) });
endrule
<EX>は、epochが合わない命令を捨て、分岐が成立したら<PC>へredirectを送ります。
// <EX>
rule ex_stage;
let x = exs.first; exs.deq;
if (x.epoch == exEpoch[0] && x.isBr) begin
redirect[0] <= tagged Valid x.target; // <PC>のMuxへ
exEpoch[0] <= !exEpoch[0];
end
endrule
waitなし、<IF>のwait、誤った命令の<ID>でのハザード、正しい命令の<ID>でのハザードの4通りを、従来手法のレジスタ版と比べました。分岐まわりのタイミングはすべて同じで、ロジックの行数は約半分(85行と44行)になりました。
注意点
- FIFOの暗黙条件はrule全体を止めます。止まってほしくない処理を同じruleに入れてはいけません。たとえば<EX>からのredirectを<PC>のruleがRWireで受けると、<PC>が止まっているサイクルにredirectが消えます。redirectは、<EX>が書くレジスタ(mkCReg)で受けます。
- 標準のFIFOは、deqした後に残っているデータをインタフェースから読めません。<PC>のように「自分が最後に出した値」に+4したい所では、中のレジスタを読む
lastメソッドを足した自作のFIFOを使います。 - 下流のwaitが上流へ組合せ回路で伝わる点は、従来手法と変わりません。FIFOの「enqできる」は下流のdeqで決まるので、<WB>から<PC>までの経路の長さは同じです。
Leave a Comment