sim-tty为我真正用来做homework和lab的模拟器,关闭gui。
sim-gui为带gui的模拟器,在Ubuntu 22.04.1 LTS
下编译,tcl版本8.6。
官网下载的sim.tar中的代码由于使用了一些旧版本gcc和tcl的特性无法在新环境下直接编译,我对源代码和makefile进行了一些修改。
Homework
4.52 SEQ-full
iaddq for SEQ
Phase | iaddq V, rB |
---|---|
F 取指 | icode:ifun <- M1[PC] rA:rB <- M1[PC+1] valC <- M8[PC+2] valP <- PC+10 |
D 译码 | valB <- R[rB] |
E 执行 | valE <- valB + valC set CC |
M 访存 | |
W 写回 | R[rB] <- valE |
PC | PC <- valP |
|
|
4.53 PIPE-stall
also named PIPE-nobypass
always stall when hazard
数据冒险条件:译码阶段(D)需要取读取的寄存器(d_srcA, d_srcB)会在后续步骤中被写入。
|
|
返回暂停条件:译码(D)执行(E)访存(M)阶段中正在运行ret指令。当ret指令进入写回阶段时下一条指令的地址就已经知悉,可以进行译码。
|
|
预测错误条件:在执行阶段(E)中的是跳转指令且跳转条件不成立。
|
|
发生数据冒险时,应将正在译码的指令卡住直到它要读取的寄存器已经被写入完毕(即写入其需要读取的寄存器的指令通过写回阶段),并在执行阶段插入气泡填充流水线。
需要返回暂停时,应卡住取指阶段直到它能够取到ret之后的正确地址(即ret指令通过访存阶段),并在译码阶段插入气泡填充流水线。
发生预测错误时,在执行和译码阶段插入气泡取代错误取出的指令,而后跳转指令进入访存阶段,取指阶段能够获得正确的后续指令地址。
表中S for stall, B for bubble
F | D | E | |
---|---|---|---|
s_data | S | S | B |
s_ret | S | B | |
s_pred | B | B | |
s_data && s_ret | S | S | B |
s_pred && * | B | B |
当数据冒险和返回暂停同时发生时,应按照数据冒险处理。若ret比产生数据冒险的指令更早进入流水线,产生数据冒险的指令根本不会被译码。故ret本身一定是产生数据冒险的指令,需要暂停。
当预测错误和其他类型一起发生(包括全部发生)时,应该总是按照预测错误处理。若预测错误后发生,无论是数据冒险还是ret都会使得产生预测错误的JXX指令被卡住无法译码。故预测错误一定先发生,而后续的指令无论如何都是被错误取出的,故应该按照预测错误处理全部取消掉。
于是F,D,E阶段的stall与bubble更新条件改为:
|
|
|
|
4.54 PIPE-full
iaddq for PIPE
|
|
4.55 PIPE-nt
never taken strategy when predict PC
在预测PC时改变策略:
|
|
执行阶段,在原本全部预测跳转的设计中,预测错误时,Select PC使用M_valA里的地址(实际上是val_P)修正分支,重新开始执行。而在本设计中,对于条件跳转预测的本来就是val_P。要使此时M_valA中的地址能够被Select PC用于修正,其中的值必须是val_C。于是修改e_valA的产生逻辑如下:
|
|
注意到对于所有JXX指令,valA除了在预测错误时修正PC之外没有其他用处(JXX并不操作内存和寄存器文件),所以逻辑可进行一些简化:
|
|
对Select PC逻辑进行修改:
|
|
最后对流水线中判断分支预测错误的语句进行修改:
|
|
|
|
4.56 PIPE-btfnt
backward taken, forward not-taken strategy when predict PC
在预测PC时改变策略:
|
|
注意到对于所有JXX指令,都不操作内存和寄存器文件,所以流水线寄存器中的val都可以被用来存储地址。这里我们使用valE存储valC(由valC和0在ALU中加法得到),用valA存储valP(直接传递)。
|
|
在JXX指令执行阶段,aluA设置为E_valC,aluB设置为0,就会在e_valE中得到valC,进而将valC传入M_valE。直接传递valA,可将valP传入M_valA。
这样,Select PC就可以使用M_valE和M_valA在分支预测错误时修正产生正确的PC。
|
|
最后对流水线中判断分支预测错误的语句进行修改:
|
|
|
|
4.57 PIPE-lf
load forwarding
只有rmmovq和pushq会在直到访存阶段才使用某个寄存器的值,而且它们都将rA寄存器中的值写入内存。
|
|
把新的加载/使用冒险条件替换进流水线控制逻辑。
Fwd A实现很简单,只要在加载转发能够生效的条件下将访存阶段读出来的内存值转发到e_valA就可以了。
|
|
|
|
4.58 PIPE-1w
register file have only one writing port now
取指 F
修改取指和指令预测逻辑,实现将pop指令取出两次:
|
|
原来的popq被拆成两半,修改后popq将%rsp值减去8,pop2将-8(%rdp)值写入rA,所以现在只有pop2需要使用寄存器标记
|
|
译码D / 写回 W
都是译码阶段完成的操作,写回阶段根据译码阶段已经产生的dst进行写回。
|
|
|
|
执行 E
|
|
访存 M
|
|
流水线控制
加载/使用数据冒险的条件需要修改。popq加载的功能移交给了pop2,所以条件应修改如下:
|
|
DIFF
|
|
Lab
Part.A
|
|
|
|
|
|
Part.B
同Homework 4.52
Part.C
首先先按照homework 4.54实现iaddq,写出朴素ncopy代码如下:
|
|
注意mrmovq (%rdi), %rcx
和rmmovq %rcx, (%rsi)
中间要隔开一条指令以避免加载使用数据冒险。这个版本的平均CPE为10.62,喜提零分。
考虑优化循环中统计正数数量的条件判断。换成条件传送需要提前计算%rax+1的值,平均CPE表现反而会更劣。转换思路,为什么不把iaddq设置成如果条件满足就执行加法呢?
修改取指F阶段功能码部分:
|
|
将iaddq的功能码设置成和jg/cmovg相同,这样在执行阶段e_Cnd就会指示CC的内容是否意味着大于0。然后在执行阶段实现条件传送:
|
|
如果条件不满足就像条件传送一样取消对寄存器的修改。这种改动并不会影响加法,因为ALU的功能码在非opq指令时始终是加法。
|
|
再修改ncopy的实现:
|
|
这个版本平均CPE为9.19。
然后再简单展开一下:
|
|
这个版本的平均CPE为7.45,已经满分了。其实继续展开还能更快,我试了下展开5次能到6.91的平均CPE。
我还做了一些其他尝试:
-
仅仅实现普通的iaddq,展开循环5次,平均CPE7.84,我没法做到满分。
代码文件
ncopy-simple.ys
pipe-full-simple.hcl
-
实现高度集成的iaddq,根据寄存器不同实现不同的功能。除了实现上面的条件加法之外,将读写内存和增加指针结合进同一条指令,可以在不进行循环展开的情况下做到7.41的平均CPE。但由于指令的实现内部直接+8,在循环展开后无法享受到统一加法的速度提升,所以展开5次平均CPE提升相对较小,但也能到6.35。
代码文件
ncopy-magic.ys
pipe-full-magic.hcl