这个问题听起来像家庭作业,但值得讨论。
我们假设有一个总是预测 NOT TAKEN 的 static 分支预测器。这是早期 SPARC 和 MIPS 实现的分支预测器类型。这样的分支预测器总是获取程序中的下一条顺序指令。
我还假设我们有一个简化的 4 阶段管道,由 Fetch (F)、Decode (D)、Execute (E) 和 Write Back (W) 组成。考虑以下简化的汇编程序:
...
0xF1: JUMP <condition>, 0xF4
0xF2: ADD r1, r2, r3
0xF3: ADD r3, r4, r1
0xF4: ADD r1, r2, r3
当分支被正确预测时,管道会正常运行。问题是当分支被错误预测时,管道会发生什么。在我们的例子中,对应于 JUMP 指令 (0xF1) 的条件得到验证的情况。
0xF1: F D E W
0xF2: F D X
0xF3: F X
0xF4: F
cycle 1 2 3 4
在 JUMP 指令的执行阶段,我们评估条件并检测必须采用分支。然而,由于分支预测器策略,我们已经获取指令0xF2 和0xF3 并解码0xF2。流水线被刷新,并在下一个时钟周期正确获取分支目标。从流水线中可以看出,我们浪费了 2 个时钟周期来获取和解码不会执行的指令。这 2 个时钟周期称为分支惩罚,在计算程序的执行时间时必须将它们考虑在内。
分支预测器的世界在现实中要复杂得多。存在更详细的静态分支预测器,例如,总是将前向跳转预测为 TAKEN,而后向跳转预测为 NOT TAKEN。为了减少 分支惩罚 周期,处理器通常使用分支目标缓冲区 (BTB),它是一个小型缓存,用于存储最近执行的 JUMP 指令的目标。如果没有 BTB,要将分支预测为 TAKEN,我们必须等到解码阶段,其中指令被识别为 JUMP 并且目标地址被解码。与此同时,我们已经获取了一条指令,然后将被刷新。另一方面,使用 BTB,我们可以在 Fetch 阶段进行分支预测:如果程序计数器在 BTB 中,我们知道 2
- 获取的指令是一个分支
- 我们有它的目标地址
所以如果可以预测分支并且如果预测为TAKEN,我们可以获取它的目标而不会受到任何惩罚。
现代处理器还采用动态分支预测器,这些预测器使用复杂的策略以及一些额外的缓冲区来避免错误预测。