SonicBOOMのリネームマップについてその構造を調査した。リネームマップは、論理レジスタから物理レジスタIDへの変換を行うことで仮想的に論理レジスタよりも多くのレジスタを扱うことができるような機能で、アウトオブオーダ実行を行うCPUではほぼ必須の機能だと考えて良い。
今回調査したいのはリネームマップ本体なので、リネームのほかの機能(freelist
, busytable
)についてはとりあえず省略する。マップの所だけを見ていくことにする。
リネームマップの入出力ポートは以下のような構成になっていた。
val io = IO(new BoomBundle()(p) { // Logical sources -> physical sources. val map_reqs = Input(Vec(plWidth, new MapReq(lregSz))) val map_resps = Output(Vec(plWidth, new MapResp(pregSz))) // Remapping an ldst to a newly allocated pdst? val remap_reqs = Input(Vec(plWidth, new RemapReq(lregSz, pregSz))) // Dispatching branches: need to take snapshots of table state. val ren_br_tags = Input(Vec(plWidth, Valid(UInt(brTagSz.W)))) // Signals for restoring state following misspeculation. val brupdate = Input(new BrUpdateInfo) val rollback = Input(Bool()) })
最初のmap_reqs
, map_resps
はリネーム情報のリクエストのためのポートだ。命令はmap_reqs
を通じて論理レジスタ番号を入力し、map_resps
を通じて物理レジスタ番号を返してもらう。通常のパスだ。
次のremap_req
は新しいリネームIDの配置操作となっている。おそらくはfreelist
から取り出した新しい物理レジスタIDを、論理レジスタ番号とのペアで挿入して新たにリネームIDに配置する。
ren_br_tags
は分岐命令毎のスナップショット取得信号だと思われる。分岐命令に遭遇したら、リネームマップのスナップショットをとって退避しておく。そして万が一分岐予測に失敗した場合、ロールバック信号brupdate
を使ってリネームマップを元に戻す。このときに、各分岐命令にはbrtag
が付けられるとドキュメントには書いてあり、それぞれのbrtag
に基づいてスナップショットのエントリに退避されるものと思われる。
以下がレジスタの定義となっている。map_table
は最新の論理レジスタ→物理レジスタのマッピングを示しており、br_snapshot
は分岐命令毎のスナップショットを保持しているということになる。map_table
は物理レジスタビット幅$\times$論理レジスタの数(32)であり、br_snapshots
は物理レジスタビット幅$\times$論理レジスタの数(32)$\times$brtags
の数となっており、とうぜんスナップショットのための配列はデカい。
// The map table register array and its branch snapshots. val map_table = RegInit(VecInit(Seq.fill(numLregs){0.U(pregSz.W)})) val br_snapshots = Reg(Vec(maxBrCount, Vec(numLregs, UInt(pregSz.W))))
remap_table
は、それぞれリネームマップにアクセスしてきた命令(同時に複数命令をリネームするので)、それぞれについてリネームを行うための最新の情報を含んだリネームマップテーブルを作り上げる。
// Uops requesting changes to the map table. val remap_pdsts = io.remap_reqs map (_.pdst) val remap_ldsts_oh = io.remap_reqs map (req => UIntToOH(req.ldst) & Fill(numLregs, req.valid.asUInt)) // Figure out the new mappings seen by each pipeline slot. for (i <- 0 until numLregs) { if (i == 0 && !float) { for (j <- 0 until plWidth+1) { remap_table(j)(i) := 0.U } } else { val remapped_row = (remap_ldsts_oh.map(ldst => ldst(i)) zip remap_pdsts) .scanLeft(map_table(i)) {case (pdst, (ldst, new_pdst)) => Mux(ldst, new_pdst, pdst)} for (j <- 0 until plWidth+1) { remap_table(j)(i) := remapped_row(j) } } }
そしてそれらを、br_snapshots
に分岐命令に応じて格納する。
// Create snapshots of new mappings. for (i <- 0 until plWidth) { when (io.ren_br_tags(i).valid) { br_snapshots(io.ren_br_tags(i).bits) := remap_table(i+1) } }
もし分岐予測に失敗したら、当該br_snapshots
からリネームマップリストを引っ張り出してきて復元するという訳だ。
when (io.brupdate.b2.mispredict) { // Restore the map table to a branch snapshot. map_table := br_snapshots(io.brupdate.b2.uop.br_tag) } .otherwise { // Update mappings. map_table := remap_table(plWidth) }
と、ここまでなら分岐命令に応じて正しく動作しそうだが、これは例外が発生した場合もちゃんと動くのだろうか?例外命令が発生してPCをアップデートしてどこかに飛ばす場合、どのようにリネームマップを復元するのだろう?
と思ったら実装があった。例外のようなケースには、io.rollback
が有効化され、それに応じて古い物理レジスタIDがロールバックされるらしい。なるほど。
// Generate maptable requests. for ((((ren1,ren2),com),w) <- ren1_uops zip ren2_uops zip io.com_uops.reverse zipWithIndex) { map_reqs(w).lrs1 := ren1.lrs1 map_reqs(w).lrs2 := ren1.lrs2 map_reqs(w).lrs3 := ren1.lrs3 map_reqs(w).ldst := ren1.ldst remap_reqs(w).ldst := Mux(io.rollback, com.ldst , ren2.ldst) remap_reqs(w).pdst := Mux(io.rollback, com.stale_pdst, ren2.pdst) }