一、实验目标:
- 了解控制冒险分支预测的概念
- 了解多种分支预测的方法,动态分支预测更要深入了解
- 理解什么是BTB(Branch Target Buffer),并且学会用BTB来优化所给程序
- 利用BTB的特点,设计并了解在哪种状态下BTB无效
- 了解循环展开,并于BTB功能进行对比
- 对WinMIPS64的各个窗口和操作更加熟悉
二、实验内容
按照实验指导文件中的实验步骤及说明,完成相关操作记录实验过程的截图:
- 给出一段矩阵乘法的代码,通过开启BTB功能对其进行优化,并且观察流水线的细节,解释BTB在其中所起的作用;
- 自行设计一段使得即使开启了BTB也无效的代码。
- 使用循环展开的方法,观察流水因分支停顿的次数减少的现象,并对比采用BTB结构时流水因分支而停顿的次数。
三、实验环境
硬件:桌面PC
软件:Windows,WinMIPS64仿真器
四、实验基础
背景知识
给定一个简单的C代码片段,实现两个4x4矩阵的相加操作。矩阵 A 和矩阵 B 相加得到矩阵 C,具体代码如下:
在遇到跳转语句的时候,我们往往需要等到MEM阶段才能确定这条指令是否跳转(通过硬件的优化,可以极大的缩短分支的延迟,将分支执行提前到ID阶段,这样就能够将分支预测错误代价减小到只有一条指令),这种为了确保预取正确指令而导致的延迟叫控制冒险(分支冒险)。
为了降低控制冒险所带来的性能损失,一般采用分支预测技术。分支预测技术包含编译时进行的静态分支预测,和执行时进行的动态分支预测。这里,我们着重介绍动态分支预测中的BTB(Branch Target Buffer)技术。
BTB即为分支目标缓冲器,它将分支指令(对应的指令地址)放到一个缓冲区中保存起来,当下次再遇到相同的指令(跳转判定)时,它将执行和上次一样的跳转(分支或不分支)预测。
在采用了BTB之后,在流水线各个阶段所进行的相关操作如下:(需要注意的是,为了填写BTB,需要额外一个周期)
所需配置
增加地址总线的数量从而可以扩大内存,否则作业一中的数据在Data区域显示不全。
点击configure->architecture,将data address bus修改为12。需要注意的是每次重新打开winmips软件后都需要设置这个参数,不然会被重新初始化为10。
五、作业一:矩阵乘法优化
原始代码
原始矩阵乘法代码:
在这一阶段,我们首先给出矩阵乘法的例子,接着将流水线设置为不带BTB功能(configure->enable branch target buffer)直接运行,观察结果进行记录;然后,再开启BTB功能再次运行,观察实验结果。将两次的实验结果进行对比,观察BTB是否起作用,如果有效果则进一步观察流水线执行细节并且解释BTB起作用原因。
矩阵乘法的代码如下:
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| .data str: .asciiz "the data of matrix 3:\n" mx1: .space 512 mx2: .space 512 mx3: .space 512 .text initial: daddi r22,r0,mx1 #这个initial模块是给三个矩阵赋初值 daddi r23,r0,mx2 daddi r21,r0,mx3 input: daddi r9,r0,64 daddi r8,r0,0 loop1: dsll r11,r8,3 dadd r10,r11,r22 dadd r11,r11,r23 daddi r12,r0,2 daddi r13,r0,3 sd r12,0(r10) sd r13,0(r11) daddi r8,r8,1 slt r10,r8,r9 bne r10,r0,loop1 mul: daddi r16,r0,8 daddi r17,r0,0 loop2: daddi r18,r0,0 #这个循环是执行for(int i = 0, i < 8; i++)的内容 loop3: daddi r19,r0,0 #这个循环是执行for(int j = 0, j < 8; j++)的内容 daddi r20,r0,0 #r20存储在计算result[i][j]过程中每个乘法结果的叠加值 loop4: dsll r8,r17,6 #这个循环的执行计算每个result[i][j] dsll r9,r19,3 dadd r8,r8,r9 dadd r8,r8,r22 ld r10,0(r8) #取mx1[i][k]的值 dsll r8,r19,6 dsll r9,r18,3 dadd r8,r8,r9 dadd r8,r8,r23 ld r11,0(r8) #取mx2[k][j]的值 dmul r13,r10,r11 #mx1[i][k]与mx2[k][j]相乘 dadd r20,r20,r13 #中间结果累加 daddi r19,r19,1 slt r8,r19,r16 bne r8,r0,loop4 dsll r8,r17,6 dsll r9,r18,3 dadd r8,r8,r9 dadd r8,r8,r21 #计算result[i][j]的位置 sd r20,0(r8) #将结果存入result[i][j]中 daddi r18,r18,1 slt r8,r18,r16 bne r8,r0,loop3 daddi r17,r17,1 slt r8,r17,r16 bne r8,r0,loop2 halt |
加载和运行原始代码:
先用asm.exe检验一下程序的正确性,在终端中输入****.\asm.exe .\exp5.1.s**** ,得到如下的结果,没有错误****(0 errors)****,即编译非常顺利。
然后加载到WinMIPS64中,不设置BTB,按下F4运行之后,得到如下结果。
由Statistic窗口观察可以发现,程序运行过程中总共发生了574次Branch Tanken Stall,我们可以通过分析和计算进行进一步的验证。
其中Branch Taken Stall 是指在程序执行过程中,遇到条件分支(branch instruction)时,由于预测分支方向为被执行(taken),但后续指令未能及时准备好执行而造成的停顿。
验证原始代码结果:
初始化阶段:
首先初始化,有一个循环,对三个矩阵进行赋初值,矩阵大小是8*8(即 64 次迭代),又因为MIPS 默认分支预测策略是恒不跳转(assume "not taken"),即预测分支不会跳转到 loop1。所以实际情况是只有最后一次(退出循环时)beq预测正确,其他 64−1=63次都发生beq预测错误,导致分支跳转的停顿。
阻塞次数计算:总计 64−1=63 次 Branch Taken Stall。
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| .text initial: daddi r22,r0,mx1 #这个initial模块是给三个矩阵赋初值 daddi r23,r0,mx2 daddi r21,r0,mx3 input: daddi r9,r0,64 daddi r8,r0,0 loop1: dsll r11,r8,3 dadd r10,r11,r22 dadd r11,r11,r23 daddi r12,r0,2 daddi r13,r0,3 sd r12,0(r10) sd r13,0(r11) daddi r8,r8,1 slt r10,r8,r9 bne r10,r0,loop1 |
矩阵乘法阶段:
矩阵乘法阶段即主体部分由三个嵌套循环组成,分别对应矩阵乘法的外层、中层和内层循环。
- 外层循环(loop2) :外层循环 8次,仅最后一次预测正确,其余 8−1=7 次均发生 Branch Taken Stall。阻塞次数为外层循环总计 7 次 Branch Taken Stall。
- 中层循环(loop3) :中层循环嵌套于外层循环中,每轮执行 8 次(外层循环 8 轮,每轮 8 次)。每轮循环中仅最后一次预测正确,其余 8−1=7 次发生 Branch Taken Stall。阻塞次数为 8×7=56 次。
- 内层循环(loop4) :内层循环嵌套于中层循环中,每轮执行 8 次(中层循环总共 8×8 轮,每轮 8 次)。每轮循环中仅最后一次预测正确,其余 8−1=7 次发生 Branch Taken Stall。阻塞次数为8×8×7=448 次。
所以主体部分阻塞总计: 7+56+448=511 次。
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| mul: daddi r16,r0,8 daddi r17,r0,0 loop2: daddi r18,r0,0 #这个循环是执行for(int i = 0, i < 8; i++)的内容 loop3: daddi r19,r0,0 #这个循环是执行for(int j = 0, j < 8; j++)的内容 daddi r20,r0,0 #r20存储在计算result[i][j]过程中每个乘法结果的叠加值 loop4: dsll r8,r17,6 #这个循环的执行计算每个result[i][j] dsll r9,r19,3 dadd r8,r8,r9 |
总体验证:
将初始化阶段与主体部分的阻塞次数相加:63+511=574 次。
这与 WinMIPS64 工具中的统计结果完全吻合,说明计算正确。
开启BTB后的效果:
设置BTB功能:
设置BTB功能(在菜单栏处选择Configure项,然后在下拉菜单中为Enable Branch Target Buffer选项划上钩)。
运行程序:
设置完BTB功能后,运行程序,运行结果如下图所示,观察Statistics窗口的结果。
点击configure开启之后,得到阻塞次数Branch Taken Stall成功下降到了从574次到148次 ,而Branch Misprediction Stall次数上升从0次到了148次。
验证开启BTB后的结果:
初始化阶段:
循环次数为64 次。启用BTB之后,第一次进入循环时,由于 BTB 尚未记录分支地址,发生一次 Branch Taken Stall。最后一轮循环结束时,发生一次 Branch Misprediction Stall(BTB 预测循环继续,但分支未被采取,需修正)。因此 Branch Taken Stall为1 次,而Branch Misprediction Stall为 1 次。
矩阵乘法阶段:
由三层嵌套循环组成:
- 外层循环(loop2) :执行 1 轮,8 次迭代。第一次进入循环时,发生一次 Branch Taken Stall。循环结束时,发生一次 Branch Misprediction Stall。因此Branch Taken Stall为1 次 ,而Branch Misprediction Stall为 1 次。
- 中层循环(loop3) :执行 8 轮,每轮 8 次迭代。每轮开始时,发生一次 Branch Taken Stall。每轮结束时,发生一次 Branch Misprediction Stall。因此Branch Taken Stall为8 次 ,而Branch Misprediction Stall为 8 次。
- 内层循环(loop4) :执行 8×8 轮,每轮 8 次迭代。每轮开始时,发生一次 Branch Taken Stall。每轮结束时,发生一次 Branch Misprediction Stall。因此Branch Taken Stall为64 次 ,而Branch Misprediction Stall为 64 次。
所以主体部分Branch Taken Stall总计为 64+8+1=73 次 ,Branch Misprediction Stall总计为64+8+1=73 次。
总体验证:
由于每次分支错误都会引起两次时钟周期的延迟,总阻塞周期数为(73+1)+(73+1)=148,与实验结果吻合。
通过启用 BTB 功能,分支预测性能显著提高。虽然引入了 Branch Misprediction Stall,但相比未启用 BTB 时 574 次阻塞,性能提升显著,程序的整体执行效率得到了优化。这验证了 BTB 对于减少分支相关停顿的有效性,同时也展示了 BTB 在分支循环结构中的运行机制。
六、作业二:设计使BTB无效的代码
逻辑设计:
要使得BTB无效,可以使每次条件判断产生不同的跳转结果。可以把矩阵大小从 8×8 缩小到 2×2 。
修改步骤:
- 将矩阵规模调整为 2×2 。
- 将初始化模块的循环次数调整为 2 次,实现双层循环。
- 矩阵乘法部分的三层嵌套循环均设置为 2 次迭代。
- 通过每次循环条件的不同跳转结果,使 BTB 的预测失效。
设计依据:
- BTB 工作原理概述:
BTB(Branch Target Buffer) 是一种缓存机制,用于存储分支指令的历史跳转目标地址。如果程序的分支行为模式稳定(例如循环结构中分支条件始终相同),BTB 能根据之前的记录准确预测跳转目标,从而减少分支预测错误。若分支行为频繁变化,BTB 会因预测错误而频繁清空和更新缓存项,导致效率下降甚至完全失效。
- 矩阵规模影响 BTB 的有效性
- 循环规模对分支行为的影响:大矩阵(8×8)嵌套循环层数多,总循环次数多。由于循环跳转行为稳定,BTB 能有效缓存分支信息,提高分支预测命中率。而小矩阵(2×2)循环次数显著减少,每次循环迭代的分支跳转结果更加不规律。
- 小矩阵导致分支跳转行为频繁变化:2×2 矩阵的嵌套循环结构较浅,分支行为变化较快。每次循环条件判断后的跳转方向可能发生变化(例如从"跳转"到"不跳转",或反之)。分支行为的变化速度快于 BTB 缓存更新速度,导致 BTB 无法保持一致的预测命中率。
- 频繁变化的分支行为造成 BTB 无效:当 BTB 的缓存项还未稳定时,跳转方向已经发生变化。缓存不断被替换,导致实际预测命中率接近随机猜测。
- 缓存污染和失效
对于小矩阵计算,分支条件的跳转目标地址较少重复性。缓存中存储的目标地址迅速被替换,导致预测失效。BTB 在频繁失效时的性能甚至可能比直接预测分支行为更低。
总结,通过减少矩阵规模(从 8×8 缩小为 2×2),循环层次和次数的显著减少导致:
- 分支跳转行为更加不规律,目标地址变化频繁。
- BTB 无法有效记录和预测跳转目标地址,逐渐失效。
- 分支预测错误率增加,性能下降。
因此,矩阵规模的缩小通过增加分支跳转的不确定性,使得 BTB 的优势无法发挥,从而验证其在不稳定分支行为下的局限性。
修改代码:
数据空间大小:
每个矩阵从521字节缩小到32字节。
|----------------------------------------------|
| mx1: .space 32 mx2: .space 32 mx3: .space 32 |
循环范围与限制:
循环上限从 8 减少到 2,包括初始化时的总次数减少。矩阵乘法循环里面所有嵌套循环(行、列、累加)范围调整。
|----------------------------------------|
| input: daddi r9, r0, 2 # 矩阵的维度(2x2 矩阵) |
地址逻辑计算:
行偏移从 6 减少到 4,对应行宽变化。
小矩阵(2×2):
- 行偏移:dsll r10, r17, 4(16 字节偏移,表示每行有 2 个元素,每个元素 8 字节)。
- 列偏移:dsll r11, r18, 3(8 字节偏移,表示每列一个元素)。
|--------------------------------------------------------------------------------------------------|
| iloop2: dsll r10, r17, 4 # 计算行索引对应的内存偏移,乘以 16 (2^4) dsll r11, r18, 3 # 计算列索引对应的内存偏移,乘以 8 (2^3) |
数据初始化模块:
初始化循环从单一循环变为双层嵌套循环,匹配矩阵规模的二维特性。
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| input: daddi r9, r0, 2 # 矩阵的维度(2x2 矩阵) daddi r17, r0, 0 iloop1: daddi r18, r0, 0 iloop2: dsll r10, r17, 4 # 计算行索引对应的内存偏移,乘以 16 (2^4) dsll r11, r18, 3 # 计算列索引对应的内存偏移,乘以 8 (2^3) |
结果计算逻辑:
处理 2×2 循环,嵌套循环深度相同但循环次数减少。
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| mul: daddi r16, r0, 2 # 矩阵维度(2x2 矩阵) daddi r17, r0, 0 # 行索引 i 初始化为 0 loop1: daddi r18, r0, 0 # 列索引 j 初始化为 0 loop2: daddi r19, r0, 0 # 中间索引 k 初始化为 0 daddi r20, r0, 0 # 用于累加结果的寄存器 r20 初始化为 0 loop3: dsll r8, r17, 4 # 计算 mx1 行偏移(i * 16) dsll r9, r19, 3 # 计算 mx1 列偏移(k * 8) |
总体修改后的代码:
- 数据空间调整: 矩阵存储空间从 512 字节减少到 32 字节。
- 循环范围调整: 循环范围从 8×8 减少到 2×2。
- 地址计算调整: 偏移量从 6 减少到 4,匹配行宽变化。
- 初始化模块改进: 由单层循环转为双层嵌套循环。
- 计算逻辑不变: 矩阵乘法算法逻辑保持一致,但次数显著减少。
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| .data str: .asciiz "the data of matrix 3:\n" mx1: .space 32 mx2: .space 32 mx3: .space 32 .text # 初始化三个矩阵的基地址 initial: daddi r22, r0, mx1 daddi r23, r0, mx2 daddi r21, r0, mx3 # 给矩阵 mx1 和 mx2 赋初值 input: daddi r9, r0, 2 # 矩阵的维度(2x2 矩阵) daddi r17, r0, 0 iloop1: daddi r18, r0, 0 iloop2: dsll r10, r17, 4 # 计算行索引对应的内存偏移,乘以 16 (2^4) dsll r11, r18, 3 # 计算列索引对应的内存偏移,乘以 8 (2^3) dadd r11, r11, r10 # 合并行列偏移,得到元素地址偏移 dadd r10, r11, r22 # 计算 mx1[i][j] 的实际地址 dadd r11, r11, r23 # 计算 mx2[i][j] 的实际地址 daddi r12, r0, 2 daddi r13, r0, 3 sd r12, 0(r10) sd r13, 0(r11) daddi r18, r18, 1 slt r10, r18, r9 # 检查 j 是否小于矩阵维度 (2) bne r10, r0, iloop2 daddi r17, r17, 1 slt r10, r17, r9 # 检查 i 是否小于矩阵维度 (2) bne r10, r0, iloop1 # 矩阵乘法运算 mul: daddi r16, r0, 2 # 矩阵维度(2x2 矩阵) daddi r17, r0, 0 # 行索引 i 初始化为 0 loop1: daddi r18, r0, 0 # 列索引 j 初始化为 0 loop2: daddi r19, r0, 0 # 中间索引 k 初始化为 0 daddi r20, r0, 0 # 用于累加结果的寄存器 r20 初始化为 0 loop3: dsll r8, r17, 4 # 计算 mx1 行偏移(i * 16) dsll r9, r19, 3 # 计算 mx1 列偏移(k * 8) dadd r8, r8, r9 # 合并行列偏移,得到 mx1[i][k] 偏移地址 dadd r8, r8, r22 # 计算 mx1[i][k] 的实际地址 ld r10, 0(r8) # 读取 mx1[i][k] dsll r8, r19, 4 # 计算 mx2 行偏移(k * 16) dsll r9, r18, 3 # 计算 mx2 列偏移(j * 8) dadd r8, r8, r9 # 合并行列偏移,得到 mx2[k][j] 偏移地址 dadd r8, r8, r23 # 计算 mx2[k][j] 的实际地址 ld r11, 0(r8) # 读取 mx2[k][j] dmul r13, r10, r11 # 计算 mx1[i][k] * mx2[k][j] dadd r20, r20, r13 # 累加到 r20 daddi r19, r19, 1 slt r8, r19, r16 # 检查 k 是否小于矩阵维度 (2) bne r8, r0, loop3 dsll r8, r17, 4 dsll r9, r18, 3 dadd r8, r8, r9 dadd r8, r8, r21 # 计算结果矩阵实际地址 sd r20, 0(r8) # 将累加结果存入结果矩阵 daddi r18, r18, 1 slt r8, r18, r16 # 检查 j 是否小于矩阵维度 (2) bne r8, r0, loop2 daddi r17, r17, 1 slt r8, r17, r16 # 检查 i 是否小于矩阵维度 (2) bne r8, r0, loop1 halt |
运行修改后的代码:
检查合法性:
先用asm.exe检验一下程序的正确性,在终端中输入****.\asm.exe .\exp5.2.s**** ,得到如下的结果,没有错误****(0 errors)****,即编译非常顺利。
不设置BTB运行:
加载到WinMIPS64中,不设置BTB,按下F4运行之后,得到如下结果。
由Statistic窗口观察可以发现,程序运行过程中总共发生了10次Branch Taken Stall ,根据前面计算的原理,我们可以计算得出初始化会发生4-1=3次Branch Taken Stall ,矩阵乘法会发生4*2-1=7次Branch Taken Stall。得到结果与计算的相吻合。
设置BTB运行:
加载到WinMIPS64中,设置BTB,按下F4运行之后,得到如下结果。
由Statistic窗口观察可以发现,程序运行过程中总共发生了20次Branch Taken Stall ,根据前面的计算原理,同样可以得到,初始化会发生3次Branch Taken Stall,矩阵乘法矩阵乘法会发生7次Branch Taken Stall,由于每次Branch Taken Stall会阻塞两个周期,所以实际是20个Branch Taken Stall周期,Branch Misprediciton Stall次数与Branch Taken Stall一致。得到计算结果与实际结果吻合。
因此,可以发现开启BTB之后的结果更糟了(糟糕了一倍左右,从10次到20次),此时开启BTB是无效的。通过矩阵规模的缩小可以使得BTB无效,矩阵规模的缩小通过增加分支跳转的不确定性,使得 BTB 的优势无法发挥,从而验证其在不稳定分支行为下的局限性。
循环展开与BTB的效果对比
循环展开:
首先,我们需要对循环展开这个概念有一定的了解。
什么是循环展开呢?所谓循环展开就是通过在每次迭代中执行更多的数据操作来减小循环开销的影响。其基本思想是设法把操作对象线性化,并且在一次迭代中访问线性数据中的一个小组而非单独的某个。这样得到的程序将执行更少的迭代次数,于是循环开销就被有效地降低了。
接下来,我们就按照这种思想对上述的矩阵乘法程序进行循环展开。要求将上述的代码通过循环展开将最里面的一个执行迭代8次的循环整个展开了,也就是说,我们将矩阵相乘的三个循环通过代码的增加,减少到了两个循环。
循环展开的代码:
- 初始化部分 (initial 和 input):为三个矩阵分配初始值。
- 循环展开部分 (mul):将最内层的循环(对 k 的循环)展开 8 次,避免了循环控制指令,减少了循环开销,提高效率。
- 累加器 (r20):用于存储中间计算结果,最后写入 mx3[i][j]。
- 循环控制:通过寄存器和条件跳转控制 i 和 j 的迭代,外层和内层的逻辑清晰。
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| .data str: .asciiz "the data of matrix 3:\n" mx1: .space 512 mx2: .space 512 mx3: .space 512 .text # 初始化三个矩阵的基地址 initial: daddi r22,r0,mx1 daddi r23,r0,mx2 daddi r21,r0,mx3 # 给矩阵赋初值 input: daddi r9,r0,64 daddi r8,r0,0 loop1: dsll r11,r8,3 dadd r10,r11,r22 dadd r11,r11,r23 daddi r12,r0,2 daddi r13,r0,3 sd r12,0(r10) sd r13,0(r11) daddi r8,r8,1 slt r10,r8,r9 bne r10,r0,loop1 # 矩阵乘法模块 mul: daddi r16,r0,8 # r16 = 8,矩阵维度 daddi r17,r0,0 # r17 = 0,外循环计数器(i) loop2: daddi r18,r0,0 # r18 = 0,内循环计数器(j) loop3: daddi r19,r0,0 # r19 = 0,嵌套循环计数器(k) daddi r20,r0,0 # r20 = 0,累加器,用于存储中间结果 # 循环展开,直接展开 8 次 k 的循环 # 第 1 次 dsll r8,r17,6 # r8 = i*64 dsll r9,r19,3 # r9 = k*8 dadd r8,r8,r9 dadd r8,r8,r22 # r8 = mx1[i][k] ld r10,0(r8) # 取mx1[i][k]的值 dsll r8,r19,6 # r8 = k*64 dsll r9,r18,3 # r9 = j*8 dadd r8,r8,r9 dadd r8,r8,r23 # r8 = mx2[k][j] ld r11,0(r8) # 取mx2[k][j]的值 dmul r13,r10,r11 # mx1[i][k]与mx2[k][j]相乘 dadd r20,r20,r13 # 中间结果累加 daddi r19,r19,1 # k++ # 第 2 次 dsll r8,r17,6 # r8 = i*64 dsll r9,r19,3 # r9 = k*8 dadd r8,r8,r9 dadd r8,r8,r22 # r8 = mx1[i][k] ld r10,0(r8) # 取mx1[i][k]的值 dsll r8,r19,6 # r8 = k*64 dsll r9,r18,3 # r9 = j*8 dadd r8,r8,r9 dadd r8,r8,r23 # r8 = mx2[k][j] ld r11,0(r8) # 取mx2[k][j]的值 dmul r13,r10,r11 # mx1[i][k]与mx2[k][j]相乘 dadd r20,r20,r13 # 中间结果累加 daddi r19,r19,1 # k++ # 第 3 次 dsll r8,r17,6 # r8 = i*64 dsll r9,r19,3 # r9 = k*8 dadd r8,r8,r9 dadd r8,r8,r22 # r8 = mx1[i][k] ld r10,0(r8) # 取mx1[i][k]的值 dsll r8,r19,6 # r8 = k*64 dsll r9,r18,3 # r9 = j*8 dadd r8,r8,r9 dadd r8,r8,r23 # r8 = mx2[k][j] ld r11,0(r8) # 取mx2[k][j]的值 dmul r13,r10,r11 # mx1[i][k]与mx2[k][j]相乘 dadd r20,r20,r13 # 中间结果累加 daddi r19,r19,1 # k++ # 第 4 次 dsll r8,r17,6 # r8 = i*64 dsll r9,r19,3 # r9 = k*8 dadd r8,r8,r9 dadd r8,r8,r22 # r8 = mx1[i][k] ld r10,0(r8) # 取mx1[i][k]的值 dsll r8,r19,6 # r8 = k*64 dsll r9,r18,3 # r9 = j*8 dadd r8,r8,r9 dadd r8,r8,r23 # r8 = mx2[k][j] ld r11,0(r8) # 取mx2[k][j]的值 dmul r13,r10,r11 # mx1[i][k]与mx2[k][j]相乘 dadd r20,r20,r13 # 中间结果累加 daddi r19,r19,1 # k++ # 第 5 次 dsll r8,r17,6 # r8 = i*64 dsll r9,r19,3 # r9 = k*8 dadd r8,r8,r9 dadd r8,r8,r22 # r8 = mx1[i][k] ld r10,0(r8) # 取mx1[i][k]的值 dsll r8,r19,6 # r8 = k*64 dsll r9,r18,3 # r9 = j*8 dadd r8,r8,r9 dadd r8,r8,r23 # r8 = mx2[k][j] ld r11,0(r8) # 取mx2[k][j]的值 dmul r13,r10,r11 # mx1[i][k]与mx2[k][j]相乘 dadd r20,r20,r13 # 中间结果累加 daddi r19,r19,1 # k++ # 第 6 次 dsll r8,r17,6 # r8 = i*64 dsll r9,r19,3 # r9 = k*8 dadd r8,r8,r9 dadd r8,r8,r22 # r8 = mx1[i][k] ld r10,0(r8) # 取mx1[i][k]的值 dsll r8,r19,6 # r8 = k*64 dsll r9,r18,3 # r9 = j*8 dadd r8,r8,r9 dadd r8,r8,r23 # r8 = mx2[k][j] ld r11,0(r8) # 取mx2[k][j]的值 dmul r13,r10,r11 # mx1[i][k]与mx2[k][j]相乘 dadd r20,r20,r13 # 中间结果累加 daddi r19,r19,1 # k++ # 第 7 次 dsll r8,r17,6 # r8 = i*64 dsll r9,r19,3 # r9 = k*8 dadd r8,r8,r9 dadd r8,r8,r22 # r8 = mx1[i][k] ld r10,0(r8) # 取mx1[i][k]的值 dsll r8,r19,6 # r8 = k*64 dsll r9,r18,3 # r9 = j*8 dadd r8,r8,r9 dadd r8,r8,r23 # r8 = mx2[k][j] ld r11,0(r8) # 取mx2[k][j]的值 dmul r13,r10,r11 # mx1[i][k]与mx2[k][j]相乘 dadd r20,r20,r13 # 中间结果累加 daddi r19,r19,1 # k++ # 第 8 次 dsll r8,r17,6 # r8 = i*64 dsll r9,r19,3 # r9 = k*8 dadd r8,r8,r9 dadd r8,r8,r22 # r8 = mx1[i][k] ld r10,0(r8) # 取mx1[i][k]的值 dsll r8,r19,6 # r8 = k*64 dsll r9,r18,3 # r9 = j*8 dadd r8,r8,r9 dadd r8,r8,r23 # r8 = mx2[k][j] ld r11,0(r8) # 取mx2[k][j]的值 dmul r13,r10,r11 # mx1[i][k]与mx2[k][j]相乘 dadd r20,r20,r13 # 中间结果累加 daddi r19,r19,1 # k++ # 结果存储到矩阵 mx3 中 dsll r8,r17,6 dsll r9,r18,3 dadd r8,r8,r9 dadd r8,r8,r21 # 计算 mx3[i][j] 的地址 sd r20,0(r8) # 将 r20 的结果存入 mx3[i][j] daddi r18,r18,1 slt r8,r18,r16 # 判断 j 是否小于 8 bne r8,r0,loop3 daddi r17,r17,1 slt r8,r17,r16 # 判断 i 是否小于 8 bne r8,r0,loop2 halt |
运行循环展开的代码:
检查合法性:
先用asm.exe检验一下程序的正确性,在终端中输入****.\asm.exe .\exp5.3.s**** ,得到如下的结果,没有错误****(0 errors)****,即编译非常顺利。
不设置BTB运行:
加载到WinMIPS64中,不设置BTB,按下F4运行之后,得到如下结果。
由Statistic窗口观察可以发现,程序运行过程中总共发生了126次Branch Taken Stall。Branch Taken Stall次数下降到了126次,刚好是作业一中未开启BTB的运行结果574次减去最内层的8*8*7=448次,即574-448=126次。
设置BTB运行:
加载到WinMIPS64中,设置BTB,按下F4运行之后,得到如下结果。
由Statistic窗口观察可以发现,程序运行过程中总共发生了20次Branch Taken Stall。Branch Taken Stall次数下降到了20次,刚好是作业一中开启BTB的运行结果148次减去最内层的8*8*2=128次,即148-128=20次。
比较分析:
比较,通过对比循环展开(未启用BTB)、使用BTB(未进行循环展开)以及未使用BTB且未作循环展开的运行结果。比较他们的Branch Tanken Stalls和Branch Misprediction Stalls的数量,并尝试给出评判,即评判这三者之间的tradeoff关系。
表 1:各种模式结果统计图
|-----------------------------|---------------|---------------|--------------|--------------|
| 模式 | 无循环展开 (关闭BTB) | 无循环展开 (启用BTB) | 循环展开 (关闭BTB) | 循环展开 (启用BTB) |
| Branch Tanken Stalls | 574 | 148 | 126 | 20 |
| Branch Misprediction Stalls | 0 | 148 | 0 | 26 |
数据总结:
- Branch Taken Stalls(分支命中延迟):
- 无循环展开(关闭BTB):574(最高,表明分支预测性能差)
- 无循环展开(启用BTB):148(显著降低,BTB提高了预测性能)
- 循环展开(关闭BTB):126(比未循环展开降低,但未启用BTB的性能仍较差)
- 循环展开(启用BTB):20(最低,循环展开与BTB结合优化效果最好)
- Branch Misprediction Stalls(分支预测错误延迟):
- 无循环展开(关闭BTB):0(未使用BTB,因此没有错误预测导致的延迟)
- 无循环展开(启用BTB):148(BTB启用后,虽然减少了Branch Taken Stalls,但存在错误预测延迟)
- 循环展开(关闭BTB):0(没有使用BTB,因此无错误预测延迟)
- 循环展开(启用BTB):26(结合BTB的循环展开减少了错误预测延迟的影响)
Tradeoff(折中关系)分析
- 无循环展开与循环展开:
- 关闭BTB:循环展开显著降低了Branch Taken Stalls(从574降低到126),表明循环展开减少了分支的频率,进而优化了流水线效率。
- 启用BTB:循环展开进一步减少了Branch Taken Stalls(从148降低到20),即便增加了少量Branch Misprediction Stalls,但总延迟显著降低。
- 启用BTB与关闭BTB:
- 对于无循环展开场景,启用BTB降低了Branch Taken Stalls(从574降到148),但引入了Branch Misprediction Stalls(148)。
- 对于循环展开场景,启用BTB进一步优化了Branch Taken Stalls(从126降到20),但引入了少量Branch Misprediction Stalls(26)。整体来看,启用BTB的收益大于其带来的误预测代价。
综合评估
- 最佳模式:循环展开+启用BTB(20+26=46,总延迟最低)。
- 次优模式:循环展开+关闭BTB(126+0=126),虽然没有误预测延迟,但分支命中延迟较高。
- 最差模式:无循环展开+关闭BTB(574+0=574),延迟最高,性能最差。
****循环展开和BTB启用的组合效果最优,通过减少分支点和提高分支预测性能,显著降低了分支相关延迟。****启用BTB虽然可能引入Branch Misprediction Stalls,但其带来的性能提升(降低Branch Taken Stalls)往往大于预测错误的代价。如果硬件资源受限且无法启用BTB,则循环展开是改善性能的次优选择。
对于较大的循环迭代次数,启用BTB能够显著降低分支预测的延迟。然而,当代码中存在多重循环嵌套且内层循环的迭代次数呈指数级增长时,仅依赖BTB的优化可能不足以解决分支预测延迟带来的性能瓶颈。在此情况下,通过循环展开进一步减少分支指令的频率,可以显著降低分支阻塞发生概率,从而提升流水线效率。这种优化策略尤其适用于高迭代次数的深度循环结构,因为其能在减少分支依赖的同时,提高指令级并行度,进而缓解分支预测机制的压力并减少性能损耗。