|
10 |
RISC-Vプロセッサの設計 (35) |
mkRegとmkCRegの違い
epochによるキャンセル制御では、exEpochとredirectをmkCRegで作りました。mkCRegは、mkRegと同じPreludeにある標準の部品です(BSC Libraries Reference Guide 2.4.2。mkRegは2.4.1)。どちらもフリップフロップは1つですが、同じサイクルの中で書いた値を、後のruleが読めるかどうかが違います。この違いを知らずにmkRegを使うと、エラーも警告も出ないまま、パイプラインの性能が半分になることがあります。
mkReg:読めるのはサイクルの始めの値
mkRegを読むと、サイクルの始めの値(FFの出力)が返ります。書いた値が見えるのは次のサイクルです。RISC-Vプロセッサの設計(9)のFIFOの図と同じ描き方をすると、1サイクルの箱の最初で読み、最後で書く形になります。

mkRegの読み書き
そのためbscは、同じサイクルに動く2つのruleを「読むrule → 書くrule」の順にしか並べません。
mkCReg:同じサイクルに書いた値を後のポートで読める
Reg#(Bool) epoch[2] <- mkCReg(2, False); // ポート0, 1
mkCReg(n, 初期値)は、1つのFFにn個の口(ポート)を付けたものです。
- ポートiで読むと、同じサイクルにポート0〜i−1で書かれた値が見えます。
- サイクルの終わりにFFに入るのは、一番後ろのポートで書かれた値です。
- ポート数の上限は5です。

mkCRegの読み書き
ポート0で書いた値は、サイクルの途中でポート1から見えます。サイクルの最初で読み、最後で書くという箱の形は、ポート0とポート1をつなげたものです。
回路としては、FFの出力に、各ポートの書き込みを優先するMuxが直列に付いたものです。いわゆるバイパス付きのレジスタで、MITのArvindらはEHR(Ephemeral History Register)と呼んでいます。Verilogでは、bsc付属のプリミティブ(CRegN5.vなど)になります。
例:<EX>が書き、<ID>が読む
<EX>がepochを反転し、同じサイクルに<ID>がそれを読む場面を、最小の形にしました。<ID>から<EX>へはパイプラインFIFOでつないでいるので、ruleは<EX>(deq)→<ID>(enq)の順に並ぶ必要があります。<EX>はサイクル2でepochを反転します。
// mkRegの場合
Reg#(Bool) epoch <- mkReg(False);
FIFO#(int) q <- mkPipelineFIFO; // ID → EX
rule ex;
q.deq;
if (cyc == 2) epoch <= !epoch; // 書く
endrule
rule id;
q.enq(cyc);
$display("cyc %0d id sees epoch=%0d", cyc, pack(epoch)); // 読む
endrule
mkCRegの場合は、宣言をReg#(Bool) epoch[2] <- mkCReg(2, False);にし、exではepoch[0]、idではepoch[1]を使います。それ以外は同じです。
bsimで動かした結果です(bsc 2025.07、Verilog(iverilog)でも同じ結果)。
| サイクル | mkRegの<EX> | mkRegの<ID> | mkCRegの<EX> | mkCRegの<ID> |
|---|---|---|---|---|
| 0 | epoch=0 | epoch=0 | ||
| 1 | 発火 | 発火 | epoch=0 | |
| 2 | epoch=0 | 発火(反転) | epoch=1 | |
| 3 | 発火 | 発火 | epoch=1 | |
| 4 | epoch=0 | 発火 | epoch=1 | |
| 5 | 発火 | 発火 | epoch=1 |
mkCRegでは、<EX>と<ID>が毎サイクル動き、<EX>がサイクル2に反転した値を、同じサイクルの<ID>が読んでいます。
mkRegでは、<EX>と<ID>が1サイクルおきにしか動きません。パイプラインの流量が半分になっています。<EX>はサイクル2に動かないので、反転も起きていません。
mkRegで何が起きたか
2つのruleには、逆向きの順序の要求があります。
- パイプラインFIFOは、<EX>(deq)→<ID>(enq)の順を要求します。
- mkRegは、読む<ID>→書く<EX>の順を要求します。
1つのruleの中の読み書きは切り離せないので、rule ex(Dequeue, Write)とrule id(Read, Enqueue)を1サイクルの箱に並べてみると、次のようになります。

1サイクルの中のruleの並び
mkRegでは、どちらの順に並べても、どこかで順序の規則を破ります。両方を満たす順序はないので、bscは2つのruleを同じサイクルに並べられません。-show-rule-relで見ると、次のように両方向の衝突が出ます。
Scheduling info for rules "RL_ex" and "RL_id":
<>
conflict:
calls to
epoch.write vs. epoch.read
...
それでも、エラーも警告も出ません。FIFOが空なら<ID>だけ、満杯なら<EX>だけが動ける状態になり、2つのruleが交互に動くだけだからです。動作としては正しく、遅いだけなので、気づきにくい落とし穴です。
mkCRegにすると、<EX>がポート0で書き、<ID>がポート1で読むので、<EX>→<ID>の順で矛盾がなくなります。
使いどころ
mkCRegが要るのは、「先に動くruleが書き、同じサイクルに後のruleがそれを読む」場所です。このプロセッサでは、次の3つがそれにあたります。
| レジスタ | 書く(前のポート) | 読む(後のポート) |
|---|---|---|
| exEpoch | <EX>(0) | <ID>(1)、<IF>(2) |
| redirect | <EX>(0) | <PC>(1) |
| PipeQのfull | deq側(0) | enq側(1) |
PipeQのfullは、パイプライン制御をFIFOで考えるの、満杯でも同じサイクルにdeqがあればenqできる、という動作そのものです。mkPipelineFIFOも、中身は同じようにCRegで作られています。
注意点
- ポートの番号は、ruleの順序に合わせます。先に動くruleに小さい番号を割り当てます。上の例でポートを逆(<EX>が1、<ID>が0)にすると、mkRegのときとまったく同じ結果になりました。1サイクルおきにしか動かず、やはり警告も出ません。
- Muxが直列に入るので、ポートを増やすほど組合せ回路の経路が延びます。同じサイクルの受け渡しが本当に要る所だけに使います。
- 1サイクル遅れてよい所は、mkRegで構いません。たとえば分岐予測の表のように、古い値を読んでも正しさが変わらないものです。