概述
分支预测基于流水线技术(提升cpu吞吐量的)。
对于超标量处理器来说,准确度高的分支预测更为重要,在取指令阶段,除了需要从I-Cache中取出多条指令,同时还需要决定下个周期取指令的地址。如果这个阶段只是简单地顺序取指令,也就是预测所有的分支指令都是不执行的,那么等到在流水线的后续阶段,例如执行阶段,发现了一条可以执行的分支指令时,就需要将流水线中执行阶段之前的全部指令都从流水线中清除,并重新从正确的地址开始取指令。这些从流水线中被flush的指令都做了无用功,浪费了处理器的功耗,降低了执行效率。如果能够在取指令阶段,就可以“预知”本周期所取出的指令中是否存在分支指令,并且可以知道它的方向(跳转或者不跳转),以及目标地址(target address)的话,那么就可以在下个周期从分支指令的目标地址开始取指令,这样就不会对流水线产生影响,提高了处理器的执行效率。这种不用等到分支指令的结果真的被计算出来,提前预测结果的过程就是分支预测。
分支预测本质上是对分支指令的结果进行预测,分支指令一般包含两个要素。
(1)方向,对于一个分支指令来说,它的方向只可能有两种,一种是发生跳转(称为taken),另一种是不发生跳转(not taken)。有些分支指令是无条件执行的,例如MIPS中的jump指令,它的方向总是跳转的,对于其他的分支指令需要根据指令中携带的条件是否成立来决定是不是发生跳转,例如MIPS中的BEQ指令,只有当指定的两个值相等的时候,才会发生跳转。
(2)目标地址,如果分支指令的方向是发生跳转,就需要知道它跳到哪里,也就是它跳转的目标地址,这个目标地址也是携带在指令中。对于RISC指令集来说,目标地址在指令中可以有两种存在形式。
Ø 直接跳转(direct)。在指令中直接以立即数的形式给出一个相对于PC的偏移值(offset),当前分支指令的PC值(或者分支指令的下一条指令的PC值)加上这个偏移值就可以得到分支指令的目标地址。由于指令的长度只有32位,它限制了立即数的大小范围,因此这种类型的分支指令,它的跳转范围一般不大,但是由于需要的信息直接携带在指令中,这样就很容易计算它的目标地址,例如在流水线的decode阶段就可以将指令中的立即数分离出来,进而计算出分支指令的目标地址。由于指令所携带的立即数一般是不会变化的,因此这种类型的分支指令是容易进行分支预测的。很多处理器的技术手册上都会建议尽量使用直接跳转的分支指令,就是为了提高分支预测的正确率,从而提高处理器的性能。
Ø 间接跳转(indirect)。分支指令的目标地址来自一个通用寄存器的值,这个寄存器的编号由指令给出,这就是说,它的目标地址是32位的值,因此可以跳转到处理器程序空问中的任意地方。但是,这个通用寄存器的值一般来自其他指令的结果,因此对于分支指令来说,可能需要等待一段时间才可以得到这个目标地址,例如需要等到流水线的执行(execute)阶段,在这段时间内进入到流水线中的指令都是有可能不正确的,这就增大了分支预测失败时的惩罚(misprediction penalty)。而且由于寄存器的值是会经常变化的,因此这种分支指令很难对目标地址进行预测。庆幸的是,程序当中大部分间接跳转的分支指令都是用来调用子程序的CALL/Return类型的指令,而这种类型的指令由于有着很强的规律性,是容易被预测的。
因此,要对一条分支指令进行预测,需要对它的方向和目标地址都进行预测。对于方向预测,需要预测这条分支指令是否会发生跳转;对于目标地址的预测,需要预测分支指令在发生跳转时的目标地址。对于普通的处理器来说,由于它的流水线深度并不深,一般都是使用静态的分支预测方式,预测分支指令总是不执行,处理器总是顺序地取指令,在流水线的后续阶段,例如execute阶段,得到了分支指令实际的方向和目标地址后,再进行判断。如果分支指令需要发生跳转,则抛弃在分支指令之后进人到流水线的所有指令(或者在MIPS中,在这些流水段放上不相关的指令,即branch delay slot);如果分支指令不需要发生跳转,则继续顺序地取指令来执行,就好像这条分支指令从来都没有发生过一样。
在流水线中,分支预测越靠前越好。如果指令从ICache取出来之后才进行分支预测,那么由于ICache中取出指令的过程可能需要多于一个周期才能够完成,当得到分支预测结果时,已经有很多后续的指令进入流水线,当得到的预测值是要发生跳转时,这些指令都需要从流水线中被抹掉(flush),这样就降低了处理器的执行效率。因此分支预测的最好时机就是在当前周期得到取指令地址的时候,在取指令的同时进行分支预测,这样在下个周期就可以根据预测的结果继续取指令。对于一条指令来说,它的物理地址是会变化的(这取决于操作系统将它放到物理内存的位置),而它的虚拟地址,也就是PC值是不会变化的。因为在一个进程内,每一个PC值对应的指令是固定的,不可能出现一个PC值对应多条指令的情况,所以使用PC值进行分支预测,只不过在进行进程切换之后,需要将分支预测器中的内容进行清空,这样可以保证不同进程之间的分支预测不会互相干扰。如果使用了ASID,那么可以将它和PC值一起进行分支预测,此时就不需要在进程切换时清空分支预测器了。
在PC值刚刚产生的那个周期,根据这个PC值来预测本周期的指令组(fetch group)中是否存在分支指令,以及分支指令的方向和目标地址,这个过程如图4.4所示。
基于取指令的地址(也就是取指令的PC值)进行分支预测是有根据的,因为一旦程序开始执行,每条指令对应的取指令地址也就固定好了,因此完全可以根据一条指令的PC值来判断这条指令是否是分支指令。只要这条分支指令第一次被执行完之后,当后面再次遇到这个PC值,就可以知道当前要取的指令是分支指令。即使发生了自修改(self-modifying)的情况,也会将分支预测器清空,重新开始进行分支预测,并不会影响分支预测的过程。当然这仅仅是能够识别出它是分支指令,对于它的方向和目标地址,还需要其他的预测方法。
分支指令的方向预测
对于分支指令来说,它的方向只有两种,一种是发生跳转(taken),另一种是不发生跳转(not taken)。
现代处理器中应用最广泛的分支预测方法都是基于两位饱和计数器(2-bit saturating counter),并以之为基础,引申出的各种分支预测的方法。例如:
- 基于局部历史的分支预测,使用BHR寄存器(Branch History Register);
- 基于全局历史的分支预测,使用GHR寄存器(Global History Register);
- TAGE预测器(TAgged GEometric history length branch predictor)。为了更高的准确率,不同的分支可能需要不同的历史长度。因此,TAGE预测器用不同长度的GHRs去映射PHTs,并且自适应地选择一个最合适的PHT。用不同长度的GHRs去映射PHTs,并且自适应地选择一个最合适的PHT。其中,T0是基础预测器,2-bit 计数器表,Ti由3-bit计数器(pred)、2-bit u、tag-bit组成。
TAGE预测器
分支指令的目标地址预测
分支指令的目标地址(target address) 分为两种:直接跳转(PC-relative)和间接跳转(absolute)。
对于直接跳转的分支指令,它的偏移值(offset)以立即数的形式固定在指令当中,所以目标地址也是固定的,只要记录下这条分支指令的目标地址就可以了,当再次遇到这条分支指令时,如果方向预测的结果是发生跳转,那么它的目标地址就可以使用以前记录下的那个值;
而对于间接跳转的分支指令,目标地址来自通用寄存器,而通用寄存器的值会经常变化,所以对这种分支指令进行目标地址的预测并不容易。庆幸的是,程序中大部分间接跳转的分支指令是用来处理子程序调用的CALL和Return指令,而这两种指令的目标地址是有规律的,因此可以对其进行预测。大部分超标量处理器都会推荐编译器的设计者,除了使用CALL/Return 指令之外,尽量减少使用其他间接跳转类型的分支指令,而多使用直接跳转类型的分支指令,这样有助于处理器提高分支预测的准确度,从而提高处理器的性能。
直接跳转
对于直接跳转(PC-relative)类型的分支指令来说,它的目标地址有两种情况。
- 不发生跳转时,目标地址 = 当前分支指令的PC值 + Sizeof(fetch group)。
- 发生跳转时,目标地址 = 当前分支指令的PC值 + Sign_ extend(offset)。
分支预测器(branch predictor)除了需要对分支指令的方向进行预测之外,还需要对目标地址进行预测。对于RISC指令集,由于计算地址所需要的偏移值(offset)在指令中是立即数的形式,所以这个偏移值不会变化,因此对于一条特定的直接跳转的分支指令来说,它的目标地址不会随着程序的执行而变化(此处不考虑代码自修改的情况,因为执行完这种代码之后,需要对分支预测器进行复位)。因此对这种分支指令的目标地址进行预测是很容易的,只需要使用一个表格,记录下每条分支指令对应的目标地址,当再次对这条分支指令进行预测时,只需要查找这个表格就可以得到预测的目标地址了。
由于分支预测是基于PC值进行的,不可能对每一个PC值都记录下它的目标地址,所以一般使用Cache的形式,这个Cache称为BTB(Branch Target Buffer)。BTB的entry里存放着分支指令的目标地址和分支类型等信息。
BTB本质上是Cache,它的结构和Cache一样,使用PC值的一部分来寻址BTB(这部分称为index),PC值的其他部分作为Tag。BTB中存放着分支指令的目标地址(Branch Target Address,BTA),因为Index部分相同的多个PC值会查找到BTB中的同个地方,所以使用Tag来进行区分,当这些PC值中存在多于一条的分支指令时,就产生了冲突,这样会造成 BTB 中对应的内容被频繁地替换,影响了分支预测的准确度。
间接跳转
对于间接跳转(absolute)类型的分支指令来说,它的目标地址来自通用寄存器,是经常变化的,所以无法通过BTB对它的目标地址进行准确的预测。
在一般的程序中,CALL指令用来调用子程序,使流水线从子程序中开始取指令执行,而在子程序中,Return指令一般是最后一条指令,它将使流水线从子程序中退出,返回到主程序中的CALL指令之后,继续执行。对于很多 RISC 处理器来说,可能在指令集中并没有直接的CALL/Return 指令,而是使用其他的指令来模拟这个功能,例如在 MIPS处理器中,使用JAL指令作为CALL指令,使用“JR $31”指令作为Return指令。对于程序中一条指定的CALL指令来说,它每次调用的子程序都是固定的,即一条CALL指令对应的目标地址是固定的,因此可以使用BTB对CALL指令的目标地址进行预测。
根据Return指令的特点,可以设计一个存储器,保存最近执行的CALL指令的下一条指令的地址,这个存储器是后进先出的(Last In First Out,LIFO),即最后一次进入的数据将最先被使用,这符合Return指令和CALL指令的特点,这个存储器的工作原理和堆栈(stack)是一样的,称之为返回地址堆栈 (Return Address Stack, RAS)。
流水线技术
细节总结
主要是条件判断通常包含多条(n)指令,而对于cpu来说必须知道这n条指令都从流水线执行完毕之后得到的结果值才能确定执行哪个分支的代码。
这样直接导致了流水线的卡顿(一次的话例如下图:条件判断3条指令则需要6个时钟周期),若果外层有大数量级的循环流水线等待时间为:循环次数*6,此过程中cpu都
是在空转。
所以:在执行条件语句时,cpu/编译器会存储指令状态推断/设置默认值 保证不阻塞流水线,也就是将条件指令当做普通指令过流水线不等待,待执行完用结果跟
自己预测或者编译器默认指定结果进行对比,如果一致说明猜对了继续执行;如果不一致那么抛弃此分支指令重新加载另外分支指令执行。