FPGA開発日記

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

BOOMv3 LSUの構成を読み解く

BOOMv3のLSUの構成を読み解いて、自作CPUの性能向上の役に立てる。

github.com

  • LSUと他のモジュールとの接続。 これはいろいろあるのだが、まずはコア本体との接続について。コア本体とのインタフェースはLSUExeIOに定義されている。

以下の信号の中で、reqはコアからのリクエストを持っており、irespは整数レジスタ向けの書き込み情報、fresp浮動小数レジスタ向けの書き込み情報が含まれている。

reqFlipped(ValidIO())なので、Valid信号付きの入力信号となる。 irespfrespDecoupledIO())なので、ValidとReady付きの出力信号となる。

class LSUExeIO(implicit p: Parameters) extends BoomBundle()(p)
{
  // The "resp" of the maddrcalc is really a "req" to the LSU
  val req       = Flipped(new ValidIO(new FuncUnitResp(xLen)))
  // Send load data to regfiles
  val iresp    = new DecoupledIO(new boom.exu.ExeUnitResp(xLen))
  val fresp    = new DecoupledIO(new boom.exu.ExeUnitResp(xLen+1)) // TODO: Should this be fLen?
}
  • LSUの内部キューについて

まずは、LSU内部を構成するLDQ、STQは、以下のような宣言でインスタンス化されている。

  val ldq = Reg(Vec(numLdqEntries, Valid(new LDQEntry)))
  val stq = Reg(Vec(numStqEntries, Valid(new STQEntry)))

要するに単なるレジスタとして定義している。これらの順序を管理しているのが以下のポインタ。 ldq_headはこれからやってくるリクエストを格納するためのポインタを示し、ldq_tailは最も優先度を高く処理すべきLDQのエントリを示す。 stq_headstq_tailも同様。 stq_commit_headは命令のコミットが確定したエントリのポインタ、stq_execute_headはこれから実行すべきSTQエントリのポインタを格納している。

  val ldq_head         = Reg(UInt(ldqAddrSz.W))
  val ldq_tail         = Reg(UInt(ldqAddrSz.W))
  val stq_head         = Reg(UInt(stqAddrSz.W)) // point to next store to clear from STQ (i.e., send to memory)
  val stq_tail         = Reg(UInt(stqAddrSz.W))
  val stq_commit_head  = Reg(UInt(stqAddrSz.W)) // point to next store to commit
  val stq_execute_head = Reg(UInt(stqAddrSz.W)) // point to next store to execute

まず、コアからの要求が発生するとLDQ/STQにエントリが確保される。これが以下の実装。

  for (w <- 0 until coreWidth)
  {
    ldq_full = WrapInc(ld_enq_idx, numLdqEntries) === ldq_head
    io.core.ldq_full(w)    := ldq_full
    io.core.dis_ldq_idx(w) := ld_enq_idx

    stq_full = WrapInc(st_enq_idx, numStqEntries) === stq_head
    io.core.stq_full(w)    := stq_full
    io.core.dis_stq_idx(w) := st_enq_idx

    val dis_ld_val = io.core.dis_uops(w).valid && io.core.dis_uops(w).bits.uses_ldq && !io.core.dis_uops(w).bits.exception
    val dis_st_val = io.core.dis_uops(w).valid && io.core.dis_uops(w).bits.uses_stq && !io.core.dis_uops(w).bits.exception
    when (dis_ld_val)
    {
      ldq(ld_enq_idx).valid                := true.B
      ldq(ld_enq_idx).bits.uop             := io.core.dis_uops(w).bits
      ldq(ld_enq_idx).bits.youngest_stq_idx  := st_enq_idx
      ldq(ld_enq_idx).bits.st_dep_mask     := next_live_store_mask

      ldq(ld_enq_idx).bits.addr.valid      := false.B
      ldq(ld_enq_idx).bits.executed        := false.B
      ldq(ld_enq_idx).bits.succeeded       := false.B
      ldq(ld_enq_idx).bits.order_fail      := false.B
      ldq(ld_enq_idx).bits.observed        := false.B
      ldq(ld_enq_idx).bits.forward_std_val := false.B

      assert (ld_enq_idx === io.core.dis_uops(w).bits.ldq_idx, "[lsu] mismatch enq load tag.")
      assert (!ldq(ld_enq_idx).valid, "[lsu] Enqueuing uop is overwriting ldq entries")
    }
      .elsewhen (dis_st_val)
    {
      stq(st_enq_idx).valid           := true.B
      stq(st_enq_idx).bits.uop        := io.core.dis_uops(w).bits
      stq(st_enq_idx).bits.addr.valid := false.B
      stq(st_enq_idx).bits.data.valid := false.B
      stq(st_enq_idx).bits.committed  := false.B
      stq(st_enq_idx).bits.succeeded  := false.B

      assert (st_enq_idx === io.core.dis_uops(w).bits.stq_idx, "[lsu] mismatch enq store tag.")
      assert (!stq(st_enq_idx).valid, "[lsu] Enqueuing uop is overwriting stq entries")
    }

    ld_enq_idx = Mux(dis_ld_val, WrapInc(ld_enq_idx, numLdqEntries),
                                 ld_enq_idx)

それぞれの条件に応じて、LDQ/STQからの発行の条件が設定される。その条件を計算するのが以下の変数。

  //---------------------------------------
  // Can-fire logic and wakeup/retry select
  //
  // First we determine what operations are waiting to execute.
  // These are the "can_fire"/"will_fire" signals

  val will_fire_load_incoming  = Wire(Vec(memWidth, Bool()))
  val will_fire_stad_incoming  = Wire(Vec(memWidth, Bool()))
  val will_fire_sta_incoming   = Wire(Vec(memWidth, Bool()))
  val will_fire_std_incoming   = Wire(Vec(memWidth, Bool()))
  val will_fire_sfence         = Wire(Vec(memWidth, Bool()))
  val will_fire_hella_incoming = Wire(Vec(memWidth, Bool()))
  val will_fire_hella_wakeup   = Wire(Vec(memWidth, Bool()))
  val will_fire_release        = Wire(Vec(memWidth, Bool()))
  val will_fire_load_retry     = Wire(Vec(memWidth, Bool()))
  val will_fire_sta_retry      = Wire(Vec(memWidth, Bool()))
  val will_fire_store_commit   = Wire(Vec(memWidth, Bool()))
  val will_fire_load_wakeup    = Wire(Vec(memWidth, Bool()))
  • can_fire_load_incoming : ロード要求がLSUに到着したとき。TLB, DC(Data Cache), LCAM(Load Queue CAM)を使用する。
    • ロード命令でLCAMを使用する、というのは、Load→Loadのコヒーレントハザードチェックのためだと思う。
    will_fire_load_incoming (w) := lsu_sched(can_fire_load_incoming (w) , true , true , true , false) // TLB , DC , LCAM
  • can_fire_stad_incoming : ストアアドレスとストアデータ要求がLSUに到着したとき。TLB, LCAM, ROBを使用する。
    • ROBを使用する、というのはコミット状態を書き込むことができる、という意味か?
  // Can we fire an incoming store addrgen + store datagen
  val can_fire_stad_incoming = widthMap(w => exe_req(w).valid && exe_req(w).bits.uop.ctrl.is_sta
                                                              && exe_req(w).bits.uop.ctrl.is_std)
    will_fire_stad_incoming (w) := lsu_sched(can_fire_stad_incoming (w) , true , false, true , true)  // TLB ,    , LCAM , ROB
  • can_fire_sta_incoming : これは実際にはすべての命令がこれに当てはまりそうな気がするのだが、STAとSTDがどういう条件で発動するのかはよくわからない。TLB, LCAM, ROBを使用し、DCacheは使用しない。
    will_fire_sta_incoming  (w) := lsu_sched(can_fire_sta_incoming  (w) , true , false, true , true)  // TLB ,    , LCAM , ROB
  • can_fire_std_incoming : データだけを使用する命令。デコード時に、is_stduopSTD || is_sta & lrs2_type == RT_FIXなので、std_incomingが単体で立ち上がることはない気がする?
  // Can we fire an incoming store datagen
  val can_fire_std_incoming  = widthMap(w => exe_req(w).valid && exe_req(w).bits.uop.ctrl.is_std
                                                              && !exe_req(w).bits.uop.ctrl.is_sta)
will_fire_std_incoming  (w) := lsu_sched(can_fire_std_incoming  (w) , false, false, false, true)  //                 , ROB
それ以外で重要そうなもの。

- HellaCache(要するにDCache)からの応答が来る予定なもの。Hellaのステートがよく分からないのだが、これはNonBlocking Cacheなのに1つのステートで管理しているのだろうか?

} .elsewhen (hella_state === h_s1) { can_fire_hella_incoming(memWidth-1) := true.B / ... 途中省略 ... / will_fire_hella_incoming(w) := lsu_sched(can_fire_hella_incoming(w) , true , true , false, false) // TLB , DC

- HellaCacheからの応答があったもの。
can_fire_hella_wakeup(memWidth-1) := true.B

when (will_fire_hella_wakeup(memWidth-1) && dmem_req_fire(memWidth-1)) {
  hella_state := h_wait

/ ... 途中省略 ... / will_fire_hella_wakeup (w) := lsu_sched(can_fire_hella_wakeup (w) , false, true , false, false) // , DC

- Load命令のリトライ。これは、TLBのアドレス変換がまだ解決しておらず、TLBがアドレスを取得するタイミングでもう一度実行するためのステート。

// Can we retry a load that missed in the TLB val can_fire_load_retry = widthMap(w => ( ldq_retry_e.valid && ldq_retry_e.bits.addr.valid && ldq_retry_e.bits.addr_is_virtual && !p1_block_load_mask(ldq_retry_idx) && !p2_block_load_mask(ldq_retry_idx) && RegNext(dtlb.io.miss_rdy) && !store_needs_order && (w == memWidth-1).B && // TODO: Is this best scheduling? !ldq_retry_e.bits.order_fail)) / ... 途中省略 ... / will_fire_load_retry (w) := lsu_sched(can_fire_load_retry (w) , true , true , true , false) // TLB , DC , LCAM

- Store命令のリトライ

// Can we retry a store addrgen that missed in the TLB // - Weird edge case when sta_retry and std_incoming for same entry in same cycle. Delay this val can_fire_sta_retry = widthMap(w => ( stq_retry_e.valid && stq_retry_e.bits.addr.valid && stq_retry_e.bits.addr_is_virtual && (w == memWidth-1).B && RegNext(dtlb.io.miss_rdy) && !(widthMap(i => (i != w).B && can_fire_std_incoming(i) && stq_incoming_idx(i) === stq_retry_idx).reduce(||)) )) / ... 途中省略 ... / will_fire_sta_retry (w) := lsu_sched(can_fire_sta_retry (w) , true , false, true , true) // TLB , , LCAM , ROB // TODO: This should be higher priority

- Load命令のWakeup。これは、物理アドレスの変換も終了したが何らかの理由で命令の実行完了が阻害されている状態。
以下における、`executed`ビットはパイプラインを流れ始めたときはtrueから開始されるが、いくつかの状況でfalseに設定される。
以下は、DCacheにアクセスしてもアービトレーションに失敗した場合?
    } .elsewhen (lcam_ldq_idx(w) =/= i.U) {
      // The load is older, and either it hasn't executed, it was nacked, or it is ignoring its response
      // we need to kill ourselves, and prevent forwarding
      val older_nacked = nacking_loads(i) || RegNext(nacking_loads(i))
      when (!(l_bits.executed || l_bits.succeeded) || older_nacked) {
        s1_set_execute(lcam_ldq_idx(w))    := false.B
        io.dmem.s1_kill(w)                 := RegNext(dmem_req_fire(w))
        can_forward(w)                     := false.B
      }
    }



val can_fire_load_wakeup = widthMap(w => ( ldq_wakeup_e.valid && ldq_wakeup_e.bits.addr.valid && !ldq_wakeup_e.bits.succeeded && !ldq_wakeup_e.bits.addr_is_virtual && !ldq_wakeup_e.bits.executed && !ldq_wakeup_e.bits.order_fail && !p1_block_load_mask(ldq_wakeup_idx) && !p2_block_load_mask(ldq_wakeup_idx) && !store_needs_order && !block_load_wakeup && (w == memWidth-1).B && (!ldq_wakeup_e.bits.addr_is_uncacheable || (io.core.commit_load_at_rob_head && ldq_head === ldq_wakeup_idx && ldq_wakeup_e.bits.st_dep_mask.asUInt === 0.U)))) / ... 途中省略 ... / will_fire_load_wakeup (w) := lsu_sched(can_fire_load_wakeup (w) , false, true , true , false) // , DC , LCAM1

- Store命令のコミット: TLBアドレス変換が完了し、データの取得が終わっていると、コミット状態に入る。この時はDCacheへの書き込みのみを行う。

// Can we commit a store val can_fire_store_commit = widthMap(w => ( stq_commit_e.valid && !stq_commit_e.bits.uop.is_fence && !mem_xcpt_valid && !stq_commit_e.bits.uop.exception && (w == 0).B && (stq_commit_e.bits.committed || ( stq_commit_e.bits.uop.is_amo && stq_commit_e.bits.addr.valid && !stq_commit_e.bits.addr_is_virtual && stq_commit_e.bits.data.valid)))) / ... 途中省略 ... / will_fire_store_commit (w) := lsu_sched(can_fire_store_commit (w) , false, true , false, false) // , DC

[blog:g:11696248318754550877:banner]