FPGA開発日記

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

SonicBOOMに関する調査 (リネームマップの調査)

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)
  }