1 目的
最近在研究伯克利的Sonicboom riscv CPU,其内部对于SFB(short-forwards branch)程序进行了优化操作,某些程序序列下能提升1.7倍的IPC;但是处理比较复杂,不是集中在一个模块处理,而是分散于各级pipline。而相应的描述资料又少,因此采用结合源代码和波形仿真的方式来进行一个学习研究。
2 SFB介绍
SFB翻译过来就是短向前跳分支,简单来说是一串代码序列,如下面一段C程序所示:
int max = 0 ;
int maxid = −1;
for ( i = 0 ; i < n ; i ++) {
if ( x [ i ] >= max ) {
max = x [ i ] ;
maxid = i ;
}
}
这段C代码实现的是最为常见的数组里面找最大值的功能,站在程序员的角度很容易理解。对应的risc-v的汇编程序如下,其中bge指令和后面的两条MV指令组成的就是典型的SFB结构。
loop :
lw x2 , 0( a0 )
bge x1 , x2 , skip
mv x1 , x2
mv a1 , t 0
skip :
addi a0 , a0 , 4
addi t0 , t0 , 0x1
j loop :
在汇编中可以看见每次循环(loop)都会有一条分支指令(bge),以及该分支下的两条MV指令,分支指令不跳转(not_taken)的话就会执行两条MV指令,跳转(taken)的话就会调到标记skip的标号(label)。功能很简单,但是对于当今的高性能CPU来说,不局限于RISC-V架构,都有着很深的流水线(pipline)深度,同时针对于分支指令(Branch)会有先进的分支预测技术来预测分支指令跳转的方向,而不用等到一般处于流水线很后的读寄存器阶段(rigister read)或功能单元的执行阶段(execute)来得到实际跳转方向。
而此时对于这种出现频率特别高的并且覆盖指令很少的分支指令来说,CPU每次都会进行预测,不可避免的会出现预测失败的情况,CPU预测失败就会刷流水线(flush pipline)并重新取指令执行,代价会不可避免的降低IPC。
3 SonicBoom中对于SFB的优化
SonicBoom在微结构上对SFB处理进行了优化,将SFB转化为CPU内部带有特殊标记的微编码(microOps),从而在CPU内部pipline中进行了处理,即使分支预测错误(mispredicted)也不再通过刷流水线的方式(flush pipline)来进行重定向(redirect)。微架构针对于SFB进行的调整具体如下:
- 在前端Frontend增加了SFB检测逻辑;
- 将检测到的SFB指令通过预解码单元(predecode)解析成set-flag和conditional-execute微编码,此时原来的branch微编码被替换了;(set-flag对应到上面例子就是bge,conditional-execute是两条MV指令。为方便描述并与code一致,set-flag称为sfb_br,conditional-execute称为sfb_shadow)
- 增加了单独的寄存器堆(a renamed predicate register file,pregfile)来存放sfb_br的实际计算结果(taken或者not_taken),该寄存器堆要支持多个写接口从而可以支持多个算中的SFB的处理;
- sfb_shadow指令会去读pregfile来得到其对应sfb_br指令的结果,sfb_br实际算出来是not_taken的话表示预测正确,sfb_shadow指令继续执行自己的操作;如果sfb_br实际算出来是taken的话,表示预测错误,那么sfb_shadow指令会完成修复工作:将指令中旧的目的物理寄存器的值(destination physical register)复制到自己的目的物理寄存器中。
根据伯克利的论文中,对于某些指令序列来说,这种针对于SFB的优化将IPC提升到了1.7倍。
下面就从实际chisel源码和EDA仿真波形来进行内部SFB处理逻辑的剖析。
后面涉及到BoomCore的pipline,为方便分析,上图:
3.1 BranchDecode
下面是FrontEnd中的BranchDecoder模块chisel源代码:
class BranchDecode(implicit p: Parameters) extends BoomModule
{
val io = IO(new Bundle {
val inst = Input(UInt(32.W))
val pc = Input(UInt(vaddrBitsExtended.W))
val out = Output(new BranchDecodeSignals)
})
val bpd_csignals =
freechips.rocketchip.rocket.DecodeLogic(io.inst,
List[BitPat](N, N, N, N, X),
is br?
| is jal?
| | is jalr?
| | |
| | | shadowable
| | | | has_rs2
| | | | |
Array[(BitPat, List[BitPat])](
JAL -> List(N, Y, N, N, X),
JALR -> List(N, N, Y, N, X),
BEQ -> List(Y, N, N, N, X),
BNE -> List(Y, N, N, N, X),
BGE -> List(Y, N, N, N, X),
BGEU -> List(Y, N, N, N, X),
BLT -> List(Y, N, N, N, X),
BLTU -> List(Y, N, N, N, X),
SLLI -> List(N, N, N, Y, N),
SRLI -> List(N, N, N, Y, N),
SRAI -> List(N, N, N, Y, N),
ADDIW -> List(N, N, N, Y, N),
SLLIW -> List(N, N, N, Y, N),
SRAIW -> List(N, N, N, Y, N),
SRLIW -> List(N, N, N, Y, N),
ADDW -> List(N, N, N, Y, Y),
SUBW -> List(N, N, N, Y, Y),
SLLW -> List(N, N, N, Y, Y),
SRAW -> List(N, N, N, Y, Y),
SRLW -> List(N, N, N, Y, Y),
LUI -> List(N, N, N, Y, N),
ADDI -> List(N, N, N, Y, N),
ANDI -> List(N, N, N, Y, N),
ORI -> List(N, N, N, Y, N),
XORI -> List(N, N, N, Y, N),
SLTI -> List(N, N, N, Y, N),
SLTIU -> List(N, N, N, Y, N),
SLL -> List(N, N, N, Y, Y),
ADD -> List(N, N, N, Y, Y),
SUB -> List(N, N, N, Y, Y),
SLT -> List(N, N, N, Y, Y),
SLTU -> List(N, N, N, Y, Y),
AND -> List(N, N, N, Y, Y),
OR -> List(N, N, N, Y, Y),
XOR -> List(N, N, N, Y, Y),
SRA -> List(N, N, N, Y, Y),
SRL -> List(N, N, N, Y, Y)
))
val (cs_is_br: Bool) :: (cs_is_jal: Bool) :: (cs_is_jalr:Bool) :: (cs_is_shadowable:Bool) :: (cs_has_rs2) :: Nil = bpd_csignals
io.out.is_call := (cs_is_jal || cs_is_jalr) && GetRd(io.inst) === RA
io.out.is_ret := cs_is_jalr && GetRs1(io.inst) === BitPat("b00?01") && GetRd(io.inst) === X0
io.out.target := Mux(cs_is_br, ComputeBranchTarget(io.pc, io.inst, xLen),
ComputeJALTarget(io.pc, io.inst, xLen))
io.out.cfi_type :=
Mux(cs_is_jalr,
CFI_JALR,
Mux(cs_is_jal,
CFI_JAL,
Mux(cs_is_br,
CFI_BR,
CFI_X)))
val br_offset = Cat(io.inst(7), io.inst(30,25), io.inst(11,8), 0.U(1.W))
// Is a sfb if it points forwards (offset is positive)
io.out.sfb_offset.valid := cs_is_br && !io.inst(31) && br_offset =/= 0.U && (br_offset >> log2Ceil(icBlockBytes)) === 0.U
io.out.sfb_offset.bits := br_offset
io.out.shadowable := cs_is_shadowable && (
!cs_has_rs2 ||
(GetRs1(io.inst) === GetRd(io.inst)) ||
(io.inst === ADD && GetRs1(io.inst) === X0)
)
}
BranchDecode模块主要完成的是分支指令和相关指令的预先解码,这是为了完成分支预测功能;除此之外,就是SFB的检测逻辑,在这篇文章中我们只关注SFB这块。
1.首先是对sfb_br的检测逻辑,
val br_offset = Cat(io.inst(7), io.inst(30,25), io.inst(11,8), 0.U(1.W))
// Is a sfb if it points forwards (offset is positive)
io.out.sfb_offset.valid := cs_is_br && !io.inst(31) && br_offset =/= 0.U && (br_offset >> log2Ceil(icBlockBytes)) === 0.U
io.out.sfb_offset.bits := br_offset
根据riscv-spec-20191213,可以得到Branch指令B类型的编码格式,如下图所示:
br_offset只选取了branch指令的imm[11:0],imm[12]为符号位。
那么满足sfb_br的判断条件如下:
- 指令必须是6条分支指令之一;
- 指令编码第31位inst[31]为0,也就是imm[12]为0,表示跳转的偏移(offset)必须是正数,也就是往大PC的方向跳转;
- branch指令的偏移量imm[11:0]不能为0;
- 最后一个条件(br_offset >> log2Ceil(icBlockBytes)) === 0.U,在这里icBlockbytes为32表示I$的block有32byte,也就是说SFB最大的offset为32,限定了shadow范围,也就是说一个sfb_br后面最多覆盖32条相应的shadow指令。
2.接下来是sfb_br后面的sfb_shadow指令,满足的条件如下:
io.out.shadowable := cs_is_shadowable && (
!cs_has_rs2 ||
(GetRs1(io.inst) === GetRd(io.inst)) ||
(io.inst === ADD && GetRs1(io.inst) === X0)
)
- 指令必须为decode表中的SLLI、SRLI、…、一直到SRL的29条指令之一;
- 下面三个条件是或的关系,也就是至少满足一个就可以:
(1)没有Rs2源寄存器,主要是带立即数I的指令;
(2)目的寄存器Rd为Rs1源寄存器,如add x10,x10,x5;
(3)指令为ADD指令,并且Rs1=x0.
对于shadow指令的检测逻辑可以看出,要求指令中必须空出一个空位置,在后面可以发现这个空位置是为了存放旧目的寄存器的。
为了方便理解,在仿真波形中观看更为直观,波形和log文件如下图所示:
其中,在波形中,红框为sfb_br指令,黄框为相关的第一条sfb_shadow指令,蓝框为相关的第二条sfb_shadow指令。结合log文件,sfb_br的指令为压缩指令:c.beqz,目的地址(pc+offset)为800032ec,pc=0x800032e4,因此offset为0x8('b1000),br_offset为低5位为5‘b1000,对应十进制是8。
中间的两条sfb_shadow指令为指令slli和压缩指令c.srli。正好满足sfb的条件。
3.2 Decode
Decode模块位于core中,处于core流水线第一级,主要是完成对指令的解码,由于整个源代码过于长,下面只贴上SFB在decode中相关的代码。
uop.ldst_is_rs1 := uop.is_sfb_shadow
// SFB optimization
when (uop.is_sfb_shadow && cs.rs2_type === RT_X) {
uop.lrs2_rtype := RT_FIX
uop.lrs2 := inst(RD_MSB,RD_LSB)
uop.ldst_is_rs1 := false.B
} .elsewhen (uop.is_sfb_shadow && cs.uopc === uopADD && inst(RS1_MSB,RS1_LSB) === 0.U) {
uop.uopc := uopMOV
uop.lrs1 := inst(RD_MSB, RD_LSB)
uop.ldst_is_rs1 := true.B
}
when (uop.is_sfb_br) {
uop.fu_code := FU_JMP
}
在分析源代码之前,先说明一下涉及到的一些常量,下面四个变量表示内部微架构使用的寄存器类型,RT_FIX表示寄存器为整型,接下来三个以此为浮点类型、pass-through类型和不是寄存器。
// Decode Stage Control Signals
val RT_FIX = 0.U(2.W)
val RT_FLT = 1.U(2.W)
val RT_PAS = 3.U(2.W) // pass-through (prs1 := lrs1, etc)
val RT_X = 2.U(2.W) // not-a-register (but shouldn't get a busy-bit, etc.)
// TODO rename RT_NAR
FU_表示一种功能单元(Funtion unit,FU),FU_JMP为其中之一。
下面开始分析decode单元中对SFB的支持。
1.首先是源寄存器Rs2为立即数类型的情况,对应的就是上文分析中三个或条件的第一条。
uop.ldst_is_rs1 := uop.is_sfb_shadow
// SFB optimization
when (uop.is_sfb_shadow && cs.rs2_type === RT_X) {
uop.lrs2_rtype := RT_FIX
uop.lrs2 := inst(RD_MSB,RD_LSB)
uop.ldst_is_rs1 := false.B
做的事情是:
- 将原始指令微编码的lrs2_rtype从RT_X更改为RT_FIX;
- 将指令的目的寄存器Rd的编号ID存放到微编码的lrs2中;
- 将ldst_is_rs1置0.
2.第二种情况为rs1=rd的情况,对应的就是上文分析中三个或条件的第二条。就是通过将微编码中的ldst_is_rs1置为shadow来标识。
3.第三种就是为ADD指令并且Rs1为x0,对应的就是上文分析中三个或条件的第三条。
} .elsewhen (uop.is_sfb_shadow && cs.uopc === uopADD && inst(RS1_MSB,RS1_LSB) === 0.U) {
uop.uopc := uopMOV
uop.lrs1 := inst(RD_MSB, RD_LSB)
uop.ldst_is_rs1 := true.B
}
直接将微编码中的uopc置为uopMOV,把目的寄存器Rd的编码ID放到了lrs1中,再将ldst_is_rs1置1.
总结来说,decode中做的事情主要是保存旧的目的寄存器ID编码到微编码中空余的逻辑源寄存器lrs1或lrs2中,那么在后面读寄存器时可以把旧的目的寄存器的值保存到微编码的rs1data或rs2data中。
3.3 Rename
Core中流水线的下一级是rename(寄存器重命名)阶段,主要是完成寄存器的重命名,将逻辑寄存器(也叫架构寄存器,riscv中整型逻辑寄存器个数为32个)来映射到数目大于其自身的物理寄存器上,从而来解决超标量处理器的WAW(写后写)和WAR(读后写)依赖性(dependence)的问题。
为了处理SFB,BoomCore微架构在rename阶段的增加了以下逻辑,源代码如下:
class PredRenameStage(
plWidth: Int,
numPhysRegs: Int,
numWbPorts: Int)
(implicit p: Parameters) extends AbstractRenameStage(plWidth, numPhysRegs, numWbPorts)(p)
{
def BypassAllocations(uop: MicroOp, older_uops: Seq[MicroOp], alloc_reqs: Seq[Bool]): MicroOp = {
uop
}
ren2_alloc_reqs := DontCare
val busy_table = RegInit(VecInit(0.U(ftqSz.W).asBools))
val to_busy = WireInit(VecInit(0.U(ftqSz.W).asBools))
val unbusy = WireInit(VecInit(0.U(ftqSz.W).asBools))
val current_ftq_idx = Reg(UInt(log2Ceil(ftqSz).W))
var next_ftq_idx = current_ftq_idx
for (w <- 0 until plWidth) {
io.ren2_uops(w) := ren2_uops(w)
val is_sfb_br = ren2_uops(w).is_sfb_br && ren2_fire(w)
val is_sfb_shadow = ren2_uops(w).is_sfb_shadow && ren2_fire(w)
val ftq_idx = ren2_uops(w).ftq_idx
when (is_sfb_br) {
io.ren2_uops(w).pdst := ftq_idx
to_busy(ftq_idx) := true.B
}
next_ftq_idx = Mux(is_sfb_br, ftq_idx, next_ftq_idx)
when (is_sfb_shadow) {
io.ren2_uops(w).ppred := next_ftq_idx
io.ren2_uops(w).ppred_busy := (busy_table(next_ftq_idx) || to_busy(next_ftq_idx)) && !unbusy(next_ftq_idx)
}
}
for (w <- 0 until numWbPorts) {
when (io.wakeups(w).valid) {
unbusy(io.wakeups(w).bits.uop.pdst) := true.B
}
}
current_ftq_idx := next_ftq_idx
busy_table := ((busy_table.asUInt | to_busy.asUInt) & ~unbusy.asUInt).asBools
}
做的事情如下:
1.对于sfb_br指令将这条指令的ftq(Fetch Target Queue,ftq)的索引ftq_idx存放在指令微编码的pdst中;
val ftq_idx = ren2_uops(w).ftq_idx
when (is_sfb_br) {
io.ren2_uops(w).pdst := ftq_idx
to_busy(ftq_idx) := true.B
}
next_ftq_idx = Mux(is_sfb_br, ftq_idx, next_ftq_idx)
在这里简单介绍一下FTQ,FTQ是前端Frontend的一个queue队列,主要用来存放来自于ICache的PC和Branch分支预测的信息。假如一次可以从ICache取16byte的指令(正常指令4条,压缩指令8条),考虑由于跳转指令引起的PC不对齐情况,一次可以取3-8条指令送到core中,那么此时这一次获取的3-8条指令都会被分配相同的ftq_idx。关于ftq_idx分配的问题,这里再说明一下对于Branch分支指令情况,如果结果预测为跳转,那么会到目的地址的Icache PC处重新取指令,那么相应的原本这条分支指令后面的指令都会是无效的,不会分配ftq_idx,对新取来的指令配成索引是ftq_idx+1;如果预测结果为不跳转,在这3-8条指令中这条分支指令后面的指令也都会配为当前的ftq_idx。
而对于我们的sfb指令来说,sfb_br的前提条件是前端预测为不跳(not_taken),而sfb_shadow指令是紧接着sfb_br之后的指令,那么sfb_shadow指令的ftq_idx肯定是大于等于其所对应的sfb_br指令的ftq_idx。这时,再分析在rename中关于sfb_br的逻辑,先把指令自身的ftq_idx存放到微编码的pdst中,然后将这个ftq_idx为索引的向量表to_busy中的这一位置1(1表示busy, 0是ready)。
在这里强调一点,BoomCore是超标量乱序处理器,分发阶段(dispatch)之后就开始乱序执行,这时思考一个问题,如果sfb_shadow指令比它对应的sfb_br指令先执行会发生什么?可能会发生sfb_shadow指令的结果提前算出来然后写回(writeback,wb)了寄存器,而sfb_br指令实际算出的结果是跳转的,应该写回的是原始的目的寄存器的值,那么处理器就出错了。因此要保证sfb_shadow指令要在sfb_br指令计算完毕之后执行,这样才能保证写回寄存器的值是正确的值。
所以说结合上面,是通过ftq_idx来确定sfb_br和sfb_shadow的先后顺序的。
2.对于sfb_shadow指令,把next_ftq_idx锁存到微编码的ppred信号中,配置ppred_busy表示busy状态或ready状态。
when (is_sfb_shadow) {
io.ren2_uops(w).ppred := next_ftq_idx
io.ren2_uops(w).ppred_busy := (busy_table(next_ftq_idx) || to_busy(next_ftq_idx)) && !unbusy(next_ftq_idx)
}
}
for (w <- 0 until numWbPorts) {
when (io.wakeups(w).valid) {
unbusy(io.wakeups(w).bits.uop.pdst) := true.B
}
}
current_ftq_idx := next_ftq_idx
busy_table := ((busy_table.asUInt | to_busy.asUInt) & ~unbusy.asUInt).asBools
可以看出,当sfb_br将相应的ftq_idx索引的向量表to_busy相应位置1后,而unbusy向量表默认为0,可以得到sfb_shadow指令微编码的ppred_busy会被置1,表示busy状态。当sfb_br唤醒(wakeup)之后,unbusy相应位才会置1,此时sfb_shadow指令微编码的ppred_busy才会置为0,表示ready。
关于sfb_shadow指令的所用的next_ftq_idx和sfb_br的ftq_idx什么关系,在源代码中不太容易分析,因此在仿真波形分析以下,波形如下:
注:这里跑的波形为coremark test。
可以发现一条sfb_br指令后面跟着两条连续的sfb_shadow指令,sfb_br指令微编码的pdst锁存的ftq_idx是8,后面连续的两条sfb_shadow指令微编码的ppred锁存的ftq_idx也是8,都是自身指令的ftq_idx。
因此,总的来说在rename这一级,通过ftq_idx的方式来保证sfb_shadow指令在sfb_br指令之后发射(issue),在下面issue模块继续分析。
3.4 Dispatch
分发(Dispatch)位于Rename阶段之后,分发阶段主要是将经过重命名之后的指令分发给发射阶段(Issue)、重排序缓存(Reorder Buffer,ROB)和LSU。关于SFB没有做额外的逻辑,因此简单结束。
3.5 Issue
发射(Issue)阶段主要完成指令的唤醒、仲裁和发射,BoomCore的Issue模块的结构是分布式、压缩的和非数据捕捉的。在Rename模块我们已经分析了sfb_shadow指令要在sfb_br指令计算出来之后才发射,那么在issue中是如何做的呢?开始分析源代码,由于issue的源码比较多,在这里只贴上sfb相关的:
val ppred = RegInit(false.B)
// these signals are the "next_p*" for the current slot's micro-op.
// they are important for shifting the current slot_uop up to an other entry.
val next_ppred = WireInit(ppred)
when (io.in_uop.valid) {
p1 := !(io.in_uop.bits.prs1_busy)
p2 := !(io.in_uop.bits.prs2_busy)
p3 := !(io.in_uop.bits.prs3_busy)
ppred := !(io.in_uop.bits.ppred_busy)
}
when (io.pred_wakeup_port.valid && io.pred_wakeup_port.bits === next_uop.ppred) {
ppred := true.B
}
//-------------------------------------------------------------
// Request Logic
io.request := is_valid && p1 && p2 && p3 && ppred && !io.kill
可以清晰的看出来,issue发起请求的时候要求ppred为高(表示处于ready状态),而ppred为sfb_shadow指令经过rename之后的ppred_busy的取反,表示还没有ready;那sfb_shadow指令什么时候处于ready状态并发射出去呢?要求sfb_shadow指令对应的sfb_br指令的结果先算出来,才能发射sfb_shadow指令。在上面源代码中可以看出sfb_br的计算结果算出来后,通过wakeup接口传递信息来匹配到sfb_shadow指令的信息,信息一致后,ppred才会ready,结合上文,可以知道这个信息就是它们微编码的ftq_idx。
顺便提一下,issue只会堵住sfb_shadow指令,不会阻塞sfb_br指令。
3.6 register-read
因为issue阶段采用的结构式非数据捕捉的,所以issue的下一级为读寄存器(register read,rr)阶段,该阶段主要是获取指令中所用到的寄存器的数据,数据来源有两种:一是数据已经是处于寄存器堆中了,直接读取就行;二是对于某些目前不在寄存器堆中但是可以马上计算出来的通过bypass通路来获取。
对于能经过issue阶段到达下一级rr阶段的,指令所使用的寄存器的值不是已经存在于寄存器堆了,就是在bypass通道可以拿到。同时对于sfb_shadow指令来说,被发射到register read阶段的,其对应的sfb_br指令的结果肯定也已算出。
下面分析相关源代码,:
// NOTE:
// rrdLatency==1, we need to send read address at end of ISS stage,
// in order to get read data back at end of RRD stage.
val rs1_addr = io.iss_uops(w).prs1
val rs2_addr = io.iss_uops(w).prs2
val rs3_addr = io.iss_uops(w).prs3
val pred_addr = io.iss_uops(w).ppred
if (enableSFBOpt) io.prf_read_ports(w).addr := pred_addr
if (enableSFBOpt) rrd_pred_data(w) := Mux(RegNext(io.iss_uops(w).is_sfb_shadow), io.prf_read_ports(w).data, false.B)
val bypassed_pred_data = Wire(Vec(issueWidth, Bool()))
for (b <- 0 until numTotalPredBypassPorts)
{
val bypass = io.pred_bypass(b)
pred_cases ++= Array((bypass.valid && (ppred === bypass.bits.uop.pdst) && bypass.bits.uop.is_sfb_br, bypass.bits.data))
}
if (enableSFBOpt) bypassed_pred_data(w) := MuxCase(rrd_pred_data(w), pred_cases)
//-------------------------------------------------------------
//-------------------------------------------------------------
// **** Execute Stage ****
for (w <- 0 until issueWidth) {
if (enableSFBOpt) exe_reg_pred_data(w) := bypassed_pred_data(w)
}
//-------------------------------------------------------------
// set outputs to execute pipelines
for (w <- 0 until issueWidth) {
io.exe_reqs(w).valid := exe_reg_valids(w)
io.exe_reqs(w).bits.uop := exe_reg_uops(w)
if (enableSFBOpt) io.exe_reqs(w).bits.pred_data := exe_reg_pred_data(w)
}
}
BoomCore为了实现更好的sfb优化,增加了sfb专用的寄存器堆prf(predicate register file,prf),prf以ppred中保存的ftq_idx为地址,以sfb_br的最终计算结果为数据。除此之外,针对于sfb还有专用的bypass通道,直接把sfb_br的计算结果bypass给sfb_shadow指令。请求sfb_br结果时,先查看的bypass通道,再查看prf。
对于sfb的bypass通道,是直接把sfb_br指令的pdst传递过来与sfb_shadow指令的ppred来匹配,相等的话就匹配上了。此时,再回顾rename中sfb_br和sfb_shadow关于ftq_idx的使用,才发现设计是很巧妙的。
pred_cases ++= Array((bypass.valid && (ppred === bypass.bits.uop.pdst) && bypass.bits.uop.is_sfb_br, bypass.bits.data))
3.7 execution-units
接下来是执行(execute,exe)阶段,执行阶段主要是通过各种功能单元(Function Unit, FU)来完成指令的执行,计算出结果,将结果写回寄存器和bypass出去。BoomCore的包括的基本FU单元主要如下所示,主要是10种,而实际BoomCore中的FU一般是由某几个FU基本单元组成,如JmpUnit是由fu_alu、fu_jmp和fu_i2f组成的。而sfb_br指令本质是branch指令,是在fu_alu里面运算;而目前的sfb_shadow指令本质是一些算数逻辑指令,也是在fu_alu里面运算。但是,回顾Decode阶段,对于sfb_br指令加了约束,它只能由FU_JMP来计算,这就和普通的branch指令进行了区分。
object FUConstants
{
// bit mask, since a given execution pipeline may support multiple functional units
val FUC_SZ = 10
val FU_X = BitPat.dontCare(FUC_SZ)
val FU_ALU = 1.U(FUC_SZ.W)
val FU_JMP = 2.U(FUC_SZ.W)
val FU_MEM = 4.U(FUC_SZ.W)
val FU_MUL = 8.U(FUC_SZ.W)
val FU_DIV = 16.U(FUC_SZ.W)
val FU_CSR = 32.U(FUC_SZ.W)
val FU_FPU = 64.U(FUC_SZ.W)
val FU_FDV = 128.U(FUC_SZ.W)
val FU_I2F = 256.U(FUC_SZ.W)
val FU_F2I = 512.U(FUC_SZ.W)
// FP stores generate data through FP F2I, and generate address through MemAddrCalc
val FU_F2IMEM = 516.U(FUC_SZ.W)
}
1.对于sfb_br的处理,BoomCore架构也是将sfb_br指令放在JmpUnit(也叫BranchUnit)来处理,具体如下面源代码:
r_val (0) := io.req.valid
r_data(0) := Mux(io.req.bits.uop.is_sfb_br, pc_sel === PC_BRJMP, alu_out)
r_pred(0) := io.req.bits.uop.is_sfb_shadow && io.req.bits.pred_data
for (i <- 1 until numStages) {
r_val(i) := r_val(i-1)
r_data(i) := r_data(i-1)
r_pred(i) := r_pred(i-1)
}
io.resp.bits.data := r_data(numStages-1)
io.resp.bits.predicated := r_pred(numStages-1)
// Bypass
// for the ALU, we can bypass same cycle as compute
io.bypass(0).valid := io.req.valid
io.bypass(0).bits.data := Mux(io.req.bits.uop.is_sfb_br, pc_sel === PC_BRJMP, alu_out)
for (i <- 1 until numStages) {
io.bypass(i).valid := r_val(i-1)
io.bypass(i).bits.data := r_data(i-1)
}
对于sfb_br指令,当pc_sel等于PC_BRJMP时,结果为1,表示跳转(也表示前端Frontend预测失败);反之,结果为0,表示不跳转(表示前端Frontend预测正确)。把结果通过iresp和bypass通道传输出去,iresp是将结果写回prf,bypass通道是将结果直接传给register read阶段。一般来说bypass要快于iresp,然后在register read优先从bypass通道获取数据结果。
补充一点,对于sfb_shadow指令,会把当前的predicated结果送到resp通道,这个信号主要是rob中使用,标示sfb_shadow指令是commit,还是arch commit,在rob中会详细说明。
2.对于sfb_shadow指令的处理
之前在介绍sfb优化时说过,sfb优化最大的特点是sfb_br预测错误时,不用flush流水线,而通过将原始目的寄存器的旧值再次写回目的寄存器即完成了修复。而实际也是这么做的,如下面源代码:
val alu_out = Mux(io.req.bits.uop.is_sfb_shadow && io.req.bits.pred_data,
Mux(io.req.bits.uop.ldst_is_rs1, io.req.bits.rs1_data, io.req.bits.rs2_data),
Mux(io.req.bits.uop.uopc === uopMOV, io.req.bits.rs2_data, alu.io.out))
r_data(0) := Mux(io.req.bits.uop.is_sfb_br, pc_sel === PC_BRJMP, alu_out)
io.bypass(0).bits.data := Mux(io.req.bits.uop.is_sfb_br, pc_sel === PC_BRJMP, alu_out)
对于一条sfb_shadow指令,如果sfb_br的指令计算结果为跳转,也就是这里的pred_data为1,那么表示前端frontend预测错误,这时需要修复,因此会把保存在rs1_data或rs2_data中的目的寄存器的旧值重新通过resp和bypass通道写回寄存器堆和传给需要的指令。而如果sfb_br的指令计算结果为不跳转,也就是pred_data为0,表示前端预测正确,sfb_shadow指令实际会执行,因此把实际的运算结果写回。
3.8 rob
重排序缓存(Reorder Buffer, rob)是将乱序执行完后的指令按序提交(commit),从而保证程序是按照程序员期望的顺序来执行的。对于CPU来说,凡是暴露给外面的状态都要是正确的状态,而commit信息就是外部调试CPU所依赖的重要信号。而对于sfb_shadow指令来说,不管其对应的sfb_br指令跳转还是没跳转,sfb_shadow指令都是执行了,那么就会在rob中提交。因此BoomCore针对于sfb情况增加了arch commit,arch commit与平常commit除了sfb情况之外,都是保持一致的,那么外部可以结合arch commit和commit来了解所有指令的提交情况。
具体源代码如下:
val rob_predicated = Reg(Vec(numRobRows, Bool())) // Was this instruction predicated out?
when (io.enq_valids(w)) {
rob_val(rob_tail) := true.B
rob_predicated(rob_tail) := false.B
} .elsewhen (io.enq_valids.reduce(_|_) && !rob_val(rob_tail)) {
rob_uop(rob_tail).debug_inst := BUBBLE // just for debug purposes
}
when (wb_resp.valid && MatchBank(GetBankIdx(wb_uop.rob_idx))) {
rob_predicated(row_idx) := wb_resp.bits.predicated
}
// Perform Commit
io.commit.valids(w) := will_commit(w)
io.commit.arch_valids(w) := will_commit(w) && !rob_predicated(com_idx)
可以看出rob_predicated默认为0,也就是除了sfb_shadow指令之外,commit和arch_commit是一致的;当收到的sfb_shadow指令的predicated信号为1时表示这条sfb_shadow指令对应的sfb_br指令预测错误,实际不应该执行,所以arch_commit是拉低的;反之,sfb_shadow指令实际是执行的,arch_commit是拉高的。
4 总结
总的来说,BoomCore中涉及sfb的流水线为7级:fetch->decode->rename->dispatch->issue->register read->wb,还有commit时的特殊处理,还是挺复杂的。而且思考一个问题,如果sfb_shadow的指令数目太长的话,如大于7,那么就会充满整个pipline,当sfb_br计算结果为实际跳转时,意味着当前pipline都浪费了,而和重定向导致的penalty 周期数差不多,那么sfb 优化带来的收益就没那么明显了。
所以只有sfb_shadow指令数目小于流水线级数,那么收益才会明显。
5 参考资料
[1]BOOM源代码
[2]Chipyard平台框架
[3]BOOM官方文档
[4]riscv官方文档:riscv-spec-20191213.
[5] Celio C , DA Patterson, K Asanović. The Berkeley Out-of-Order Machine (BOOM): An Industry- Competitive, Synthesizable, Parameterized RISC-V Processor.