
计算机系统 III 硬件第二章:指令级并行(ILP)及其开发
本文系统介绍指令级并行的核心概念,包括流水线基础回顾、相关性(数据相关、名相关、控制相关)、冒险(结构冒险、数据冒险、控制冒险)、动态调度、Tomasulo算法、分支预测等内容,是计算机系统 III 硬件部分的重要章节。
[迁移说明] 本文最初发布于
blog.zzw4257.cn,现已迁移并在本站进行结构化整理与增强。
第二章:指令级并行 (ILP) 及其开发
复习 (Review) - 流水线基础回顾
在深入动态调度和指令级并行之前,我们先回顾一下流水线的基础概念和相关问题。
关键概念
- 流水线 (Pipelining): 一种将指令执行过程分解为多个阶段,并让不同指令的各个阶段重叠执行的技术,以提高处理器吞吐率。
- 经典五级流水线 (RISC): IF (取指), ID (译码/读寄存器), EX (执行/地址计算), MEM (访存), WB (写回)。
- 相关性 (Dependences): 程序中指令间的内在约束,是程序的属性。
- 数据相关 (Data Dependences): 一条指令需要等待前面指令的结果。 (e.g.,
FLD F0, 0(R1)后FADD.D F4, F0, F2,FADD 依赖于 FLD 的结果 F0)。 - 名相关 (Name Dependences): 两条指令使用相同的寄存器名,但没有真正的数据流动。
- 反相关 (Anti-dependence): 后一条指令写寄存器,前一条指令读该寄存器 (WAR)。 e.g.,
FDIV.D F2, F6, F4后FADD.D F6, F0, F12。 - 输出相关 (Output-dependence): 两条指令写同一个寄存器 (WAW)。 e.g.,
FDIV.D F2, F6, F4后FSUB.D F2, F6, F14。
- 反相关 (Anti-dependence): 后一条指令写寄存器,前一条指令读该寄存器 (WAR)。 e.g.,
- 控制相关 (Control Dependences): 一条指令的执行与否取决于前面条件分支指令的结果。
- 数据相关 (Data Dependences): 一条指令需要等待前面指令的结果。 (e.g.,
- 冒险 (Hazards): 流水线组织方式导致的、妨碍下一条指令在预定周期开始执行的情况,是流水线组织的属性。
- 结构冒险 (Structure Hazards): 所需资源(如硬件单元)繁忙。
- 数据冒险 (Data Hazards): 指令执行顺序与数据相关性冲突。
- 写后读 (RAW - Read After Write): 对应真正的数据相关。
- 读后写 (WAR - Write After Read): 对应反相关。
- 写后写 (WAW - Write After Write): 对应输出相关。
- 控制冒险 (Control Hazards): 分支指令导致后续指令取指不确定。
解决数据冒险
- 转发 (Forwarding / Bypassing): 将计算结果从产生它的流水线阶段直接传递给需要它的后续指令的较早阶段,避免停顿。
- 插入气泡/停顿 (Forwarding with Bubble / Stall): 当转发无法完全解决冒险时(如 Load-Use 冒险),需要插入空操作周期。
- 代码调度 (Code Scheduling to Avoid Stalls): 编译器或程序员通过重排指令顺序,将无关指令插入到可能产生停顿的地方,以填充流水线。
- 例如,在
A = B + C的计算中,如果LD Rb, B和LD Rc, C之后紧跟DADD Ra, Rb, Rc,可能会因为 Load-Use 冒险产生停顿。通过插入其他不相关的指令可以避免。
- 例如,在
解决控制冒险
- 分支停顿 (Stall on Branch): 等待分支结果确定后再取后续指令,代价较大。
- 分支预测 (Branch Prediction):
- 静态分支预测: 基于典型行为(如循环分支向后跳转通常发生,条件分支向前跳转通常不发生)。
- 动态分支预测: 硬件记录分支历史行为,并据此预测未来。
- 分支历史表 (BHT - Branch History Table):
- 1-位预测器: 记录上次分支是否发生。缺点:循环末尾和循环开始会连续两次预测错误。
- 2-位预测器: 使用两位状态机,对短期行为变化有更好的容忍度,能避免1-位预测器在循环中的两次错误,通常只在循环最后一次迭代时预测错误。
- 分支历史表 (BHT - Branch History Table):
- 延迟槽 (Delayed Branch): 分支指令之后的一条或多条指令(在延迟槽中)总是被执行,无论分支是否发生。编译器负责填充有用的指令或
nop。- 思考: 延迟槽是一个好的设计吗?RISC-V ISA 手册指出其基础整数 ISA 没有 分支延迟槽,表明现代设计倾向于避免它,因为它增加了编译器复杂性,并可能在不同微架构实现间造成困扰。
- 分支目标缓冲 (BTB - Branch-Target Buffer / Branch-Target Cache):
- 缓存最近发生过的分支指令的目标地址和预测信息。
- 当取指 PC 命中 BTB 且预测发生时,可以立即从预测的目标地址取指,消除或减少分支惩罚。
- 好处: 更快获取分支目标处的指令;可以一次提供多条指令(对多发射处理器重要);分支折叠(branch folding)可能实现无延迟的无条件跳转,有时甚至无延迟的条件跳转。
缓冲器的重要性
- 指令缓冲器 (Instruction Buffer): 在存储器和指令译码单元之间添加,用于平滑指令流,解决访存冲突(哈佛结构中指令和数据缓存同在主存时,或多体交叉存储器有局限时)。
- 先行指令缓冲器、先行数据读缓冲器、数据写缓冲器 等都是为了实现重叠执行、提高流水线效率。
课后思考 (回顾练习题)
题目: 考虑以下代码:
FADD.D R1, R2, R4
FADD.D R2, R1, 1
FSUB.D R1, R4, R5- 指出所有的流水线冒险 (RAW, WAR, WAW)。
- 分析这些冒险并给出你的解决方案。
解答提示:
冒险识别:
FADD.D R1, R2, R4(指令1)FADD.D R2, R1, 1(指令2)- RAW on R1: 指令2读R1,指令1写R1。
- WAR on R2: 指令2写R2,指令1读R2。
FSUB.D R1, R4, R5(指令3)- RAW on R4: 指令3读R4,指令1读R4 (这里不是RAW,是两条指令都读R4,没有写操作在中间)。
- WAW on R1: 指令3写R1,指令1写R1。
- WAR on R4: 指令3读R4,指令1读R4 (同上)。
- RAW on R1 from Instr2? 如果指令2的R1结果还没写回,指令3就读R1,这也是RAW。
更正和精确化冒险分析:
- 指令1:
FADD.D R1, R2, R4 - 指令2:
FADD.D R2, R1, 1- RAW for R1: 指令2的源操作数R1依赖于指令1的目的操作数R1。
- WAR for R2: 指令2的目的操作数R2与指令1的源操作数R2同名。
- 指令3:
FSUB.D R1, R4, R5- WAW for R1: 指令3的目的操作数R1与指令1的目的操作数R1同名。
- (指令3的源操作数R4依赖于指令1的源操作数R4,这不是冒险,是正常的读取)
解决方案:
- RAW on R1 (指令1 -> 指令2): 通过转发解决。如果FADD.D的执行结果能在EX阶段结束时就绪,可以转发给下一条指令的EX阶段。如果延迟较长,可能需要停顿。
- WAR on R2 (指令1 -> 指令2): 在简单顺序流水线中,WAR通常不成问题,因为读操作总是在写操作之前发生。但在乱序执行中,需要寄存器重命名解决。
- WAW on R1 (指令1 -> 指令3): 在简单顺序流水线中,写操作按序进行,旧值会被新值覆盖,通常不会导致错误结果,但可能影响精确异常。在乱序执行中,需要寄存器重命名解决,确保指令按序提交。
§2.1 动态调度 (Dynamic Scheduling)
动态调度的动机
简单流水线技术的一个主要局限性是它们使用顺序指令发射和执行 (in-order instruction issue and execution)。 如果一条指令因相关性而停顿,后续无相关的指令也必须等待。
例子:
FDIV.D F4, F0, F2 ; F4 = F0 / F2
FSUB.D F10, F4, F6 ; F10 = F4 - F6
FADD.D F12, F6, F14 ; F12 = F6 + F14FADD.D 指令不能执行,因为 FSUB.D 对 FDIV.D 的依赖导致流水线停顿;然而 FADD.D 本身并不依赖于流水线中任何未完成的指令。
动态调度的思想
- 思想: 动态调度 (Dynamic Scheduling)
- 方法: 乱序执行 (out-of-order execution)
动态调度的实现
为实现乱序执行,我们将简单五级流水线的ID阶段本质上分解为两个阶段:
- 发射 (Issue - IS): 指令译码,检查结构冒险。(顺序发射)
- 读操作数 (Read Operands - RO): 等待数据冒险解除,然后读取操作数。(乱序执行)
⚠️ 新的冒险 乱序执行引入了 WAR (读后写) 和 WAW (写后写) 冒险的可能性,这些在五级整数流水线及其顺序浮点扩展中通常不存在或不构成问题。
解决乱序执行带来的新冒险
- 记分板算法 (Scoreboard Algorithm): 一种调度指令的方法。
- Tomasulo 算法 (Tomasulo’s Approach): 由 Robert Tomasulo 提出,通过硬件中的寄存器重命名 (register renaming) 来最小化 WAW 和 WAR 冒险。
§2.1 Tomasulo 算法
Tomasulo 算法核心思想
- 跟踪操作数可用性: 最小化 RAW 冒险。当指令的操作数就绪时,指令就可以执行。
- 硬件寄存器重命名: 消除 WAR 和 WAW 冒险。通过为指令结果分配临时的“保留站”作为目标,而不是直接写入物理寄存器,直到结果准备好通过公共数据总线 (CDB) 广播。
Tomasulo 算法的三个步骤
指令经历以下三个主要步骤:
发射 (Issue):
- 从指令队列头部获取下一条指令 (FIFO)。
- 如果存在匹配且空闲的保留站 (reservation station),则将指令发射到该保留站,并带上操作数值(如果操作数当前在寄存器中)。
- 如果没有空闲的保留站,则发生结构冒险,指令停顿,直到有保留站或缓冲器空闲。
- 如果操作数不在寄存器中 (即正在由其他功能单元计算),则跟踪将产生这些操作数的功能单元 (通过其保留站标签)。
- !!! success “寄存器重命名” 这一步通过将指令的目标寄存器与保留站关联起来,实现了寄存器重命名,从而消除了不在寄存器中的 WAR 和 WAW 冒险。
执行 (Execute):
- 当一个保留站中的所有操作数都可用时,该操作就可以在相应的功能单元执行。
- Load 和 Store 指令需要两步执行过程:
- 当基址寄存器可用时,计算有效地址。
- 然后将有效地址放入 Load 缓冲器或 Store 缓冲器。
写结果 (Write Result):
- 当结果可用时,将其写到**公共数据总线 (CDB - Common Data Bus)**上。
- 从 CDB,结果被写入到目标寄存器以及任何等待该结果的保留站 (包括 Store 缓冲器)。
- Store 指令:其值和地址都就绪后,结果被缓冲在 Store 缓冲器中,直到存储单元空闲,然后才写入内存。
Tomasulo 算法的数据结构
主要有三个表格/结构来支持 Tomasulo 算法:
- 指令状态表 (Instruction Status table): (仅为理解算法,非实际硬件部分)
- 跟踪每条指令的当前状态 (Issue, Execute, Write Result)。
- 保留站表 (Reservation Stations table):
- 每个保留站维护已发射操作的状态。每个保留站有七个字段:
Op: 要对源操作数执行的操作。Qj, Qk: 将产生相应源操作数的保留站的标签 (如果操作数未就绪)。若操作数已就绪且来自寄存器,则为空或指向寄存器。Vj, Vk: 源操作数的值 (如果已就绪)。Busy: 表明该保留站及其功能单元是否被占用。A: 对于 Load/Store 指令,用于保存内存地址计算的信息 (通常是立即数字段)。
- 每个保留站维护已发射操作的状态。每个保留站有七个字段:
- 寄存器状态表 (Register Status table / Field Qi):
- 对于每个寄存器,记录包含了将把结果存入该寄存器的操作的保留站的标签 (Qi)。如果寄存器中的值是有效的 (没有等待写入),则 Qi 为空或指向该寄存器本身。
课堂练习:Tomasulo 算法分析 (Practice in class - Page 1 & 2)
假设:
- 加法指令 (ADD) 需要 2 个时钟周期。
- 乘法指令 (MUL) 需要 10 个时钟周期。
- 除法指令 (DIV) 需要 40 个时钟周期。
- 加载指令 (LD/FLD) 需要 1 个时钟周期 (EX阶段计算地址,WB阶段通过CDB写回)。
- 所有指令的发射(IS)阶段需要1个周期。
- 所有指令的写回(WB)阶段需要1个周期。
指令序列:
I1: FLD F6, 34(R2)
I2: FLD F2, 45(R3)
I3: FMUL.D F0, F2, F4 ; F4 初始值假设在寄存器中
I4: FSUB.D F8, F2, F6
I5: FDIV.D F10, F0, F6
I6: FADD.D F6, F8, F2问题: 使用 Tomasulo 方法,每条指令完成需要多少个周期?(幻灯片第二页给出了甘特图和表格总结)
分析思路与考点:
- 顺序发射 (IS): 指令按程序顺序发射,每周期发射一条(如果保留站可用)。
- 数据依赖与保留站 (Qj, Qk):
FMUL.D的F2依赖FLD F2。FMUL.D发射时,F2还未就绪,其 Qj/Qk 字段会指向FLD F2所在的保留站 (或Load Buffer)。当FLD F2写回CDB时,FMUL.D的保留站会捕获F2的值。FSUB.D的F2依赖FLD F2,F6依赖FLD F6。FDIV.D的F0依赖FMUL.D,F6依赖FLD F6。FADD.D的F8依赖FSUB.D,F2依赖FLD F2。
- 乱序执行 (EX): 一旦操作数就绪,指令就可以开始执行,不受前面未就绪指令的阻塞。
- 写回 (WB) 与 CDB 广播: 执行完成后,结果通过CDB广播给所有等待它的单元(寄存器堆和保留站)。
- WAR/WAW 消除:
FADD.D F6, F8, F2(I6) 写F6,而FLD F6, 34(R2)(I1) 也写F6。这是 WAW。通过寄存器重命名(FADD.D的结果暂存在其保留站,直到它准备通过CDB写回,此时寄存器状态表会更新为指向FADD.D的保留站),旧的FLD F6结果如果先写回,会更新物理寄存器F6;之后FADD.D的结果再写回时会覆盖它。FSUB.D F8, F2, F6(I4) 读F6,而FADD.D F6, F8, F2(I6) 写F6。这是 WAR。由于FADD.D会在其保留站中等待操作数,并不会过早写入F6从而影响FSUB.D读取旧的F6值。
根据幻灯片 P2 的甘特图和表格: (假设指令按顺序编号 I1 到 I6)
| 指令 | Fi | Fj | Fk | IS (发射) | EX (执行开始-结束) | WB (写回) | 完成周期 |
|---|---|---|---|---|---|---|---|
| FLD F6 | F6 | 34+R2 | - | 1 | 2 (地址) - 2 | 3 (LD为1周期EX) ->应该为 3 (EX完成),4 (WB) (表中L.D的EX是1,WB在EX后1拍) | 4 |
| FLD F2 | F2 | 45+R3 | - | 2 | 3 (地址) - 3 | 4 (LD为1周期EX) ->应该为 4 (EX完成),5 (WB) | 5 |
| FMUL.D F0 | F0 | F2 | F4 | 3 | 6 (F2在5完成) - 15 | 16 | 16 |
| FSUB.D F8 | F8 | F2 | F6 | 4 | 6 (F2在5,F6在4完成) - 7 | 8 | 8 |
| FDIV.D F10 | F10 | F0 | F6 | 5 | 17 (F0在16,F6在4完成) - 56 | 57 | 57 |
| FADD.D F6 | F6 | F8 | F2 | 6 | 9 (F8在8,F2在5完成) - 10 | 11 | 11 |
注意:幻灯片P2的L.D指令的ex和wb周期标示似乎与前面“LD instruction need 1 clock cycle”的假设有出入。如果LD的EX是1个周期完成数据获取,那么WB应该在其后一个周期。表格中LD的EX是3,WB是4,这暗示EX本身占用了2个周期(例如,IS=1, EX开始=2, EX结束=3, WB=4)。这里我们按照表格的周期来理解。 更正理解:LD的EX为1周期,表示地址计算和访存共1周期。所以IS=1, EX=2(开始执行), EX_end=2(执行完毕), WB=3。这里幻灯片表格中的 is, ex, wb 指的是完成该阶段的周期号。
- FLD F6: IS (1), EX (2-2), WB (3).
- FLD F2: IS (2), EX (3-3), WB (4). P2的表格是最终答案,它的
is ex wb列指的是完成该阶段的周期。 - FLD F6: is=1 (发射完成), ex=3 (执行完成@cycle3), wb=4 (写回完成@cycle4)
- FLD F2: is=2 (发射完成), ex=4 (执行完成@cycle4), wb=5 (写回完成@cycle5)
- FMUL.D: is=3, F2在cycle5就绪, F4假设初始就绪。所以FMUL.D最早可以在cycle6开始执行。EX持续10周期,到cycle15结束。wb=16。
- FSUB.D: is=4, F2在cycle5就绪, F6在cycle4就绪。所以FSUB.D最早可以在cycle6开始执行。EX持续2周期,到cycle7结束。wb=8。
- FDIV.D: is=5, F0在cycle16就绪, F6在cycle4就绪。所以FDIV.D最早可以在cycle17开始执行。EX持续40周期,到cycle56结束。wb=57。
- FADD.D: is=6, F8在cycle8就绪, F2在cycle5就绪。所以FADD.D最早可以在cycle9开始执行。EX持续2周期,到cycle10结束。wb=11。
时间轴上的标记 (4, 5, 8, 11, 16, 57) 是各指令完成WB的时刻。
Tomasulo 算法总结
- 主要贡献:
- 动态调度
- 寄存器重命名 (消除 WAW 和 WAR 冒险)
- Load/Store 解耦 (Load/Store buffer)
- 通常比记分板算法性能更好
- 缺点:
- 结构复杂性 (保留站、CDB、关联逻辑)
- 性能受限于公共数据总线 (CDB) 的带宽 (通常只有一个CDB)。
- Load 和 Store 只有在访问不同地址时才能安全地乱序执行。如果访问相同地址:
- Load在Store前(程序顺序),交换它们会导致WAR。
- Store在Load前(程序顺序),交换它们会导致RAW。
- 交换两个访问相同地址的Store会导致WAW。
- 指令级并行 (ILP) 方法的局限性直接导致了向多核 (multicore) 的发展。
问题: 乱序执行 (out-of-order execution) 是否意味着乱序完成 (out-of-order completion)? 答: 是的。在 Tomasulo 算法中,指令可以乱序执行并在CDB上乱序写回结果。这与后面将看到的带有 ROB (Reorder Buffer) 的硬件推测不同,ROB 会强制指令按序提交。
§2.2 基于硬件的推测 (Hardware-Based Speculation)
为了进一步提高性能,特别是在存在控制依赖(分支)的情况下,引入了基于硬件的推测。
核心思想
- 动态分支预测: 选择要推测执行的指令路径。
- 推测执行: 在控制相关解决之前允许指令执行(并有能力撤销错误推测序列的影响)。
- 动态调度: 处理基本块的不同组合的调度。
关键组件:重排序缓冲 (ROB - Reorder Buffer)
ROB 是实现推测执行的核心。它是一个为未提交指令结果提供的缓存。
- ROB 中的条目通常包含:指令类型、目标地址(寄存器或内存)、值、状态(如 Issue, Executing, Write Result, Commit)。
带 ROB 的 Tomasulo 算法扩展
- 指令发射 (Issue):
- 指令从指令队列发射到保留站 (RS)。
- 同时,在 ROB 中分配一个条目。ROB 的条目号用于标识这条指令。
- 如果操作数在寄存器中且有效,则复制到 RS。
- 如果操作数未就绪,RS 等待产生该操作数的 ROB 条目号。
- 目标寄存器的状态更新为指向该指令的 ROB 条目号。
- 执行 (Execute):
- 当 RS 中的操作数就绪(从寄存器或通过 CDB 从其他 ROB 条目获得),指令开始执行。
- 写结果 (Write Result - WB):
- 当指令执行完成,结果被写入其在 ROB 中的条目。
- 结果同时通过 CDB 广播给所有等待它的 RS 和 ROB 条目。
- 此时结果尚未写入物理寄存器堆或内存。
- 提交 (Commit):
- 按序提交: ROB 头部指令的结果被提交。
- 如果指令是普通运算指令,其结果从 ROB 写入物理寄存器。
- 如果指令是 Store,其数据和地址从 ROB 发送到内存。
- 如果指令是错误预测的分支,则清空 ROB 中该分支之后的所有指令,并恢复寄存器状态。
- 如果发生异常,同样清空后续指令,不更新状态,跳转到异常处理。
- ROB 条目被释放。
✅ 推测的关键 允许指令乱序执行,但强制它们按序提交,并在指令提交前阻止任何不可撤销的操作(如更新状态或触发异常)。ROB 以与 Tomasulo 算法中保留站扩展寄存器集相同的方式提供了额外的“临时”寄存器。
课堂练习:带 ROB 的 Tomasulo (Hardware-Based Speculation - Page 9-11, 12-37)
指令序列 (同前):
I1: FLD F6, 34(R2)
I2: FLD F2, 45(R3)
I3: FMUL.D F0, F2, F4
I4: FSUB.D F8, F2, F6 ; 注意,幻灯片P13 FSUB.D 的第二个源操作数是F8,应该为F6
I5: FDIV.D F10, F0, F6
I6: FADD.D F6, F8, F2假设 (同前,但增加了 Commit 阶段):
- LD: 1周期EX
- ADD/SUB: 2周期EX
- MUL: 10周期EX
- DIV: 40周期EX
- Issue, WB, Commit 各1周期。
问题1 (Page 9): 当 FMUL.D 准备好提交 (Commit) 时,状态表是什么样的?
- 这意味着 FMUL.D (ROB #3) 已经完成了 WB (写结果到ROB)。
- 在它前面的 FLD F6 (ROB #1) 和 FLD F2 (ROB #2) 必须已经 Commit。
- FSUB.D (ROB #4) 可能已经 WB。
- FDIV.D (ROB #5) 可能正在 EX。
- FADD.D (ROB #6) 可能已经 WB。
根据幻灯片 P10-11 的表格 (状态为 FMUL.D 准备提交时):
保留站 (Reservation Station) - 示例Mult1/Mult2可能用于FMUL/FDIV
- Mult1 (用于 FMUL.D F0, F2, F4, 目标ROB #3):
Busy=no(已完成执行并写结果到ROB)Op=MUL,Vj=Mem[45+Reg[R3]](来自ROB#2的值),Vk=Regs[F4]
- Mult2 (用于 FDIV.D F10, F0, F6, 目标ROB #5):
Busy=yesOp=DIV,Qj=#3(等待ROB#3的F0),Vk=Mem[34+Reg[R2]](来自ROB#1的F6)
- Add1/Add2/Add3 (可能用于FSUB.D, FADD.D)
ROB (Reorder Buffer)
| NO. | Busy | Instruction | Status | Dest | Value |
|---|---|---|---|---|---|
| 1 | no | FLD F6, 34(R2) | Commit | F6 | Mem[34+Regs[R2]] |
| 2 | no | FLD F2, 45(R3) | Commit | F2 | Mem[45+Regs[R3]] |
| 3 | yes | FMUL.D F0, F2, F4 | WB | F0 | #2 × Regs[F4] |
| 4 | yes | FSUB.D F8, F6, F2 | WB | F8 | #1 - #2 |
| 5 | yes | FDIV.D F10, F0, F6 | EX | F10 | (pending) |
| 6 | yes | FADD.D F6, F8, F2 | WB | F6 | #4 + #2 |
寄存器状态 (Register Status)
| Reg | F0 | F2 | F4 | F6 | F8 | F10 | … | F30 |
|---|---|---|---|---|---|---|---|---|
| ROB | #3 | (no) | (no) | #6 | #4 | #5 | … | (no) |
| Busy | yes | no | no | yes | yes | yes | … | no |
| (F2, F4 的 Busy=no, ROB为空表示其值在物理寄存器中且有效,因为 ROB#2 已提交,F4 假设初始有效) |
考点: 理解 ROB 的作用,指令如何按序提交,以及各状态表如何协同工作。
问题2 (Page 12-37): 使用基于硬件的推测,每条指令完成(提交)需要多少周期? 幻灯片 P14-P37 详细展示了逐个周期的状态变化。 总结表 (P37):
| Instruction | Issue | Exec Comp (EX完成) | Writeback (WB到ROB完成) | Commit (提交完成) |
|---|---|---|---|---|
| FLD F6, 34(R2) | 1 | 3 | 4 | 5 |
| FLD F2, 45(R3) | 2 | 4 | 5 | 6 |
| FMUL.D F0, F2, F4 | 3 | 6-15 (F2在C5就绪) | 16 | 17 |
| FSUB.D F8, F6, F2 | 4 | 6-7 (F6在C4,F2在C5) | 8 | 18 |
| FDIV.D F10, F0, F6 | 5 | 17-56 (F0在C17就绪) | 57 | 58 |
| FADD.D F6, F8, F2 | 6 | 9-10 (F8在C18,F2在C6) | 11 | 59 |
| (注意FSUB.D的F8依赖于ROB#4,FADD.D的F8依赖于FSUB.D的结果,即ROB#4。FSUB.D的F6依赖ROB#1,F2依赖ROB#2。FADD.D的F2依赖ROB#2) |
执行完成 (Exec Comp) 周期的计算:
- FMUL.D: is=3。操作数 F2 (来自 ROB#2, WB@C5), F4 (初始就绪)。MUL 需要 10 周期。开始执行 = max(3, 5)+1 = C6。结束执行 = C6+10-1 = C15。
- FSUB.D: is=4。操作数 F6 (来自 ROB#1, WB@C4), F2 (来自 ROB#2, WB@C5)。SUB 需要 2 周期。开始执行 = max(4, 4, 5)+1 = C6。结束执行 = C6+2-1 = C7。
- FDIV.D: is=5。操作数 F0 (来自 ROB#3, WB@C16), F6 (来自 ROB#1, WB@C4)。DIV 需要 40 周期。开始执行 = max(5, 16, 4)+1 = C17。结束执行 = C17+40-1 = C56。
- FADD.D: is=6。操作数 F8 (来自 ROB#4, WB@C8), F2 (来自 ROB#2, WB@C5)。ADD 需要 2 周期。开始执行 = max(6, 8, 5)+1 = C9。结束执行 = C9+2-1 = C10。
核心原则:顺序发射 (In-order Issue),乱序执行/写回ROB (Out-of-Order Execution/Writeback),顺序提交 (In-order Commit)。
硬件推测的优点:
- 指令根据 ROB 按序完成(提交)。
- 可以实现精确异常 (precise exception)。
- 易于扩展到整数寄存器和整数功能单元。 缺点:
- 硬件非常复杂。
§2.3 利用多发射和静态调度开发 ILP (Exploiting ILP Using Multiple Issue and Static Scheduling)
多发射处理器 (Multiple-Issue Processors)
目标是每个时钟周期发射和执行多于一条指令。
- 单发射 (Single-issue) vs. 多发射 (Multiple-issue) 时空图: 多发射处理器能在相同时间内完成更多指令。
两种主要的多发射处理器类型:
超标量 (Superscalar):
- 每个时钟周期发射的指令数不固定 (例如1-8条,有上限)。取决于代码的具体情况和可用资源。
- 若上限为 n,则称为 n-发射 (n-issue) 处理器。
- 可以通过编译器静态调度,或基于 Tomasulo 算法动态调度。
- 是目前通用计算中最成功的方法。
超长指令字 (VLIW - Very Long Instruction Word):
- 每个时钟周期发射的指令数是固定的 (例如4-16条),这些指令构成一条长指令或一个指令包。
- 指令包内的指令并行性由指令明确表示。
- 指令调度由编译器静态完成。
- 已成功应用于数字信号处理和多媒体应用。
超标量 vs. VLIW
- 超标量对程序员透明: 处理器可以检测下一条指令是否可以流出,无需重排指令。未优化的代码也能运行(效果可能不好)。
- VLIW 依赖编译器: 编译器负责发现并行性并将操作打包到指令字中。
- VLIW 的问题:
- 程序代码长度增加: 大量循环展开以提高并行度;指令字中的操作槽不能总是被填满。
- 锁步机制 (Lockstep): 任何操作部件暂停,整个处理器都必须暂停。
- 机器码不兼容: 不同 VLIW 架构的指令格式通常不同。
- VLIW 的问题:
基于静态调度的多发射技术
- 典型超标量处理器: 每周期可发射1-8条指令。
- 指令顺序流出,流出时进行冲突检测。
- 实现方式(MIPS示例):
- 假设每周期流出两条指令:1条整数指令 + 1条浮点指令。
- Load/Store/Branch归为整数指令。
- 冲突检测分两阶段:包内冲突检测 -> 与正在执行指令的冲突检测。
- 硬件增加:
- 若浮点 Load/Store 使用整数部件,会增加对浮点寄存器的访问冲突 -> 增加浮点寄存器的读/写端口。
- 流水线中指令数量加倍,转发路径也需增加。
基于动态调度的多发射技术
- 扩展的 Tomasulo 算法: 支持双路超标量。
- 每周期发射两条指令(例如,一条整数,一条浮点)。
- 指令按序流向保留站 (RS),否则会破坏程序语义。
- 分离整数和浮点的表结构,使它们可以同时发送到各自的保留站。
- 示例:循环展开与动态调度 (P53-P59)
- 代码: 一个简单的向量元素与标量相加的循环。
Loop: LD X2, 0(X1) // X2 = array element ADDI X2, X2, 1 // increment X2 SD X2, 0(X1) // store result ADDI X1, X1, 8 // increment pointer (8 bytes per element) BNE X2, X3, Loop // branch if not last (X3 holds loop end condition/counter) - 假设:
- 每周期流出1条整数指令和1条浮点指令(即使相关)。
- 整数ALU和地址计算有整数部件;每种浮点操作有独立的流水化浮点功能部件。
- 指令流出和写结果各占1周期。
- 有动态分支预测部件和独立的分支条件计算部件。
- 分支指令单独流出,无延迟槽,分支预测完美。但在分支完成前,后续指令只能取指和流出,不能执行。
- 结果产生延迟:整数1周期,Load 2周期,浮点加法3周期。(写结果占1周期)
- 不带推测的执行 (P56-P57):
- 指令流出率高,但执行效率不高。15条指令执行了19个时钟周期 (IPC = 15/19 ≈ 0.79)。
- 原因是数据相关的分支和ALU部件成为瓶颈。
- 解决方案:增加加法器,将ALU功能与地址计算功能分开。
- 带硬件推测的执行 (P58-P59):
- 分支是关键性能限制因素时,推测帮助显著。
- 非推测流水线的完成率迅速落后于发射率。
- 推测的优势依赖于准确的分支预测。
- 错误的推测不仅不提高性能,反而通常会损害性能并显著降低能效。
- 代码: 一个简单的向量元素与标量相加的循环。
超流水线 (Superpipelined)
- 每个流水线阶段被进一步细分,使得多个指令可以在一个时钟周期内分时共享。这种处理器称为超流水线处理器。
- 对于每时钟周期可以流出 n 条指令的超流水线计算机,这 n 条指令不是同时流出,而是每 1/n 个时钟周期流出一条指令。
- 实际上,超流水线计算机的流水线周期是 1/n 个(主)时钟周期。
- MIPS R4000 流水线结构 (8级超流水线):
- IF (取指前半), IS (取指后半), RF (译码/读寄存器/冒险检查), EX (执行/地址计算/分支判断), DF (数据取前段), DS (数据取后段), TC (Tag检查), WB (写回)。
- Load延迟为2个(主)时钟周期 (从EX结束到DS结束)。