我们邀请了香山团队的同学在年后给大家分享若干技术报告
算是给大家凑一个S阶段 😂
这样我就可以 🕊🕊🕊 了
经典体系结构的4类优化方法, 我们已经介绍了其中两类
听完这节课, 你就知道为什么我们最后才讲流水线了
教科书上的经典5级流水线: 取指 -> 译码 -> 执行 -> 访存 -> 写回
但在实际的处理器设计中, 上述假设不再成立
将访存单独作为一个阶段并无好处
/--- frontend ---\ /-------- backend --------\
+-----+ <--- 3. computation efficiency
+--> | FU | --+
+-----+ +-----+ | +-----+ | +-----+
| IFU | --> | IDU | --+ +--> | WBU |
+-----+ +-----+ | +-----+ | +-----+
^ +--> | LSU | --+
| +-----+
1. instruction supply ^
2. data supply --+
接下来我们讨论4级流水线: 取指 -> 译码 -> 执行(合并了访存) -> 写回
单周期:
+-----+ inst ---> +-----+ ... ---> +-----+ ... ---> +-----+
| IFU | valid ---> | IDU | valid ---> | EXU | valid ---> | WBU |
+-----+ <--- ready +-----+ <--- ready +-----+ <--- ready +-----+
流水线:
+----+ <- stage reg +----+ +----+
+-----+ -> |inst| -> +-----+ -> |....| -> +-----+ -> |....| -> +-----+
| | +----+ | | +----+ | | +----+ | |
| IFU | valid ---> | IDU | valid ---> | EXU | valid ---> | WBU |
| | | | | | | |
+-----+ <--- ready +-----+ <--- ready +-----+ <--- ready +-----+
握手时消息传递到下一级流水, 否则等待
从逻辑上理解, 流水段寄存器也可以位于下游模块内部, 作为接收消息的缓冲区
假设取指, 译码, 执行, 写回的延迟都是1ns(先不考虑访存)
上面只是理想情况, 实际上有很多问题需要解决
在流水线中, 当前周期不能执行当前指令的情况
在流水线设计中需要检测出冒险, 并正确处理它们
流水线中的不同阶段需要同时访问同一个部件
T1 T2 T3 T4 T5 T6 T7
+----+----+----+----+
I1: add | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I2: ld | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I3: sub | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I4: xor | IF | ID | EX | WB |
+----+----+----+----+
大部分结构冒险可从设计上完全避免, 使其在CPU执行过程中不会发生
有一些结构冒险还是无法完全避免
处理方式: 等
一个好消息: 如果我们从总线视角来看, 就无需实现专门的结构冒险检测和处理逻辑
不同阶段的指令依赖同一个寄存器数据, 且至少有一条指令写入该寄存器
T1 T2 T3 T4 T5 T6 T7
+----+----+----+----+
I1: add a0,t0,s0 | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I2: sub a1,a0,t0 | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I3: and a2,a0,s0 | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I4: xor a3,a0,t1 | IF | ID | EX | WB |
+----+----+----+----+
上述数据冒险称为写后读(Read After Write, RAW)冒险
T1 T2 T3 T4 T5 T6 T7 T8 T9
+----+----+----+----+
I1: add a0,t0,s0 | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I2: nop | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I3: nop | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I4: sub a1,a0,t0 | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I5: and a2,a0,s0 | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I6: xor a3,a0,t1 | IF | ID | EX | WB |
+----+----+----+----+
编译器尝试寻找一些没有数据依赖关系的指令, 在不影响程序行为的情况下调整其顺序
(1) add a0,t0,s0 (1) add a0,t0,s0
(2) sub a1,a0,t0 (5) add t5,t4,t3
(3) and a2,a0,s0 ---> (6) add s5,s4,s3
(4) xor a3,a0,t1 (2) sub a1,a0,t0
(5) add t5,t4,t3 (3) and a2,a0,s0
(6) add s5,s4,s3 (4) xor a3,a0,t1
编译器只能尽力而为, 实在找不到, 就只能插入nop
考虑load-use冒险(一种特殊的RAW冒险)
T1 T2 T3 .... T? T? T? T? T?
+----+----+--------------+----+
I1: ld a0,t0,s0 | IF | ID | EX | WB |
+----+----+--------------+----+
+----+----+----+----+
I?: nop X 30? | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I?: sub a1,a0,t0 | IF | ID | EX | WB |
+----+----+----+----+
在真实的SoC中, 软件几乎无法预测访存延迟 😂
采用计分板(scoreboard)为每个通用寄存器记录是否将要被写入
检测到RAW冒险后的一种简单处理方式: 等
果壳将计分板的维护放在IS阶段, 避免ID阶段的工作过多成为关键路径
T1 T2 T3 T4 T5 T6 T7 T8 T9
+----+----+----+----+
I1: add a0,t0,s0 | IF | ID | EX | WB |
+----+----+----+----+
+----+--------------+----+----+
I2: sub a1,a0,t0 | IF | ID | EX | WB |
+----+--------------+----+----+
+--------------+----+----+----+
I3: and a2,a0,s0 | IF | ID | EX | WB |
+--------------+----+----+----+
+----+----+----+----+
I4 xor a3,a0,t1 | IF | ID | EX | WB |
+----+----+----+----+
其实scoreboard是个很好的检测方案
跳转指令会改变指令执行顺序, 导致IFU可能会取到不该执行的指令
I4需要等到beq指令在T5计算出跳转结果, 才知道应该取哪条指令
T1 T2 T3 T4 T5 T6 T7
+----+----+----+----+
I1: 100 add | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I2: 104 ld | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I3: 108 beq 200 | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I4: ??? ??? | IF | ID | EX | WB |
+----+----+----+----+
检测方法: 可以在IFU中进行预译码(pre-decode), 专门识别跳转指令
一种简单的解决方案: 等
T1 T2 T3 T4 T5 T6 T7
+----+----+----+----+
I1: add | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I2: ld | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I3: 00000000 | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I4: ??? (fetch error) | IF | ID | EX | WB |
+----+----+----+----+
T1 T2 T3 T4 T5 T6 T7
+----+----+----+----+
I1: add | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+
I2: ld | IF | ID | EX |
+----+----+----+
+----+----+
I3: 00000000 | IF | ID |
+----+----+
+----+
I4: ??? (fetch error) | IF |
+----+
+----+----+----+----+
I5: inst pointed by mtvec | IF | ID | EX | WB |
+----+----+----+----+
若在T4时ld指令发生地址不对齐异常, 则需要冲刷ld指令以及更年轻的指令, 使其不继续执行
根据异常的类型更新mcause和mepc
检测各种RISC-V异常的模块
0 - Instruction address misaligned - IFU
1 - Instruction access fault - IFU
2 - Illegal Instruction - IDU
3 - Breakpoint - IDU
4 - Load address misaligned - LSU
5 - Load access fault - LSU
6 - Store/AMO address misaligned - LSU
7 - Store/AMO access fault - LSU
8 - Environment call from U-mode - IDU
9 - Environment call from S-mode - IDU
11 - Environment call from M-mode - IDU
12 - Instruction page fault - IFU
13 - Load page fault - LSU
15 - Store/AMO page fault - LSU
中断可在任意指令的任意阶段捕获并处理
实现了简单的冒险处理后, NPC就可以跑仙剑了
但我们刚才只是粗暴地阻塞来解决冒险, 现在可以思考如何尽可能消除阻塞, 让流水线流起来
阻塞的来源有很多, 我们希望了解阻塞最频繁的来源
通过性能计数器进行profiling!
根据当前的设计, 你觉得阻塞最频繁的事件是什么?
我们之前的cache都是状态机控制的
T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12
+--------------+----+----+----+
I1 | IF | ID | EX | WB |
+--------------+----+----+----+
+--------------+----+----+----+
I2 | IF | ID | EX | WB |
+--------------+----+----+----+
+--------------+----+----+----+
I3 | IF | ID | EX | WB |
+--------------+----+----+----+
我们希望icache在连续命中的情况下, 每周期都能读出指令
T1 T2 T3 T4 T5 T6 T7 T8
+----+----+----+----+----+----+
I1 | IF1| IF2| IF3| ID | EX | WB |
+----+----+----+----+----+----+
+----+----+----+----+----+----+
I2 | IF1| IF2| IF3| ID | EX | WB |
+----+----+----+----+----+----+
+----+----+----+----+----+----+
I3 | IF1| IF2| IF3| ID | EX | WB |
+----+----+----+----+----+----+
通过这种方式, 指令供给断开的概率 = icache缺失率
dcache可以保留状态机的设计, 可以认为LSU一次只能处理一条访存指令
T1 T2 T3 T4 T5 T6 T7 T8
+----+----+----+----+----+----+----+----+
I1: mul | IF1| IF2| IF3| ID |MUL1|MUL2|MUL3| WB |
+----+----+----+----+----+----+----+----+
+----+----+----+----+----+----+----+----+
I2: mul | IF1| IF2| IF3| ID |MUL1|MUL2|MUL3| WB |
+----+----+----+----+----+----+----+----+
+----+----+----+----+----+----+----+----+
I3: mul | IF1| IF2| IF3| ID |MUL1|MUL2|MUL3| WB |
+----+----+----+----+----+----+----+----+
除法器较难流水化, 一般采用多周期
一个想法: 计算指令结果并非WB阶段才产生, 能否提前拿到?
T1 T2 T3 T4 T5 T6 T7
+----+----+----+----+
I1: add a0,t0,s0 | IF | ID | EX | WB |
+----+----+----+----+
+----+--------------+----+----+
I2: sub a1,a0,t0 | IF | ID | EX | WB |
+----+--------------+----+----+
+----+----+----+----+
I1: add a0,t0,s0 | IF | ID | EX | WB |
+----+----+----+----+
|
V
+----+----+----+----+
I2: sub a1,a0,t0 | IF | ID | EX | WB |
+----+----+----+----+
I1中a0的新值在T4时刻已经可以从EX-WB的流水段寄存器中读出
还需要根据寄存器号码判断是否可以转发
一个考察大家训练是否到位的问题: CSR指令的转发需要注意什么?
想法: 与其被动等待, 不如主动出击
这就是分支预测, 属于投机执行技术, 有固定模式: 投机执行-检查-恢复
仅根据指令本身来预测
Software should also assume that backward branches will be predicted taken and
forward branches as not taken, at least the first time they are encountered.
根据分支指令在过去一段时间内的历史行为来预测当前行为
一般采用饱和计数器
分支预测器如何设计, 是一个体系结构的设计空间探索问题
分支预测的准确率直接影响指令供给, 在乱序执行处理器中更重要
分支指令的计算结束前, 后续指令的执行都是投机的, 可能被冲刷
fence.i指令的语义是, 让fence.i之后的取指操作可以看到fence.i之前的store结果
T1 T2 T3 T4 T5 T6 T7
+----+----+----+----+
I1: add | IF | ID | EX | WB |
+----+----+----+----+
+----+----+----+----+
I2: fence.i | IF | ID | EX | WB |
+----+----+----+----+
+----+
I3: ??? may be stale | IF |
+----+
+----+----+----+----+
I4: sub | IF | ID | EX | WB |
+----+----+----+----+
也即, 在保证icache和dcache一致性的基础上, 再冲刷流水线, 可保证后续取指操作能看到之前的store结果
这么多流水级, 若每一级指令类型不同, 可能会有不同的行为
共11种指令
只考虑4级流水线, 也有\(11^4=14641\)种可能
你不会愿意设计那么多测试用例的 😂
大部分教科书已经把流水线的原理讲得很清晰了
我们有足够理由把流水线放在最后讲, 来介绍真实的流水线设计