RISCV不同阶段前递问题/load-use冒险的停顿技术/优化控制冒险发生时浪费的时钟周期问题分析/ID计算加速的专用前递通路问题/PC清理-预测加速问题/流水线清理问题

RISC-V

Posted by Bruce Lee on 2024-02-03

load-use冒险的不同阶段前递问题

穷举了load指令后跟算术运算指令和存储指令的所有冒险类型.
load指令后跟随算术指令,会发生load-use冒险中的EX冒险,即后指令在进行EX阶段就需要ld指令中RD存储的值.此时由于晚了一个周期,当后指令执行EX阶段时,ld指令还在执行MEM阶段.所以我们需要流水线停顿和前递
load指令后跟存储指令时分为两种情况:
1.当ld指令的rd和sd的rs2一致时,发生的是上诉的冒险类型,用相同的处理.
2.当ld指令的rd和sd的rs1一致时,发生的是在MEM阶段的数据冒险,只需要前递就可以了,具体分析:
ld指令的要加载的值在MEM阶段结束后,存储在MEM/WB寄存器中,sd指令的写入值,在MEM阶段的开始时,需要被数据寄存器读取.

存储指令后跟加载指令不会产生任何冒险

load-use冒险的停顿技术

只有在ld指令后的指令是在EX阶段就出现数据冒险时,才会发生停顿.
处理器中,有很多数据通路单元都是空执行的,就是所谓的执行后的数据被抛弃,不被使用.
在之前就叙述过,整个指令内部是无法停顿的,只有指令与指令之间是可以停顿的(现在想来,这句话其实是废话)
我们只要让流水线的某一个周期内,在某一个阶段是空执行的就新.但是空执行之后产生的废弃数据并没有被抛弃,而是随着流水线被写入流水线寄存器,甚至是写入了寄存器堆中.所以我们需要一个手段就是使其不可写入.
最简单直接的就是将这个空阶段的控制信号全部写0,这样就没有权限写入任何寄存器堆了.写入流水线寄存器是无所谓的,因为流水线寄存器总是可以被写入的,只是我们需要将流水线寄存器中的写入信号失效就可以了.
我们还需要将后续已经上了流水线的指令暂停,这些指令保存在流水线寄存器中,我们只需要使这个流水线寄存器不可写就行了.
流水线停顿,不仅是消耗了时钟周期,而且在实现上,也需要资源.所以我们应该尽早的发现load-use+EX冒险

应该尽早发现:IF/ID寄存器发现load-use+EX冒险

当后指令在IF/ID寄存器时,ld指令此时在ID/EX寄存器中,我们需要检测ID/EX寄存器的MemRead信号,和ID/EX.RegisterRd于IF/ID.RegisterRs1和2的比较值,然后决定是否让PC寄存器和IF/ID寄存器冻结.
当后续指令已经流水在IF/ID寄存器中时,PC寄存器已经指向了下一条地址.
我们应该将PC寄存器不可更改,然后将IF/ID寄存器不可更改.同时在下一个周期时,将0写入ID/EX寄存器的控制字段.

PC寄存器和IF/ID流水线寄存器的不可更改的实现方法

根据217的示意图,应该是实现将控制信号传入PC,ID/EX寄存器,让其不可被更改.

PC寄存器被改写的两种情况和时机

在流水线中,每一个时钟周期,都会使PC改写.
每一条指令,被读取放入IF/ID寄存器中时,PC+4已经被计算出来,送入了二选一多选器,这个多选器在默认的时候应该是选择PC+4选项.
当分支指令在计算偏移地址和条件时,结果存储在EX/MEM阶段,此时才能决定是否跳转.
同时呢,控制ID阶段多选器的branch信号也是在MEM阶段被计算出来:branch信号被赋予有效时意味着这是跳转指令,同时条件为真.

优化控制冒险发生时浪费的时钟周期问题分析

分支条件指令,如果按照正常的指令执行步骤走,在一个5级流水线中,当指令在MEM阶段才能得出PC的更改值.
当遇见分支指令时,暂停流水线,等到MEM阶段后,得出跳转结果,然后再将指令送上流水线.
这里遇见的问题是,每遇见一次分支指令,都会浪费3个周期.
从两个方面来优化计算:

分支指令EX阶段提前至ID阶段

地址计算加速

分支指令的地址计算是由当前PC值和立即数扩展相加得出的,立即数需要扩展,然后与PC相加.
为了使用更少的硬件资源:
PC值是保存在IF/ID寄存器中,可以直接取出来,不浪费时间.
立即数保存在IF/ID寄存器中也可以直接取出来,但是需要对其进行符号位扩展为64位,所以这里需要有一个立即数生成器,立即数生成器本来就在ID阶段.所以并没有添加任何的额外硬件.
使用专用的加法器更好,而不是使用ALU.
因此需要添加一个加法器.

条件计算加速

读出两个寄存器,然后将这两个寄存器进行比较运算,得出结果

用于ID计算加速的专用前递通路问题

因为分支指令需要使用到寄存器中的值,所有同样会面临数据冒险问题.
由以上的所有分析,所以我们需要为了分支条件指令在ID计算加速设计一个专用的用于ID阶段的前递数据通路单元.
这个前递单元同样需要接入来自ID/EX寄存器,EX/MEM寄存器阶段前递数据.
由寄存器堆取出,寄存器很快,不浪费时间,
同样有前递取出的数值,也是从流水线寄存器取出的,速度很快,不浪费时间.

对于比较值的计算加速

当得到两个源操作数后,比较是否相等,我们可以添加一个异或单元,然后接入一个位或单元,就可以得出结果.
这两个单元很快,不需要很多的时间,所以可以接受.

IF阶段的PC清理问题

我们知道,当ID阶段结束时,我们得出了跳转地址,此时已经有一条预测指令上了流水线,并且存储在IF/ID流水线寄存器中.
如果预测错误,我们需要对其进行清理,原文中提到,不仅需要让该指令的控制信号为0,还不能让其有任何操作动作.
在IF/ID寄存器的指令,并没有通过ID阶段计算出控制值.
我们可以使用IF.Flush信号,直接对IF/ID流水线寄存器清理.

加速之后的分支跳转指令在EX阶段执行问题

我们将分支指令提前到了ID阶段,但是此时它依然会按照正常的流水线行走,来到EX阶段,此时会重新计算比较结果和地址位置,然后在MEM阶段更改PC值.
这是一个很大的问题,因为分支指令执行了两次!
我们在PC清理问题中,使用了IF.Flush信号,对IF/ID流水线寄存器进行了清理.
同样为了避免重复执行分支指令,我们同样需要对ID/EX流水线寄存器进行清理,同时避免影响到分支指令之前的指令.

静态分支预测

一般采用总是预测不跳转或者总是预测跳转.
这是最简单的预测,但是遇见一个总是跳转代码段或者总是不跳转代码段,性能损失也是很大.
所以一般不采用静态分支预测,起码使用最简单的动态分支预测都要好很多.

动态分支预测

最简单的动态预测

使用一个分支预测缓存,这个缓存是一个比特位.记录上一次跳转结果.
由于局部原理,当上一次跳转了,这一次大概率也是要跳转的.
当比特位为1时,表示上一次跳转过了,那么这一次就预先加载目标地址.
为0时,表示上一次没跳转,那么这一次就加载默认的下一条指令.

其他的动态预测

都是基于上面的简单原理进行额外扩展的.
比如使用两个比特位来预测跳转,同时使用多个分支预测器,来预测,同时选择预测结果最好的
相关预测器和锦标赛分支预测器都是这些变体.

遇见分支指令时,PC预测加载提速问题

我们知道,只有想分析了该指令是否是分支指令,我们才能进行一系列的ID加速计算,和PC的预测加载.
如果我们需要在ID阶段才能得出这是分支指令,那么此时已经过去了一个周期,预测已经来不及了.
如果不能解决提前识别分支指令,那么动态预测就是笑话.
那么静态预测的总是跳转预测也是笑话.
识别分支指令,可能需要在指令寄存器那里添加额外的硬件检测对应字段.

预测时,加载的目标地址问题

使用分支目标缓存,用于存储上一次目标地址.
这样在决定预测跳转时,直接将上一次目标地址放在PC中.

在ARMv8中应用的减少条件分支指令的方法

使用条件移动指令
这里的条件分支指令的减少,在与将一种条件分支指令的功能类型进行了包装.
当这一类型就是条件移动指令这个功能类型.如果使用条件分支指令来实现这种功能,那么会造成很大的开销,但是得到的却很少(仅仅是将一个寄存器赋值了)

当动态预测错误时,清洗流水线问题

对于静态预测,所有的结果都是写死的,出现了错误,直接清洗就行了.
但是对于动态预测,我们如何知道流水线上加载的指令是错误的呢?
动态预测使用了分支预测缓存,当分支指令结果出来时,会首先与分支预测缓存的值进行逻辑运算,用于判断现在加载在流水线上的指令是否预测正确.如果不正确,才执行清理.


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !