一、实验目标:
- 了解MIPS的五级流水线,和在运行过程中的所产生的各种不同的流水线冒险;
- 通过指令顺序调整,或旁路与预测技术来提高流水线效率;
- 更加了解流水线细节和其指令的改善方法;
- 更加熟悉MIPS指令的使用。
二、实验内容
- 给出一段代码(最好是矩阵加法或有意义的代码),带有明显的数据相关,要求通过流水线执行来发现数据相关处;记录统计信息(Statistics窗口的信息);
- 对初始代码进行指令序列调整,以获得性能提升,记录统计信息(Statistics窗口的信息);
- 启动forward功能以获得性能提升,记录统计信息(Statistics窗口的信息);
- 用循环中的连续乘法做功能冲突展示,然后将后续无关指令填充到他们之间,实现优化。
四、实验环境
硬件:桌面PC
软件:Windows,WinMIPS64仿真器
五、作业一:数据相关优化
原始代码的数据相关情况
C代码说明:
给定一个简单的C代码片段,实现两个4x4矩阵的相加操作。矩阵 A 和矩阵 B 相加得到矩阵 C,具体代码如下:
for(int i = 0; i < 4; i++) for(int j = 0; j < 4; j++) C[i][j] = A[i][j] + B[i][j]; |
转为MIPS语言:
根据上述的C代码,我们将其转换成MIPS语言,然后运行,并进行分析。
MIPS代码如下:
.data a: .word 1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4 b: .word 4,4,4,4,3,3,3,3,2,2,2,2,1,1,1,1 c: .word 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 len: .word 4 control: .word32 0x10000 data: .word32 0x10008 .text start: daddi r17,r0,0 daddi r21,r0,a daddi r22,r0,b daddi r23,r0,c ld r16,len(r0) loop1: slt r8,r17,r16 beq r8,r0,exit1 daddi r19,r0,0 loop2: slt r8,r19,r16 beq r8,r0,exit2 dsll r8,r17,2 dadd r8,r8,r19 dsll r8,r8,3 dadd r9,r8,r21 dadd r10,r8,r22 dadd r11,r8,r23 ld r9,0(r0) ld r10,0(r10) dadd r12,r9,r10 sd r12,0(r11) daddi r19,r19,1 j loop2 exit2: daddi r17,r17,1 j loop1 exit1: halt |
将代码加载到WinMIPS64中:
先用asm.exe检验一下程序的正确性,在终端中输入.\asm.exe .\exp1.s,得到如下的结果,没有错误(0 errors),即编译非常顺利。
然后将这段代码加载到WinMIPS64中进行模拟运行,需要注意的是实验前请保证winMIPS64配置中“Enable Forwarding”没有选中。将“Enable Forwarding”选项取消选中是为了清楚地观察程序在没有转发(数据旁路)的情况下如何产生和处理数据相关。
从Statistic窗口记录可以看到本程序运行过程中总共产生了多少次RAW的数据相关。运行结果从Statistic窗口(如图4)可以看出,本程序运行过程中总共产生了220处RAW的数据相关,且有509个周期。接下来,我们对产生数据相关的代码逐个分析,请列出产生数据相关的代码,并在下一步中进行分析和优化。
指令序列调整之后的数据相关情况
r16存储指令提前:
原代码中,r16 在 len(r0) 加载完成后立即参与loop1的运算,造成数据相关。将 ld r16,len(r0) 提前到初始化代码之前,使其在进入循环前就完成数据加载,避免后续指令等待,就能避免此数据冲突。
修改前:
start: daddi r17,r0,0 daddi r21,r0,a daddi r22,r0,b daddi r23,r0,c ld r16,len(r0) # 发生数据冒险 loop1: slt r8,r17,r16 |
修改后:
start: ld r16,len(r0) # 将冲突的指令提前,防止阻塞 daddi r17,r0,0 daddi r21,r0,a daddi r22,r0,b daddi r23,r0,c loop1: slt r8,r17,r16 |
beq指令滞后:
在 loop1 循环中,slt 指令对 r8 赋值后立即被 beq 使用,导致数据相关,发生了数据冒险。可以将 beq r8,r0,exit1 放在 daddi r19,r0,0 之后。由于 beq 跳转到 exit1 后直接终止循环,不影响逻辑正确性,所以可将beq指令滞后。
修改前:
loop1: slt r8,r17,r16 beq r8,r0,exit1 # 发生数据冒险 daddi r19,r0,0 |
修改后:
loop1: slt r8,r17,r16 daddi r19,r0,0 beq r8,r0,exit1 # 将冲突的指令滞后,防止阻塞 |
不冲突指令提前:
在 loop2 中,slt r8,r19,r16 和 beq r8,r0,exit2 存在数据相关。即便开启前推也无法完全消除阻塞。具体原因如下:
- 前推只能消除执行阶段的等待:前推主要作用于指令在执行阶段的结果传递,当指令执行完成后,立即将结果传递给下一条依赖指令使用,而不是等待结果写回寄存器。这样消除了部分等待时间。
- 分支指令和依赖的数据:在代码中,beq 指令依赖于前一条指令 slt 的结果来判断是否跳转。因为 beq 指令需要等待 slt 完成,并获取其计算结果(r8 的值)后才能决定是否跳转。而这类分支判断属于控制依赖,与普通的算术和逻辑指令不同,前推无法提前预测 beq 的执行结果。
- 控制冒险导致的延迟:即使在有前推的情况下,分支指令(如 beq)与条件判断(如 slt)之间的依赖关系会产生控制冒险。即使前推能够快速传递 slt 的结果给 beq,处理器仍然需要在判断是否跳转之前等待 slt 执行完成。因此,这种控制依赖关系在有前推的情况下也会产生阻塞。
因此,这种情况下,前推无法完全消除阻塞。我们可以将 daddi r19,r19,1 提前至 slt 和 beq 之间。daddi r19,r19,1 与其他指令无冲突,可以提前。通过将无依赖的指令插入其间,来减少控制冒险带来的等待。
修改前:
loop2: slt r8,r19,r16 beq r8,r0,exit2 # 发生数据冒险
dsll r8,r17,2 dadd r8,r8,r19 dsll r8,r8,3
dadd r9,r8,r21 dadd r10,r8,r22 dadd r11,r8,r23
ld r9,0(r0) ld r10,0(r10) dadd r12,r9,r10 sd r12,0(r11) daddi r19,r19,1 # 可提前的不冲突指令 j loop2 |
修改后:
loop2: slt r8,r19,r16 daddi r19,r19,1 # 将后面不冲突指令提前 beq r8,r0,exit2 |
前推消除阻塞:
在 loop2 内,dsll r8,r17,2 和后续对 r8 的多次操作会导致数据相关。由于观察之后不能进行顺序调换,而开启前推功能可以在写回之前,将计算结果直接传递给依赖指令,减少等待时间。因而无需调整指令顺序,阻塞自然消除。
loop2: slt r8,r19,r16 daddi r19,r19,1 # 将后面不冲突指令提前 beq r8,r0,exit2 # 无数据冒险 dsll r8,r17,2 # 开启前推 dadd r8,r8,r19 # dsll r8,r8,3 # |
不冲突指令滞后:
ld r10,0(r10) 和 dadd r12,r9,r10 存在取数-使用数据冒险。即使开启前推,仍会发生阻塞。因为前推可以让算术或逻辑指令的结果在执行后立即传递给依赖指令,但无法加速内存访问。由于 ld 访问了内存数据,即使在结果一生成后就传递给 dadd,数据也无法在 ld 完成之前传递,依赖指令 dadd 依然会等待 ld 指令完成,导致阻塞。因此可以将 dadd r11,r8,r23 指令延后,使其插入到 ld r10 和 dadd r12 之间,从而消除数据冒险。
后面的r12的存储也发生了冲突,dadd r12, r9, r10 和 sd r12, 0(r11) 之间也存在数据依赖。sd 依赖于 dadd 计算的结果存储在 r12 中。但开启前推之后就不会发生阻塞了。因为 dadd r12, r9, r10 是一个算术运算指令,开启前推后,dadd 的结果会在其执行阶段结束时直接传递给 sd。这样一来,sd 无需等待 dadd 指令写回 r12 的结果,而是可以直接使用 dadd 的计算结果进行存储操作。
修改前:
dadd r9,r8,r21 dadd r10,r8,r22 dadd r11,r8,r23 # 可滞后的不冲突指令 ld r9,0(r0) ld r10,0(r10) dadd r12,r9,r10 # 发生数据冒险 sd r12,0(r11) # 发生数据冒险 |
修改后:
dadd r9,r8,r21 dadd r10,r8,r22 ld r9,0(r9) ld r10,0(r10) dadd r11,r8,r23 # 将前面不冲突指令滞后 dadd r12,r9,r10 sd r12,0(r11) # 开启前推可消除此阻塞 |
不开启前推,重新运行:
在调整指令顺序后,关闭前推功能再次运行,结果显示总共发生185次RAW数据相关,共有479个周期,如图5所示,相较于优化前(220次)减少了35次RAW相关,减少了30个周期。优化结果正确。
Forwarding功能开启之后的数据相关情况
开启Forwarding功能:
点开configure下拉窗口,给Enable Forwarding选项左侧点上勾。
再次运行程序:
开启了Forwarding功能之后,我们再次运行,查看结果。可以观察到RAW下降到了0次,时间周期上也减少到了294(最初为509个时间周期)。
六、作业二:结构相关优化
原始代码的结构相关情况
流水线中的结构相关,指的是流水线中多条指令在同一时钟周期内争用同一功能部件现象。即因硬件资源满足不了指令重叠执行的要求而发生的冲突。
在WinMIPS64中,我们可以在除法中观察到这种现象。要消除这种结构相关,我们可以采取调整指令位置的方法进行优化。在这个部分,我们首先给出几条C代码,然后将该代码翻译成MIPS代码(为了观察的方便,我们这里MIPS代码并不是逐一翻译,而是调整代码,使得其他部分数据相关已经优化,而两条除法指令连续出现),运行并查看结果。接着,调整代码序列,重新运行。观察优化效果。
C代码:
下面是给出的C代码:
a = a / b c = c / d e = e + 1 f = f + 1 g = g + 1 h = h + 1 i = i + 1 j = j + 1 |
原始MIPS代码:
根据上述的C代码,实验指导中给出数据相关优化的指令如下。
.data a: .word 12 b: .word 3 c: .word 15 d: .word 5 e: .word 1 f: .word 2 g: .word 3 h: .word 4 i: .word 5
.text start: ld r16,a(r0) ld r17,b(r0) ld r18,c(r0) ld r19,d(r0) ld r20,e(r0) ld r21,f(r0) ld r22,g(r0) ld r23,h(r0) ld r24,i(r0) ddiv r16,r16,r16 ddiv r18,r18,r19 daddi r20,r20,1 daddi r21,r21,1 daddi r22,r22,1 daddi r23,r23,1 daddi r24,r24,1 halt |
运行原始MIPS代码:
通过观察,我们可以发现,两个连续的除法产生了明显的结构相关,第二个除法为了等待上一个除法指令在执行阶段所占用的资源,阻塞了9个周期。
显然,这样的连续的除法所导致的结构相关极大的降低了流水线效率,为了消除结构相关,我们需要做的是调整指令序列,将其他无关的指令塞入两条连续的除法指令中。
给出指令序列的调整方案并给出流水线工作状态的截图,做出解释。
修改原始MIPS代码:
但是通过检查代码,实际给出的MIPS代码与C语言代码的含义并不相符,所以我将其补充完整了,修改后的MIPS代码如下。
- 数据段中新增变量声明:添加了 j: .word 6,初始化变量 j 的值为 6。
- 在文本段中加载变量 j 的值:增加了一条指令 ld r25,j(r0),将变量 j 的值加载到寄存器 r25 中。
- 在文本段中增加对 j 的自增操作:增加了指令 daddi r25,r25,1,即 r25 = r25 + 1,实现 j = j + 1 的操作。
- 在文本段中增加对变量各个变量的存储操作:增加了 sd 指令将寄存器中的结果存储到各个变量中。
修改后:
.data a: .word 12 b: .word 3 c: .word 15 d: .word 5 e: .word 1 f: .word 2 g: .word 3 h: .word 4 i: .word 5 j: .word 6 # 定义整数变量 j,初始值为 6 .text start: ld r16,a(r0) ld r17,b(r0) ld r18,c(r0) ld r19,d(r0) ld r20,e(r0) ld r21,f(r0) ld r22,g(r0) ld r23,h(r0) ld r24,i(r0) ld r25,j(r0) # 将变量 j 的值加载到寄存器 r25 ddiv r16,r16,r17 # r16 = r16 / r17,即 a = a / b ddiv r18,r18,r19 daddi r20,r20,1 daddi r21,r21,1 daddi r22,r22,1 daddi r23,r23,1 daddi r24,r24,1 daddi r25,r25,1 # r25 = r25 + 1,即 j = j + 1 sd r16,a(r0) # 将结果存储到变量中 sd r18,c(r0) sd r20,e(r0) sd r21,f(r0) sd r22,g(r0) sd r23,h(r0) sd r24,i(r0) sd r25,j(r0) halt |
运行修改后MIPS代码:
程序运行前将configure->architecture->division latency改为10,原因是为了模拟除法指令的执行延迟,也就是指令 ddiv r16, r16, r17 和 ddiv r18, r18, r19 所需的周期数。在 MIPS 体系结构中,除法操作通常比加法、减法等简单算术运算要慢得多,因此需要额外的周期数来完成。设置除法延迟为 10 周期可以更真实地反映程序在处理除法指令时的延迟开销。
Statistics窗口的结果显示发生了一次数据相关。这是由于两条除法指令由于共享同一个除法组件,造成结构冲突,导致程序运行时出现数据相关的阻塞。我们可以通过在两条除法指令之间插入足够的其他指令来减少阻塞。具体解释如下:
- 除法延迟设置为10个周期:MIPS的除法操作通常较慢,因为其计算比其他简单运算(如加法)复杂。在这里设置10周期的延迟(division latency),是为了模拟这种真实的执行延迟。
- 除法组件的冲突:两条除法指令 ddiv r16, r16, r17 和 ddiv r18, r18, r19 都需要使用除法组件。由于除法组件每次操作需要10个周期,因此第一条除法指令还在运行时,第二条指令就会被阻塞,等待除法组件空闲。
- 避免阻塞的方法:为了让除法组件在执行第一条指令时,第二条除法指令能够顺利等待组件释放,我们需要在两条除法指令之间插入 至少9条其他指令(等价于9个周期的延迟)。这样可以确保当第二条除法指令到达时,第一条除法操作已接近完成或完成,使得组件空闲。
- 存储组件的结构冲突:即使插入了9条指令,最后一条存储指令(sd)会占用存储组件,并可能发生结构冲突。但由于此时并没有马上用到存储组件的其他关键操作,因此影响不大。
- 第二条除法指令的阻塞影响:当第二条除法操作后紧接的指令排在第九条时,这个指令的存储操作就可能会真正引发一次存储组件的结构冲突。这种结构冲突会引发一次阻塞,因为这时程序必须等待存储组件空闲,造成略微的延迟。
- 指令序列调整后的结构相关情况
- 指令序列调整:
- 将第一个除法指令提前:第一个除法指令ddiv r16, r16, r17可以在加载r16和r17之后的第二条指令位置执行。这样可以更早开始第一个除法操作,让它提前完成,为第二个除法指令提供更多时间间隔。
- 插入9条指令之间隔两个除法指令:两个除法指令ddiv r16, r16, r17和ddiv r18, r18, r19都需要使用除法组件,而除法组件每次操作需要10个周期。为了防止组件冲突,我们在第一条除法指令和第二条除法指令之间插入 9条其他指令。这样,当第二条除法操作到达时,第一个除法操作大部分已完成,组件将变得空闲。
- 将除法结果存储延迟到最后:两个除法指令分别更新寄存器r16和r18的值,但需要将它们的结果存储回内存。为了避免存储组件的结构冲突,将r16和r18的存储操作安排在所有计算完成后再执行,这样可避免其他操作频繁占用存储组件,降低阻塞发生的可能性。
修改前:
start: ld r16,a(r0) ld r17,b(r0) ld r18,c(r0) ld r19,d(r0) ld r20,e(r0) ld r21,f(r0) ld r22,g(r0) ld r23,h(r0) ld r24,i(r0) ld r25,j(r0) ddiv r16,r16,r17 # 发生数据冒险 ddiv r18,r18,r19 # 发生阻塞 daddi r20,r20,1 daddi r21,r21,1 daddi r22,r22,1 daddi r23,r23,1 daddi r24,r24,1 daddi r25,r25,1 sd r16,a(r0) # 存储指令也要调整 sd r18,c(r0) sd r20,e(r0) sd r21,f(r0) sd r22,g(r0) sd r23,h(r0) sd r24,i(r0) sd r25,j(r0) halt |
修改后:
start: ld r16,a(r0) ld r17,b(r0) ld r18,c(r0) ddiv r16,r16,r17 # 第一个除法指令提前 ld r19,d(r0) ld r20,e(r0) ld r21,f(r0) ld r22,g(r0) ld r23,h(r0) ld r24,i(r0) ld r25,j(r0) daddi r20,r20,1 daddi r21,r21,1 ddiv r18,r18,r19 # 第二个除法指令距离第一个有9条指令 daddi r22,r22,1 daddi r23,r23,1 daddi r24,r24,1 daddi r25,r25,1 sd r20,e(r0) sd r21,f(r0) sd r22,g(r0) sd r23,h(r0) sd r24,i(r0) sd r25,j(r0) sd r16,a(r0) # r16和r18存储最后进行 sd r18,c(r0) halt |
运行指令调整后的MIPS代码:
将修改后的MIPS代码运行,运行结果如下图17所示。从Cycles窗口(图18所示)我们可以发现时间周期从42降至32(减少了10个时间周期),而从Statistics窗口(图19所示)数据相关RAW数也从1到0,即无数据相关。因此最终的优化结果十分显著。