Article #1143

既に発行済みのブログであっても適宜修正・追加することがあります。
We may make changes and additions to blogs already published.
posted by sakurai on November 5, 2026 #1143

パイプライン制御を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

Your email address will not be published.

You may use Markdown syntax. If you include an ad such as http://, it will be invalidated by our AI system.

Please enter the numbers as they are shown in the image above.