轻松读懂移动处理器 CPU微架构全解析
当然,还有更复杂的情况:
if (a > 5)
b = c;
else
b = d;
将其按照汇编语言编写出来:
cmp a, 5 ; a > 5 ?
ble L1
mov c, b ; b = c
br L2
L1: mov d, b ; b = d
L2: ...
这里的第二条指令是一个条件分支指令(ble 是小于或等于转移指令,ble L1 表示如果寄存器 a 小于等于 5 就转移到 L1 这行执行 mov d,b 这条指令)。
这条指令抵达“执行单元”的时候,位于流水线前段的取指单元和解码单元肯定已经拾取和解码了若干条指令,但是哪一条指令才应该接下来被执行呢?应该是第三、四条还是第五条呢?
在第二行的条件分支指令完成之前,处理器只能等待。处理器平均六条指令就会遇到一条分支指令,因此流水线设计带来的大部分性能提升优势此时会被丧失掉。
为此处理器必须进行“猜测”,按照猜测结果进行取指并推测哪些指令能开始执行,不过这些指令的执行结果并不会被递交(写回)直到分支指令的执行结果完成。
如果猜错的话,这部分指令的结果就会被扔掉,这也意味着这些指令对应的时间或者周期数会被浪费掉。当然如果猜中的话,处理器就能全速运行了。
那么如何去做“猜测”呢?
一个办法是所谓的静态分支预测,例如在指令编码格式里留出一个位元作为预测信息,编译器编译的时候,对这个位元进行标记,告诉处理器该跑那条分支,不过这对于已经采用了不具备这类条件执行指令的旧式 ISA 二进制程序来说这样显然是不可能的。
另一个办法就是“运行过程中”进行猜测即动态分支预测,通常是采用类似被称作“片上分支预测表”的单元来记录最近分支的地址以及用一个位元指示出哪一条分支是最近是否被采用的。
不过现在大多数处理器实际上是用两个位元来作标记的,因此单次的分支“不跳转”并不会马上导致一般的“跳转”预测出现反转(这对于循环边界来说是很重要的)。不过两位元分支预测并有考虑到分支相关性,所以人们后来有采用两级分支预测来解决这个问题,使得预测精度大大提高。
动态分支表需要占用相当可观的芯片面积,但是另一方面来说分支预测对流水线化处理器是相当重要的,所以是物有所值的。
不过就算是最好的分支预测技术也可能会猜错,对于超级流水线或者说深流水线来说就会有很多指令的结果会被扔掉,这样的情况被称作 mispredict penalty(误预测性能惩罚)。
像 Pentium III 这类具备非常先进分支预测技术的处理器,在遇到分支预测失败的时候,也会出现 10~15 个周期的性能损失,因此即使正确命中了 90% 的分支,也会因为分支误预测导致 30% 的性能损失,所以 Pentium III 其实很多时候会出现 30% 的时间在走冤枉路。
人们在 ISA 中引入条件执行指令(predicated instruction),希望籍此尽量减少分支,例如上面的例子,引入名为 cmovle 的判定指令后,可以写成这样:
cmp a, 5 ; a > 5 ? mov c, b ; b = c cmovle d, b ; if le, then b = d
cmovle 的作用是“当小于或者等于的时候就进行赋值”,只有在条件为“真”的时候才会递交执行,因此被称作条件执行指令。
采用了判定指令后,原来的 5 条指令变成 3 条,避免了两条分支指令,cmp 和 mov 可以并行执行实现 50% 的性能提升,消除了分支预测错误导致的大量误预测惩罚。
ARM 从一开始具备完整的判定指令集,而 MIPS 和 x86 后了也都添加了条件赋值指令,IA64(EPIC)中几乎每条指令都具备条件执行功能。