本文档将会对 BPU 中的重要结构以及对外接口进行描述,描述粒度将深入代码级别,文档中描述的结构将与香山分支预测单元 Chisel 版本代码保持一致,信号结构及名称亦来自于 Chisel 版本代码。
文档适用于已经了解了香山分支预测单元基础设计,并想要深入了解信号细节的同学使用。可以根据验证需要的内容有选择的进行阅读,或随时进行查阅。
其中,FTB项与完整预测结果接口涉及 BPU 产生预测结果的方式,建议每位同学进行阅读。
本文档将会对 BPU 中的重要结构以及对外接口进行描述,描述粒度将深入代码级别,文档中描述的结构将与香山分支预测单元 Chisel 版本代码保持一致,信号结构及名称亦来自于 Chisel 版本代码。
文档适用于已经了解了香山分支预测单元基础设计,并想要深入了解信号细节的同学使用。可以根据验证需要的内容有选择的进行阅读,或随时进行查阅。
其中,FTB项与完整预测结果接口涉及 BPU 产生预测结果的方式,建议每位同学进行阅读。
FTB 项是香山中分支预测块的核心数据结构,它存储了生成一个分支预测块所需的信息。在BPU进行预测时,初始的分支预测块首先由一个读出的 FTB 项生成。然后,这个分支预测块会传递到后续的预测器中,后续的预测器会读取其中的信息并加以修改,生成最终的预测结果。
因此,要理解分支预测块的结构,首先需要理解FTB项的结构。一个FTB项对应着一个分支预测块,其大致结构如下:
首先,需要明确一个信息:无论是分支预测块还是FTB项,它们可以容纳的指令数量都被设定为一个特定的上限(在香山当前版本中为16条RVC指令),被称为最大预测长度。这意味着,如果我们需要记录分支预测块内某一条指令的位置,可以使用一个固定长度的位向量,来指定这条指令相对于预测块起始地址的偏移量。
分支预测块执行流程的决定因素是其中的分支指令信息。其余的指令都被视为普通指令,不影响程序的执行流。因此,在一个分支预测块中,我们只需记录下其中的分支指令位置即可,而普通指令的位置我们并不关心。
因此,FTB项中定义了两种类型的分支指令槽(slot)——brSlots
和tailSlot
,用于存储分支预测块中的分支指令。目前香山版本中,brSlots
只含有一个 slot,而 tailSlot
则是一个单独的 slot,总计含有两个 slot。
在最大预测长度内的指令中,如果出现了分支指令,FTB项会将其记录在对应的slot内,并将该slot置为有效。如果分支指令出现过多,达到了FTB项的容纳上限,那么超出上限的分支指令则会交给下一个FTB项来存储。在每个 slot中,我们会记录一条分支指令相对于预测块起始地址的偏移量(offset),同时还会记录其跳转目标地址等信息。
在RISC-V中,分支指令主要分为两种类型:条件分支和无条件跳转。因此,对于一个分支预测块来说,最多只会包含一条无条件跳转指令。因为一旦执行到这条指令,程序的执行流就会发生改变,后续的指令将不再执行。因此,我们定义了一种类型的 slot 叫做 tailSlot
,专门用来存储这条无条件跳转指令。而对于条件分支指令,则将它们存储在brSlots
中。
tailSlot
如其名,位于整个预测块中的最后一个 slot。这也是因为一旦填充了无条件跳转指令,程序必定跳转,之后的指令就由其他预测块进行处理,因此后续的指令情况我们无需关心。但在无条件跳转指令之前的指令中,我们需要关心是否存在条件分支指令,因为条件分支指令既有可能跳转,也有可能不跳转。因此,我们需要记录下条件分支指令的相关信息。
考虑一种情况:如果从预测块起始PC开始直到最大预测长度,都没有出现无条件跳转指令,反而出现了两条条件分支指令,这样一来,tailSlot
就会空闲,而第二条条件分支指令又无法存储,造成空间的浪费。
为了解决这个问题,香山采用了一种方法:设置一个 sharing
标记。我们可以直接将第二条分支指令存储到tailSlot
中,并将sharing标记设置为真,表示第二条条件分支指令共享了无条件跳转指令的 tailSlot
。这样,就有效利用了tailSlot的空间。
预测块中的 isCall
、isRet
和isJalr
字段是为tailSlot
服务的。如果tailSlot中记录的是无条件跳转指令,这几个字段会进一步指示该跳转指令的类型。在FTB项中还有一个字段 always_taken
,用来记录每个Slot中存储的条件分支指令是否总是被预测为跳转。如果是的话,后续的预测器可以直接采用这一预测结果。
通过FTB项,我们可以得知一个分支预测块中的指令情况,包括分支指令的位置、类型等。这些信息会交给后续的预测器,由后续的预测器来预测更为精确的跳转目标、是否跳转等信息。
接口定义: src/main/scala/xiangshan/frontend/FTB.scala
本节中描述了 FTB 项的完整结构定义,其所含的信号如下:
Bool
Vec(numBrSlot, new FtbSlot(BR_OFFSET_LEN))
(接口列表详见FtbSlot)new FtbSlot(JMP_OFFSET_LEN, Some(BR_OFFSET_LEN))
(接口列表详见FtbSlot)UInt(log2Up(PredictWidth).W)
Bool()
Bool()
Bool()
Bool()
Bool()
Vec(numBr, Bool())
说明:pftAddr 和 carry
在这里,pftAddr
是Partial Fallthrough Addr的缩写。Fallthrough Addr 表示,如果预测块中没有跳转,那么程序将会顺序执行到达的地址。换句话说,如果一条无条件跳转指令的偏移为5,那么 Fallthrough Addr 对应的偏移便是6。这一信号主要用于获取函数调用后程序的返回地址,可以将这一概念理解为预测块的结束地址。
Partial 表示部分地址,这是由地址的表示方法决定的。在这里,地址的表示方法如下:
pc: | ... |<-- log(predictWidth) -->|<-- log(instBytes) -->|
^ ^
| |
carryPos instOffsetBits
pftAddr
便是只记录了中间的偏移部分(即长度为 log(predictWidth) 的部分),之后结合当前 PC 才能生成完整 PC。但由于有可能产生进位,因此单独记录一个 carry
位,carryPos 是预测块中的指令地址中 会产生进位的位置
另外,last_may_be_rvi_call
也是对该地址的一个辅助信号,表示无条件跳转指令槽中的指令是一个RVI类型的 call 指令。由于pftAddr在计算时默认认为指令长度为压缩指令长度,因此计算结束地址只会加 2 字节。但如果实际的call指令并不是压缩指令,就会导致返回地址计算错误。RAS会根据这一信号修正这个错误。
接口定义: src/main/scala/xiangshan/frontend/FTB.scala
该接口定义了 FTB 表项中 slot:
UInt(log2Ceil(PredictWidth).W)
UInt(offsetLen.W)
UInt(TAR_STAT_SZ.W)
(TAR_STAT_SZ = 2)Bool()
Bool()
src/main/scala/xiangshan/frontend/FrontendBundle.scala
该接口定义了完整的分支预测结果,每流水级预测结果中都会包含该接口。
完整分支预测结果接口与 FTB 项相差无几,并最初由 FTB 项生成。两个Slot被拆分成了单独的信号:slot_valids
, targets
, offsets
, is_br_sharing
等,另外增加了若干字段例如 br_taken_mask
, jar_target
便于后续预测器提供精确预测结果,另外 hit
表示 FTB 项是否命中,即本轮预测中的 PC 索引到了某个 FTB 项。
完整接口列表如下:
hit FTB表项是否命中
Bool()
slot_valids slot有效位,表示 ftb 项中的每个 slot 是否有效
Vec(totalSlot, Bool())
targets 每个 slot 对应的跳转目标地址
Vec(totalSlot, UInt(VAddrBits.W))
offsets 每个 slot 中的指令相对于预测块起始地址的偏移
Vec(totalSlot, UInt(log2Ceil(PredictWidth).W))
fallThroughAddr 预测块的结束地址
UInt(VAddrBits.W)
fallThroughErr FTB项中记录的 pftAddr
有误
Bool()
is_jal 预测块内包含jal指令
Bool()
is_jalr 预测块内包含jalr指令
Bool()
is_call 预测块内包含call指令
Bool()
is_ret 预测块内包含ret指令
Bool()
last_may_be_rvi_call 无条件跳转指令槽中的指令可能为一个 RVI 类型的 call 指令信号。
Bool()
is_br_sharing 最后一个slot (tailSlot) 中存储的是条件分支指令信号
Bool()
br_taken_mask 分支预测结果,每个分支(slot)对应一位,表示该分支是否被预测为 taken
Vec(numBr, Bool())
jalr_target 本预测块中的 jalr 的跳转目标
UInt(VAddrBits.W)
接口定义:src/main/scala/xiangshan/frontend/FrontendBundle.scala
接口类型:BranchPredictionRedirect
该接口定义了分支预测单元的重定向请求,主要用于重定向分支预测器的状态。
BranchPredictionRedirct
接口继承自 Redirct
接口,所含信号较多,BPU 重定向只会用到其中一部分接口,文档描述的结构中只包含 BPU 用到的接口内容。
重定向请求有两种来源,与 IFU 预译码信息对比生成的重定向请求,后端执行过程中的重定向请求。
在重定向请求中,关键信息是cfiUpdate
,即控制流指令信息。这一信息对应着一条指令,也就是BPU发生预测错误的指令。举例来说,如果 BPU 在预测块中指示第三条指令为普通指令不发生跳转,但预译码显示第三条指令是一条无条件跳转指令,必定会跳转。在这种情况下,BPU 发生了预测错误,FTQ 产生了重定向请求。在重定向请求中,cfiUpdate
将会对应这条无条件跳转指令。
cfiUpdate
中的信息可大致分为三种类型的信息:
foleded_hist
代表全局折叠历史,histPtr
代表全局历史指针,而其余信息则是维护分支历史的辅助信息。要详细了解这些信息的含义,请参阅 BPU顶层模块
。level
信息的含义是,重定向是否包含本条指令,如果不包含,重定向请求的接收方将认为这条指令已经被执行了,下次预测将从下一条指令开始。BPU
顶层会默认不包含本条指令,收到重定向请求后,会将本条指令的执行情况统计到分支历史当中。
以下为重定向接口的详细信号列表:
level: 重定向请求是否包括本位置,低表示在本位置后重定向,高表示在本位置重定向。
UInt(1.W)
cfiUpdate: 控制流指令更新信息
接口类型: CfiUpdateInfo
接口列表
UInt(VaddrBits.W)
Bool
Bool
Bool
UInt(VaddrBits.W)
Bool
UInt((log2Ceil(numBr)+1).W)
Bool
AllFoldedHistories(foldedGHistInfos)
AllAheadFoldedHistoryOldestBits(foldedGHistInfos)
UInt((numBr+1).W)
CGHPtr
UInt(log2Up(RasSize).W)
UInt(log2Up(RasCtrSize).W)
CGHPtr
CGHPtr
CGHPtr
接口定义: src/main/scala/xiangshan/frontend/FrontendBundle.scala
该接口定义了分支预测器的更新请求,主要用于更新分支预测器的状态,文档只列出了在 BPU 中使用的接口。
更新请求与每一个分支预测块一一对应,当 FTQ 中的一个分支预测块被执行过以后,FTQ 将会为这个分支预测块产生一个更新请求,来指导预测器进行训练,因此更新请求中的一个重要职责就是向 BPU 反馈指令的真实执行情况。当然在香山分支预测单元中,更新请求还会负责 FTB 项的更新
更新请求中的信息可大致分为四类:
更新请求的接口列表如下:
UInt(VAddrBits.W)
new FTBEntry()
FTBEntry
)Bool()
Vec(numBr, Bool())
Vec(numBr+1, Bool())
Bool()
ValidUndirectioned(UInt(log2Ceil(PredictWidth).W))
UInt(VAddrBits.W)
new SpeculativeInfo
foled_hist
一项)
AllFolededHistories(foldedGHistInfos)
UInt(MaxMetaLength.W)
接口定义: src/main/scala/xiangshan/frontend/BPU.scala
PredirectIO 是分支预测器整体的对外接口,它主要处理了分支预测器(BPU)与取指目标队列(FTQ)之间的交互,主要包含以下几个部分:
BpuToFtqIO
DecoupledIO(new BpuToFtqBundle())
BpuToFtqBundle
继承自 BranchPredictionResp
,没有额外的信号BranchPredictionResp
)FtqToBpuIO
Valid(new BranchPredictionRedirect)
BranchPredictionRedirect
)Valid(new BranchPredictionUpdate)
BranchPredictionUpdate
)FtqPtr
BPUCtrl
Bool()
Bool()
Bool()
Bool()
Bool()
Bool()
Bool()
UInt(PAddrBits.W)
接口定义: src/main/scala/xiangshan/frontend/FrontendBundle.scala
该接口定义了分支预测器预测的所有结果信息,包含了每一阶段的预测结果,以及最后一个流水级输出的相关信息。
BranchPredictionBundle
BranchPredictionBundle
)UInt(MaxMetaLength.W)
Ftq_Redirect_SRAMEntry
AllFoldedHistories(foldedGHistInfos)
AllAheadFoldedHistoryOldestBits(foldedGHistInfos)
UInt((numBr+1).W)
CGHPtr
UInt(log2Up(RasSize).W)
UInt(log2Up(RasCtrSize).W)
CGHPtr
CGHPtr
CGHPtr
UInt(VAddrBits.W)
FtqEntry
FtqEntry
)接口定义: src/main/scala/xiangshan/frontend/FrontendBundle.scala
该接口定义了每个流水级对外输出的分支预测结果信息,
Vec(numDup, UInt(VAddrBits.W))
numDup仅为寄存器复制,其中信号完全相同Vec(numDup, Bool())
Vec(numDup, Bool())
new FtqPtr
Vec(numDup, new FullBranchPrediction)
FullBranchPrediction
)在香山分支预测单元中,其所有子预测器以及 Composer 的类实现都是继承自子预测器基类(BasePredictor),并且子预测器接口(BasePredictorIO)也是在子预测器基类中进行定义。因此我们可以认为,Compser 和所有子预测器都含有相同的接口。
在子预测的理解及验证当中,我们最直接的外界交互是发生在子预测器接口,以及子预测器基类中定义的一些变量,因此在子预测器的验证之前,强烈建议你阅读并理解本节文档。
子分支预测器接口的大致内容及使用方法,已在香山分支预测单元(BPU)基础设计
中进行了介绍,本节文档将专注于接口的信号细节。
接口定义: src/main/scala/xiangshan/frontend/BPU.scala
每个子分支预测器都需要实现该接口,该接口定义了子分支预测器的输入输出接口。
注意:其中某些信号被定义为了 numDup
个数量,其中每个信号完全一样,多个相同信号是为其他因素考虑。
详细的信号列表如下:
reset_vector 重置向量,reset 时 BPU 的 pc 会被重置为该值
UInt(PAddrBits.W)
in BPU 向子分支预测器发送的信息
DecoupledIO(new BasePredictorInput)
Vec(numDup, UInt(VAddrBits.W))
Vec(numDup, new AllFoldedHistories(foldedGHistInfos))
AllFoldedHistories
)UInt(HistoryLength.W)
BranchPredictionResp
BranchPredictionResp
)out 子分支预测器向 BPU 发送的信息(包含s1, s2, s3 预测结果信息)
new BasePredictorOutput
继承自 BranchPredictionResp
BranchPredictionResp
)ctrl BPU 子预测器使能控制信号,主要用于控制各预测器是否开启
BPUCtrl
Bool()
Bool()
Bool()
Bool()
Bool()
Bool()
Bool()
s0_fire s0阶段握手成功信号
Vec(numDup, Bool())
s1_fire s1阶段握手成功信号
Vec(numDup, Bool())
s2_fire s2阶段握手成功信号
Vec(numDup, Bool())
s3_fire s3阶段握手成功信号
Vec(numDup, Bool())
s2_redirect s2阶段重定向信号
Vec(numDup, Bool())
s3_redirect s3阶段重定向信号
Vec(numDup, Bool())
s1_ready s1阶段是否准备好接收信息 (方向:由子预测器输出)
Bool()
s2_ready s2阶段是否准备好接收信息 (方向:由子预测器输出)
Bool()
s3_ready s3阶段是否准备好接收信息 (方向:由子预测器输出)
Bool()
update BPU 向子分支预测器转发的更新请求
Valid(new BranchPredictionUpdate)
BranchPredictionUpdate
)redirect BPU 向子分支预测器转发的重定向请求
Valid(new BranchPredictionRedirect)
BranchPredictionRedirect
)其中,流水线控制信号将会在下面内容中进行进一步的说明。
接口定义:src/main/scala/xiangshan/frontend/FrontendBundle.scala
接口类型:AllFoldedHistories(foldedGHistInfos))
全局折叠历史的接口信息仅为一个 FoldedHistory
的列表
MixedVec(gen.map{case (l, cl) => new FoldedHistory(l, cl, numBr)})
而 FoldedHistory
的接口信息也仅有一项
UInt(compLen.W)
也就是说,全局折叠历史接口实际上是一个存储了折叠后历史的列表,其中每一项都是一个特定长度的折叠历史。
在子预测器基类中定义了若干信号,并在每个子预测器中都可以访问到,并在其中还进行了若干连线。
其中多数信号都较容易理解 ,我们需要重点关注的是其中每个流水的 pc,这还会涉及到你对于流水线控制信号的掌握。因此接下来,我们将会介绍在子预测器中需要关注的流水线控制信号的含义,以及 s1_pc, s2_pc, s3_pc 信号的含义。
其中流水线控制信号共分为 3 组:
子预测器基类中的 pc 信号共有四组,s0_pc_dup
, s1_pc_dup
, s2_pc_dup
, s3_pc_dup
。每组信号中有多个 pc 信号,他们完全相同,为考虑一些其他因素所复制。因此我们可单纯把他们看做 s0_pc
, s1_pc
, s2_pc
, s3_pc
。
他们的用法可参照下图:
他们与流水线控制信号的关系是:
s0_pc
直接由输入接口中的 in.s0_pc
连接而来s0_fire
生效时,下一周期 s1_pc
会输出 s0_pc
的值s1_fire
生效时,下一周期 s2_pc
会输出 s1_pc
的值s2_fire
生效时,下一周期 s3_pc
会输出 s2_pc
的值也就是说,fire
信号是会影响到下一个周期的数据是否有效,比如 s0_fire
信号会影响 s1 流水的数据是否有效,s1_fire
信号会影响到 s2 流水的数据是否有效。
而 fire
信号是否有效取决于本流水级数据是否有效 以及 下一流水级是否 ready。例如 s1_fire
信号有效的条件必须包含 s1 阶段数据有效,以及子预测器输出的 s2_ready
信号为有效,此时便可认为,s1 阶段数据处理完成,并且 s2 阶段就绪,下一周期数据将被直接送入 s2 阶段。
因此在子预测器中,以 s1 阶段为例,s1_ready
可以阻塞数据进入 s1 阶段,当 s1_ready
生效时,下一周期 s1 阶段的数据便会生效。而当 s1_fire
有效的同时,说明 s1 阶段数据已经生效,预测器也已经将 s1 阶段的结果生成,下一周期将直接被送入 s2 阶段。
redirect
信号则相对明确。以 s2 为例,当 s2_redirect
有效时,说明 s2_fire
生效的同时 s2 预测结果与上周期 s1 预测结果不同。