自作RISC-Vコアのテストをいろいろやっていく中で、あれ?これどうやって実現すればいいんだろう、というのがある。 今引っかかっているのは、RASからの回復(1日に1時間くらいしか時間が取れないので全く進んでいない)のメカニズムだ。
突っかかったケースとしては、
- JALによりRASに戻りアドレスが積まれる
- 投機的に別のJALが呼ばれてRASが積みあがる
- 最初のJALが完了(そして分岐先が異なったため後続の命令がフラッシュ)される
- コミットされるには別の命令が遅れているためコミットされない。このため積みあがったRASのインデックスが元に戻らない
- そのため正しい場所からフェッチが始まり、RET命令をフェッチしたとしてもただしRASの場所からアドレスを読み取れない
この状況下で、あれ?BRUに分岐命令が積まれておりこれがアウトオブオーダで実行された場合、どうやって順番分岐を処理すればいいんだ、てなった。 分岐命令のフラッシュをコミット時にしていれば問題ないが、今の実装ではコミット待たずにフェッチ先を更新、Branch Tagにより後続の命令をフラッシュしているため、分岐命令の順番が異なると問題が生じる。
と、ここに来てFetch Target Queueの役割が何となくわかるようになった。なるほど、命令がフロントエンドからバックエンドに渡るときに分岐命令の順番をFTQに保存しておき、分岐命令の結果がアウトオブオーダで戻ってきたとしてもFTQで整列するのか。 FTQで整列した最も古い、フラッシュされていない命令の分岐命令の結果が確定すれば、その時点でフェッチ方向を確定させればよい。
という訳で、FTQのエントリを眺めてみるとそれ系の情報が並んでいる。is_call
とか、is_ret
とか、私が想像しているものと一緒だ。
なるほど、私の考え方は間違っていないっぽい。ではこれに従って自作CPUの実装を変更しよう(地獄)。
/** * Bundle to add to the FTQ RAM and to be used as the pass in IO */ class FTQBundle(implicit p: Parameters) extends BoomBundle with HasBoomFrontendParameters { // // TODO compress out high-order bits // val fetch_pc = UInt(vaddrBitsExtended.W) // IDX of instruction that was predicted taken, if any val cfi_idx = Valid(UInt(log2Ceil(fetchWidth).W)) // Was the CFI in this bundle found to be taken? or not val cfi_taken = Bool() // Was this CFI mispredicted by the branch prediction pipeline? val cfi_mispredicted = Bool() // What type of CFI was taken out of this bundle val cfi_type = UInt(CFI_SZ.W) // mask of branches which were visible in this fetch bundle val br_mask = UInt(fetchWidth.W) // This CFI is likely a CALL val cfi_is_call = Bool() // This CFI is likely a RET val cfi_is_ret = Bool() // Is the NPC after the CFI +4 or +2 val cfi_npc_plus4 = Bool() // What was the top of the RAS that this bundle saw? val ras_top = UInt(vaddrBitsExtended.W) val ras_idx = UInt(log2Ceil(nRasEntries).W) // Which bank did this start from? val start_bank = UInt(1.W) // // Metadata for the branch predictor // val bpd_meta = Vec(nBanks, UInt(bpdMaxMetaLength.W)) }