冒险处理
在本次实验,我们终于要开始解决上次实验中遗留的最大问题,也就是冒险了。简单来说,导致上次实验的简单流水线 CPU 无法正确连续执行的因素都称为“冒险”。根据冒险出现的原因,冒险一般分为三类:结构冒险、数据冒险与控制冒险。
结构冒险
回顾 LC3 的 CPU 架构,它的数据和指令是可以共用存储器的,因此,如果将它流水线化,在写入数据的同时读取指令,就会产生存储器需要同时读写的问题,这就是结构冒险,这样的架构称为冯诺依曼架构。
好消息是,我们的流水线 CPU 中这一问题并不会出现,原因是我们采用了哈佛结构,将数据存储器与指令存储器分开,于是,自然不会再出现写入数据同时读取指令的问题。
数据冒险
前递
考虑如下的 RV32I 指令序列:
addi x1, x0, 1
nop
addi x2, x1, 1
理论上,在第三条指令进入 ID 阶段,需要读取 x1 的数据时,第一条指令进入 MEM 阶段,还没有到写回的时间。类似的,对如下的指令序列,第一条指令甚至只进入了 EX 阶段就面临第二条指令在 ID 阶段需要读取 x1 的问题:
addi x1, x0, 1
addi x2, x1, 1
这个问题的根源事实上在于 ID 阶段与 WB 阶段的依赖关系,不过,更进一步地思考可以发现,这个问题并不像想象中一样不可解决。
在 EX 阶段和 MEM 阶段结束后,x1 的新值已经在 ALU 中被算出,只是尚未写回寄存器堆而已。而实际使用寄存器堆的值进行计算的是在 ID 结束后的 EX 阶段。如果这时直接把计算出的新值传递给 ALU 作为操作数,程序完全可以正常运行。这就叫做前递。
上面的两种情况即对应两种前递方式,分别是从 MEM 段(EX 段结束后)前递到 EX 段与从 WB 段 (MEM 段结束后)前递到 EX 段。实际判别条件也即(注意可能 EX 段读取的两个寄存器都是 MEM/WB 段正需要写入的):
- MEM/WB 段写使能为 1,且写入非 x0;
- EX 段某读寄存器地址等于 MEM/WB 段的写地址;
- 若为 MEM 段,写回的数并非数据存储器读取结果。(这种情况的处理见下一小节)
一些例外
满足上述三个条件的事实上未必是数据冒险,因为可能 EX 段并不真的需要读寄存器(例如第二个操作数为立即数的情况)。不过,这并不会导致代码出错——即使前递被触发,ALU 也并不会选择前递到的寄存器值作为操作数。
若想真正精确定位需要前递的情况,译码器单元需要加入两个特殊的控制信号——寄存器读使能。其含义正如字面意思,当需要读寄存器时,读使能有效(高电平)。
事实上,利用类似的想法,根据下一部分气泡的设置可以发现,去掉第三个条件也可以使前递正确进行。
值得注意的是,如下的指令序列会出现 MEM 与 WB 同时检测到前递, 请根据这个例子思考此时应当如何前递:
addi x1, x0, 1
addi x1, x1, 1
addi x2, x1, 1
气泡
利用前递处理数据冒险的好处是,流水线 CPU 不需要进行任何停顿。但接下来的数据冒险例子就没有那么友善了:
lw x1, 0(x3)
addi x2, x1, 1
这种冒险被称为读取-使用冒险(Load-Use Hazard),由于读取在 MEM 段结束才能完成,这时无法直接前递, 而是必须等待一个周期。如果直接前递,则 EX 段的最大延迟就会再加上存储器的读取延迟,这是我们不能接受的。
不能前递的理由
按照书本上所介绍的,EX 段正在执行时,lw 指令的结果还没有从存储器中读出。由于时光不能倒流,我们便无法通过前递的方式将尚未产生的结果向前传递。当然,我们可以等一段时间,直到存储器的结果读出后再前递到 EX 段,并完成 EX 段的后续计算。这样虽然实现了前递,但是 EX 段的延迟就会变得十分巨大,从而影响整条流水线的性能。
为检测何时发生了此冒险,我们采用如下的判断流程:
- EX 段的指令为读取内存的指令;
- EX 段指令写入寄存器地址非零;
- ID 段某读寄存器地址等于 EX 段的写地址。
错误的判断方式
一个看起来自然的判断方式是,前两步与前递时相同,而最后一个条件为 MEM 段的写回数据选择为数据存储器的结果。
然而,如此判断会导致 Load-Use Hazard 与前递同时发生的一些情况中无法正确执行,可以思考如何构造反例。
若 Load-Use 冒险发生,我们就必须插入一个气泡,也即假装两条指令间有一个 nop 指令。
为了达到插入气泡的效果,原本的下一条指令,ID 阶段的指令将被忽略,也即在下一时钟上升沿清空 ID/EX 段间寄存器;而还未执行的 IF 前(PC)、IF/ID 段间寄存器需要停驻一个周期,让气泡在它们之前通过。
在下一个时钟周期,由于 ID/EX 停驻了,在 EX 阶段的还是刚才的指令,而这次在 WB 阶段检测出需要前递, 此时前递就不存在问题了。
控制冒险
第三种冒险是由跳转指令产生的控制冒险。值得一提的是,我们的 CPU 中采用默认不跳转的原则,当 B 类指令不发生跳转时,并不会产生控制冒险,只有跳转发生时才会出现问题。
按照给出的数据通路,所有跳转都是在 EX 阶段进行检测的,检测后下一次的 PC 会变为正确的跳转地址。因此,我们需要在数据通路中插入两个气泡,也即仿照上次实验中人工添加的两个 nop 指令:在 EX 阶段检测出跳转后,按照加 4 的错误地址读取到的 IF/ID 段间寄存器与 ID/EX 段间寄存器都应清空。