推测执行

从超标量到推测执行:指令级并行的极限挖掘

Posted by CloudingYu on April 15, 2026

一、课程回顾:Tomasulo 算法与 ROB

1.1 发展路径

单周期流水线乱序执行的发展路径:

  • 流水线提速有限,于是引入乱序执行(Out-of-Order Execution)。
  • 为保证乱序执行的正确性,设计了保留站(Reservation Station)作为指令队列。
  • 乱序发射(issue)、顺序提交(commit)需要重排序缓冲区(Reorder Buffer, ROB)。
  • ROB 使得指令按原始顺序提交,实现精确异常(Precise Exception)——任何指令发生异常时,处理器状态与顺序执行一致。

Tomasulo 练习题结果有时只减少了很少几个时钟周期——”费了这么大劲儿,就这点提升”。但这正是体系结构优化的常态,每一个微小进步在当时都是巨大的创新。

1.2 Tomasulo 硬件代价

乱序执行带来的硬件开销很大:

  • 增加了大量保留站(本质上是寄存器)
  • 增加了CDB(Common Data Bus,公共数据总线)——所有功能单元共享
  • 增加了 Load/Store Buffer
  • 增加了 ROB

1.3 记分板与 Tomasulo 的关系

  • 没有 ROB 的乱序执行方案叫做记分板(Scoreboard)算法。
  • 加了 ROB 之后就是 Tomasulo 算法——指令乱序执行完成后,按顺序从 ROB 中提交,保证精确状态。

1.4 CISC 内部转 RISC

一个重要的工程思想:

  • RISC(Reduced Instruction Set Computer)指令简单、流水线级数少、主频高,容易添加各种优化逻辑。
  • CISC(Complex Instruction Set Computer,如 x86/AMD64)指令复杂,直接做乱序执行非常困难。
  • 解决方案:对外提供 CISC 指令集(给编译器/用户用),内部将 CISC 指令翻译为 RISC 微操作(micro-ops)执行。
  • 现代 x86 处理器内部实际上是一个 RISC 内核,流水线通常有十几级

x86/AMD64 看起来还是 CISC 指令集,但等到了 CPU 内部,实际上已经变成了 RISC 内核。


二、从单发射到多发射

2.1 为什么要多发射

前面的优化(流水线、乱序执行)已经做了,但还不够快。自然的想法是:

  • 既然已经有了乱序执行,指令在 CPU 内部”随便捣鼓”,那能不能一个周期发射多条指令
  • 条件也成熟了:memory 总线已经是 64 位,一个周期本来就能读两条 32 位指令。

反正都乱序了,也别在乎哪条指令先来后到,多给点指令就行。

2.2 多发射面临的问题

一次发射多条指令并不简单:

  1. 数据依赖(Data Dependence):后面的指令可能依赖前面指令的结果。
  2. 结构冲突(Structural Hazard):多条指令同时需要同一个功能单元。
  3. 分支指令:遇到跳转指令时,后续指令是否有效取决于分支结果——预测错了后面全白做。

2.3 保留站对主频的影响

添加保留站等硬件会 slow down max clock frequency(降低最大时钟频率)。这是一个矛盾:

  • 流水线本来就是为了提高主频。
  • 加入乱序执行的复杂逻辑后,关键路径变长,反而可能降低主频。
  • 需要在IPC(每周期指令数)提升主频下降之间做权衡。

三、超标量与超长指令字

3.1 超标量处理器(Superscalar)

超标量(Superscalar):每个时钟周期可以发射不固定数量的多条指令(通常 2-8 条)。

  • 指令按序发射(in-order issue),在发射时进行冒险检测(hazard detection)。
  • 根据检测结果决定本周期实际发射几条。
  • 静态调度动态调度两种方式。

3.2 超长指令字(VLIW)

超长指令字(Very Long Instruction Word, VLIW):每个时钟周期发射固定数量的指令,打包成一个”长指令”。

  • 指令长度可达一百多位到几百位。
  • 每个槽位(slot)对应一种功能单元,填什么指令是固定的。
  • 编译器负责分析代码的数据依赖和并行性,将指令安排到合适的槽位。
  • 典型应用:DSP 芯片(如 TI 的数字信号处理器),适合大量规则计算(卷积、傅里叶变换等)。

3.3 超标量 vs VLIW 对比

特性 超标量(Superscalar) VLIW
每周期发射数 动态决定(不固定) 固定
并行性分析 硬件完成 编译器完成
硬件复杂度 较低
编译器复杂度 较低
槽位组合 任意组合 固定槽位
典型应用 通用 CPU(Intel/AMD) DSP、特定领域处理器

“屁股决定脑袋”——做硬件的人希望编译器多干点活,做编译器的人希望硬件自己处理并行性。


四、静态调度超标量示例

4.1 VLIW 循环展开示例

以一个简单循环 x[i] = x[i] + s 为例,假设一个 VLIW 处理器每周期可同时发射 5 条指令(两条访存、两条浮点运算、一条整数/分支):

  • 循环展开 7 次后:
    • 运行时间:9 个时钟周期产出 7 个结果
    • 每个结果约需 1.29 个周期
    • 平均每周期发射 2.5 条指令(5 个槽位只填充了约一半)

4.2 循环展开的代价

  • 代码膨胀:展开后代码变长,占用更多的指令 Cache 空间。
  • 代码太长会导致指令 Cache 频繁换入换出(thrashing),反而降低性能。
  • 未填充的空槽位浪费了硬件资源。

可以采用智能编码、指令压缩等方法缓解,但做起来又要加很多额外的硬件。

4.3 VLIW 的锁步机制

早期 VLIW 采用锁步(lock-step)机制:任何一个功能单元出现停顿时,整个处理器都要停顿。这是为了实现简单,但在遇到 Cache Miss 时代价巨大。


五、动态多发射与推测执行

5.1 扩展 Tomasulo 算法支持多发射

将 Tomasulo 算法扩展为支持每周期发射两条指令(一条整数/分支 + 一条浮点/访存):

  • 实际现代处理器一般发射 3-4 条,超过 4 条的不多。
  • 实现方式:可以半个周期发一条(利用时钟两个边沿),或直接加宽总线带宽一次读取多条。

5.2 无推测执行的多发射示例

以一个简单循环(LD → ADD → SD → DADDIU → BNE)为例,展开 3 次(15 条指令),每周期发射 2 条:

  • 第 1 周期:发射 LD 和 DADDIU(无依赖冲突)
  • 第 2 周期:发射 SD 和第 2 轮的 LD
  • 第 3 周期:只能发射 BNE 一条(遇到分支指令不能同时发射下一条)
  • ADD 指令需要等待 LD 的结果写入 CDB 后才能执行

没有推测执行时,三个循环共 15 条指令需要 19 个时钟周期

5.3 加入推测执行

  • 有了 ROB,可以大胆进行推测执行(Speculative Execution)。
  • BNE 指令在第 7 周期执行,没有推测时后续指令必须等到第 7 周期确认分支结果后才能开始。
  • 有推测执行后:BNE 后面的指令可以在第 4 周期就提前发射并执行,不必等分支结果。
  • 如果预测正确,第 8 周期 BNE 提交后,后续指令第 9 周期直接提交。
  • 结果:从 19 个周期降到 14 个时钟周期,节省了 5 个周期。

费了这么大劲儿,用了这么多智慧,就是为了再挤那么一点点的时间。

5.4 推测执行与精确状态

ROB 使得推测执行安全可靠:

  • 推测执行的结果暂存在 ROB 中,不写入体系结构寄存器。
  • 预测正确:按顺序从 ROB 提交到寄存器文件。
  • 预测错误:丢弃 ROB 中的推测结果,从正确分支重新执行。
  • 任何指令发生异常时,其上下文状态与顺序执行完全一致——精确异常

六、提高取指带宽

6.1 多种提高取指带宽的方法

  • 每个时钟周期提取 4-8 条指令。
  • 利用时钟的上升沿和下降沿(DDR 技术的本质原理):
    • 时钟频率 100MHz,上升沿和下降沿都访问 memory → 等效 200MHz
    • 再在两个沿之间插入采样点 → 一个时钟周期取 4 次(四分频)
  • 为什么不直接提高时钟频率?因为频率太高会不稳定(毛刺等问题),所以在不提高时钟频率的前提下”压榨硬件”。

七、分支预测与 BTB

7.1 分支目标缓冲区(BTB)

分支目标缓冲区(Branch Target Buffer, BTB):

  • 本质是一个查找表:记录历史上遇到的分支指令及其跳转目标地址。
  • 当前 PC 值同时送入指令存储器BTB
  • 如果 BTB 命中且预测为”跳转”,直接从 BTB 获取目标地址,无需等待分支计算。

7.2 BTB 的工作流程

  1. 当前 PC 送入 BTB 查找:
    • 命中 → 预测为分支指令,直接获取目标地址
    • 未命中 → 可能不是分支指令,也可能是新的分支指令
  2. 后续执行确认预测是否正确:
    • 预测正确 → 继续
    • 预测错误 → 不立即删除该项(给一次免费错误机会),类似两位预测器的”宽容”策略
    • 新发现的分支指令 → 加入 BTB

7.3 分支预测的代价分析

以一个量化例子说明(参考《计算机体系结构:量化研究方法》):

  • 预测准确率 90%,BTB 命中率 90%
  • BTB 命中但预测错误的概率:$0.9 \times 0.1 = 9\%$
  • 不在 BTB 中但实际是分支的概率:$10\%$
  • 总分支代价:$0.09 + 0.1 = 0.19$(约 0.38 个周期的动态代价)

现代高性能处理器的分支预测错误代价约为 15 个时钟周期。前面费了那么大劲攒了几个 clock,一个 Cache Miss 就把几百个周期扔进去了——”千日砍柴一日烧”。

7.4 预测准确率的重要性

  • 现代预测器准确率可达 90%-95%
  • 代码特征影响预测准确率:大量循环(如图像处理)中的分支几乎都是”跳转”,预测容易;控制流复杂的代码预测困难。

八、寄存器重命名

8.1 为什么需要更大的物理寄存器集

  • 指令集中可见的寄存器数量有限(如 RISC-V 的 32 个通用寄存器),受制于指令编码宽度(32 位中每一位都很宝贵)。
  • CPU 内部可以使用更大的物理寄存器文件进行寄存器重命名(Register Renaming)。
  • 这是 ROB 的一种替代/补充方法:使用明确的、更大的物理寄存器集合,消除 WAR 和 WAW 冲突。

ISA 限制了你能看到的寄存器数量,但进入 CPU 内部后,用 Tomasulo 的方法重新给它改名就好。


九、指令级并行的极限

9.1 Amdahl 定律回顾

Amdahl 定律(Amdahl’s Law):如果程序中比例为 $x$ 的部分可以被加速 $n$ 倍,则整体加速比为:

\[S = \frac{1}{(1-x) + \frac{x}{n}}\]

当 $n \to \infty$ 时,$S \to \frac{1}{1-x}$。即不可并行的部分决定了加速的上限。

9.2 理想处理器的 ILP 实验

假设一个”理想处理器”:无限寄存器、完美分支预测、完美存储器别名分析、完美缓存。

在这种理想条件下测试不同 benchmark 的 ILP:

条件 每周期可发射指令数上限
理想(无限) 约 75-150(取决于程序)
可实现(64 发射) 约 30-50
实际(4 发射) 约 2-4

9.3 发射宽度的收益递减

从实际数据可以看出:

  • 4 → 8 → 16 发射:性能增长明显
  • 16 → 32 → 64 发射:增长趋缓
  • 64 → 128 → 256:几乎没有额外收益

工程决策方法:盯着性能-成本曲线,找到”拐点”。4 条太少,8 条”咬咬牙也能搞定”,16 条”再去申请点经费”,32 条以上”再给钱也没用了”。

9.4 当前 ILP 的实际水平

到目前为止,ILP 的实际水平是每个周期发射 3-4 条指令。这就是指令级并行的瓶颈。要进一步提升性能,需要从更高层次挖掘并行性——线程级并行(Thread-Level Parallelism, TLP)。


十、线程与进程的区别

特性 线程(Thread) 进程(Process)
内存空间 共享同一内存空间 各自拥有独立的内存空间
数据通信 直接通过共享内存访问 需要通过消息机制(IPC)
通信效率 低(打包、协议、解包)
隔离性 弱(需要同步机制) 强(互相不影响)
示例 一个程序内的多个执行流 Word 和浏览器是两个进程

十一、Cache 与存储层次结构预告

  • 指令执行的优化已经提升到一定程度,继续提升收益递减。
  • 真正的瓶颈是访存效率
  • 一个 Cache Miss 就可能浪费几十到几百个时钟周期,远超指令优化攒出来的那点收益。

多级 Cache 结构:

  • L1 Cache:速度最快,容量小
  • L2 Cache:速度次之,容量更大
  • L3 Cache:速度更慢,容量最大
  • L4 Cache:极少使用,因为级数太多会使控制逻辑过于复杂

一级不行弄二级,二级不行弄三级,三级再不行——冷静一下,不要四级了。