跳转至

第 3 周

结构冒险

在一个运算单元正在被占用时,又来一个东西要使用它。比如一条指令的 Instruction fetch 和另一条指令的 Data access 撞车。

怎么解决?直接等?时间代价太大。

像哈佛架构那样,把指令存储器和数据存储器分开?那就要加硬件。不过这也比下面两种冒险的解决方案简单得多了。

数据冒险

  • 不同的指令之间可能会有数据上的依赖。
  • 比如一个指令的操作数是上一个指令的结果。

怎么解决?又是直接等?时间代价还是太大。

或者,直接抢先一步把信号送过来?

data_hazard_forwarding.png

但是,这种方案没法应付所有的情况。比如这里。

data_hazard_forwarding_bubble.png

这里需要暂停一个时钟周期,否则会触发时间悖论。

不过还有一种方法,需要软硬件的协同。

1
2
3
4
5
6
7
ld x1, 0(x31)               ld x1, 0(x31)
ld x2, 8(x31)               ld x2, 8(x31)
add x3, x1, x2        ->    ld x4, 16(x31)
sd x2, 24(x31)       /      sd x2, 24(x31)
ld x4, 16(x31)      -       add x3, x1, x2
add x5, x1, x4              add x5, x1, x4
sd x5, 32(x31)              sd x5, 32(x31)

前递技术,抢先一步

1
2
3
4
5
sub x2, x1, x3
and x12, x2, x5
or x13, x6, x2
add x14, x2, x2
sd x15, 100(x2)

上面那一些东西有哪些地方发生了数据冒险?

forwarding.png

所以说,在把东西放到 ALU 之前,你现在要多判断一些东西:

  • 有没有发生数据冒险,要不要来个前递?
  • 前递的值本来来自 ID/EX,现在还可能来自以前的 EX/MEM 和 MEM/WB。作为 rs1 还是 rs2?

forwarding_paths.png

forwarding_conditions.png

双份的数据冒险

1
2
3
add x1, x1, x2
add x1, x1, x3
add x1, x1, x4

当同时有两个数据冒险出现的时候,那么对于某条指令,它使用的数据当然是来自离它时间上更近的那个。

data_hazard_which_way.png

和 load 相关的数据冒险

load-use_data_hazard.png

也就是

  • ID/EX.MemRead 是 1
  • 并且 ID/EX.Rd = IF/ID.Rs1 或者 IF/ID.Rs2

控制冒险

不同的指令之间可能指令的控制流上的依赖,比如 RISC-V 的 B 型指令需要的那些符号位。

control_hazard.png

一种方法是等;另一种方法是预测。

control_hazard_prediction.png

不过,如果预测出错……在之前,判断是否跳转是在 MEM 阶段。那么这样的话会直接把流水线 CPU 打成单周期 CPU:

branch_hazard_stall.png

等会!真的只能在 MEM 阶段判断吗?

beq 和 bne 只是比较两个寄存器的值。何不把这一步在 ID 阶段就做掉呢?

stall_on_branch.png

那这样的话,就要在 ID 阶段加一个加法器计算 PC 和立即数的和,再加一个两个寄存器的比较器。

分支延迟槽

不过这样的话,这个跳转指令之后仍然有个指令有可能要被换成 bubble。但是,如果我们能在这里放上一条“不管怎么样都有用”的指令的话……

branch_delay_slot.png

code_scheduling.png

数据冒险的变化

另外,前递也要专门为这个改动作变化,因为之前的前递都是把值扔到 EX 阶段,但是现在需要扔到 ID 阶段。

data_hazards_for_branches.png

一种解决方案是和 ld 指令一样地暂停。

还有,如果你的 b 型指令和 ld 指令缠上了,那么你中断的时钟周期还要多一个。

data_hazards_for_branches_2.png

data_hazards_for_branches_3.png

流水线的实施

pipelining_implementation.png