分支指令的方向预测

对于分支指令来说,它的方向只有两个:发生跳转(taken)和不发生跳转(nottaken),因此可以用1 和0 来表示。

很多分支指令的方向是有规律可循的。

方式一:last-outcom prediction

               分支指令的方向预测_第1张图片

  • 其准确度,无法接受;

方式二:基于两位饱和计数器的分支预测

        基于两位饱和计数器的分支预测方法并不会马上使用分支指令上一次的结果,而是根据一条分支指令前两次执行的结果来预测本次的方向,这种方法可以用一个有着 4 个状态的状态机来表示。

       分支指令的方向预测_第2张图片

  • 特点:状态机处于饱和状态时,只有两次预测失败,才会改变预测的结果;
  • 初始状态,一般建议是strongly not taken, 或者是weakly not taken;
  • 核心理念:
    • 当一条分支指令连续两次执行的方向都一样时,那么该分支指令在第三次执行时也会有同样的方向;
    • 如果一条分支指令只是偶尔发生了一次方向的改变,那么这条分支指令的预测值不会马上跟着改变;
    • 因此这种方式的分支预测就好像是有一定时间的延迟一样,分支方向偶尔的变化将会被过滤掉。
  • 为什么不继续扩大位宽?
    • 2bits的正确率已经很高了;
    • 增加位宽后,会引起复杂度的上升和更多的存储资源,开销要远大于精度的提高;

每个PC都应该有一个饱和计数器

  • 因为每一条pc,都可能是分支指令;
  • 如果是这样,那就需要2^30 * 2 bits的空间来存储;
  • 考虑到肯定不是每条都是分支指令,因此做如下简化: 分支指令的方向预测_第3张图片
    • PHT: 记录每个PC对应的两位饱和计数器的值;其大小为:2^k * 2 bits;
    • k值的大小,对预测准确度的影响:
      • 分支指令的方向预测_第4张图片
    • 上面的update, 有三个来源:
      • 预测结果进行更新;
        • 不行,此时的预测结果可能是错误的,不能进行更新;
      • BRU执行结果出来后,进行更新;
        • 不行,因为OOO, 可能处于在错误的分支上;
      • commit阶段,进行更新;
        • 只能在这个阶段进行更新,这个时候是最准确的;
        • 不太会影响预测精度,因为两位饱和计数器,本身是有延迟的;
    • 这种方式的问题:k bits相同的PC, 对应相同的两位饱和计数器,会互相干扰;
      • 称之为:aliasing 问题;
      • 自然有中立性的别名,和破坏性的别名;
      • 虽然有问题,但是实现简单,轻微的预测准确度降低,可以接受;
    • 如何解决aliasing问题?
      • 可以使用hash的方式;
      • hash算法,可以将32位的PC,压缩成固定长度的较小值;

                     分支指令的方向预测_第5张图片

  • 局限性:准确率很难达到98%以上,现在的处理器,已经不会直接使用这种方式了; 

 方式三:基于局部历史的分支预测

  •  上述方式的问题:对于很有规律的分支指令,预测准确率可能会很低;分支指令的方向预测_第6张图片
  • 上述pattern的预测准确率为0!!, 且该准确率和初始状态有关,但是也不是很高;
  • 解决方式:增加分支历史寄存器(Branch History Register, BHR);
    • 将branch inst每次的执行结果,移位到BHR中,记录其历史状态;
    • 也称之为两级分支预测

                分支指令的方向预测_第7张图片 

  • 从上图可以看出,可能某条PC,在真正执行的时候,只会用到PHT中的几个entry;
  • 怎么知道BHR需要几个bits?
    • --找循环周期,即连续相同的数有几个bits, 则循环周期就是几个bits;
    • 11000_11000..., 循环周期为3;
  • 这种方式需要训练时间:
    • 使PHT中的饱和计数器到达饱和状态的这段时间称为训练时间(training time)
    • 在这段时间内,由于计数器没有到达饱和状态,因此分支预测的准确度是比较低的,训练时间的长短取决于BHR寄存器的位宽,一个位宽很大的BHR寄存器需要更多的时间来找出规律。
  • 这种方式的问题:
    • 每条分支指令都有自己的BHR,PHT,n bits的BHR, 需要2^n x 2bits,需要极大的空间;
    • 将每条pc的BHR组合在一起,称之为分支历史寄存器表(BHRT or BHR);
  • 解决方式:
    • 一般只使用PC的一部分来寻址BHT;
    • 使用PC的一部分,来寻址PHT;
    • 分支指令的方向预测_第8张图片
    • 当然,这种方式,也会遇到aliasing问题;
    • 极限情况下,可以将PHT简化成一个;
      • 分支指令的方向预测_第9张图片
      • 上述会有两种冲突的情况:
        • PC的k相同,此时两条PC对应同一个BHR, 也就对应同一个PHT的entry,会相互影响;
        • PC对应的BHR不同,但是其BHR的内容是一样的,对应同一个PHT entry, 也会相互影响;
      • 解决方式:对pc值做处理(可以有多种算法,此处仅举例一个);
      • 分支指令的方向预测_第10张图片
  • 总结
    •  此种方式,仅考虑分支指令,自身在过去的执行情况;
    • 如果分支指令的循环周期太长,可能需要很大的BHR,会导致很长的训练时间;PHT也会占用很多资源;
    • 如果BHR太小,又会导致不能完全体现指令的规律性;
    • 但是与基础的两位饱和计数器比较,已经是很大的进步了;

 方式四:基于全局历史的分支预测 

        这种方式,是考虑到某条分支指令,其taken与否,是与前面的分支指令的执行结果相关联;

              分支指令的方向预测_第11张图片

        上图中,b1,b2发生,则b3一定不会发生;           

  •  因此称之为基于全局历史的分支预测;
    • 新增一个寄存器,全局历史寄存器,GHR;
    • 用有限位宽的GHR, 记录最近执行的所有分支指令的结果;
      • 每遇到一条分支指令,就在commit阶段(不一定,后面会讲到),将分支指令的结果插入GHR右边,左边值抛弃;

           分支指令的方向预测_第12张图片

  • 预测过程:
    • 只使用一个GHR寄存器,记录最近所有的分支指令的结果;
    • 给每个PC设置一个PHT;
    • 执行时,根据PC,找到PHT, 再根据GHR, 找到该PHT中的entry, 得到预测结果;
    • 在commit时,再根据GHR和PC,找到该entry, 根据实际结果,更新该entry;
    • 实际情况中,无法使用,因为每个PC一个PHT,占用的空间太大了; 
  • 一些解决方式:
    • 分支指令的方向预测_第13张图片
    • 分支指令的方向预测_第14张图片
    • 4.21中的PHTS中的很多entry都没用到,因此使用图4.22的方式处理;
    • 分支指令的方向预测_第15张图片
    • 4.22中,不同PC,可能会对应相同的GHR,从而对应相同的PHT的entry,会相互影响,这里给出两种解决方式;

怎么决定采用哪种预测方式?(BHR/GHR) 

        这两种分支预测的方法各有优缺点,都有自身的局限性,有些分支指令使用基于局部历史的预测方法会有不错的效果,而有些分支指令则更适合使用基于全局历史的预测方法。可以设计一种自适应的分支预测方法,根据不同的分支指令的执行情况自动地选择这两种分支预测方法 ;

                        分支指令的方向预测_第16张图片

                                分支指令的方向预测_第17张图片

Choise PHT: CPHT, 由分支指令的PC值,来寻址对应的entry;

工作流程如下:

  • 当 P1 预测正确,P2 预测错误时(1/0),计数器减 1;
  • 当 P1 预测错误,P2预测正确时(0/1),计数器加1;
  • 当P1和P2预测的结果一样时(1/1或0/0),不管预测正确与否,计数器都保持不变。

分支预测的更新

        分支预测的更新,包括两部分:历史寄存器,即BHR, GHR,以及两位饱和计数器;

  • 历史寄存器,即BHR, GHR;

BHR的更新

        BHR记录的是当前的分支指令,自身在过去的执行情况;

        一般情况下,只有在循环体很短的时候,才会出现一条分支指令在流水线提交阶段更新BHR时,流水线上又出现了这条分支指令进行分支预测的情况;

        所以,可以采用commit阶段更新BHR,可以简化设计

GHR的更新

有三个时间点可以更新(三种方式倒着看):

  • 在fetch阶段,分支预测结果出来后,更新GHR;       
    • 综合来看,使用这种方式,可以使得后续的分支指令,用到最新的GHR, 并且这条分支指令预测失败时,即使后面的分支指令使用了GHR, 但是他们是处于错误的分支上,会被kill;                
  • 在BRU中,结果被计算出来后,更新GHR;
    • 相较于第三种,如果是普通的处理器,距离取指令阶段,时间更短,不会对后续的分支指令,产生太大的影响;
    • 但是对于超标量处理器,这个时间段内(fetch <-> exe), 会存在非常多的指令,而这些分支指令,有可能存在于错误的路径上,所以exe后,去更新GHR,也不一定是正确的,所以也存在GHR无法即使更新的问题;
  • 在commit阶段,更新GHR;
    •  问题:因为流水线很深,且是多issue的,因此,一旦分支指令退休时,再更新GHR,分支指令后面很多指令都已经进入流水线了,如果这里面存在分支指令,那么这些分支指令,没有享受到这条分支指令的结果;
    • 分支指令的方向预测_第18张图片
  • 采用第一种方式后,因为对GHR的更新,是speculative的,所以需要对GHR进行恢复;
    • commit阶段修复法;
      • 分支指令的方向预测_第19张图片
      • fetch阶段的GHR, 称之为Speculative GHR;
      • commit阶段的GHR,称之为Retired GHR;
      • 如果在commit阶段,发现分支预测失败,则直接将Retire GHR, 更新到Speculative GHR;
      • 这种方式,会导致mis-prediction penalty较大,从fetch到commit阶段的错误指令,都需要kill,流水线不能尽早的执行正确路径上的指令;
    • checkpoint修复法;
      • 分支指令的方向预测_第20张图片
      • 每条分支指令,在预测结果出来时,将原来的GHR先进行备份,备份的内容,称之为checkpoint GHR;
        • 此处的备份,不是直接copy, 而是将预测值相反的值,移入到checkpoint GHR中,这样后面mis-taken的时候,可以直接复制;
      • 考虑in-order处理器,BRU的计算结果出来后,如果是正确的,则继续执行,如果是错误的,则将这条指令之前保存的checkpoint GHR(顺序读取), 恢复到GHR中,并从对应的目标地址开始取指令;
      • 考虑ooo的处理器,分支指令是乱序执行的,即使BRU结果出来了,但是这个分支指令可能在错误的分支上,所以还是需要一个retire GHR作为补充,在commit阶段,判断是否预测正确,如果错误,则将retire GHR的值,复制到speculative GHR中;
      • 看作是commit阶段修复法的补充,增加了在流水线执行阶段的恢复; 

    

  • 两位饱和计数器;

        不管在哪个阶段,都是在commit阶段对PHT进行更新;

你可能感兴趣的:(risc-v,risc-v)