总体架构
- 分支预测和指令缓存,通过FTQ达到解耦的目的;
- FTQ将请求送给ICache,进行取指;
- 取出的指令码通过预译码初步检查分支预测的错误并及时冲刷预测流水线;
- 检查后的指令送入指令缓冲并传给译码模块,最终形成后端的指令供给;
分支预测
分支预测单元采用一种多级混合预测的架构,主要包括:
- 下一行预测器(Next Line Predictor,以下简称 NLP)
- 由uBTB组成;
- 提供无bubble的预测,在预测请求的下一拍,就能得到结果;
- 精确预测器(Accurate Predictor,以下简称 APD)
- 由 FTB、TAGE-SC、ITTAGE、RAS 组成;
- 其中,FTB、TAGE、RAS 的延迟为 2 拍;
- SC、ITTAGE 的延迟为 3 拍;
一次预测会经历三个流水级,每一个流水级都会产生新的预测内容。这些分布于不同流水级的预测器组成了一个覆盖预测器 (overriding predictor)。
相较于上一代雁栖湖架构,分支预测器的最大区别在于预测块的定义方式。
- 南湖架构中,BTB 被替换成了 FTB (Fetch Target Buffer),每一个 FTB 项都形成一个预测块。
- 我们不仅对下一个块的起始地址做预测,同时也对当前块的结束点做预测。
顶层逻辑
负责预测逻辑和流水线握手逻辑,以及全局分支历史的管理。
与FTQ的交互
- 各级流水线都会连接FTQ
- 当第一个预测流水线,预测出了结果,或者后续的流水线,发现预测结果与之前的不一致,和FTQ的握手vld会拉高,将预测信息写入或者进行修改;
全局分支历史管理
南湖架构实现了接近完全准确的全局分支历史
- speculative update
- 每次预测,都会根据预测块内部的分支指令个数,以及预测方向,计算新的全局历史,并将其用在新的预测中;
- 覆盖逻辑中加入全局历史的比较
- 一旦位于后面的流水级推测更新后,与之前的流水线结果不同时(条件分支个数,或者时执行的结果不同),会flush流水线(?),重新开始预测;
- 预测后存储全局历史的副本
- 在预测结束后,当次预测使用的全局历史会存储到 FTQ 中,在误预测恢复时读出并送回 BPU
之所以说“接近”完全准确,是因为 BPU 会忽略那些从未跳转的条件分支指令,它们不会被记录在 FTB 中,也就不会包含在分支历史里;
下一行预测器 (NLP)
旨在用较小的存储开销提供一个无空泡的快速预测流。
它的功能主要由 uBTB 提供。
对于给定的起始地址 PC,uBTB对从 PC 开始的一个预测块做出整体预测。
NLP
- 用分支历史和 PC 的低位异或索引存储表;
- 从表中读出的内容直接提供了最精简的预测:
- 下一个预测块的起始地址
nextAddr;
- 这个预测块是否发生分支指令跳转
taken;
- 跳转指令相对起始地址的偏移
cfiOffset;
- 是否在条件分支跳转
takenOnBr;
- 块内包含分支指令的数目
brNumOH;
- 另外还提供分支指令的相关信息以更新分支历史;
- 我们摒弃了 tag 匹配的做法,这会带来一个问题:
- 在有 tag 匹配的情况下,如果一个预测块没有命中,会将下一个预测块的 PC 置为当前 PC 加预测宽度;
- 通常,为了避免浪费,如果在预测块中没有分支指令,则训练时不会写入 uBTB。
- 在这个前提下,如果没有 tag 匹配机制,则很容易把没有分支指令(在 tag 匹配机制下不命中)的预测块预测为另一个跳转的块。
- 针对这种情况,我们引入了另一种预测机制:对当前 PC 是否可能存在有效的分支指令进行预测;
- 这个预测机制的存储结构由取指 PC 直接索引,它的查询结果表示了该取指 PC 是否被写入过。
- 在它指示该 PC 没被写入过的时候,会把下一个 PC 预测为当前 PC 加预测宽度。
精确预测器 (APD)
为提高总体预测准确率,减少预测错误带来的流水线冲刷,南湖架构实现了延迟更高,同时也更为精确的预测机制。
精确预测器包括取指目标缓冲 FTB、条件分支方向预测器 TAGE-SC、间接跳转预测器 ITTAGE 和返回地址栈 RAS。
FTB
- FTB 是 APD 的核心。
- APD 的其他预测部件所作出的预测全部依赖于 FTB 提供的信息。
- FTB 除了提供预测块内分支指令的信息之外,还提供预测块的结束地址。
- 对于 FTB 来说,FTB 项的生成策略至关重要。
- 记 FTB 项的起始地址为
start
,结束地址为end
,具体策略如下:
- FTB 项由
start
索引,start
在预测流水线中生成,实际上,start
基本遵循如下原则之一:
start
是上一个预测块的end
start
是来自 BPU 外部的重定向的目标地址;- FTB项内最多记录两条分支指令,其中第一条一定是条件分支;
- end 一定满足三种条件之一:
end
-start
= 预测宽度end
是从start
开始的预测宽度范围内第三条分支指令的 PCend
是一条无条件跳转分支的下一条指令的 PC,同时它在从start
开始的预测宽度范围内;- 注意:这种训练策略下,同一条分支指令可能存在于多个 FTB 项内。
- 和论文中的实现一样,我们只存储结束地址的低位,而高位用起始地址的高位拼接得到;
- 论文链接:A ScalableFront-EndArchitectureforFastInstructionDeliveryhttps://download.csdn.net/download/zhangshangjie1/89865369
- 和 AMD的做法相似,我们还对 FTB 项中的条件分支指令记录“总是跳转”位;
- 该位在第一次遇到该条件分支跳转时置 1,在它值为 1 的时候,该条件分支的方向总是预测为跳转,也不用它的结果训练条件分支方向预测器;
- 当该条件分支遇到一次执行结果为不跳转的时候,将该位置 0,之后它的方向由条件分支方向预测器预测。
TAGE-SC
TAGE-SC 是南湖架构条件分支的主预测器,它的大致逻辑继承自上一代雁栖湖架构的 TAGE-SC-L。
目前的实现中,TAGE 的延迟是 2 拍,SC 的延迟是 3 拍。
TAGE 利用历史长度呈几何级数增加的多个预测表,可以挖掘极长的分支历史信息。它的基本逻辑如上图所示。
- 由一个基预测表和多个历史表组成,基预测表用 PC 索引
- 而历史表用 PC 和一定长度的分支历史折叠后的结果异或索引,不同历史表使用的分支历史长度呈几何级数关系。
- 在预测时,还会用 PC 和每个历史表对应的分支历史的另一种折叠结果异或计算 tag,与表中读出的 tag 进行匹配:
- 如果匹配成功则该表命中;
- 最终的结果取决于命中的历史长度最长的预测表的结果;
- 在南湖架构中,每次预测最多同时预测 2 条条件分支指令。
- 在访问 TAGE 的各个历史表时,用预测块的起始地址作为 PC,同时取出两个预测结果
- 它们所用的分支历史也是相同的。
- TAGE还有一些其他的特性,如备选预测逻辑,useful域段,sc校正等功能,这些功能参考:TAGE predictor
ITTAGE
- RISC-V 指令集中
jalr
指令支持以寄存器取值加一立即数的方式指定无条件跳转指令目标地址。- 不同于在指令中直接编码跳转偏移量的
jal
指令,jalr
的跳转地址需要借助寄存器访问间接获取,因而被称为间接跳转指令。- 由于寄存器的值可变,于是相同
jalr
指令的跳转地址可能很多样,所以 FTB 记录固定地址的机制难于准确预测这种指令的目标地址。- 在香山处理器中,
jalr
指令的预测机制体现为 FTB,RAS 与 ITTAGE 的协作。
- FTB 会记载
jalr
指令的最近一次跳转地址,部分jalr
指令的跳转地址相对固定,仅靠 FTB 就足以达到很高的预测准确率;- 函数返回是
jalr
指令中较为常见的应用场景,它和函数调用指令有着显著的配对性,可以使用具有栈结构的 RAS 进行预测;- 不符合以上特征的
jalr
指令交由 ITTAGE 预测。ITTAGE是一种准确率很高的间接跳转预测器,它的基本结构如上图所示。
- ITTAGE 在 TAGE 的主要区别在于,每个表项在 TAGE 表项的基础上加入了所预测的跳转地址。
- 由于每个 FTB 项仅存储至多一条间接跳转指令信息,ITTAGE 预测器每周期也最多预测一条间接跳转指令的目标地址。
- ITTAGE 和 TAGE 的内部逻辑基本相同,此处不再重复。
RAS
RAS 是一个寄存器堆实现的栈存储结构
- 它对
call
指令的下一条指令的地址进行记录- 在 FTB 认为预测块会在
call
指令跳转时压栈- 并在 FTB 认为预测块在
ret
指令跳转时弹出。- 每一项包含一个地址和一个计数器
- 当重复压栈同一个地址时,栈指针不变,计数器加一,用于处理程序中递归调用的情况。
- 每次预测后,栈顶项和栈指针都会存入 FTQ 的存储结构,用于误预测时恢复。
预测器的训练
总的来说,为防止错误执行路径对预测器内容的污染,各部分预测器在预测块的所有指令提交后进行训练。
它们的训练内容来自自身的预测信息和预测块中指令的译码结果和执行结果,它们会被从 FTQ 中读出,并送回 BPU。其中,
- 自身的预测信息会在预测后打包传进 FTQ 中存储;
- 指令的译码结果来自 IFU 的预译码模块,在取到指令后写回 FTQ;
- 而执行结果来自各个执行单元。
在 BPU 收到来自其外部的重定向请求时,会把曾进行过推测更新的元素(全局历史、RAS 栈顶项等)进行恢复。
预测块 分支预测单元 (BPU) 每次给取指目标队列 (FTQ) 的请求基本单位,它描述了一个取指请求的范围,以及其中分支指令的情况
预测宽度 每次预测提供给取指单元的最大指令流宽度,在南湖架构中与取指宽度相同,都为 32 字节。当 FTB 预测未命中时,目标地址默认为当前地址和预测宽度相加
覆盖预测器 一种多个不同延迟的预测器的组织形式,延迟大的、相对更准确的预测器被放在后面的流水级,其产生的预测结果会与前面的预测器进行比较,如果不同则会冲刷流水线,整体预测结果以最准确的预测器为准
全局分支历史 指令流中所有条件分支指令的执行结果序列,每一条分支指令的执行结果作为一位(0/1)存在于全局分支历史中,一般以移位寄存器的方式实现
备选预测 TAGE 类预测器一种优化,当对长历史预测结果信心不足时选择次长历史下的命中结果作为最终预测,可提升整体预测正确率
取指目标队列 (FTQ)
FTQ 是分支预测和取指单元之间的缓冲队列:
- 主要职能是暂存 BPU 预测的取指目标,并根据这些取指目标给 IFU 发送取指请求。
- 另一重要职能是暂存 BPU 各个预测器的预测信息,在指令提交后把这些信息送回 BPU 用作预测器的训练,因此它需要维护指令从预测到提交的完整的生命周期。
- 由于后端存储 PC 的开销较大,当后端需要指令 PC 的时候,会到 FTQ 读取。
- 由于 BPU 基本无阻塞,它经常能走到 IFU 的前面,于是 BPU 提供的这些还没发到 IFU 的取指请求就可以用作指令预取,FTQ 中实现了这部分逻辑,直接给指令缓存发送预取请求
FTQ 是一个队列结构,但队列中每一项的内容是根据其自身特点存储在不同的存储结构中的。这些存储结构主要包括以下几种:
ftq_pc_mem: 寄存器堆实现,存储与指令地址相关的信息,包括如下的域
startAddr
预测块起始地址nextLineAddr
预测块下一个缓存行的起始地址isNextMask
预测块每一条可能的指令起始位置是否在按预测宽度对齐的下一个区域内fallThruError
预测出的下一个顺序取指地址是否存在错误ftq_pd_mem: 寄存器堆实现,存储取指单元返回的预测块内的各条指令的译码信息,包括如下的域
brMask
每条指令是否是条件分支指令jmpInfo
预测块末尾无条件跳转指令的信息,包括它是否存在、是jal
还是jalr
、是否是call
或ret
指令jmpOffset
预测块末尾无条件跳转指令的位置jalTarget
预测块末尾jal
的跳转地址rvcMask
每条指令是否是压缩指令ftq_redirect_sram: SRAM 实现,存储那些在重定向时需要恢复的预测信息,主要包括和 RAS 和分支历史相关的信息
ftq_meta_1r_sram: SRAM 实现,存储其余的 BPU 预测信息
ftb_entry_mem: 寄存器堆实现,存储预测时 FTB 项的必要信息,用于提交后训练新的 FTB 项;
指令在 FTQ 中的生存周期
指令以预测块为单位,从 BPU 预测后便送进 FTQ,直到指令所在的预测块中的所有指令全部在后端提交完成,FTQ 才会在存储结构中完全释放该预测块所对应的项。
- 预测块从 BPU 发出,进入 FTQ,
bpuPtr
指针加一,初始化对应 FTQ 项的各种状态,把各种预测信息写入存储结构;如果预测块来自 BPU 覆盖预测逻辑,则恢复bpuPtr
和ifuPtr;
- FTQ 向 IFU 发出取指请求,
ifuPtr
指针加一,等待预译码信息写回- IFU 写回预译码信息,
ifuWbPtr
指针加一,如果预译码检测出了预测错误,则给 BPU 发送相应的重定向请求,恢复bpuPtr
和ifuPtr
- 指令进入后端执行,如果后端检测出了误预测,则通知 FTQ,给 IFU 和 BPU 发送重定向请求,恢复
bpuPtr
、ifuPtr
和ifuWbPtr
- 指令在后端提交,通知 FTQ,等 FTQ 项中所有的有效指令都已提交,
commPtr
指针加一,从存储结构中读出相应的信息,送给 BPU 进行训练;预测块
n
内指令的生存周期会涉及到 FTQ 中的bpuPtr
、ifuPtr
、ifuWbPtr
和commPtr
四个指针,当bpuPtr
开始指向n+1
时,预测块内的指令进入生存周期,当commPtr
指向n+1
后,预测块内的指令完成生存周期。
取指令单元(Instruction Fetch Unit)
南湖架构的 IFU 采用了 4 级流水线的结构,相较于雁栖湖版本 IFU 的设计做了非常大的简化,这得益于采用分支预测 - 指令缓存解耦的取指令架构。
一个取指令请求从 FTQ 发出之后在 IFU 中经历了下面几个阶段:
- 从 FTQ 发送过来的取指令请求包含了一个 32 bytes 指令码 (称为一个指令块 ) 的起始地址和下一个跳转目标的地址,
- 在
IFU0
阶段同时发送请求给 IFU 流水线和 ICache 模块。IF1
阶段会做一些简单计算(例如这个指令块里每个 2 bytes, 即每一条可能的指令的 PC )。IF2
阶段等到指令缓存返回最多两个 cache line 的数据(因为可能存在这个 指令块 跨行 的情况)之后,第一步先做指令切分,将在取指令地址之外的指令码抛弃得到有效范围的指令码。送入预译码器进行预译码,同时将 16 bits 的压缩指令扩展为 32 bits 的指令。IF3
阶段首先会将预译码结果送到 分支预测检查器 里,发现错误就会在下一拍刷新 IFU 流水线并把信息发送给 FTQ 刷新预测器并重新取指令。未发现错误的缓存在指令缓冲队列(IBuffer)里等待译码。IF3
阶段还会根据地址翻译的结果向指令 MMIO 模块发起取指令请求,同时转变为 MMIO 取指令模式,指令一条一条顺序执行。- IFU 控制逻辑还需要 处理半条 RVI 指令 的情况。
预译码
预译码器将经过切分的 16 个 16 bits 指令码进行译码,得到部分指令信息:
- 是否是跳转指令
- 跳转指令类型
- 以及是否是压缩指令等
- 对于跳转指令还会计算它的目标地址。
主要是为了给 分支预测检查器 :
- 提供指令信息
- 正确的目标地址
- 以及及时更新预测器中的指令信息
另一方面,预译码器也会将压缩指令(如果这个指令块里有的话)扩展为 32 bits 的长指令以便于后续简化译码逻辑。
分支预测检查
分支预测检查器在拿到指令的预译码信息之后主要针对以下几个错误检查:
- jump 指令预测不跳转的错误:针对
jal
和ret
这两种种必定跳转的指令检查,如果这个块的 有效指令范围 内有这两种指令,且预测为不跳转,则视为预测错误。- 非跳转指令的预测错误:如果预测为跳转的指令不在有效指令范围内,或者在有效指令范围内但是不是一条跳转指令,则视为预测错误。
- 目标地址错误:对于可以通过指令码知道目标地址的跳转指令(除了
jalr
之外的跳转指令),如果在有效指令范围内且预测跳转并且跳转目标地址和正确地址不匹配,则视为预测错误。在发现错误后,分支预测检查器挑选出指令顺序最靠前的预测错误指令,把错误信息(错误指令在块里的位置、指令预译码信息、正确的目标地址)传递给 FTQ ,同时清空 IFU 流水线。IFU 等待 FTQ 重新发取指令请求。
跨行指令处理(Cross-line Fetch)
由于我们的指令块包括了 32 bytes 的指令码,相当于半个 cache line(64 Bytes)的大小,如果这个块的起始地址在后半个 cache line 里,那么完全有可能发生块的范围跨过两个 cache line 的情况;
因此在指令缓存支持一次取两个 cache line 以保证这种情况下的指令吞吐。具体做法是当 FTQ 发现块的起始地址在后半个 cache line 里,就发起对指令缓存两个相邻 cache line 的请求。
有效指令范围
一个取指令块的有效范围由 FTQ 给出的起始地址和跳转指令的 index(如果有跳转的话)共同确定,如果这个块没有跳转指令,则默认指令有效范围为起始地址开始的 256 bits。
有效指令范围可能被 IFU 的检查重新确定,主要包括:
- 分支预测检查发现有未预测跳转的
jal
和ret
指令时,需要重新将有效指令范围缩短到第一条这样的跳转指令;- 前一个块有半条 RVI 的情况,紧随其后的这个块的第一个 16 bits 不在有效指令范围内。
- MMIO 请求的块的指令有效范围只有 32 bits;
MMIO 取指令
在
IF3
阶段,如果 ITLB 发现这个地址是 MMIO 空间的,IFU 就启动 MMIO 取指令模式:
- 向指令 MMIO 模块发送请求,指令 MMIO 模块向 MMIO 总线发送
Get
请求 64 bits 的数据- 等待总线返回后根据 IFU 的请求地址对数据进行裁剪,返回指令码。
- IFU 将指令码进行扩展之后发送给指令缓冲队列。
- 同时,IFU 阻塞流水线,侦听 ROB 的 commit 信号,直到指令执行完后发送前端重定向取下一条指令。
MMIO 请求每次只取一条指令,因此在这种模式下处理器的指令执行速度会变得非常慢。
半条 RVI 指令的处理
当一个指令块在
IF3
阶段发现它的最后 2 bytes 是一条 RVI 指令的前半部分时,我们把这条 RVI 指令算在这个块里,同时我们取两个 cache line 的机制保证后半部分是一定可以被取到的,因此我们只需要在发生这种情况的时候置一个标识位,当下一个块来的时候把第一个 2 bytes 排除在指令的有效范围之外即可。
指令缓存(Instruction Cache)
缓存部分不做过多描述;
译码单元 (Decode Unit)
基本功能
指令从指令缓存中取出,送进指令缓冲(队列)中暂存,然后以每周期 6 条的的速度送入译码单元译码,再传给下一个流水级。
指令融合 (Instruction Fusion)
译码单元支持了一些指令的融合,在
FusionDecoder
模块中会基于连续两条指令的 32bit 数据完成指令融合。对非连续的两条指令,目前香山没有支持指令融合。目前,香山支持如下情况的指令融合:
- clear upper 32 bits / get lower 32 bits:
slli r1, r0, 32
+srli r1, r1, 32
- clear upper 48 bits / get lower 16 bits:
slli r1, r0, 48
+srli r1, r1, 48
- clear upper 48 bits / get lower 16 bits:
slliw r1, r0, 16
+srliw r1, r1, 16
- sign-extend a 16-bit number:
slliw r1, r0, 16
+sraiw r1, r1, 16
- shift left by one and add:
slli r1, r0, 1
+add r1, r1, r2
- shift left by two and add:
slli r1, r0, 2
+add r1, r1, r2
- shift left by three and add:
slli r1, r0, 3
+add r1, r1, r2
- shift zero-extended word left by one:
slli r1, r0, 32
+srli r1, r0, 31
- shift zero-extended word left by two:
slli r1, r0, 32
+srli r1, r0, 30
- shift zero-extended word left by three:
slli r1, r0, 32
+srli r1, r0, 29
- get the second byte:
srli r1, r0, 8
+andi r1, r1, 255
- shift left by four and add:
slli r1, r0, 4
+add r1, r1, r2
- shift right by 29 and add:
srli r1, r0, 29
+add r1, r1, r2
- shift right by 30 and add:
srli r1, r0, 30
+add r1, r1, r2
- shift right by 31 and add:
srli r1, r0, 31
+add r1, r1, r2
- shift right by 32 and add:
srli r1, r0, 32
+add r1, r1, r2
- add one if odd, otherwise unchanged:
andi r1, r0, 1
+add r1, r1, r2
- add one if odd (in word format), otherwise unchanged:
andi r1, r0, 1
+addw r1, r1, r2
addw
and extract its lower 8 bits (fused intoaddwbyte
)addw
and extract its lower 1 bit (fused intoaddwbit
)addw
andzext.h
(fused intoaddwzexth
)addw
andsext.h
(fused intoaddwsexth
)- logic operation and extract its LSB
- logic operation and extract its lower 16 bits
OR(Cat(src1(63, 8), 0.U(8.W)), src2)
- mul 7-bit data with 32-bit data