流水线CPU
简单梳理了一下书上的内容,增加了一点点自己的理解~
1 流水线概述
流水线是一种能使多条指令重叠执行的实现技术。
以洗衣系统为例,流水线可以提高洗衣系统的吞吐率,它不会缩短洗一次衣服的时间,但是当有很多衣服需要洗时,吞吐率的提高减少了完成整个任务的时间。
同样的原理也可以用于处理器,即采用流水线的方式执行指令。
RISC-V指令通常包含五个步骤:
- 从存储器中取出指令。
- 读存储器并译码指令。
- 执行操作或计算地址。
- 访问数据存储器中的操作数(如有必要)。
- 将结果写入寄存器(如有必要)。
所有的流水线阶段都需要一个时钟周期,所移流水线的时钟周期必须足够长以满足最慢的操作。
1.1 流水线冒险
1.1.1 结构冒险
**硬件不支持多条指令在同一周期执行。**因缺乏硬件支持而导致指令不能在预定的时钟周期内执行的情况。
1.1.2 数据冒险
**由于一个步骤必须等待另一个步骤完成而导致的流水线停顿叫做数据冒险。**也成为流水线数据冒险,因无法提供指令执行所需数据而导致指令不能在预期的时钟周期内执行。
例如:
ADD x19,x0,x1
SUB x2,x19,x3
在不做任何干预的情况下,这一数据冒险会严重地阻碍流水线。add指令知道第五个阶段才写结果,这将浪费三个时钟周期。
解决方法:前递或旁路。
前递
当且仅当目标阶段在时间上晚于源阶段时,前递路径才有效。
前递地效果很好,但不能满足所有的流水线停顿。当前一条指令是load x1时,前一条指令需要在第四个阶段之后才能得到数据,因此流水线需要停顿一个阶段来处理载入-使用型数据冒险,也就是流水线停顿,通常称为气泡。
1.1.3 控制冒险
**需要根据一条指令的结果做出决定,而其他指令正在执行。**也成为分支冒险,由于取到的指令并不出所需要的,或者指令地址的流向不是流水线所预期的没导致正确的指令无法在正确的时钟周期内执行。
停顿
在取出分支指令后立即停顿,一直等到流水线确定分支指令的结果并知道要从哪个地址取下一条指令为止。
这种情况当然能够有效地解决冒险,但是停顿的时间过长,会大大降低处理器的效率。
预测
总是预测条件分支指令不会发生跳转,如果预测正确,流水线将全速前进,只有条件分支指令发生跳转时,流水线才会停顿。
分支预测
预测一些分支指令发生跳转,而另一些不发生跳转。
2 流水线数据通路和控制
2.1 数据通路
数据通路的五个部分
- IF:取指令
- ID:指令译码和读寄存器堆
- EX:执行或计算地址
- MEM:数据存储器访问
- WB:写回
将单周期的数据通路划分之后:
从左到右的指令流动过程中存在两个特殊情况:
- 在写回阶段,它将结果写回位于数据通路中段的寄存器堆中。(数据冒险)
- 在选择下一PC值时,在自增PC值与MEM阶段的分支地址之间进行选择。(控制冒险)
从右到左的数据流向不会对当前指令造成影响,但会影响后续指令。
指令存储器只在第一个阶段被使用,而在其他四个阶段中允许被其他指令共享,为了保留在其他四个阶段中指令的值,必须把从指令存储器中读取的数据保存在寄存器中。因此必须将寄存器堆放置在每个阶段之间的分割线上。
- IF/ID的寄存器位宽为96位,同时存储取出的32位指令以及自增的64位PC地址。
- 在写回阶段的最后没有流水线寄存器,所有的指令都必须更新处理器中的某些状态,如寄存器堆、存储器或PC。因此,单独的流水线寄存器对于已经被更新的状态来说是多余的。
- 每条指令都会更新PC,因此PC可以被看作是一个流水线的寄存器。
LD指令
LD x1, 40(x5)
含义:x1=Memory[x5+40]
其中x5是rs1,x1是rs2
1. 取指
使用PC中的地址从存储器中读取指令,然后将指令放入IF/ID流水线寄存器中。PC的指令自增4,然后写回PC。这个PC值(自增前的)也保存在IF/ID的流水线急促请你中,以备后续的指令使用(例如beq)。计算机并不知道当前正在提取的是哪一种指令,因此它必须位任何一种指令做好准备,并且将所有可能用到的信息沿流水线传播出去。
2. 指令译码和读寄存器堆
IF/ID寄存器提供一个立即数字段,以及两个将要读取的寄存器编号。读出的值、立即数和PC都存在ID/EX流水线寄存器中。
3. 执行或地址计算
从ID/EX流水线寄存器中读取一个寄存器的值和一个符号扩展的立即数(ld指令是这样的),并且使用ALU部件将他们相加,结果储存在EX/MEM流水线寄存器中。(同时另外一个寄存器的值也存储在EX/MEM流水线寄存器中)
4. 存储器访问
从流水线中读取要写入的数据,然后将ALU计算的结果作为地址,存储器进行读操作。
5. 写回
从MEM/WB流水线寄存器中读取数据,并将它写入寄存器堆中。
事实上上图是错误的,因为在这个时候IF/ID的指令已经是加载指令之后的指令了,因此此时目标寄存器的编号是错误的。我们需要把目标寄存器编号写入到后续的流水线寄存器中,第五阶段再传回来。
SD指令
SD x1, 40(x5)
Memory[x5+40]=x1
与LD指令基本相同,只是在第四阶段读取存储器改为写存储器,而在WB之后不会进行任何操作。
【注意】
- 如果要将相关信息从一个阶段传递到下一个阶段,需要将他放进流水线寄存器中。
- 在流水线数据通路设计中的每一个逻辑部件只能在单个流水线阶段中被使用,否则就会发生结构冒险。因此,这些部件的控制只能与一个流水线阶段相关联。
2.2 流水线控制
和单时钟CPU一样,流水线处理器也需要一些控制信号来控制相关逻辑部件进行的操作。(也可以自行规定)
控制信号名称 | 1 | 0 |
---|---|---|
PCSrc | 分支目标 | PC+4 |
memread | 存储器读使能 | 存储器不读 |
memwrite | 存储器写使能 | 存储器不写 |
regwrite | regfile写使能 | regfile不写 |
ALUsrc(rs2的来源) | 来自立即数 | 来自reg |
MemtoReg | 寄存器写数据的输入值来自数据存储器 | 寄存器写数据的输入值来自ALU |
与单周期实现的情况一样,假定PC在每个时钟周期都被写入,因此PC没有单独的写入信号。同理,流水线寄存器也没有单独的写入信号,因为它们也需要在每个时钟周期都被写入。
执行阶段 | 需要设置的控制信号 |
---|---|
IF | |
ID | |
EX | ALUop ALUSrc |
MEM | Branch(也就是PCsrc) MemRead MemWrite |
WB | MemtoReg RegWrite |
控制信号在流水线寄存器中的传递:
2.3 数据冒险:前递与停顿
在这种情况下,流水线是这样的:
我们只需要理解如何将EX阶段的操作数前递出去的操作。
添加前递之后的流水线数据通路如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RtgfNeH8-1654967011588)(https://raw.githubusercontent.com/hinsew/photo/master/img/20220612000622.png)]
2.3.1 冒险
EX冒险
在开头的代码中,and所需要的x2寄存器的值需要在sub指令中被计算出来,当写回之后才能够被正确读取。也就是说,在执行and的EX阶段的时候,满足下列条件:
EX/MEM.RegisterRd=ID/EX.RegisterRs1(2) =x2
在这样的情况下,我们就需要将数据前递,也就是将sub指令中ALU计算的结果(储存在EX/MEM流水线寄存器中)前递给ALU。
因为x0寄存器的值一直都是0,因此可以在条件中加入一条寄存器编号不为0。
综上所述,条件:
-
EX/MEM.RegWrite=1
-
EX/MEM.RegisterRd!=0
-
EX/MEM.RegisterRd=ID/EX.RegisterRs1
前递控制信号:FowardA=10
(等于Rs2时,ForwardB=10)
MEM冒险
or指令要进行EX阶段所需要的操作数来自sub指令计算的结果,此时sub指令已经处于要进行WB阶段的状态。在这种情况下,ALU的计算结果已经是中间指令and的结果了,而寄存器堆还没有被写入,因此读取寄存器堆也不能够成功获得寄存器x2的值,在这种情况下就需要将MEM/WB寄存器中所存储的x2寄存器正确的值前递到ALU中。
综上所述,条件:
-
MEM/WB.RegWrite=1
-
MEM/WBRegisterRd!=0
-
MEM/WB.RegisterRd=ID/EX.RegisterRs1
前递控制信号:
前递控制信号:FowardA=01
(等于Rs2时,ForwardB=01)
WB阶段不存在数据冒险,因为我们假定ID阶段的指令读取的寄存器与WB阶段要写入的寄存器相同时,寄存器堆能够提供正确的结果。也就是说,寄存器堆提供了另一种形式的前递,只不过这种前递发生在寄存器内部。
在具体实现的过程中,可以控制时钟信号变高(posedge clk)时写入,时钟信号变低(negedge clk)时读取来完成这种内部的前递。
除此之外,还存在另一种复杂的前递。例如:
ADD x1,x1,x2
ADD x1,x1,x3
...
这一段代码需要对同一个寄存器进行读写,在这种情况下,x1的值应该是MEM阶段前递的数据,因为MEM阶段中的结果就是最近的结果。因此,最终的MEM冒险前递条件为:
-
MEM/WB.RegWrite=1
-
MEM/WBRegisterRd!=0
-
MEM/WB.RegisterRd=ID/EX.RegisterRs1
-
not(EX冒险条件)
2.3.2 停顿
在前递的过程中,我们事实上做了这样的操作:
但在ld指令中,一定要在MEM阶段进行完才能读取到正确的x2的值,但and指令又要求在ld指令执行MEM阶段,and之力量能够执行EX阶段(同一个时钟周期)开始的时候就得到正确的x2的值,这显然时不可能的。因此我们必须将流水线停顿一个时钟周期以获得正确的寄存器数据。
停顿条件:(在and指令进入IF之前停顿)
- ID/EX.MemRead=1
- ID/EX.RegisterRD=IF/ID.RegisterRs1(Rs2)
结果:stall the pipeline
停顿之后,前递指令就可以处理这个冒险了。(就会发生MEM冒险,把MEM/WB寄存器中正确的寄存器值前递给ALU)
当ID阶段被停止时,IF阶段也需要被停止,否则就会丢失一条指令。我们只需要禁止PC寄存器和IF/ID寄存器的改变就可以组织这两条指令的执行。如果这两个寄存器被保护,那么下一个时钟周期中,IF就会在此读取and下一条的指令,也就是or指令,同时在ID阶段的寄存器就会继续使用IF/ID流水线寄存器中相同的字段读寄存器,流水线就能够正常运转了。
在流水线停顿的这一个时钟周期里,从(and的)EX阶段开始的流水线的后半部分必须执行没有任何效果的指令,也就是空指令。设置空指令也很简单,只需要接触EX、MEM和WB阶段的七个控制信号,就可以产生一个不进行任何操作的指令,也就是空指令。
综上所述,加入了前递单元和冒险检测单元的数据通路如下图所示:
2.4 控制冒险
除了上述的数据冒险之外,我们还会遇到另一种情况,也就是当条件分支出现的时候。如果不进行任何操作的话,那么流水线的示意图就是这样的:
这样就错误的执行了beq和ld之间的三条指令。因此我们需要进行优化。
2.4.1 假设分支不发生
预测条件分支不发生并持续执行顺序指令流。一旦条件分支发生,已经被读取和译码的指令就被丢弃,流水线就继续从分支目标开始执行。
而当分支发生的时候,就需要清除此时正处于IF、ID、EX阶段的三条指令。
2.4.2 缩短分支延迟
把计算分支目标地址的Adder从EX阶段挪到ID阶段即可,在每一个指令中都会计算,只有在需要的时候才会跳转。
而困难的地方在于判断是否跳转本身。
相等检测单元
- 将相应为进行异或操作。
- 再对结果进行位或操作。
(异或后,一旦不同该位就为1,一旦结果的任何一位中是1,就说明两个数不相等。)
但同时也会出现两个复杂的因素:
- 在ID阶段需要将指令译码,决定是否需要将指令旁路至相等检测单元,并且完成相等测试以防止指令是一条分支指令。
- 在ID阶段分支所需的比较值需要在之后才产生。
例如:
ADD x1, x2, x3
beq x1, x5, 40
此时beq所需要的x1寄存器的值要在add指令的EX阶段完成后才能得到正确的值,而在一个时钟周期内,beq正处于ID阶段,它又需要在时钟周期开始就得到正确的x1的值,因此停顿是必须的。
为了清除IF阶段的指令,需要添加一条IF.Flush的控制线,他将IF/ID流水线寄存器中的指令字段设置为0,将已经取到的指令变成一条空指令,不进行任何操作,也不改变任何状态。
2.4.3 动态分支预测
对于五级的流水线,预测分支不发生就已经满足需要了,但对于更多级的流水线,分支预测错误的代价会增大。因此,可以进行动态分支预测。
检查指令中的地址,查看上一次该指令执行时条件分支是否发生了跳转,如果答案是肯定的,则从上一次执行的地址中取出指令。