计算机体系结构——分支预测

分支预测解决流水线冲突中的控制冒险。在指令集中,分支指令包括条件指令和非条件指令。对无条件指令来说,无需进行条件判断,就可以获得跳转后的地址;但是对条件跳转指令,无论是条件直接跳转还是条件间接跳转指令(RISC-V 无此类指令),都需要在执行阶段才可以确定是否跳转,这样会影响取值阶段的效率。

举个简陋的例子说明分支预测的可行性:

if (data[c] >= 128)
    sum += data[c];
T = branch taken
N = branch not taken

data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N  N  N  N  N  ...   N    N    T    T    T  ...   T    T    T  ...
       = NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT  (easy to predict)

可以发现,由于分支指令之间的相关性,根据分支历史可以预测出当前分支指令的方向与地址。

不过要注意一点,即使分支预测成功率达到 90% 也是非常糟糕的。因为现代处理器流水线深度太深了,出错一次就要 flush 整个流水线,造成了性能的损失。

一、基本部件

分支预测基本部件

  • BTB(Branch Target Buffer, 分支目标缓冲器)
    用于预测目标地址。缓存保存了最近执行过的分支指令的 PC 值,以及它们的跳转目标地址。

  • 两比特饱和计数器

四个状态:

  • 强跳转
  • 弱跳转
  • 强不跳转
  • 弱不跳转

当执行一条分支指令时,会根据指令是否跳转来更新两比特饱和计数器。一个 2bit 状态机的核心思路就是:这次预测错了不要紧,再给你一次机会,还是预测错了的话那再改变预测结果

二、预测方向

2.1 静态预测

静态预测不依赖任何过去执行过的指令和历史信息。

  • 总预测接下来的指令都(不)跳转

Accuracy of Prediction

可以看出,这种预测准确率都在 50%以上,最高的还达到了 99.4%。

  • BTFN 跳转(Back Taken, Forward Not Taken),如果所跳转的目标地址位于跳转指令的前方(比当前指令晚执行),则不跳转;如果所跳转的目标地址位图跳转指令的后方(比当前指令早执行)则跳转。

  • 某些指令一律预测跳转,其他指令一律预测不跳转

2.2 动态预测

动态分支预测使用运行时收集的关于跳转或未跳转的分支信息来预测分支结果。

2.2.1 基于PC的分支预测器

直接将“两比特饱和计数器”组织成一维表格,称为预测器表格,并直接使用 PC 值的一部分进行索引。譬如使用 PC 的后 10 位作为索引,则仅需要维护 1000 个表项的表格。

“一级”是指其索引仅仅采用指令本身的 PC 值。

该方法简单,但索引机制过于简单。像低 10 位相同但高位不相同的 PC,就会指向同样的表项,这样肯定会产生干扰,这种情况称为别名(aliasing)。别名的存在会对分支预测的准确度产生影响,如果两条别名的分支指令的方向是相同的,例如都会发生跳转,那么它们对应的两位饱和计数器就会保持在饱和状态,此时不会对这两条分支指令的预测准确度产生负面的影响;如果两条别名的分支指令的方向不同,那么它们对应的两位饱和计数器就会一直无法处于饱和状态,当然就降低了分支预测的准确度。

可以采用一些更高级的方法来避免别名情况的发生,比较典型的方法就是使用哈希(Hash),对 PC 值进行处理之后再去寻址 PHT,如下图所示。

2.2.2 基于分支历史的分支预测器

如引言所示,分支指令一般都有规律,因此可以根据分支历史来进行预测。Branch history 可分为局部历史和全局历史。局部历史是指每个分支指令自己的分支跳转历史,全局历史是指所有分支指令的指令跳转历史。

局部分支预测器

使用一个分支历史寄存器(Branch History Register, BHR)来记录一条分支指令在过去的历史状态。BHR 中有 n 个 bit,记录该分支指令前 n 次的历史状态(发生跳转或者不发生跳转,用 1 或 0 表示)。

使用 BHR 去寻址 PHT(Pattern History Table),PHT 的大小是 2 n ∗ 2 2 ^ n * 2 2n2 bit。 PHT 和基于 PC 值的分支预测器一样,存储 BHR 的每种取值对应的两位饱和计数器的值。当一条分支指令得到结果时,就将 PHT 中对应的计数器值读出来,并根据分支结果对其进行更新,然后将这个结果写回到 PHT 中即可。

下面使用两个例子来说明 BHR 和 PHT 是如何配合进行分支预测的。

示例一

假设有一条分支指令,它的 BHR 寄存器的宽度是两位,意味着这条分支指令前两次的结果会被保存在 BHR 寄存器中。BHR 有四种不同的值00、01、10 和 11,其中 0 表示没有发生跳转,1 表示发生了跳转,这时需要一个有着 4 个表项(entry)的 PHT,用来和 BHR 的四种取值一一对应,称为 entry0、entryl、entry2 和 entry3。

假设这条分支指令有如下的执行顺序 taken→not taken→taken→not taken→taken…,这可以表示为 l0_01_l0…(从第三个开始),因此 BHR 寄存器中会交替出现 10 和 01。从上述执行顺序可以看出,当 BHR 的值为 10 时,下一个值肯定是1,也就是这条分支指令下次肯定是要跳转的;当 BHR 中的值是 01 时,下一个值为 0,也就是这条分支指令下次肯定不会发生跳转。

当 BHR 的值为 10 时,会寻址到 PHT 当中的 entry2,对于这个表项中的饱和计数器来说,因为每次都会发生跳转,所以这个计数器会到达Strongly taken 的饱和状态,这样当这条分支指令进行分支预测时,如果发现它的 BHR 值为 10,从 PHT 中寻址到的计数器值就是 11,也就可以预测出这条分支指令会发生跳转。

当 BHR 的值是 01 的情况也是类似的,因为根据这条分支指令的执行情况,01 之后肯定会跟着 0,而 BHR 的值为 01 时,会寻址到 PHT中 的 entry1,它对应的计数器因为总是不发生跳转,恻肯定会停留在Strongly not taken 的饱和状态,这样当这条分支指令在进行分支预测的时候,如果 BHR 的值为 01,侧从 PHT 中寻址到的计数器的值就是 00,也就可以预测到这条分支指令是不发生跳转的。

示例二

如果一条分支指令每执行两次就改变分支的方向,即 TTNNTTNNTTNN·.这可以用一个序列表示为110011001100…,这种方式仍可以用上面两位的 BHR 进行预测。在这个序列中,11之后必然跟着0,10之后必然跟着0,00 之后必然跟着1,01之后必然跟着1。也就是说,序列当中每两位的数后面跟着的数值都是唯的,称这个序列的循环周期为 2。

经过一段时间之后,PHT 就可以捕捉到这条分支指令的规律,entry0 和 entryl 中的两个计数器由于输人总是 1,会停留在饱和的 Strongly taken 状态;entry2 和 entry3 中的两个计数器由于输人总是 0,会停留在饱和的 Strongly not taken 状态。

更宽的 BHR 可以记录到分支指令更多的历史信息,这可以提高分支预测的准确度。只要分支历史寄存器 BHR 的宽度 n 不小于序列的循环周期 p,那么就可以对该序列进行完美的预测。

如果一个序列中,连续相同的数最多有 p 位,那么这个序列的循环周期就为 p。例如序列“11000_11000_11000…”,因为有两个连续的 1,三个连续的 0,故循环周期为 3,而不是 2;序列“00000111_00000111…”中,有五个连续的 0,三个连续的 1,故循环周期为 5,而不是3。

在以上讲述中,每条分支指令都有白己对应的 BHR 和 PHT。如果为每条分支指令都配一个 BHR 和 PHT,这样需要很大的存储空间。将所有分支指令的 BHR 组合在一起称为分支历史寄存器表(Branch HistoryRegister Table, BHRT 或 BHT),在实际当中,BHT 不可能照顾到每个 PC,一般都是使用 PC 的一部分来寻址 BHT,这就相当于一些 PC 会共用一个 BHR,同时,PHT 由于要占用大量的存储空间,更需要被复用,可以采用如下图所示的方法。

和基于 PC 的分支预测器一样,由于使用了 PC 的一部分来寻址 BHT 和 PHTs 会遇到重名(aliasing)的情况,这些重名的分支指令会使用同一个 BHR 或 PHT,相互之间有了干扰,这样就会使分支预测准确度有所下降。

为了避免这种情况,可以将 PC 值和对应的 BHR 值进行一定的处理,使用处理之后的值来寻址 PHT,如下图所示。

全局分支预测器

当一条分支指令的循环周期太大时,就需要一个宽度很大的 BHR 寄存器,这会导致过长的训练时间,并且 PHT 也会随之占用更多的存储器资源。同时,局部分支预测器并没有考虑到一条分支指令之前的其他分支指令对自身预测结果的影响。有些时候,一条分支指令的结果并不取决于白身在过去的执行情况,而是和它前面的分支指令的结果是息息相关。因此就有了全局分支预测器。

和局部分支预测器一样,全局分支预测器也有一个寄存器来记录历史状态,这个寄存器被称为 GHR(Global History Register,全局历史寄存器)来将所有分支指令关联起来,即使用一个 k 比特的 GHR 来记录所有最近 k 条分支指令的历史。当然 GHR 寄存器不可能记录下所有分支指令的执行结果,因为 GHR 寄存器的宽度是不能无限大的。一般都是使用一个有限位宽的 GHR 来记录最近执行的所有分支指令的结果,每当遇到一条分支指令时,就将这条分支指令的结果插人到 GHR 寄存器的右边。

2.2.3 锦标赛分支预测

锦标赛分支预测(又称混合分支预测或组合分支预测)是一种博采众长的分支预测方法,其基本原理是将两个或以上的分支预测方法进行结合,充分发挥各预测方法的优势,以进一步提高分支预测的准确度。

三、预测地址

对于直接跳转的分支指令,由于它的偏移值(offset)是以立即数的形式固定在指令当中,所以它的目标地址也是固定的,只要记录下这条分支指令的目标地址就可以了,当再次遇到这条分支指令时,如果方向预测的结果是发生跳转,那么它的目标地址就可以使用以前记录下的那个值。

而对于间接跳转的分支指令来说,由于它的目标地址来自通用寄存器,而通用寄存器的值是会经常变化的,所以对这种类型的分支指令来说,进行目标地址的预测并不是一件很容易的事情,但是庆幸的是,程序中大部分间接跳转类型的分支指令是用来处理子程序调用的 CALL 和 Return 指令,而这两种指令的目标地址是有规律可循的,因此可以对其进行预测。

BTB

BTB

  • 使用容量有限的缓存保存最近执行过的分支指 PC 值,以及它们的跳转目标地址。对于后续要取指的每条 PC 值,将其与 BTB 中存储的各个 PC 值进行比较,如果出现匹配,则预测这是分支指令,并使用其对应存储的跳转目标地址作为预测的跳转地址。
  • 是其缺点之一是不能将 BTB 容量做到太大, 否则面积和时序都无法接受。
  • 另一个缺点是对于间接跳转(indirect Jump/Branch )指令的预测效果并不理想,这主要是由于间接跳转分支的目标地址是使用寄存器索引的操作数(基地址寄存器)计算所得,而寄存器中的值随着程序执行可能每次都不一样,因此 BTB 中存储的上次跳转的目标地址并不一定等于本次跳转的目标值。不过,RISC-V 指令架构无需担心这点,因为 RISC-V 没有条件间接跳转指令。

RAS

  • 使用容量有限的硬件堆栈来存储函数调用的返回地址。
  • 间接跳转指令可以用于函数的调用和返回,而函数的调用和返回在程序中往往是成对出现的,因此可以在函数调用(使用分支跳转指令)时将当前 PC 值加 4(或者加 2), 即其顺序执行的下一条指令的 PC 值压入 RAS 中,等到函数返回(使用分支跳转指令〉时将 RAS 中的值弹出,这样就可以快速地为该函数返回的分支跳转指令预测目标地址。

在 NutShell 处理器中,在做分支预测时会用取指 PC 索引 BTB 表项, 如果 PC 高位与读到的 BTB 表项的标签匹配则认为 BTB 命中, 再根据 BTB 中记录的指令类型判断跳转方向和跳转目标. 如果类型为条件分支指令, 则需要访问模式历史表 (PHT) 来判断是否跳转; 如果类型为返回指令, 则选择返回地址栈 (RAS) 的栈顶内容作为跳转目标; 如果类型为直接或间接跳转指令, 则选择 BTB 中记录的跳转目标.

参考文档

  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的 RISC-V 静态分支预测 Verilog 代码示例: ``` module static_branch_predict ( input [31:0] pc, input [31:0] inst, input [1:0] op, output reg predict_taken, output predict_pc ); reg [31:0] target; reg taken; always @ (pc, inst, op) begin case (op) 2'b00: begin // BEQ if (inst[31:25] == 7'b0000000 && inst[11:7] == 5'b00000) begin target = pc + {{20{inst[31]}}, inst[7], inst[30:25], inst[11:8], 1'b0}; taken = (inst[19] == 1'b0); end end 2'b01: begin // BNE if (inst[31:25] == 7'b0000000 && inst[11:7] == 5'b00001) begin target = pc + {{20{inst[31]}}, inst[7], inst[30:25], inst[11:8], 1'b0}; taken = (inst[19] == 1'b0); end end // 其他分支指令类似 default: begin taken = 1'b0; end endcase predict_taken = taken; if (taken) begin predict_pc = target; end else begin predict_pc = pc + 4; end end endmodule ``` 在这个示例中,我们仍然只考虑了 BEQ 和 BNE 两种分支指令,但这次我们使用了一种静态分支预测算法:始终预测分支不被执行(即 predict_taken 始终为 0)。这种算法的原理是,许多分支指令的分支目标地址都是在循环中或者是在一些不太可能被执行的分支语句中,因此我们可以采用始终预测分支不被执行的策略,来提高分支预测的准确率。 需要注意的是,静态分支预测算法的准确率可能会因为程序的不同而有所差别,因此在实际应用中,需要根据具体情况选择适合的分支预测算法,并对其进行优化和调整,以提高预测准确率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值