FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

富岳のプロセッサA64FXのマイクロアーキテクチャマニュアルを読む(分岐予測)

チョット分け合ってA64FXのマニュアルを読みなおしている。分岐予測について調べたかったのでもう一度読み直してまとめている。

条件分岐予測には私の理解では大きく2種類があって、

  • 局所分岐予測:条件分岐命令毎に分離した履歴バッファを利用する。つまり条件分岐命令のPCアドレスを使用して、この命令の場合はこの履歴、別の命令の場合は別の履歴、というようにテーブルを作って管理する。

    • ただし無尽蔵に巨大なテーブルを作ることは出来ないため、PCのアドレスに基づいて1024~4096程度のエントリを持ち、それぞれのPC値に基づいてテーブル情報のアップデートを行う。
  • 広域分岐予測:局所分岐予測では、異なる分岐命令間の相関関係を使って予測することができないため、プログラムのシーケンス中で分岐がどのような順番で実行されるのかについて記録を行いその情報に基づいて予測を行う。この場合プログラムシーケンス中に有意な相関関係がない場合、高い精度を持つことができない。

    • この分岐予測のためには、Global History Register (GHR) が必要になる。GHRのビット数がmならば、過去のm回の分岐予測の結果を記録している。

A64FXのマニュアルを見る限り、フロントエンドが採用しているのは、

  • Small Taken Chain Predictor (S-TCP) :

    • グローバル分岐予測
    • Takenのチェインを作りループが発生すればそれをループと検出する。ループを検出すると実行パスに従って分岐予測を行う。
    • これ、もとになる論文が見つからなかった。参考文献はあるのだろうか?
  • Loop Prediction Table (LPT) :

    • ローカル分岐予測
    • Taken、Not Takenが連続した回数を記録し、その履歴をもとに分岐方向を予測する。
    • LPTは8エントリで構成され、分岐命令を8命令まで記録できる。
    • これはつまり、「Takenが10回なら次はNot Taken」「Not Takenが10回続けば次はTaken」といった予測をするのだろうか?記録できる種類は1エントリにつき1種類かな?カウント回数とそれに基づくTaken / Not Takenの情報か。
  • Branch Weight Table (BWT)

    • Branch Weight TableとGlobal History Register (GHR)に基づく分岐予測方式。
    • ここではPiecewise linearアルゴリズムに基づいて分岐予測をすると書いてあるが、Piecewise linearというのは「区分線形関数」というらしいが、ここはあまり気にしなくても良い気がする。重要なのは線形関数によって予測が計算されているということである。
    • 分岐予測の方法、以下の線形関数を用いてPredictionを行う。図では乗算として書いてあるがもちろん乗算は使っていないだろう。NとTが1と-1なので符号反転して加算しているだけであると思われる。あとはWeight Tableがどの程度のビット幅であるのかが気になるくらい。
f:id:msyksphinz:20210117012822p:plain
  • Branch Weight Tableのアップデート方法、図では乗算として書いてあるがもちろん乗算は使っていないだろう。これがどういう理屈でこのようになっているのかについては良く分からない。
f:id:msyksphinz:20210117012841p:plain
  • Branch Target Buffer (BTB)
    • これは分岐予測における分岐先のアドレスを記録するためのバッファで、分岐方向を記録しているわけではない。A64FXの場合は4-way、2048エントリの構成となっている。
    • 相対分岐予測の場合は分岐先アドレスをその場で計算できる。
    • 間接分岐命令については分岐先アドレスが異なる可能性があるため、Rehashと呼ばれる機構を採用している。
      • 分岐方向履歴
      • ターゲットヒストリレジスタ(つまりこれは間接分岐命令におけるジャンプ先アドレスのことらしい)
      • をhashし、アドレス予測に使用しているらしい。

SonicBOOMのデザインを読み解く (ロードストアのトレースログ)

ロードストアのトレースログについては、以下のオプションを変更してリコンパイルすることで出力されるようになるらしい。リコンパイルには例によってかなり時間がかかってしまうのだけれども。

  • src/main/scala/common/parameters.scala
diff --git a/src/main/scala/common/parameters.scala b/src/main/scala/common/parameters.scala
index 1a0c25fd..ca1d7edf 100644
--- a/src/main/scala/common/parameters.scala
+++ b/src/main/scala/common/parameters.scala
@@ -92,9 +92,9 @@ case class BoomCoreParams(
   clockGate: Boolean = false,

   /* debug stuff */
-  enableCommitLogPrintf: Boolean = false,
+  enableCommitLogPrintf: Boolean = true,
   enableBranchPrintf: Boolean = false,
-  enableMemtracePrintf: Boolean = false
+  enableMemtracePrintf: Boolean = true

 // DOC include end: BOOM Parameters
 ) extends freechips.rocketchip.tile.CoreParams

これで同じようにシミュレーションを行うと、以下のようにトレース情報に加えてメモリアクセスの情報が出力される。

                3425 3 0x00000000800031fa 0x034000ef jal     pc + 0x34 x 1 0x00000000800031fe
                3432 3 0x000000008000322e 0x00000517 auipc   a0, 0x0 x10 0x000000008000322e
                3436 3 0x0000000080003232 0x1d250513 addi    a0, a0, 466 x10 0x0000000080003400
                3438 3 0x0000000080003236 0x6108 c.ld    a0, 0(a0) x10 0xdeadbeef00000000
MT 0000000000003df6 01 00 3 0080003400 0000000000000000 deadbeef00000000
                3439 3 0x0000000080003238 0x8082 ret
                3441 3 0x00000000800031fe 0x87aa c.mv    a5, a0 x15 0xdeadbeef00000000
                3442 3 0x0000000080003200 0x85be c.mv    a1, a5 x11 0xdeadbeef00000000
                3442 3 0x0000000080003202 0x00000517 auipc   a0, 0x0 x10 0x0000000080003202
                3442 3 0x0000000080003206 0x1ee50513 addi    a0, a0, 494 x10 0x00000000800033f0
                3442 3 0x000000008000320a 0xaefff0ef jal     pc - 0x512 x 1 0x000000008000320e
                3443 3 0x0000000080002cf8 0x7159 c.addi16sp sp, -112 x 2 0x0000000080023300
                3443 3 0x0000000080002cfa 0xf406 c.sdsp  ra, 40(sp)
                3443 3 0x0000000080002cfc 0xf022 c.sdsp  s0, 32(sp)
                3443 3 0x0000000080002cfe 0x1800 c.addi4spn s0, sp, 48 x 8 0x0000000080023330
MT 0000000000003dfb 02 01 3 0080023328 000000008000320e 2a3549fad288d2d0
MT 0000000000003dfb 02 01 3 0080023320 0000000080023380 9b41f14b36cbd0ae
                3444 3 0x0000000080002d00 0xfca43c23 sd      a0, -40(s0)
MT 0000000000003dfc 02 01 3 0080023308 00000000800033f0 b0d68963f68c7912
                3445 3 0x0000000080002d04 0xe40c c.sd    a1, 8(s0)
                3445 3 0x0000000080002d06 0xe810 c.sd    a2, 16(s0)
                3445 3 0x0000000080002d08 0xec14 c.sd    a3, 24(s0)
                3445 3 0x0000000080002d0a 0xf018 c.sd    a4, 32(s0)
MT 0000000000003dfd 02 01 3 0080023338 deadbeef00000000 605e758e69ca86a0
MT 0000000000003dfd 02 01 3 0080023340 0000000080003480 24726ea7be9d2eb6
MT 0000000000003dfd 02 01 3 0080023348 0000000080023200 a85fb40e04d7f7e8
MT 0000000000003dfd 02 01 3 0080023350 0000000000000000 c29b4f52e59f9f04
                3446 3 0x0000000080002d0c 0xf41c c.sd    a5, 40(s0)
MT 0000000000003dfe 02 01 3 0080023358 deadbeef00000000 bfde1696b4cac50a
                3447 3 0x0000000080002d0e 0x03043823 sd      a6, 48(s0)
                3447 3 0x0000000080002d12 0x03143c23 sd      a7, 56(s0)

なるほど、最初のMTはMemTraceのことか、マニュアル運転のことかと思った。

  • src/main/scala/lsu/lsu.scala
    if (MEMTRACE_PRINTF) {
      when (commit_store || commit_load) {
        val uop    = Mux(commit_store, stq(idx).bits.uop, ldq(idx).bits.uop)
        val addr   = Mux(commit_store, stq(idx).bits.addr.bits, ldq(idx).bits.addr.bits)
        val stdata = Mux(commit_store, stq(idx).bits.data.bits, 0.U)
        val wbdata = Mux(commit_store, stq(idx).bits.debug_wb_data, ldq(idx).bits.debug_wb_data)
        printf("MT %x %x %x %x %x %x %x\n",
          io.core.tsc_reg, uop.uopc, uop.mem_cmd, uop.mem_size, addr, stdata, wbdata)
      }
    }

うーん、例えば以下のログについて考えてみると、

MT 0000000000003df6 01 00 3 0080003400 0000000000000000 deadbeef00000000
  • TSCカウンタ(これはtimeレジスタとは違い、ずっと動いているタイプのものらしい)
  • マイクロオペレーション
  • マイクロオペレーション内のメモリコマンド
  • メモリアクセスサイズ
  • アドレス
  • 書き込みデータ(読み込みと書き込みが両方表示されているようだが、たぶんアトミック操作のためかしら?)

が情報として並んでいるようだ。uopsは本当にコミットの情報をそのまま出しているのかな?uop内にはどのような情報が入っているのかというと、

  • src/main/scala/common/micro-op.scala
class MicroOp(implicit p: Parameters) extends BoomBundle
  with freechips.rocketchip.rocket.constants.MemoryOpConstants
  with freechips.rocketchip.rocket.constants.ScalarOpConstants
{
  val uopc             = UInt(UOPC_SZ.W)       // micro-op code
  val inst             = UInt(32.W)
  val debug_inst       = UInt(32.W)
  val is_rvc           = Bool()
  val debug_pc         = UInt(coreMaxAddrBits.W)
  val iq_type          = UInt(IQT_SZ.W)        // which issue unit do we use?
  val fu_code          = UInt(FUConstants.FUC_SZ.W) // which functional unit do we use?
  val ctrl             = new CtrlSignals

...

結構たくさん情報が入っている。これはちょっと簡単には追いかけられそうにないな。

SonicBOOMのデザインを読み解く (LSUによるロード命令)

SonicBOOMの動作解析、次はLSUによるロード命令の動作を見てみよう。ロード命令はLSUにより発行されるが、命令自体は整数ユニットとは別のイシューユニットで発行される。

以下のアセンブリ命令を実行して波形を取得した。

    .section    .text
    .global     simple_load
simple_load:

    la      x10, test_data
    ld      x10, 0(x10)

    ret


    .section    .data
    .align      3
test_data:
    .dword  0xdeadbeef00000000
    .dword  0xdeadbeef00000001

アドレス生成とリクエストのタイミングダイアグラムはこのようになっていた。mem_unitでアドレスの計算を行い、それを即時lsuユニットに転送する。lsuユニットにはDTLBが入っておりアドレスを仮想アドレスに変換してからDCacheへリクエストを行う。ここまでが1サイクルで実行されているっぽい?

DCacheにヒットすると2サイクルで返ってくるようだ。

f:id:msyksphinz:20210115013602p:plain

Chisel側のソースコードを読んでみる。まず、mem_unitsはExeUnitの内部に存在しており、それ自体は外部のLSUと接続されている。

  • src/main/scala/exu/core.scala
  // Load/Store Unit & ExeUnits
  val mem_units = exe_units.memory_units
  val mem_resps = mem_units.map(_.io.ll_iresp)
  for (i <- 0 until memWidth) {
    mem_units(i).io.lsu_io <> io.lsu.exe(i)
  }

リクエストがmem_unit側からLSUに渡されるようになっている。LSUユニットにはDTLBが入っており、そこで物理アドレスへの変換が行われている。

  • src/main/scala/lsu/lsu.scala
  val dtlb = Module(new NBDTLB(
    instruction = false, lgMaxSize = log2Ceil(coreDataBytes), rocket.TLBConfig(dcacheParams.nTLBEntries)))

  io.ptw <> dtlb.io.ptw

  for (w <- 0 until memWidth) {
    dtlb.io.req(w).valid            := exe_tlb_valid(w)
    dtlb.io.req(w).bits.vaddr       := exe_tlb_vaddr(w)
    dtlb.io.req(w).bits.size        := exe_size(w)
    dtlb.io.req(w).bits.cmd         := exe_cmd(w)
    dtlb.io.req(w).bits.passthrough := exe_passthr(w)
  }
  dtlb.io.kill                      := exe_kill.reduce(_||_)
  dtlb.io.sfence                    := exe_sfence

  val exe_tlb_miss  = widthMap(w => dtlb.io.req(w).valid && (dtlb.io.resp(w).miss || !dtlb.io.req(w).ready))
  val exe_tlb_paddr = widthMap(w => Cat(dtlb.io.resp(w).paddr(paddrBits-1,corePgIdxBits),
                                        exe_tlb_vaddr(w)(corePgIdxBits-1,0)))

見ている感じでは、DCacheには物理アドレスを入力しているようだ。

f:id:msyksphinz:20210115013628p:plain

SonicBOOMのデザインを読み解く (DispatcherとFrontendのReplay動作)

前回の解析で、イシューユニットで命令があふれてしまった場合にどのように動作するのかが解析できなかった。いくつか構成を変えて新しいコンフィグレーションを生成してみる。

StrangeBoomConfigという新しいコンフィグレーションを作ってその動作を確かめてみる。このコンフィグレーションには、整数の発行ユニットのエントリ数を5に絞っている。

+/**
+  * Strange BOOM.
+  */
+class WithStrangeBooms extends Config((site, here, up) => {
+  case BoomTilesKey => up(BoomTilesKey, site) map { b => b.copy(
+    core = b.core.copy(
+      fetchWidth = 8,
+      decodeWidth = 5,
+      numRobEntries = 130,
+      issueParams = Seq(
+        IssueParams(issueWidth=2, numEntries=24, iqType=IQT_MEM.litValue, dispatchWidth=5),
+        IssueParams(issueWidth=5, numEntries=5,  iqType=IQT_INT.litValue, dispatchWidth=5),
+        IssueParams(issueWidth=2, numEntries=32, iqType=IQT_FP.litValue , dispatchWidth=5)),
+      numIntPhysRegisters = 128,
+      numFpPhysRegisters = 128,
+      numLdqEntries = 32,
+      numStqEntries = 32,
+      maxBrCount = 20,
+      numFetchBufferEntries = 40,
+      enablePrefetching=true,
+      numDCacheBanks=1, // Duplicate the DCache. For Science
+      ftq = FtqParameters(nEntries=40),
+      fpu = Some(freechips.rocketchip.tile.FPUParams(sfmaLatency=4, dfmaLatency=4, divSqrt=true))),
+    dcache = Some(DCacheParams(rowBits = site(SystemBusKey).beatBytes*8,
+      nSets=64, nWays=8, nMSHRs=8, nTLBEntries=32)),
+    icache = Some(ICacheParams(fetchBytes = 4*4, rowBits = site(SystemBusKey).beatBytes*8, nSets=64, nWays=8, prefetch=true))
+  )}
+  case SystemBusKey => up(SystemBusKey, site).copy(beatBytes = 16)
+  case XLen => 64
+  case MaxHartIdBits => log2Up(site(BoomTilesKey).size)
+})
+

シミュレーション結果がこうなった。5サイクルに一度FetchバスのReady信号がAssertされているので、1命令発行されるたびに新たにイシューユニットに取り込まれているという感じだな。

f:id:msyksphinz:20210114014631p:plain

この辺のデザインのフローを確認してみよう。

f:id:msyksphinz:20210114014849p:plain

C言語のマクロであんまり使わないけど(低レイヤプログラミングにおいて)猛烈に便利な演算子

これは自分用メモ。

ご存知C言語にはマクロと呼ばれる#defineなどを使ったルール記述が可能だ。有名なものだと#define, #ifdef, #endif などのものだ。これを使えばコンパイル時に様々なオプションでソースコードを改変することができ、グローバルに持たせておきたい値を設定するときに有効だったりする(まあプリプロセッサによる下処理とか現代的なプログラミング言語から見てみるとあまり便利じゃなかったりとかあるかもしれないが、それは目をつぶる)。

このマクロは定数を定義するだけでなく、簡単な関数のように使えるところが便利だ。例えばデバッグビルドとリリースビルドでprintf()を挿入したいかどうかを決めるとき、

#ifdef DEBUG
  #define DEBUG_PRINT(x) fprintf(stder, x);
#else // DEBUG
  #define DEBUG_PRINT(x)
#endif // DEBUG

みたいな感じで定義しておけばデバッグビルド(DEBUG有効時)のみDEBUG_PRINT("hello")で出力が行われ、リリースビルド時にはそのメッセージは消える、みたいなことができるようになる。このように、関数ほどではないにせよ、引数を取って簡単な簡単な処理を記述できるような、簡単な関数を作ることができる機能がある。

これは、実は私たち低レイヤプログラマにとってみれば地味に便利な機能だったりする。このマクロはC言語だけでなく、gcc経由でアセンブリファイルにも適用可能だからだ。

  • test.S
#define TEST_LD( load_inst, addr_reg) load_inst x10, 0(addr_reg)

_start:
TEST_LD(ld, x20)
TEST_LD(lw, x21)

上記のようなアセンブリファイルを、直接asに流すのではなくgccを経由してアセンブルすることでいったんプリプロセッサを通すことができる。最終的に以下のようなアセンブリファイルとしてアセンブルされる。

$ riscv64-unknown-elf-gcc test.S   # gcc(コンパイラドライバ)を経由することでプリプロセッサが動く
# さらに注: この方式は拡張子が.Sの場合に有効で、.sの場合には使えないとのコメントを頂いた。ありがとうございます。
_start:
ld x10, 0(x20)
lw x10, 0(x21)

さらに便利なのは、連結演算子により文字の連結を行う機能だ。C言語のマクロには###という演算子があり、詳細は以下がとても分かりやすい。

wisdom.sakura.ne.jp

#演算子はダブルクォーテーション付きで文字を連結する。詳細は上記リンク。 ##演算子はダブルクォーテーション無しで文字を連結する。上記のアセンブリファイルに対するマクロの適用では、経験上こちらの方が良く使う。

#define V_LD_ELEM( EEW, vreg, base_addr_reg ) vle ## EEW ##.v vreg, base_addr_reg

V_LD_ELEM(8, v10, x10)
V_LD_ELEM(16, v11, x11)
V_LD_ELEM(32, v12, x12)
V_LD_ELEM(64, v13, x13)

上記のプログラムは以下のように処理される。このように、命令の生成など、様々な命令のバリエーションの生成で使用することができる。検証パタンの大量生成において、かなり便利だ。

vle8.v v10, x10
vle16.v v11, x11
vle32.v v12, x12
vle64.v v13, x13

SonicBOOMのデザインを読み解く (リソースが満タンになった場合のFetcherの挙動はどうなるか)

次に、以下のようにALUのリソースを一気に使い切るようなコードを見てみよう。これはDispatcherがC.ADDIを8命令フルにDispatchするがALUの数が足りず、すべての命令を発行することができない場合にFetcherがどのようにリロードするのかを観察する。

000000008000322e <simple_add>:
    8000322e:   0505                    addi    a0,a0,1
    80003230:   0589                    addi    a1,a1,2
    80003232:   060d                    addi    a2,a2,3
    80003234:   0691                    addi    a3,a3,4
    80003236:   0715                    addi    a4,a4,5
    80003238:   0799                    addi    a5,a5,6
    8000323a:   081d                    addi    a6,a6,7
    8000323c:   08a1                    addi    a7,a7,8
    8000323e:   0925                    addi    s2,s2,9
    80003240:   09a9                    addi    s3,s3,10
    80003242:   0a2d                    addi    s4,s4,11
    80003244:   0ab1                    addi    s5,s5,12
    80003246:   0b35                    addi    s6,s6,13
    80003248:   0bb9                    addi    s7,s7,14
    8000324a:   0c3d                    addi    s8,s8,15
    8000324c:   0cc1                    addi    s9,s9,16
    8000324e:   0d45                    addi    s10,s10,17
    80003250:   0dc9                    addi    s11,s11,18
    80003252:   0e4d                    addi    t3,t3,19
    80003254:   854a                    mv      a0,s2
    80003256:   8082                    ret

この場合、どうもDispatcherの幅はALUの最大幅と一緒になるようだ(つまり5)。従ってC.ADDI命令が最大幅でひたすら発行され続けることになる。

f:id:msyksphinz:20210109011335p:plain

上記はGigaBoomConfigの様子だが、MegaBoomConfigの場合はどうなるのか。MegaBoomConfigの場合はDispatch幅が4なのでそれだけの幅いっぱいで発行されるのだろうか。

f:id:msyksphinz:20210109011428p:plain

その通りだった。発行幅4で最大幅で発行され続ける。

これはALUの個数が5つ(MegaBoomConfigの場合は4つ)あるのでこれで良いが、そうでない、例えば乗算命令などはどうなるのだろうか?以下のようなプログラムを作ってDispatchの様子を確かめてみたいと思う。以下のようなプログラムを用意した。

0000000080003256 <simple_mul>:
    80003256:   02a50533                mul     a0,a0,a0
    8000325a:   02b585b3                mul     a1,a1,a1
    8000325e:   02c60633                mul     a2,a2,a2
    80003262:   02d686b3                mul     a3,a3,a3
    80003266:   02e70733                mul     a4,a4,a4
    8000326a:   02f787b3                mul     a5,a5,a5
    8000326e:   03080833                mul     a6,a6,a6
    80003272:   031888b3                mul     a7,a7,a7
    80003276:   03290933                mul     s2,s2,s2
    8000327a:   033989b3                mul     s3,s3,s3
    8000327e:   034a0a33                mul     s4,s4,s4
    80003282:   035a8ab3                mul     s5,s5,s5
    80003286:   036b0b33                mul     s6,s6,s6
    8000328a:   037b8bb3                mul     s7,s7,s7
    8000328e:   038c0c33                mul     s8,s8,s8
    80003292:   039c8cb3                mul     s9,s9,s9
    80003296:   03ad0d33                mul     s10,s10,s10
    8000329a:   03bd8db3                mul     s11,s11,s11
    8000329e:   854a                    mv      a0,s2
    800032a0:   8082                    ret

波形を観測していると、おや、最大Dispatch幅で発行され続けている。これはいったいどういう仕組みだ?

f:id:msyksphinz:20210109011453p:plain

ログ上では、パイプライン上に1サイクルずつ依存しない乗算命令が完了していることが分かる。

                3832 3 0x0000000080003256 mul     a0, a0, a0 x10 0x0000000000000000
                3833 3 0x000000008000325a mul     a1, a1, a1 x11 0x0000000000000001
                3834 3 0x000000008000325e mul     a2, a2, a2 x12 0x400035400b139000
                3835 3 0x0000000080003262 mul     a3, a3, a3 x13 0x400232c4d50f9000
                3836 3 0x0000000080003266 mul     a4, a4, a4 x14 0x0000000000000000
                3837 3 0x000000008000326a mul     a5, a5, a5 x15 0x0000000000000000
                3838 3 0x000000008000326e mul     a6, a6, a6 x16 0x0000000000000000
                3839 3 0x0000000080003272 mul     a7, a7, a7 x17 0x0000000000000000
                3840 3 0x0000000080003276 mul     s2, s2, s2 x18 0x0000000000000000
                3841 3 0x000000008000327a mul     s3, s3, s3 x19 0x0000000000000000
                3842 3 0x000000008000327e mul     s4, s4, s4 x20 0x0000000000000000
                3843 3 0x0000000080003282 mul     s5, s5, s5 x21 0x0000000000000000
                3844 3 0x0000000080003286 mul     s6, s6, s6 x22 0x0000000000000000
                3845 3 0x000000008000328a mul     s7, s7, s7 x23 0x0000000000000000
                3846 3 0x000000008000328e mul     s8, s8, s8 x24 0x0000000000000000
                3847 3 0x0000000080003292 mul     s9, s9, s9 x25 0x0000000000000000
                3848 3 0x0000000080003296 mul     s10, s10, s10 x26 0x0000000000000000
                3849 3 0x000000008000329a mul     s11, s11, s11 x27 0x0000000000000000

どういう仕組みになっているのか見ているが、どうもint_issue_unitがその辺の管理をしているように見える。ここで発行制御を行っている訳か。ではこのint_issue_initがあふれてしまった場合はどうするのだろう?パラメータを変えて別のRTLを生成して観察してみようか。

f:id:msyksphinz:20210109011631p:plain

SonicBOOMのデザインを読み解く (DispatcherとFrontendのReplay動作)

SonicBOOMのデザインを読み解いている。引き続き以下のようなプログラムを動かして命令フェッチャーの動きを観察している。

000000008000322e <simple_add>:
    8000322e:   b0002573                csrr    a0,mcycle
    80003232:   301025f3                csrr    a1,misa
    80003236:   0205e593                ori     a1,a1,32
    8000323a:   30159073                csrw    misa,a1
    8000323e:   8082                    ret

前回解析したように、全部の命令をIssueできなかった場合、Dispatchできる部分までなるべくDispatchし、次にIssueできる場所からReplayを行う。この時のReplay動作について少し見てみる。

まず、Fetch BufferとしてF3・F4が存在しているが、F3は命令キャッシュから応答のあった命令を格納するためのレジスタであり、F3とF4の間に命令を分解して最大で8命令をDispatchできるように拡張している。

f:id:msyksphinz:20210107010808p:plain

で、最後の命令までDispatchできなかった場合は、どうも解析しているとなんとs0ステージからやり直しているように見える。これはバックエンドからのリダイレクトフラッシュ信号に基づいてPCの再生成を行っているようだ。

f:id:msyksphinz:20210107010815p:plain

CSR命令の場合は、毎度毎度命令を実行するたびにパイプラインフラッシュを行っているようだ。