想象一家爆满的餐厅:订单墙上的菜单按先后顺序排得整整齐齐,但大厨根本没按顺序炒。他先瞄一眼谁最慢——炖汤、焖牛腩——先把它们架上火,再抽空炒两个快手菜。于是后点的菜先出锅,传菜窗口却永远按点单顺序叫号。客人从不觉得乱,因为端上桌的菜永远是对的。 现在的处理器比这家餐厅更夸张:指令像订单一样涌进来,有的要等内存慢悠悠地回数据,有的要算十几个周期。如果傻傻地按顺序一条条执行,芯片上几亿个晶体管大部分时间都在发呆。于是硬件设计师搬来了大厨的智慧——乱序执行(Out-of-Order Execution,OoO):操作数一就绪就抢先执行,谁也不等谁;但改寄存器、写内存这些“端菜”动作,必须严格按程序顺序提交。要做到这一点,靠的是两个厨房神器:保留站(Reservation Station)负责盯食材备料,重排序缓冲区(Reorder Buffer,ROB)负责按号叫餐。

岗位关联

读懂乱序执行,你就拿到了看懂 CPU/GPU 性能、推理引擎调用与系统调优的钥匙——任何“程序莫名变慢”的现象,背后都可能是依赖链在作祟。

目录
  1. 可视化演示
  2. 乱序执行原理
  3. 操作数冒险
  4. 保留站结构
  5. Tomasulo算法
  6. 重排序缓冲区
  7. 性能对比分析
  8. 现代工程实践
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Tomasulo算法保留站调度、ROB重排序缓冲区提交和乱序执行时序对比。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Tomasulo算法保留站调度 指令发射:指令进入保留站等待操作数 操作数就绪:通过公共数据总线CDB广播 RS: Issue → Wait → Execute → Write 保留站调度流程

乱序执行原理

想象一家周末爆满的餐厅,前台把订单按先后顺序贴在墙上一长条。新来的大厨并不老老实实按顺序炒——他先瞄一眼哪些菜最慢(炖汤、烤肉),先把这些慢菜架上火,再趁空隙炒两个快菜。于是后点的菜反而先出锅,但传菜窗口永远按订单先后叫号。处理器面临同样的窘境:如果指令严格按顺序执行,遇到一条很慢的指令(比如读内存、做乘法),后面所有指令都得干等,昂贵的算力白白闲置。

乱序执行(Out-of-Order Execution,OoO)正是处理器版的“聪明大厨”:只要一条指令的操作数已经就绪、执行单元空闲,它就可以越过前面还在等待的指令提前执行。前提是——最终改动寄存器或内存状态时,必须严格按程序顺序来。这就是“执行可以乱、结果不能乱”。

现代乱序处理器把指令旅程拆成五站:取指(Fetch)、译码(Decode)、发射(Dispatch)、执行(Execute)、提交(Commit)。前两站按顺序走,发射之后的执行可以“插队”,而提交这一站又把顺序拉回来。衡量这趟旅程效率的指标叫 IPC(每周期指令数),IPC 越高,说明大厨把灶台用得越满。

大厨的目标不是“按顺序做菜”,而是“在相同时间里端出更多菜”。乱序执行也一样——它牺牲表面的顺序,换取了处理器最宝贵的资源:时间。

IPC=N指令N周期IPC = \displaystyle\frac{N_{指令}}{N_{周期}}
  1. 指令总数程序编译后生成的机器指令条数,比如一段求和循环译码后得到 100 条指令。
    N指令N_{指令}
  2. 周期总数处理器完成这些指令所消耗的时钟周期,周期是 CPU 的最小时间节拍。
    N周期N_{周期}
  3. 整体含义IPC 表示每个时钟周期平均完成几条指令,它是乱序执行调度效率的核心度量。
    IPC=N指令N周期IPC = \displaystyle\frac{N_{指令}}{N_{周期}}

操作数冒险

两道菜都要用到同一锅老火高汤:高汤没炖好,第二道菜就算下了锅也缺味。这种“食材还没准备好”的冲突,在流水线里叫数据冒险(Data Hazard)。它本质上是一场“先后次序”之争:谁先做、谁后做,搞错了菜就废了。

最典型的是 RAW(先写后读)冒险:上一条指令要写、下一条指令要读同一个寄存器,后者必须等前者写完。此外还有 WAR(先读后写)——后面的写操作太心急,把前面还没读的旧值冲掉了;以及 WAW(写后写)——两条指令写同一个寄存器,乱序后说不清最后该留谁。再往外延伸,还有结构冒险(硬件资源不够用)和控制冒险(分支跳转不知道走哪条路)。

经典五级流水线遇到 RAW 冒险时只能插入停顿(stall),让流水线冒几个“气泡”干等;OoO 处理器则靠一条条“绕行通道”——没有操作数的指令先在站里等着,不堵后面人的路。冒险越少,流水线越饱满,总耗时越短。

高汤没炖好就先做别的菜——乱序执行用“错峰做菜”把冒险造成的空洞填平,这正是它存在的最初理由。

T流水线=(k+N−1+s)×t时钟T_{流水线} = (k + N - 1 + s) \times t_{时钟}
  1. 流水线级数 k例如五级流水线 k=5,指令像流水线工人一样一站站向前传送。
    k=5k = 5
  2. 指令数 N 与停顿 sN 是总指令条数,s 是所有冒险造成的停顿周期总和,s 越大流水线越“漏水”。
    N,sN, s
  3. 整体含义第一条指令 k 个周期出结果,之后理想情况下每周期出一条;停顿 s 把总时间直接拉长。
    T流水线=(k+N−1+s)×t时钟T_{流水线} = (k + N - 1 + s) \times t_{时钟}

保留站结构

大厨在每口灶旁边放一个备料盘。菜谱下来后先在盘里备着,盘边贴一张便签:还差三号灶的高汤。高汤一端出来,立刻抄进盘里,火候一到马上下锅。这个备料盘,就是硬件里的保留站(Reservation Station,RS)。它存在的意义只有一个字:等。

在经典设计中,指令从寄存器文件取操作数后直奔执行单元;一旦操作数没就绪,后续指令全被堵死在流水线上。保留站把“取数”和“执行”拆开:指令发射时先住进保留站,保留站负责盯操作数,操作数齐了再发给后方的执行单元。这样前面堵塞后面照跑,谁也不挡谁的道。

每个保留站保存三类信息:指令本身、操作数是否就绪、还缺哪个“生产者”(哪条指令会写出这个结果)。执行单元空闲时,保留站中操作数就绪的指令立即发射(Dispatch)。注意保留站不是缓存最终结果的地方,它只为等待而生——真正的结果通过公共数据总线广播回来。

备料盘越多,能同时等位的菜越多,但备料盘也要占厨房。保留站的数量,直接决定乱序窗口的大小,也决定芯片的面积与功耗成本。

URS=T忙T总U_{RS} = \displaystyle\frac{T_{忙}}{T_{总}}
  1. 忙时间保留站在一个统计窗口内真正等待、发射操作数所占用的周期数。
    T忙T_{忙}
  2. 总时间统计窗口的总周期数,窗口越大统计越平稳。
    T总T_{总}
  3. 整体含义利用率越高说明备料盘越“满”,但同时也暗示硬件资源可能过度投入。
    URS=T忙T总U_{RS} = \displaystyle\frac{T_{忙}}{T_{总}}

Tomasulo算法

光有备料盘还不够,得有人管“盘里的肉到底是谁家的”。老办法是看客人点的单(寄存器名);但乱序炒菜时,同名食材可能来自不同批次。Tomasulo 算法的妙招是:备料盘上的便签不写“牛排”,而写“三号灶正在煎的那块”,等三号灶喊“好了”再取。

Tomasulo 算法由 IBM 的 Robert Tomasulo 于 1967 年为 360/91 浮点单元提出,核心是寄存器重命名(Register Renaming):指令发射时,目的寄存器被映射为一个保留站标签;后续指令读该寄存器时,实际去等那个标签对应的写回,而不是死等寄存器这个“名字”。

这一改名,让 WAR、WAW 彻底消失——它们本是“名字撞车”,物理位置上各写各的互不干扰。执行完成后,结果通过公共数据总线(CDB)广播,所有等着这个标签的保留站在同一周期抄收,这就是数据前递(Forwarding)。注意,最初的 Tomasulo 算法还没有 ROB,它是后来才被加上用于精确异常的。

无论餐厅多豪华,一道“炖三小时的老火汤”的时间下界由食材本身决定。Tomasulo 能消灭“名字撞车”,却消灭不了“真依赖”——真依赖就是关键路径。

T关键=∑i∈D链latiT_{关键} = \sum_{i \in D_{链}} lat_{i}
  1. 依赖链程序中必须严格串行的指令序列,比如“LD 取数 → MUL 用它 → ADD 再用它”。
    D链D_{链}
  2. 单条延迟链上第 i 条指令的执行延迟,单位是时钟周期。
    latilat_{i}
  3. 整体含义无论乱序窗口多大,执行时间都不可能短于关键路径总和——这是理论下界。
    T关键=∑i∈D链latiT_{关键} = \sum_{i \in D_{链}} lat_{i}

重排序缓冲区

菜做完不能直接塞给客人,传菜窗口必须按点单顺序叫号。于是餐厅在大堂设了一块“叫号板”——菜出锅先放在叫号板对应的格子,轮到它的号才端出去。这块叫号板就是重排序缓冲区(Reorder Buffer,ROB)。它把“做完”和“交付”彻底分开。

ROB 是一个按程序顺序排列的队列,每条指令发射时占一个表项,表项里存着指令信息和它要写的目的寄存器的旧值。执行完成后指令不立刻生效,而是挂进 ROB 等待;只有 ROB 队头的指令完成,它才能提交(Commit),把结果正式写入寄存器或内存。队头不动,后面再急也得等。

这次“按序提交”带来两个关键能力:一是精确异常——中断或异常发生时,所有未提交指令可以整体作废,机器状态精确回滚到某条指令之前,操作系统才能正确响应;二是分支恢复——分支预测错误时,把分支之后未提交的指令全清掉,用保存的旧值还原寄存器,再沿正确方向重新执行。

大厨再随性,叫号板上的顺序一丝不乱。乱序执行把“做完”与“交付”分开,ROB 就是交付那一道的秩序闸门。

T恢复=WROB×t时钟T_{恢复} = W_{ROB} \times t_{时钟}
  1. 作废数量分支误预测后需要从 ROB 清空的未提交指令条数。
    WROBW_{ROB}
  2. 周期时间每个时钟周期的物理时间,单位秒或纳秒。
    t时钟t_{时钟}
  3. 整体含义恢复代价 = 作废指令数 × 周期时间,这就是“猜错分支”要交的罚款。
    T恢复=WROB×t时钟T_{恢复} = W_{ROB} \times t_{时钟}

性能对比分析

请两位厨师:一位是“流程大师”,配了五个灶加一堆备料盘和叫号板;一位是“老实厨师”,只有两个灶按单做。顾客少时流程大师浪费电;客流爆满时老实厨师只能让人干等。顺序执行与乱序执行,正是这两位厨师的比拼。

乱序执行的收益来自指令级并行(Instruction-Level Parallelism,ILP):程序里同时就绪、互不依赖的指令越多,乱序窗口越大,优势越明显。代价则是硬件复杂度:保留站、ROB、比较器、广播总线都要占面积和功耗。这是一笔实打实的“厨房装修费”。

实测中,OoO 相比顺序执行 IPC 大约提升 30%~50%,但功耗可能翻倍;而遇到长依赖链或密度极高的分支,收益会大幅缩水。因此移动 CPU 倾向“温和乱序”(发射宽度窄、ROB 浅),服务器 CPU 倾向“深度乱序”(大窗口、多发射)。芯片设计师每天做的,就是在这笔账上找平衡。

厨房升级值不值,取决于客流——程序里藏着的并行度,是乱序执行唯一能榨取的财富。

P=C×V2×fP = C \times V^{2} \times f
  1. 电容 C芯片内逻辑门翻转所需的电容,硬件越多 C 越大,乱序部件是“耗电大户”。
    CC
  2. 电压与频率V 是工作电压,f 是时钟频率;频率越高每秒翻转次数越多。
    V2,fV^{2}, f
  3. 整体含义乱序部件把 C 撑大,功耗呈平方级上升,这是深度乱序的隐藏账单。
    P=C×V2×fP = C \times V^{2} \times f

现代工程实践

再好的厨房也需要好菜谱。如果菜谱上写满“等上一个菜做好再做下一个”,大厨再灵也没招。处理器和编译器必须联手:编译器负责把菜谱写顺,硬件负责临场发挥。

编译器负责静态调度:循环展开、指令重排、软件流水,尽量把无依赖的指令排到一起;硬件负责动态调度:运行期实时判断操作数是否就绪、执行单元是否空闲,再决定谁先上灶。访存延迟是最大瓶颈,预取与缓存分级负责提前“备菜”。

现代 CPU 还有两大助攻:分支预测器(准确率普遍超过 95%)大幅减少控制冒险;缓存与预取机制降低访存延迟。对做 AI 的工程师来说,这意味着:改一行指令顺序、加一个并行分支、减少依赖的长链,都可能让模型推理速度明显变化——这些“玄学”背后,正是乱序执行与依赖链在博弈。

菜谱写得好,大厨才能如鱼得水;菜谱写得烂,再豪华的厨房也是摆设。工程师要做的,就是给“大厨”递上一份依赖尽量少的好菜谱。

μ实际=IPC理想1+r误预测×w恢复\mu_{实际} = \displaystyle\frac{IPC_{理想}}{1 + r_{误预测} \times w_{恢复}}
  1. 理想 IPC没有分支误预测时的理论吞吐,是乱序窗口给出的上限。
    IPC理想IPC_{理想}
  2. 误预测率与惩罚r 是分支误预测的比例,w 是每次误预测浪费的周期数。
    r误预测,w恢复r_{误预测}, w_{恢复}
  3. 整体含义分母把惩罚摊到每条指令头上,分支预测越差,乱序的执行功力被稀释得越狠。
    μ实际=IPC理想1+r误预测×w恢复\mu_{实际} = \displaystyle\frac{IPC_{理想}}{1 + r_{误预测} \times w_{恢复}}

AI 应用场景

场景1:AI芯片的访存乱序调度

阶段:AI 芯片微架构设计与性能建模

解决的问题:GPU/NPU 访存慢,cache miss 时顺序加载会把整个访存流水线堵死;用乱序的加载-存储队列让无关访存先执行,隐藏延迟。

具体案例:英伟达 GPU 的 warp 调度器本质就是“乱序执行”——当某一组线程在等显存返回时,调度器立刻切换到另一组无依赖的线程继续算,就像保留站换一批操作数就绪的指令上灶。芯片模拟器中大量使用 Tomasulo 风格的状态表(scoreboard)来跟踪 warp 的依赖。

场景2:推理引擎的算子并行调度

阶段:模型编译与图优化

解决的问题:神经网络计算图里,有些算子互相依赖,有些毫无关系——顺序执行时无关算子只能排队;AI 编译器用类似保留站的“依赖追踪”把无关算子调度到不同计算流上。

具体案例:TensorRT 在构建 engine 时分析算子间的数据依赖,把独立的卷积、归一化算子并行排布到多个 CUDA 流上;依赖算子则像等 CDB 广播一样,等待上游算子输出就绪再触发。手写推理引擎时,通常用一个依赖计数器表(类似保留站的便签)管理算子的触发条件。

场景3:训练数据管线的预取优化

阶段:训练性能调优

解决的问题:数据加载、预处理、上送 GPU 三步天然有依赖,加载慢时 GPU 干等;用“流水线 + 依赖缓冲”式的预取,让 CPU 提前准备下一批数据。

具体案例:PyTorch DataLoader 的 num_workers 与 prefetch_factor 就是“保留站”式缓冲——数据没备好不阻塞主训练,备好了(操作数就绪)立刻“发射”给 GPU。调大 prefetch_factor 相当于在 GPU 门口多摆几个备料盘,但备料盘太多也会占用显存,这正是保留站面积与收益权衡的翻版。

Python 演示

python86 行
class TomasuloSimulator:    """极简Tomasulo乱序执行模拟器。    指令格式: (操作, 目的寄存器, 源1, 源2, 延迟)    源寄存器为None表示不需要。    输出: 每条指令的发射/完成/提交周期。    """     def __init__(self, program):        self.program = program        self.n = len(program)        self.issue_t = [-1] * self.n        self.finish_t = [-1] * self.n        self.commit_t = [-1] * self.n        self.latest_writer = {}        self.rs = []        self.issued = 0        self.committed = 0        self.cycle = 0     def run(self):        while self.committed < self.n:            self.issue_one()            self.execute_step()            self.commit_one()            self.cycle += 1     def issue_one(self):        if self.issued < self.n:            op, dest, s1, s2, lat = self.program[self.issued]            deps = set()            for s in (s1, s2):                if s is not None and s in self.latest_writer:                    deps.add(self.latest_writer[s])            if dest is not None:                self.latest_writer[dest] = self.issued            self.issue_t[self.issued] = self.cycle            self.rs.append([self.issued, lat, deps])            self.issued += 1     def execute_step(self):        new_rs = []        for i, rem, deps in self.rs:            ready = all(self.finish_t[p] >= 0 for p in deps)            if ready:                rem -= 1                if rem == 0:                    self.finish_t[i] = self.cycle                else:                    new_rs.append([i, rem, deps])            else:                new_rs.append([i, rem, deps])        self.rs = new_rs     def commit_one(self):        if (self.committed < self.n and                self.finish_t[self.committed] >= 0):            self.commit_t[self.committed] = self.cycle            self.committed += 1  def main():    prog = [        ("LD",  "R1", "R2", None, 2),   # 访存较慢        ("MUL", "R3", "R1", "R5", 4),   # 依赖R1, 等I0        ("ADD", "R4", "R2", "R5", 1),   # 无依赖, 提前完成        ("SUB", "R6", "R3", "R1", 1),   # 依赖I1和I0        ("ST",  None, "R4", "R3", 2),   # 依赖I2和I1    ]    sim = TomasuloSimulator(prog)    sim.run()    names = ["LD", "MUL", "ADD", "SUB", "ST"]    print("指令 发射 完成 提交   (周期从0开始)")    for i in range(sim.n):        print("%-4s %4d %4d %4d" % (            names[i], sim.issue_t[i], sim.finish_t[i], sim.commit_t[i]))    i1, i2 = 1, 2    if (sim.finish_t[i2] < sim.finish_t[i1] and            sim.commit_t[i2] > sim.commit_t[i1]):        print("乱序执行: ADD(周期%d)比MUL(周期%d)先完成。" % (            sim.finish_t[i2], sim.finish_t[i1]))        print("按序提交: ADD(周期%d)却比MUL(周期%d)后提交。" % (            sim.commit_t[i2], sim.commit_t[i1]))  if __name__ == "__main__":    main()
目的:用事件循环模拟 Tomasulo 乱序执行,输出每条指令的发射/完成/提交周期,直观展示“ADD 先完成却后提交”的乱序执行核心现象。
  1. self.latest_writer[dest] = self.issued寄存器重命名:把目的寄存器映射到当前发射的指令号,后续读它的人去等这条指令。
  2. deps.add(self.latest_writer[s])保留站记下“生产者”指令号,相当于便签上写“等三号灶的高汤”。
  3. ready = all(self.finish_t[p] >= 0 for p in deps)操作数监听:所有生产者都写完(CDB 广播过),才算就绪。
  4. self.finish_t[i] = self.cycle执行完成,结果“上 CDB”;用数组标记完成,等待者据此解除阻塞。
  5. self.commit_t[self.committed] = self.cycleROB 队头完成才提交,按程序顺序退休,一次只退一条。

常见误区

误区指令都不按顺序执行了,结果肯定和原来不一样,乱序执行一定会引入计算错误。

正解恰恰相反,乱序执行只是“执行”乱,提交(真正写寄存器/内存)严格按程序顺序。ROB 就是这道闸门:指令做完先挂进 ROB 等待,轮到它在程序里的顺序才真正生效。好比大厨随便先做什么菜,但传菜口必须按点单顺序叫号,客人吃到的菜永远不错位。

为何常见:直觉上“顺序执行”与“结果正确”绑定,容易把执行顺序与提交顺序混为一谈。

误区程序性能全靠编译器重排指令,硬件只是照着执行,跟乱序执行没关系。

正解编译器做的是静态调度,只能看到编译期的信息;Tomasulo 这类动态调度在运行期根据真实数据相关性实时决策——访存是否命中缓存、执行单元忙闲,只有运行期才知道。两者是“菜谱写得好”与“大厨临场发挥”的关系,缺一不可。

为何常见:现代编译器优化确实很强,容易让人忽略硬件动态调度的存在。

误区保留站里存的也是寄存器值,跟寄存器堆差不多,只是多了一层缓冲。

正解保留站保存的不是最终数据,而是“指令 + 操作数就绪状态 + 生产者标签”。它类似备料盘上贴的“还差三号灶的牛排”字条,而不是寄存处里的真值;真正数据要等 CDB 广播过来才抄收。职责完全不同:寄存器堆是“仓库”,保留站是“等菜台”。

为何常见:都叫“站/堆”,又都在 CPU 核心内,容易混淆存储职责。

误区只要上了乱序执行,任何程序都会跑得飞快。

正解IPC 提升取决于程序本身的指令级并行度。如果程序是一条长长的依赖链(比如循环内反复累加同一个变量),乱序窗口再大也找不到可并行指令,性能收益趋近于零。好比主厨再厉害,面对一只“必须炖三小时才能上桌”的硬骨头也只能干等。

为何常见:厂商宣传的多核/并行加速容易让人高估乱序收益,忽视了依赖链的存在。

误区分支猜错了,把后面多取的指令丢弃,重新取一遍就好了,状态会自动还原。

正解丢弃指令只是第一步,关键在恢复现场。误预测路径上的指令可能已经改写了寄存器,必须借 ROB 中保存的旧值把寄存器逐一还原,再沿正确路径重新执行。就像发现上错菜,得先撤掉错菜、恢复厨房用料,才能给客人补做。

为何常见:以为“指令丢弃时状态自动还原”,忽略了乱序执行早已修改了物理状态。

面试题

Q1. 乱序执行中,保证“执行结果最终与顺序执行完全一致”的关键部件是?

单选题 · easy

  • A.保留站
  • B.公共数据总线
  • C.重排序缓冲区ROB← 正确答案
  • D.分支预测器

答案:C。ROB 是“叫号板”,负责按程序顺序提交;保留站负责等操作数,CDB 负责广播结果,分支预测器负责猜跳转方向。核心考题是“执行可以乱、提交必须按序”——这个秩序由 ROB 保障。

Q2. 关于保留站的作用,下列说法正确的是?

单选题 · easy

  • A.它是 CPU 的一级缓存,用来缓存指令
  • B.操作数未就绪的指令先在保留站等待,就绪后发射执行← 正确答案
  • C.它在分支预测错误时恢复寄存器现场
  • D.它强制指令按程序顺序执行

答案:B。保留站像备料盘,职责是“等”——等操作数齐了再发射;A 说成缓存,C 是 ROB 的职责,D 与乱序执行的初衷相反。

Q3. RAW(先写后读)数据冒险的本质是?

单选题 · mid

  • A.两条指令同时写同一个寄存器
  • B.后续指令要读的寄存器,必须先等前序指令写回完成才能读到← 正确答案
  • C.两条指令同时读同一个寄存器
  • D.指令访存时地址未对齐

答案:B。RAW 是“高汤没炖好后面的菜不能下锅”:前面的写必须先完成。WAR 是后面的写抢跑冲掉旧值,WAW 是两条写最后不知留谁;地址未对齐是访存问题,与数据冒险无关。

Q4. Tomasulo 算法消除 WAR/WAW 冒险的核心手段是?

单选题 · mid

  • A.寄存器重命名 + 保留站← 正确答案
  • B.增大 L2 缓存
  • C.提高主频
  • D.软件流水

答案:A。Tomasulo 靠“改名”消灭名字撞车——目的寄存器映射成标签,各写各的物理位置;缓存、主频、软件流水都不是解决 WAR/WAW 的核心手段。

Q5. ROB(重排序缓冲区)的主要职责是?

单选题 · mid

  • A.缓存取到的机器指令
  • B.按程序顺序提交指令,保障精确异常与状态回滚← 正确答案
  • C.加速乘法运算
  • D.预测分支跳转方向

答案:B。ROB 就是叫号板:队头完成才能提交,保证程序可见状态按序变化,异常时可精确回滚;A 是指令缓存的事,C 是执行单元的事,D 是分支预测器的事。

Q6. 当分支预测错误时,处理器如何处理?

单选题 · hard

  • A.清空 ROB 中该分支之后所有未提交指令,并用保存的旧值恢复寄存器← 正确答案
  • B.等待分支结果自然退休
  • C.重新编译程序
  • D.仅丢弃误预测的指令,不恢复寄存器

答案:A。误预测路径上的指令可能已改写寄存器,必须清空未提交指令并用旧值还原现场;B 是消极等待,C/D 都不是硬件能做的事。这就是 ROB 的“状态回滚”能力。

Q7. 公共数据总线(CDB)在 Tomasulo 算法中的作用是?

单选题 · mid

  • A.传输取指阶段的指令
  • B.把执行结果同时广播给所有等待该结果的保留站← 正确答案
  • C.连接外设和内存控制器
  • D.每周期只允许发射一条指令

答案:B。CDB 相当于“大厨喊我做好了”的广播喇叭,所有等着该结果的保留站同时抄收;A 是取指阶段的事,C 是系统总线,D 限制发射宽度的是硬件设计而非 CDB。

Q8. 下列哪些属于流水线冒险类型?

多选题 · mid

  • A.数据冒险← 正确答案
  • B.结构冒险← 正确答案
  • C.控制冒险← 正确答案
  • D.功耗冒险

答案:ABC。经典三类冒险:数据(RAW/WAR/WAW)、结构(共享硬件不够用)、控制(分支方向未知);功耗是设计代价,不是流水线冒险。

Q9. 关于乱序执行的叙述,正确的有?

多选题 · hard

  • A.指令可以乱序执行,但必须按序提交← 正确答案
  • B.保留站数量越多,所有程序性能一定越好
  • C.分支误预测会作废分支之后未提交的指令← 正确答案
  • D.乱序执行会改变程序的语义结果

答案:AC。乱序执行只是把完成与交付分开,语义结果与顺序执行完全一致(A 对、D 错);保留站多不代表所有程序都变快,依赖链长的程序收益很小(B 错);分支误预测靠 ROB 作废未提交指令(C 对)。

Q10. 设 I1=MUL、I2=ADD 互不依赖,I2 先执行完毕,则 I2 提交必须满足什么条件?

单选题 · hard

  • A.等到 I1 提交之后才允许提交← 正确答案
  • B.等一个时钟周期就能提交
  • C.等程序执行完毕
  • D.等 I1 的保留站被释放后即可提交

答案:A。无依赖指令可以乱序执行完,但不能乱序提交——I2 要等队头 I1 提交后才能退休,这正是 ROB 的秩序所在。B 是具体时序,C 过于极端,D 混淆了保留站与 ROB 的职责。