跳转至

第 5 周

循环预测器

在之前我们已经说过了关于 b 型指令的一大堆事情。在实现的时候,我们可以假设它成功或者是失败。

不过,不管是总假设为成功还是总假设为失败,都是在蒙。有没有一种可能,让我们可以“聪明一点地”预测?

一位的预测器

用一位来记录上次跳转的结果。如果上一次成功,那么这一次也假设成功;如果上一次失败,那么这一次也假设失败。

但是这样会被 hack:

1
2
3
4
outer:
inner:
beq ..., inner
beq ..., outer

会连续失败两次。那么我们不如……

两位的预测器

two_digit_predict.png

多发射处理器

multiple-issue.png

也就是同一个时刻会发射出多条指令。

问题来了,我们在流水线处理器中所经历的那些冒险……在这里就会更加严重。

静态多发射

编译器会把那些互相不冲突的指令放在同一个“发射槽”里面,以规避各种问题。

动态多发射

CPU 自己会分析指令流,然后在每一个时钟周期中选择那些合适的指令。

编译器可以预先排列指令来帮助 CPU,不过最终还是让 CPU 在运行时自己做选择。

猜测 Speculation

对于某个指令,尽量早地开始对它的执行。在之后,得检查自己的预测正不正确,如果不正确的话就要把已经进入后面部件的指令清理掉。

超标量 Superscalar

一个时刻内同时发射出来的指令的条数是不确定的,不过有上限。上限为 n 的处理器就叫做 n 发射处理器。

只搞软件的程序员们一般来说不会注意到它的存在,因为选择哪些指令来并行处理是由处理器自己处理的。

目前来说,这种方法对于通用计算来说是最有效的。

超长指令字(Very Long Instruction Word,VLIW)

一个时刻内同时发射出来的指令的条数是确定的,而这些指令会被一并压进一个指令字里面。比如 32 位指令的 CPU 要做 4 发射,那么指令字就会变成 128 位。

至于选哪些指令字去压……这里是让编译器来处理。这有些时候会导致一些兼容性的问题。

所以这种方法一般用在数字信号处理和多媒体的处理。

various_multiple_issue.png

什么大叠

静态安排的超标量多发射科技

这大概可以分成两个阶段:

  • 首先,检测在将要发射的那些指令里有没有什么冲突。先要把那些一开始就能执行的指令拿出来。
  • 然后还要看看这些被选中的指令是否和正在执行的指令有冲突。

RISC-V 是这样实现超标量的:

  • 每个时钟周期里面可以放两条指令,一条整型指令,一条浮点型指令。
    • load、store 和 branch 算整型指令。
  • 于是这样就有两条 32 位的指令。
    • 首先从内存里面把它们取出来。
    • 然后看一下有哪些可以立即被执行(可能有 0 到 2 条)。
    • 把它们发到相应的处理部分上。
  • 另外,浮点型指令被认为是“附加的指令”,它们的 EX 阶段有 2 个时钟周期。

int_and_fp_instruction.png

可以预见,这样的话,在原先的流水线处理器上要添加的硬件并不算很多。

risc-v_static_dual.png

VLIW 科技

编译器会把那些指令打包成一个一个一个指令包。

vliw_flow.png

超级流水线

对于它,它在每个时钟周期内可以打出 n 条指令,每 1/n 个周期打出一条。

super-pipeline.png

双发射 RISC-V 的冒险

现在有更多的指令被并行执行了!

EX 的数据冒险

很明显你不能把这两条指令放在一个指令包里面:

add x10, x0, x1
ld x2, 0(x10)

要把它们分开。必要的话得插入 nop。

Load-use 冒险

还是得额外延迟一个周期。

循环展开和寄存器重命名

1
2
3
4
5
ld   x31, 0(x20)
add  x31, x31, x21
sd   x31, 0(x20)
addi x20, x20, -8
blt  x22, x20, loop

loop_unrolling_1.png

我们可以通过一些手段来消除循环周期之间的依赖:

loop_unrolling_2.png

动态的多发射

现在,让我们抛弃编译器所做的那么大的努力(说抛弃其实也不是那么抛弃),让 CPU 自己选择同时执行哪些指令。

dynamically_scheduled_cpu.png