将陆续上传本人写的新书《自己动手写CPU》(尚未出版),今天是第15篇,我尽量每周四篇
上一章建立了原始的OpenMIPS五级流水线结构,但是只实现了一条ori指令,从本章开始,将逐步完善。本章首先讨论了流水线数据相关问题,然后修改OpenMIPS以解决该问题,并在5.3节验证了解决效果。接着对逻辑、移位操作与空指令的指令格式、用法、作用进行了一一说明,在5.5节通过扩展OpenMIPS实现了这些指令,最后编写测试程序,对实现效果进行了检验。
我们在第4章实现的五级流水线结构很简单,如果按照“简单即美(Simple is Beautiful)的标准,那么我们的流水线是美的,但是不完美,因为现实往往是复杂的,一个简单的流水线是解决不了如此多的现实问题的,本节探讨的数据相关问题就是其中一个问题。在我们实现逻辑、移位操作等其它指令之前,必须先讨论这个问题,因为这个问题已经影响到测试程序的编写了。
流水线中经常有一些被称为“相关”的情况发生,它使得指令序列中下一条指令无法按照设计的时钟周期执行,这些“相关”会降低流水线的性能。流水线中的相关分为三种类型。
(1)结构相关:指的是在指令执行的过程中,由于硬件资源满足不了指令执行的要求,发生硬件资源冲突而产生的相关。比如:指令和数据都共享一个存储器,在某个时钟周期,流水线既要完成某条指令对存储器中数据的访问操作,又要完成后续的取指令操作,这样就会发生存储器访问冲突,产生结构相关。
(2)数据相关:指在流水线中执行的几条指令中,一条指令依赖于前面指令的执行结果。
(3)控制相关:指流水线中的分支指令或者其他需要改写PC的指令造成的相关。
结构相关、控制相关将在后续指令分析中讨论,本节重点讨论数据相关的问题。流水线数据相关又分为三种情况:RAW、WAR、WAW。
对于第4章建立的原始OpenMIPS五级流水线而言,从ori指令的实现过程可以知道,只有在流水线回写阶段才会写寄存器(实际上其余指令也是一样的,在后面实现其余指令时,对这一点会更加清楚),因此不存在WAW相关。又因为只能在流水线译码阶段读寄存器、回写阶段写寄存器,所以不存在WAR相关,所以OpenMIPS的流水线只存在RAW相关。RAW相关有三种情况。
(1)相邻指令间存在数据相关
考虑如下代码。
1 ori $1,$0,0x1100 # $1 = $0 | 0x1100 = 0x1100
2 ori $2,$1,0x0020 # $2 = $1 | 0x0020 = 0x1120
第1条ori指令会写寄存器$1,随后的第2条ori指令需要读出$1的数据,但是第1条ori指令在回写阶段才会将其运算结果写入$1,而第2条ori指令在译码阶段就需要读取$1的值,此时第1条ori指令还处于执行阶段,所以得到的必然不是第1条ori指令计算得出的结果,按这个值运算,必然会出错。如图5-1所示。这种情况可以称为相邻指令间存在数据相关,针对OpenMIPS具体情况,也可以称为流水线译码、执行阶段存在数据相关。
(2)相隔1条指令的指令间存在数据相关
考虑如下代码。
1 ori $1,$0,0x1100 # $1 = $0 | 0x1100 = 0x1100
2 ori $3,$0,0xffff # $3 = $0 | 0xffff = 0xffff
3 ori $2,$1,0x0020 # $2 = $1 | 0x0020 = 0x1120
第1条ori指令会写寄存器$1,第3条ori指令在译码阶段需要读取寄存器$1,此时第1条ori指令还处于访存阶段,所以得到的必然也不是正确的值。如图5-2所示。这种情况可以称为相隔1条指令的指令间存在数据相关,针对OpenMIPS具体情况,也可以称为流水线译码、访存阶段存在数据相关。
(3)相隔2条指令的指令间存在数据相关
考虑如下代码。
1 ori $1,$0,0x1100 # $1 = $0 | 0x1100 = 0x1100
2 ori $3,$0,0xffff # $3 = $0 | 0xffff = 0xffff
3 ori $4,$0,0xffff # $4 = $0 | 0xffff = 0xffff
4 ori $2,$1,0x0020 # $2 = $1 | 0x0020 = 0x1120
第1条ori指令会写寄存器$1,第4条ori指令在译码阶段需要读取寄存器$1,此时第1条指令处于回写阶段,在回写阶段最后的时钟上升沿才会将运算结果写入$1,所以第4条ori指令得到的不是正确的寄存器$1的值。如图5-3所示。这种情况可以称为相隔2条指令的指令间存在数据相关,针对OpenMIPS具体情况,也可以称为流水线译码、回写阶段存在数据相关。
其中相隔2条指令存在数据相关(即流水线译码、回写阶段存在数据相关)这种情况,在第4章设计的Regfile模块中已经得到了解决,Regfile模块部分代码如下。
module regfile(
......
);
......
/****************************************************************
*********** 第三段:读端口1的读操作 *********
*****************************************************************/
// raddr1是读地址、waddr是写地址、we是写使能、wdata是要写入的数据
always @ (*) begin
......
end else if((raddr1 == waddr) && (we == `WriteEnable)
&& (re1 == `ReadEnable)) begin
rdata1 <= wdata;
......
end
/****************************************************************
*********** 第四段:读端口2的读操作 *********
*****************************************************************/
// raddr2是读地址、waddr是写地址、we是写使能、wdata是要写入的数据
always @ (*) begin
......
end else if((raddr2 == waddr) && (we == `WriteEnable)
&& (re2 == `ReadEnable)) begin
rdata2 <= wdata;
......
end
endmodule
在读操作中有一个判断,如果要读取的寄存器,是在下一个时钟上升沿要写入的寄存器,那么就将要写入的数据直接作为结果输出。如此就解决了相隔2条指令存在数据相关的情况。
对于相邻指令间存在数据相关、相隔1条指令的指令间存在数据相关这两种情况,有三种解决方法。
(1)插入暂停周期:当检测到相关时,在流水线中插入一些暂停周期,如图5-4所示。
(2)编译器调度:编译器检测到相关后,可以改变部分指令的执行顺序,如图5-5所示。
(3)数据前推:将计算结果从其产生处直接送到其他指令需要处或所有需要的功能单元处,避免流水线暂停。如图5-6所示的例子,新的$1值实际在第1条ori指令的执行阶段已经计算出来了,可以直接将该值从第1条ori指令的执行阶段送入第2条ori指令的译码阶段,从而使得第2条ori指令在译码阶段得到$1的新值。也可以直接将该值从第1条ori指令的访存阶段送入第3条ori指令的译码阶段,从而使得第3条ori指令在译码阶段也得到$1的新值。
读者需要注意,第(3)种方法有一个前提就是新的寄存器的值可以在执行阶段计算出来,如果是加载指令,那么就不满足这个前提,因为加载指令在访存阶段才能获得最终结果,这是一种load相关,本书将在实现加载存储指令的时候考虑这种情况,本章暂不考虑。
下一次将介绍OpenMIPS对数据相关问题的解决措施,敬请关注!