BOOMv3のLSUの構成を読み解いて、自作CPUの性能向上の役に立てる。
- LSUと他のモジュールとの接続。
これはいろいろあるのだが、まずはコア本体との接続について。コア本体とのインタフェースは
LSUExeIO
に定義されている。
以下の信号の中で、req
はコアからのリクエストを持っており、iresp
は整数レジスタ向けの書き込み情報、fresp
は浮動小数点レジスタ向けの書き込み情報が含まれている。
req
はFlipped(ValidIO())
なので、Valid信号付きの入力信号となる。
iresp
とfresp
はDecoupledIO())
なので、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_head
とstq_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_std
はuopSTD || 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]