BPU 顶层模块
Categories:
BPU 顶层整体的功能和结构已经在之前的文档中粗略的描述,对于验证 BPU 顶层的同学来说,可能还需要更加细致的描述。由于 BPU 顶层功能较多,本节将 BPU 划分为了几大功能点进行更进一步的描述。但由于 BPU 顶层细节过多,更进一步的细节需参照代码进行理解。
生成器维护方法
通过香山的基础设计文档,我们知道,BPU 顶层是通过一个个生成器来维护 s0 周期的各类变量的,例如 PC、分支历史等,并且其核心概念是,通过预测结果重定向信号决定采用哪个流水级的结果。
BPU 顶层中所有的生成器共有 6 个:
- npcGen 维护 pc
- ghistPtrGen 维护全局历史指针
- ghvBitWriteGens 维护全局历史写入数据
- foledGhGen 维护折叠历史
- lastBrNumOHGen 维护上周期最后一个生效的分支指令位置
- aheadFhObGen 维护分支历史最老位
其中,除了 npcGen
以外,其余生成器都会在本文档中进行介绍。本节中我们着重探讨一下生成器的产生下一次预测的方法。
在代码中你可以看到生成器用类似下面这种方式进行定义:
val npcGen = new PhyPriorityMuxGenerator[UInt]
接下来,代码通过多条语句对生成器的数据来源进行注册:
npcGen.register(true.B, reg, ...)
npcGen.register(s1_valid, s1_target, ...)
npcGen.register(s2_redirect, s2_target, ...)
npcGen.register(s3_redirect, s3_target, ...)
npcGen.register(do_redirect.valid, do_redirect.bits.cfiUpdate.target, ...)
每一行被称作一次注册,在一次注册中第一个信号参数是数据有效信号,第二个信号参数包含具体的数据。 生成器的优先级也是按照注册的顺序来决定,越往后优先级越高,因此,同一时刻的优先级从低到高依次为:
- s0 阻塞的数据
- 根据 s1 预测结果更新后的数据
- 根据 s2 预测结果更新后的数据
- 根据 s3 预测结果更新后的数据
- BPU 外部重定向中的数据
这样一来,我们就可以在预测结果重定向有效时,避免采用较早流水级的预测结果,而采用纠正后的预测结果。也使得我们可以将外部重定向请求作为最高优先级去处理。
我们可以得出所有生成器产生 s0 信号的方法:在所有数据有效信号中,如果只有一个有效的,则选取它对应的数据,如果有多个数据有效信号生效,则选取优先级最高的数据。
全局分支历史
我们知道,全局分支历史在 BPU 顶层进行维护,维护的策略与 PC 的维护策略一致。即在每个阶段流水级预测结果产生之后,会根据相应信号对全局分支历史进行更新。
顶层为了维护全局分支历史定义了两组信号
- ghv 存储了全局分支历史(最大长度 256)
- ghist_ptr 全局分支历史指针,指向全局分支历史当前的位置
与 s0_pc
, s1_pc
, s2_pc
一样,BPU 顶层为全局历史指针也维护了每一阶段的信号 s0_ghist_ptr
, s1_ghist_ptr
, s2_ghist_ptr
,但 ghv
中的内容是位置固定的,我们仅通过 ghist_ptr
来定位当前的全局分支历史从哪里开始。
通过 ghist_ptr 计算当前全局分支历史
ghist_ptr
的使用仅在 BPU 顶层可见,而我们向子预测器传入的,是全局历史寄存器中的数据根据 ghist_ptr
所移位之后的全局分支历史。在子预测器拿到的全局分支历史中,最低位对应全局分支历史的最新位,最高位对应全局分支历史的最老位。
那么是怎样进行移位的呢,我们首先来看一下全局历史是怎样在 ghv
中进行存储的。
|===== ghist =====>| =======>|
n ^ 0
ghist_ptr
如上图所示,序列表示整个 ghv
寄存器,ghist_ptr
指向 ghv
中的某个位置,这个位置代表了全局分支历史的最新位。当需要添加一位新的全局历史记录时,首先将 ghist_ptr
减 1,然后将该位写在其所指向的位置。当 ghist_ptr
减到 0 后,又会循环回来指向最高位,因此会覆盖之前写入的全局分支历史。
但不管怎样,从 ghist_ptr
所指向的位置开始,指针越增加,历史越老。因此,当我们需要计算当前全局分支历史时,只需要将 ghv
寄存器循环右移 ghist_ptr
位即可。
全局分支历史的更新
全局分支历史的更新策略与 pc
更新的策略一致,在每一个流水级都需要根据当前流水级的预测结果生成一个 当前流水级的指针及 ghv
的更新说明,最终都送给相关生成器来处理。
ghv
的更新说明即 用于指导 ghv
寄存器的更新的某些信息。香山 BPU 中维护了两个信息来完成这一职责:
ghv_wdata
需要向 ghv 中写入的数据ghv_wens
写入位掩码
最终更新时,只需要将 ghv_wens
所标识的位写入 ghv_wdata
的对应位即可。
因此每个流水级需要负责产生三组信息:ghist_ptr
,ghv_wdata
, ghv_wens
。
具体地,预测结果中最多含有两条分支指令,我们只需将实际情况来设置这几个信息即可,举几种情况的例子:
- 只有第一个槽有效,并且其中条件分支指令被预测为不跳转。则将
ghv_wens
的下一个位置置 0 ,ghv_wens
的对应位置置 1, 同时ghist_ptr
减一。 - 两个槽都存放了条件分支指令,第一条被预测为不跳转, 第二条被预测为跳转。此时
ghist_ptr
应该减二,并且其他两个信息应该指示向ghv
中写入 01。
此处在生成器中只维护了一个 ghv_wdata
信息(通过 ghvBitWriteGens
生成器维护),ghv_wens
并没有通过生成器来维护。这是因为此处使用了一个小技巧,使用了生成器的 ghv_wdata
最终输出的是被选中阶段的结果,而 ghv_wens
将所有阶段的 ghv_wens
进行按位或来使用。
这是基于如下考虑的:
- 如果较晚的流水线阶段有效。全局历史指针被恢复到较老的位置,即便被早期流水的
ghv_wens
修改了较新位置的历史也没关系。 - 如果较早的流水线阶段有效。全局历史指针继续向较新的位置更新,而后期流水线会因为 redirect 未生效而不把
ghv_wens
置位。
分支折叠历史
送入预测器的分支折叠历史也是由顶层 BPU 来维护的,BPU 为了缩短折叠历史的更新延迟,维护了很多变量,来支持分支折叠历史的快速更新,我们将会重点介绍这一策略,并介绍每一个变量的作用。
在开始之前,我们先来看一下分支折叠历史是怎样定义的,结构又是怎样的。
分支折叠历史
如果你查看了 BPU 全局接口的文档,你就会知道,子预测器拿到的是一个不同长度位向量的数组,代表了各种长度的折叠历史,而这些折叠历史都是由全局分支历史压缩而成。
对于全局分支历史,我们有一个存放全局分支历史的寄存器,长度为 256。为了方便举例,我们假设全局分支历史的长度为 15 位,并且经过移位之后,我们可以拿到一个这样的分支历史:最低位是最新的历史记录,最高位是最老的历史记录。
此时如果我们需要用这 15 位,产生一个 6 位的折叠历史,会使用异或的策略进行压缩,具体过程是这样的:
h[5] h[4] h[3] h[2] h[1] h[0]
h[11] h[10] h[9] h[8] h[7] h[6]
^ h[14] h[13] h[12]
---------------------------------------------------------------
h[5]^h[11] h[4]^h[10] ... h[0]^h[6]^h[12]
即将其按照上面的方式排列之后,将每一位上的值进行异或,结果便是求出的长度为 6 的折叠历史。
分支折叠历史更新方法
此时我们想要对这一分支折叠历史进行更新,当我们向全局分支历史插入一位新历史时,是从最低位插入的,也就是说原来的 h[0] 变为了 h[1],如果我们想求此时的分支折叠历史,只需要再进行一遍异或运算。但这样的效率太低了,因为异或的操作有可能变得特别长,我们可以来探寻一下一次更新对分支折叠历史的影响。
上述例子中,插入一位新历史之前,6 位折叠历史的生成是按照下面这种排列生成的
h[5] h[4] h[3] h[2] h[1] h[0]
h[11] h[10] h[9] h[8] h[7] h[6]
h[14] h[13] h[12]
插入一位新历史之后变成了下面这样
h[4] h[3] h[2] h[1] h[0] h[new]
h[10] h[9] h[8] h[7] h[6] h[5]
(h[14]) h[13] h[12] h[11]
我们可以发现一些规律
插入前:
h[5] {h[4] h[3] h[2] h[1] h[0] }
h[11] {h[10] h[9] h[8] h[7] h[6] }
{ h[14] h[13] h[12]}
插入后:
{h[4] h[3] h[2] h[1] h[0] } h[new]
{h[10] h[9] h[8] h[7] h[6] } h[5]
{ (h[14]) h[13] h[12]} h[11]
大括号中的内容发生了整体的左移,h[5] 和 h[11],由最高位变到了最低位。那么表现在压缩后的历史上不就是我们常见的循环左移吗!
但其中有且仅有两个位的值发生了变化,一个是新加入的 h[new],一个是被舍弃掉的 h[14]。h[new] 肯定在第一位,被舍弃的位置也是固定的。因此我们想要完成一次更新,只需要知道 新插入历史的值 和 前一次历史的最老位即可。循环移位后,将这两个位置根据实际情况进行一次修改便可拿到更新后的折叠历史。
更新方法实现
BPU 顶层为了实现这种更新,正是通过维护最老位,这通过两个额外的变量来实现:
- ahead_fh_oldest_bits 全局分支历史的最老位,还额外往前存储了若干位
- last_br_num_oh 上一次预测最后一个生效的分支指令在第几个槽
在这里有一处为时序所优化的点,因为当流水级的预测结果出来时,全局历史指针才能通过跳转情况进行更新,等到全局历史指针更新完再来更新最老位会增加时延。因此我们将跳转情况维护起来,等到下一周期用的时候再来用跳转情况更新最老位。
此时的最老位也需要多往前维护几位,因为在使用时,利用跳转情况更新后,前面较新的几位就会变成最老位了。
所以与折叠历史相关的生成器共有三个:foldedGhGen
, lastBrNumOhGen
, aheadFhObGen
每次折叠历史更新时需要的信息分别是:
- 更新前的折叠历史信息
- 全局分支历史最老位(ahead_fh_oldest_bits)
- 上次预测的跳转情况(last_br_num_oh)
- 本次更新是否有指令跳转
- 本次更新的跳转情况:最后一个生效的分支指令在第几个槽
每次折叠历史更新时,都需要根据 last_br_num_oh
和 ahead_fh_oldest_bits
求出真正的最老位,然后通过最老位与本次更新的跳转情况将其中的若干位进行修改,最后进行循环左移,便完成了更新操作。
流水线控制方法
流水线控制是 BPU 功能的核心,逻辑也最为复杂,BPU 顶层中所有的流水线控制信号如下:
- s1_valid, s2_valid, s3_valid 表示对应流水数据生效
- s1_ready, s2_ready, s3_ready 表示对应流水已准备好继续上一流水级的预测
- s1_component_ready, s2_component_ready, s3_component_ready 表示对应流水子预测器的 ready 情况
- s0_fire, s1_fire, s2_fire, s3_fire 握手成功信号,表示该流水数据生效,并成功传递给了下一流水
- s1_flush, s2_flush, s3_flush 表示当前流水是否需要冲刷
- s2_redirect, s3_redirect 表示当前流水在 fire 的同时,是否预测结果不同,需要产生预测结果重定向
valid, ready 与 fire
我们会逐步来介绍每个信号的作用,首先我们来看 fire
信号,这一信号表示的含义是流水线握手成功,数据成功传给了下一流水。这标志着本周期结束时,本流水级的预测也随之结束,下周期开始时,下一流水级的预测即将开始。
这需要两个条件:
valid
本流水级的数据是有效的。ready
与component_ready
分别指示了 BPU 顶层与预测器的下一流水级是否就绪。
当这两个信号同时置高时,fire
信号有效,表示握手成功。如果我们单独把一次预测拿出来,那么时序应该是这样的(实际中,大多数时间每个流水线都是一直有效的):
上文中提到的四组信号,除了 component_ready
是由预测器输出,其余信号皆需 BPU 顶层来维护,而最终暴露给子预测器的,只有 fire
一组信号。
我们接下来以 s2 为例分别来看每个信号是如何维护的。
ready 信号
s2_ready := s2_fire || !s2_valid
该赋值语句是一个组合电路赋值,也就是说,s2_ready
信号是与本周期的 s2_fire
和 s2_valid
直接相关联的,分为以下两种情况:
s2_valid
信号在本周期无效,说明 s2 流水级目前是空的,自然可以接受新的数据,则s2_ready
有效s2_valid
信号在本周期有效,说明 s2 流水级目前有数据还未传递给下一级,但如果s2_fire
,那么本周期就会传递给下一级。此时s2_ready
有效,刚好指示数据可以在下一拍流入。
valid 信号
s2_valid
信号目前为止维护是相对简单的,与 s1_fire
信号和 s2_ready
信号相关。其关系为:
- 当
s1_fire
有效,说明数据传进来,下一周期s2_valid
有效。 - 当
s2_fire
有效,说明数据流出去,下一周期s2_valid
无效。
fire 信号
fire 信号相对特殊,但对于中间的流水级来说,维护非常简单,例如
s2_fire := s2_valid && s3_components_ready && s3_ready
只需考虑当前流水级的 valid
和下一流水级的 ready
即可。
但对 s0_fire 来说,没有 valid 信号,因此其直接等于 s1_components_ready && s1_ready
对于 s3_fire 来说,没有下一级的 ready 信号,因此其直接等于 s3_valid
加入 flush 和 redirect
我们知道,当流水线出现预测结果不同时,需要产生预测结果重定向信号,并且将之前的流水线清空。flush
和 redirect
正是在做这两项工作。redirect
表示当前流水级是否需要重定向,flush
则表示当前流水级是否需要冲刷。
redirect 信号
s2_redirect
的产生方式如下:
s2_redirect := s2_fire && s2_redirect_s1_last_pred
也就是说,当 s2_fire
时,并且 s2 的预测结果与上一周期保存的 s1 预测结果不同时,这个信号便有效。之后该信号将会连接到子预测器的输入,与 BPU 预测结果的输出,分别指导子预测器和 FTQ 的状态恢复。
flush 信号
flush 信号是用于指导流水线冲刷的,例如 s3 重定向有效时,说明错误的预测结果已经流入流水线, s1 和 s2 此时全都是基于错误的结果来预测的,因此需要进行流水线冲刷,使之前的流水级都暂停工作,等待新的预测结果流入。
具体地,他们之间有如下关系:
s2_flush := s3_flush || s3_redirect
s1_flush := s2_flush || s2_redirect
也就是说,某个流水级 redirect
有效,之前的流水级的 flush 全都会被置为有效。那么 flush 具有什么作用呢?答案是指导 valid 信号,如果本周期 valid 信号有效,但 fire 信号未生效,说明错误的数据没有被下一流水取走,此时 flush 有效后,在下一周期 valid 就会立即变为无效,以这种方式来避免错误数据长期存储在流水线中。
但 flush 信号对 valid 信号的影响,也根据每一个流水级的不同而有一定差异。例如
- s1 流水级。虽然 flush 有效,但是如果此时
s0_fire
有效,说明新数据流入,那么下周期 valid 依然有效。 - s2 流水级。flush 有效,那么必定下周期不会 valid(因为 s1 也肯定被 flush),此时就可以直接将 valid 置为无效。但还存在一种特殊情况,
s2_redirect
发生时,s2_flush
并没有被置为有效,此时如果发生s1_fire
,s1 的错误预测结果也可能流入,此时还需根据s1_flush
信号来决定s2_valid
是否有效。
flush 的使用较复杂,更详细的细节还需参考代码进行理解。
重定向恢复逻辑
当 FTQ 发往 BPU 的重定向请求生效时就说明所有流水级的预测结果都是不正确的,此时应该将所有流水级进行冲刷,这可以通过将 s3_flush
置为有效来实现。因此有
s3_flush := redirect_req.valid
在 BPU 中,重定向请求送入后被延迟一周期才正式使用,因此 s1_valid
的信号也需要对 flush
信号的响应做出一些改变。当重定向请求(延迟前)有效时,s1_valid
下周期立即被置为无效,不需要再去参考 s0_fire
信号了。
此时 npcGen
等生成器也需要直接去采用重定向请求中的数据来生成,这就相当于将 BPU 的状态重定向到出错之前状态的过程。但注意 BPU 默认的重定向等级为 flushAfter
,即重定向请求会对应一条预测错误的指令,而 BPU 会认为这条指令虽然预测错了,但是已经被纠正并且交由后端执行了,因此下一次预测可以直接从下一条指令开始。
所以在重定向恢复时,不仅需要将重定向接口中的信息恢复,还需要将这条预测错误的指令的执行情况也更新到历史中去。