你了解CPU吗?(四)

1.写在前面

前面我们已经介绍完了CPU的流水线的工作的原理,以及影响流水线的操作的几个因素,结构冒险、数据冒险、控制冒险。但是我没有更深层次的介绍完。现在我们需要更深层次的介绍这些东西。然后如果篇幅够的话,我们可以介绍下中断的一些知识。

2.数据冒险:前递与停顿

现在从一个更实际的例子出发,看看在程序真正执行的时候会发生什么。现在来看一个下面的指令序列,具体的指令序列如下:

sub x2,x1,x3 // Register z2 written by sub
and x12,x2,x5 // lst operand(x2) depends on sub
or x13,x6,x2 // 2nd operand(x2) depends on sub
add x14,x2,x2 // 1st(x2) & 2nd(x2) depend on sub
sd x15,100(x2) // Base(x2) depends on sub

后四条指令都想关于第一条指令(sub)中得到的存放在寄存器x2中的结果。假设寄存器x2在sub指令执行之前的值为10,在执行之后值为-20,那么程序员希望在后续指令中引用寄存器x2时得到的值为-20.

在这里插入图片描述

最后的潜在危险,当一个寄存器在同一个时钟周期内既被读取又被写入时会发生什么?我们假定写操作发生在一个时钟周期的前半部分,而读操作发生在后半部分。所以读操作会得到本周期内被写入的值。这种假定与很多寄存器堆的实现是一致的。在这种情况下不会发生数据冒险。

如上图,在第五个时钟周期之前,对寄存器X2的读操作并不能返回sub指令的结果。因此,图中的add和sd指令得到正确结果-20.但是and和or指令却会得到错误的结果是10.在这种类型的图中,每当相关线在时间线上表示为后退时,这个问题就会变得很明显。

在第三个时钟周期也就是sub指令的EX指令阶段结束时就可以得到想要的结果。那么在and和or指令中是什么时候才真正需要这个数据呢?答案是在and和or指令的EX阶段开始的时候,分别对应第四和第五个时钟周期。因此,只要可以一得到相应的数据就将其前递给等待该数据的单元,而不是等待其可以从寄存器堆中的读取出来,就可以不需要停顿地执行这段指令了。

接下来的内容,我们只考虑如何解决将EX阶段产生的操作数前递出去的问题,该数据可能是ALU或是有效地址的计算结果。这意味着当一个指令试图在EX阶段使用的寄存器是一个较早的指令在WB阶段要写入的寄存器时。我们需要将数据作为ALU的输入。

命令流水线寄存器字段是一种更精确的表示相关关系的方法。例如,ID/EX.RegisterRs1表示一个寄存器的编号,它的值在流水线寄存器ID.EX中,也就是这个寄存器堆中第一个读端口的值。该名称的第一部分,也就是点号左边,是流水线寄存器的名称;第二部分是寄存器中字段的名称。使用这种表示方法,可以得到两对冒险的名称:

1a.EX/MEM.RegisterRd = ID/EX.RegisterRs1
1b.EX/MEM.RegisterRd = ID/EX.RegisterRs2
2a.MEM/WB.RegisterRd = ID/EX.RegisterRs1
2b.MEM/WB.RegisterRd = ID/EX.RegisterRs2

在本节开头的代码中,指令序列中的第一个冒险发生在寄存器x2上,位于sub指令 sub x2,x1,x3的结果和and指令and x12,x2,x5的第一个读操作数之间。这个冒险可以在and指令位于EX阶段、sub指令位于MEM阶段时被检测到,因此这种冒险属于1a类型:

EX/MEM.RegisterRd = ID/EX.RegisterRs1 = x2

那么我们可以继续看之前的指令,来看看对应的冒险类型,具体的冒险类型如下:

  • sub指令和or指令之间存在类型为2b的冒险:

    2b.MEM/WB.RegisterRd = ID/EX.RegisterRs2 = x2
    
  • 在sub指令和add指令之间的两个相关性都不是冒险,因为在add指令的ID阶段寄存器堆已经可以提供x2的正确值了。

  • 在sub指令和sd指令之间不存在数据冒险,因为sd指令在sub指令将结果写回至x2之后才读取x2的值。

因为并不是所有的指令都会写回寄存器,所以这个策略是不正确的,它有时会不应该前递的时候也将数据前递出去。

一种简单的解决方案是检查RegWrite信号是否是有效的,检查流水线寄存器在EX和MEM阶段的WB控制字段以确定RegWrite信号是否有效。现在我们可以检测冒险了。一半的问题已经解决了,剩下一半的问题是前递正确的数据

在这里插入图片描述

如果我们可以从任何流水线寄存器而不仅仅是ID/EX中得到ALU的输入,那就可以前递正确的数据。通过在ALU的输入上添加多选器再辅以适当的控制,就可以在存在数据冒险的情况下全速运行流水线。

现在,假设需要前递的指令只有这四种形式:add、sub、and和or指令。下图是ALU和流水线寄存器在添加前递之前和之后的特写。

在这里插入图片描述

下面的图是ALU多选器的控制线的值,它选择寄存器堆的值或是被前递的值中一个。

在这里插入图片描述

现在给出检测冒险的条件以及解决相应的控制信息:

  1. EX冒险

    if(EX/MEM.RegWrite and (EX/MEM.RegisterRd != 0) and (EX/MEM.RegisterRd = ID/EX.RegisterRs1)) ForwardA = 10
    
    if(EX/MEM.RegWrite and (EX/MEM.RegisterRd != 0) and (EX/MEM.RegisterRd = ID/EX.RegisterRs2)) ForwardB = 10
    

    这种情况是将前一条指令的结果前递到任何一个ALU的输入中。如果前一条指令想要写寄存器堆,并且将要写的寄存器的编号与ALU输入口A或B要读取的寄存器编写一致(前提是该寄存器编号不为0),那么就控制多选择器直接从EX/MEM流水中取值。

  2. MEM冒险

    if(MEM/WB.RegWrite and (MEM/WB.RegisterRd != 0) and (MEM/WB.RegisterRd = ID/EX.RegisterRs1)) ForwardA = 01
    
    if(MEM/WB.RegWrite and (MEM/WB.RegisterRd != 0) and (MEM/WB.RegisterRd = ID/EX.RegisterRs2)) ForwardB = 01
    

    正如上文所述,在WB阶段不存在冒险,因为我们假定在ID阶段的指令读取的寄存器与WB阶段要写入的寄存器相同时,寄存器堆能够提供正确的结果。也就是说,寄存器堆提供了另外一种形式的前递,只不过这种前递发生在寄存器堆内部。

    一种复杂的潜在数据冒险是在WB阶段指令的结果、MEM阶段指令的结果和ALU阶段指令的源操作数之间发生的。例如:在一个寄存器中对一组数据做求和操作时,一系列的指令将会读和写一个相同的寄存器:

    add x1,x1,x2
    add x1,x1,x3
    add x1,x1,x4
    

    在这种情况下,结果应该是来自MEM阶段前递数据,因为MEM阶段中的结果就是最近的结果。因此MEM阶段中的结果就是最近的结果。因此,MEM冒险的控制应该是:

    if(MEM/WB.RegWrite and (MEM/WB.RegisterRd != 0) and not(EX/MEM.Register and (EX/MEM.RegisterRd != 0) and (EX/MEM.RegisterRd = ID/EX.RegisterRS1)) and (MEM/WB.RegisterRd = ID/EX.RegisterRs1)) ForwardA = 01
    
    if(MEM/WB.RegWrite and (MEM/WB.RegisterRd != 0) and not(EX/MEM.Register and (EX/MEM.RegisterRd != 0) and (EX/MEM.RegisterRd = ID/EX.RegisterRS2)) and (MEM/WB.RegisterRd = ID/EX.RegisterRs1)) ForwardB = 01
    

在这里插入图片描述

为了支持前递EX阶段的结果,这个操作所需要添加的硬件。注意EX/MEM.RegisterRd字段是ALU指令或者加载指令的目标寄存器。

3.数据冒险与停顿

当一条指令试图在加载指令写入一个寄存器之后读取这个寄存器时,前递不能解决此处的冒险。当加载指令后跟着一条需要读取加载指令结果的指令时,流水线必须被阻塞以消除这些指令组合带来的冒险。

因此,除了一个前递单元外,还需要一个冒险检测单元。该单元在ID流水线阶段操作,从而可以在加载指令和相关加载指令结果的指令之间加入一个流水线的阻塞。这个单元检测加载指令,冒险控制单元的控制逻辑满足如下条件:

if (ID/EX.MemRead and ((Id/EX.RegisterRd = IF/ID.RegisterRs1) or(ID/EX.RegisterRd = IF/ID.RegisterRs2))) stall the pipeline

我们在加载指令和R型指令中使用RegisterRd也就是指令的7至11位。第一行测试是为了查看指令是否是加载指令,只有加载指令需要读取数据存储器。接下来的两行检测在EX阶段的加载指令的目标寄存器是否与ID阶段的指令中某一个源寄存器相匹配。如果条件成立,指令会停顿一个时钟周期。在一个时钟周期后,前递逻辑就可以处理这个相关并继续执行程序了。

如果处于ID阶段的指令被停顿了,那么在IF阶段中指令也一定被停顿,否则已经取到的指令就会丢失。只需要简单地禁止PC寄存器和IF/ID流水线寄存器的改变就可以阻止这两条指令的执行。如果这些寄存器被保护,在IF阶段的指令就会继续使用相同的PC值取指令,同时在ID阶段的寄存器就会继续使用IF/ID流水线寄存器中相同的字段读寄存器。再回到我们的洗衣例子中,这就像是你重新开启洗衣机洗相同的衣服并且让烘干机继续空转一样。当然,就像烘干机那样,EX阶段开始的流水线后半部分必须执行没有任何效果的指令,也就是空指令

那么如何在流水线中插入空指令?解除EX、MEM和WB阶段的七个控制信号就可以产生一个没有任何操作的指令,也就是空指令。通过识别ID阶段的冒险,我们可以通过将ID/EX流水线寄存器中EX、MEM和WB的控制字段设置为0来向流水线中插入一个气泡。这些不会产生负面作用的控制值在每个时钟周期向前传递并产生适当的效果,在控制值均为0的情况下,不会有寄存器或者存储器被写入数据。

具体的实现细节如下:

在这里插入图片描述

and指令所在的流水线执行槽变成nop指令,并且所有在and指令之后的指令都被延后了一个时钟周期。就像水管中出现了一个气泡那样,这个停顿气泡延后了它之后的所有的指令的执行,并且随着每个时钟周期沿着流水线及程序前进,直到其退出流水线。在上面的例子中,在冒险使得and指令和or指令在第4个时钟周期内重复了它们在第3个时钟周期内做过的事情:and指令读寄存器和解码啊,or指令从指令存储器中重新取了一谝指令。这种重复看起来就像是停顿一样,它的影响是拉伸了and指令和or指令,并且延后了取第2个and指令的时间。

在这里插入图片描述

和原来一样,前递单元控制ALU多选择器,用相应的流水线寄存器中的值替换通用寄存器中的值。冒险检测单元控制PC和IF/ID流水寄存器的写入,以及在实际控制值和全0之间选择的多选器。如果加载-使用冒险被检测为真,则冒险检测单元会停顿并清除所有控制字段。

重点:尽管编译器通常依赖于硬件来解决冒险并保证指令正确执行,但编译器仍然需要理解流水线以获得最优性能。否则,未预料到的停顿就会降低编译后代码的性能。

4.控制冒险

我们先看如下的图,具体的如下:

在这里插入图片描述

控制冒险就是计算机对分支的执行的结果不确定性,而无法决定流水线的下一条指令的执行的地址。从而导致流水线的失效。

4.1假设分支不发生

阻塞流水线直到分支完成的策略非常耗时。一种提升分支阻塞效率的方法是预测条件分支不发生并持续执行顺序指令流。一旦条件分支发生,已经被读取和译码的指令就将被丢弃,流水线继续从分支目标处开始执行。如果条件分支不发生的概率是50%,同时丢弃指令的代价又很小,那么这种优化方式可以减少一半由控制冒险带来的代价。

想要丢弃指令,只需要将初始控制值变为0即可,这与指令停顿以解决加载-使用的数据冒险类似。不同的是,丢弃指令的同时也需要改变当分支指令到达MEM阶段时IF、ID和EX阶段的三条指令;而在加载-使用的数据停顿中,只需要将ID阶段的控制信号变为0并且将该阶段的指令从流水线中过滤出去即可。丢弃指令,意味着我们必须能够将流水线中IF、ID和EX阶段中的指令都清除。

4.2缩短分支延迟

一种提升分支性能的方式是减少发生分支时所需要的代价。到目前为止,我们假定分支所需的下一PC值在MEM阶段才能被获取,但如果我们将流水线中的条件分支指令提早移动执行,就可以刷新更少的指令。需将分支决定向前移动,需要两个操作提早发生:计算分支目标地址和判断分支条件。其中,将分支地址提前进行计算是相对简单的。在IF/ID流水线寄存器中已经得到了PC值和立即数字段,所以只需将分支地址从EX阶段移动到ID阶段即可。当然,分支地址的目标计算将会在所有指令中都执行,但只有在需要时才会被使用。

困难的部分是分支决定本身。对于相等时跳转指令,需要在ID阶段比较两个寄存器中的值是否相等。相等的判断方法可以是先将相应位进行异或操作,再对结果按位进行或操作。将分支检测移动到ID阶段还需要额外的前递和冒险检测硬件,因为分支可能依赖还在流水线中的结果,在优化后依然要保证运行正确。

同时这儿还有两个复杂的因素:

  1. 在ID阶段需要将指令译码,决定是否需要将指令旁路至相等检测单元,并且完成相等测试以防指令是一条分支指令,此时可以将PC设置为分支目标地址。对分支指令的操作数进行前递的操作原先是由ALU前递逻辑处理的,但是在ID阶段引入相等检测单元后就需要添加新的前递逻辑。需要注意的是,旁路获得的分支指令的源操作数既可以从EX/MEM流水线寄存器中获得,也可以从MEM/WB流水线寄存器中获得。
  2. 在ID阶段分支比较所需的值可能在之后才会产生,因此可能会产生数据冒险,所以指令停顿也是必须的。

尽管这很困难,但是讲条件分支指令的执行移动到ID阶段的确是一个有效的优化,因为这将分支发生时的代价减轻至只有一条指令,也就是分支发生时正在取的那条指令。

为了清除IF阶段的指令,我们添加了一条称为IF.Flush的控制线,它将IF/IF流水线寄存器中指令字段设置为0.将寄存器清空的结果是将已经取到的指令转换成一条nop指令,该指令不进行任何操作,也不改变任何状态。

我们来看下面的一个例子,具体的汇编的指令如下:

36 sub x10,x4,x8
40 beq x1,x3,16 // PC-relative branch to 40 + 16 * 2 =72
44 and x12,x2,x5
48 or x13,x2,x6
52 add x14,x4,x2
56 sub x15,x6,x7
...
72 ld x4,50(x7)

具体的图如下:

在这里插入图片描述

在第3个时钟周期的ID阶段决定分支执行必须被执行,因此选择72作为下一PC跳转地址,并且将下个时钟周期获取到的指令置0.第4个时钟周期显示了地址72中的指令被获取,并且因为分支发生而在流水线中产生了一个气泡或者nop指令。

4.3动态分支预测

假定条件分支不发生是一种简单的分支预测形式。在这种形式下,我们预测分支不发生,并在预测错误时清空流水线。对于简单的五级流水线来说,这种方法再结合基于编译的预测,就基本足够了。而对于更深的流水线,从时钟周期的角度来说,分支预测错误的代价会增大。与之相似,对于多发射的情况,从指令丢失的角度来说,分支预测错误的代价也会增大。两者组合起来意味着在一个激进的流水线中,简单的静态预测机制机会在性能上造成非常多的浪费。

于是有了一种另外的方法,检查指令中的地址,查看上一次该指令执行时条件分支是否发生了跳转,如果答案是肯定的,则从上一次执行的地址中取出指令。这种技术称为动态分支预测。

这种方法的一种实现方案是采用分支预测缓存或分支历史表。分支预测缓存是一块按照分支指令的低位地址定位的小容量存储器。这块存储器包含了一个比特,用于表明一个分支最近是否发生了跳转。

该预测使用一种最简单的缓存,事实上,我们并不知道该预测是否是正确的这个位置可能已经被另一条拥有相同低位地址的条件分支指令的跳转的状态所替换。不过,这并不会影响这种预测方法的准确性。预测只是一种我们希望是正确的假设,所以我们会在预测发生的方向上进行取舍。如果这个假设最终证明是错误的,这个不正确的预测指令就会被删除,它的预测位也会被置为相反值,之后正确的指令序列会被取值并执行。

这种1位的预测机制在性能上有一个缺点:即使一个条件分支总是发生跳转,但一旦其不发生跳转时,就会造成两次预测错误,而不是只造成一次错误。尤其是循环中发生。

所以我们需要的是两位的分支预测机制。具体的如下:

在这里插入图片描述

2位动态预测机制仅仅使用特定分支的信息。研究表明,对于相同的预测位来说,同时使用局部分支和最近执行分支的全局行为的信息能够获得更好的预测准确率。这种预测器被称为相关预测器。一个典型的相关预测器杜宇每个分支都提供2位预测器,预测器之间的选择基于分支的上一次执行时跳转还是不跳转。因此,全局分支行为可以被看作在预测查找表中添加了一个额外的索引位。

另一种分支预测方法是使用锦标赛预测器。锦标赛分支预测器对于每个分支使用多种预测器对于每个分支使用多种预测器,并最终给出一个最佳的预测结果。典型的锦标赛预测器对每个分支地址使用两个预测:一个基于局部信息,而另一个基于全局分支行为。一种选择器用于选择采取哪一个预测器的信息进行预测。这个选择器的操作与1位或2位预测器类似,选择两个预测器中更准确的那个。

最终的图如下:

在这里插入图片描述

5.写在最后

我们逐步解释了指令的流水化,从单时钟周期数据通路开始,之后加入了流水线寄存器、前递路径、数据冒险检测、分支预测以及在分支预测错误或加载-使用数据冒险清楚指令的机制。由于篇幅的原因,这篇博客到此就结束了,下一篇博客我们要介绍另外一种控制冒险中断。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值