从零开始学RISC:第十八篇
分支预测:预见1
一个程序中的指令之间存在相关性,如时间相关性和空间相关性。对于循环语句,一次次的跳转其实是类似的,只有到达极限才会停下来。直接让CPU预测程序下一步干什么,然后运行,而不是傻乎乎的等待,这样子可以显著提高运行效率——大多数情况下是的。
既然如此,我们的分支预测器一定要早于分支结果计算出来就生效。因此,需要将其放在ID级。
静态预测器
静态预测器很傻,但也很老实:它是“一根筋”,只会预测分支要么跳转、要么不跳转,自己不会做出改变。
我们需要留出哪些接口?需要判断当前指令是否为分支指令,以及输出。
1 | module Static_Predictor #( |
1-bit分支预测
什么是1-bit分支预测?它会为每条指令保存 1-bit 的信息,来表示这条分支上一次的结果:
1:上次跳转了0:上次没跳转
下次再遇到这条分支时,就直接预测它会重复上一次的行为。很显然,它是一种 一级动态分支预测器,只看“这条分支上次怎样”。
它有什么优点?简单,很简单的预测,而且不是傻乎乎的在一棵树上吊死,会改变一次。但它也有一个很显然的缺点:每次循环结束附近会产生两次错误,这是因为一次偶然变化会导致它接下来再错一次:
1 | T T T T N T T T // 实际分支 |
代码也很简单。这里,我们选用PC的前8位字地址作为索引。这个INDEX_BITS越大,索引效果越好。当然,不建议一味拉高,拉太大了会引入额外的电路,损耗面积。
1 | module Dynamic_1bit_Predictor #( |
GShare分支预测
GShare是一种 两级动态分支预测器,它不只看“这条分支上次怎样”,还会看最近若干次分支的全局历史。其关键思想是,当前这条分支的结果,往往和前面最近发生过哪些分支有关。
GShare通常包含两部分:
- 全局历史寄存器 GHR(Global History Register):记录最近 n 次分支的结果,类似 1-bit 的分支预测;
- 模式历史表 PHT(Pattern History Table):通常是一张 2-bit 的表,代表一个饱和计数器,用来表示“强跳转 / 弱跳转 / 弱不跳转 / 强不跳转”。
这样看来,GShare和 2-bit分支预测很像。但事实上,GShare做了独特的一步:它将索引进行了计算:
索引 = 分支地址低位 XOR 全局历史 GHR
也就是:
- 取分支指令 PC 的低几位
- 和 GHR 做按位异或(XOR)
- 得到最终索引
为什么?只用PC的话,很多分支可能映射冲突;只用 GHR,不同分支又区分不开。把两者 XOR 起来,相当于共享了分支自身的信息和最近历史上下文,这就是Share。
Gshare需要之前的PC传入,以及预测是否正确来更新PHT。因此,需要多几个接口。
1 | module Dynamic_Gshare_Predictor #( |
BPU封装
一个较好的方式是写一个统一的BPU模块,随后使用generate case来实例化对应的分支预测:
1 |
|
测试
继续使用CoreMark测试:
| 分支预测类型 | 总Tick数 | 总周期数 | 错误预测数 | 基准速度比 |
|---|---|---|---|---|
| 无分支预测 | 5292624 |
53336415000 |
/ |
1.000x |
| 静态分支预测 | 5011712 |
50501545000 |
163426 |
1.056x |
| 1-bit(4I )动态分支预测 | 5035325 |
50730805000 |
167403 |
1.051x |
| 1-bit(8I )动态分支预测 | 4998735 |
50371755000 |
142873 |
1.059x |
| GShare(8I1M)动态分支预测 | 4971079 |
50093755000 |
120647 |
1.065x |
| GShare(8I2M)动态分支预测 | 4975420 |
50139215000 |
124155 |
1.064x |
| GShare(8I4M)动态分支预测 | 4984823 |
50232025000 |
130558 |
1.062x |
| GShare(8I8M)动态分支预测 | 4993908 |
50323525000 |
138736 |
1.060x |
可以看到,GShare的效果确实很好。




