先问一个问题:一条指令从内存里被取出来之后,CPU 到底是按什么顺序执行它的?
如果你问没接触过体系结构的人,他会说当然是按代码写好的顺序从头到尾跑。如果你问写过编译器的人,他会说编译器在生成汇编时就重排过指令了。但真正决定最终执行次序的,是现代高性能 CPU 内部那套乱序执行(Out-of-Order Execution)引擎——它根本不管你代码里写的先后关系,只看一条铁律:你的源操作数准备好了没有。
这篇文章要拆的,就是这套"倒着干活"的架构。我会讲清楚为什么要打乱指令顺序、打乱之后凭什么还能保证算出来的结果和顺序执行一模一样、以及这套机制从取指到提交的完整工作流程。它适合三类人看:写性能敏感代码的开发者、正在学计算机组成原理的读者、以及纯粹好奇 CPU 内部到底在干什么的人。看完你至少能回答一个问题:为什么有些 CPU 单核算得这么快,而"快"的背后又付出了什么代价。
1. 乱序执行的核心思路:CPU 为什么非要"倒着干活"
1.1 顺序执行的痛点:CPU 其实一直在等人
要理解乱序执行,先得看顺序执行卡在哪。
早期的 CPU 指令是一条一条按顺序走的:取指、译码、执行、写回。看起来很干净,但实际跑起来非常浪费。原因很简单——指令之间经常有依赖,而依赖背后站着一个巨大的"访存延迟"。
我举个例子。假设程序里有这么一段:
load r2, [r1] // 从内存地址 r1 读数据到 r2 mul r5, r2, r3 // 乘法,需要 r2 add r6, r4, r2 // 加法,也需要 r2第一条 load 如果发生缓存未命中,可能需要几百个周期才能从内存把数据拿回来。第二条 mul 因为要用 r2,只能干等。第三条 add 也要用 r2,也只能干等。
问题来了:后面如果还有几十条指令压根不用 r2,它们本来可以立刻算完,但在顺序执行的流水线里,它们全都堵在 load 后面,谁也别想动。CPU 的算术单元、逻辑单元全部空转,几十上百个周期就这么白白浪费掉。
这就是顺序执行最大的痛点:流水线的前端一直在取指令,后端却经常因为一条慢指令全面停摆。本质上不是 CPU 算得不够快,而是它在等人——等内存、等缓存、等一条慢吞吞的依赖链。
1.2 "倒着干活"的本质:让等的人先靠边,让能干的人先干
乱序执行的核心思想,用一句话说就是:把没有依赖关系的指令,提前到前面去执行;把需要等待的指令,往后挪一挪。执行顺序被打乱了,但从外部看,最终结果必须和按顺序执行完全一致。
你可以把它理解成一家餐厅的后厨。客人点单的顺序是:A 桌要一份需要炖 40 分钟的汤,B 桌要一份两分钟的炒青菜,C 桌要一份十分钟的蒸鱼。如果严格按点单顺序做,A 桌汤炖着的时候,B 桌和 C 桌只能干瞪眼。聪明的后厨会把炒青菜和蒸鱼先做了,汤在炖着也不耽误出菜速度。只要最后所有菜都端到正确的桌子上,客人不会关心后厨是先炒了青菜还是先炖了汤。
指令乱序执行也是同一个道理。CPU 内部有一条指令队列,每条指令都在等待自己的"食材"——也就是源操作数。只要操作数都齐了,哪怕这条指令在程序里排在很后面,也可以先被送进执行单元去算;反之,就算你排在前面,只要操作数没到,你也只能蹲在队列里等着。
1.3 乱序执行到底解决了什么问题
往大了说,乱序执行解决的是三件事:
- 隐藏访存延迟。现代 CPU 的性能瓶颈早就不是运算速度,而是内存和缓存的速度差。乱序执行能在一次 cache miss 的几百个周期里,把后面几百条独立指令全部提前算完,相当于把"等内存"的时间用来"做计算"。
- 提高单核指令级并行度(ILP)。单核 CPU 的并行度不靠多线程,而靠单个线程内部指令之间的并行。乱序窗口越大,能找到的并行指令就越多,每个周期能退休的指令数(IPC)也更高。
- 让执行单元尽量保持忙碌。现代 CPU 往往同时有多个整数单元、浮点单元、访存单元,如果严格顺序执行,绝大多数单元都在闲置。乱序调度能把不同类型的指令分散到不同单元里,让后端资源满载。
当然,代价也很明显:硬件复杂度直线上升、功耗猛增、面积变大。所以并不是所有 CPU 都采用乱序执行。嵌入式领域很多低功耗 CPU 到现在还是顺序执行,因为省电、简单、面积小。乱序是高性能单核的"标配烧钱方案"。
2. 核心组件拆解:四个零件各管什么
乱序执行不是魔法,它由几个非常具体的硬件组件协同完成。想搞懂整条链路,先得认识这四个核心部件。
2.1 保留站和指令窗口:等菜的排队区
保留站(Reservation Station)是整个乱序调度的中枢。每条被译码的指令,会在保留站里占一个位置,同时记录三样关键信息:这条指令要做什么运算、第一个源操作数就绪了没有、第二个源操作数就绪了没有。
关键机制在于"广播监听"。执行单元一旦算出结果,结果会被广播到一条公共数据总线(CDB)上。保留站里的每一条指令都会同时去看总线上这个结果是不是自己正在等的源操作数,如果是,就立刻把自己的源操作数填上。这个动作非常快,所有保留站条目是同一条总线同时监听的,不需要 CPU 一个个去比对。
把所有等待中的指令放到一起看,就构成了一个"指令窗口"。窗口越大,能在同一时刻观察和调度的指令就越多,越容易找到可以提前执行的指令。当然,窗口越大硬件成本也越夸张,这部分后面再细说。
2.2 寄存器重命名:给同名寄存器"改名换姓"
程序员眼里只有一套寄存器,比如 x86 里的 rax、rbx、rcx。但在乱序执行里,同一个架构寄存器在不同时刻会被写入多个不同的值,如果所有指令都去抢同一个物理寄存器,就会产生大量本不该有的等待关系。
解决办法是搞一个"物理寄存器池",比架构寄存器的数量多得多。硬件维护一张映射表,动态地把"架构寄存器"映射到某个"物理寄存器"上。每次遇到一条要写 r1 的新指令,就不再去用原来那个物理寄存器,而是从池子里另取一块新的,更新映射表。
这个动作叫做"寄存器重命名"。它是解决数据冒险的重头戏,具体怎么消掉假依赖,第 4 章会详细展开。你只要先记住:重命名让"同一个名字"在不同时间点上变成了不同的实物,指令之间就不会因为名字相同而互相堵车。
2.3 重排序缓冲(ROB):乱序干完活,顺序交答卷
既然执行顺序是乱的,那最终怎么保证结果正确?靠的就是重排序缓冲(Reorder Buffer,ROB)。
ROB 本质上是一块环形缓冲,每条指令在进入保留站之前,会先按程序顺序分配一个 ROB 条目。执行完的指令把结果写回到自己的 ROB 条目里,但这一步不会直接修改架构寄存器。只有等到 ROB 里排在它前面的所有指令都提交了,它才能正式"退休"(commit),把结果写进真正的架构寄存器,或者真正写进内存。
换句话说:执行是乱序的,但提交永远是按顺序的。这样做的价值非常巨大:
- 任何时刻,处理器对外呈现的架构状态,都能精确对应到程序顺序中的某一条指令执行完的状态。
- 如果某条指令触发了异常,CPU 可以精确地指出"是第几条指令出的错",然后放弃它之后所有乱序执行的结果,回滚到异常点。这叫"精确异常",乱序执行能把这一点做得比想象中更干净。
- 如果分支预测错了,已经执行完但还没提交的指令,直接整批作废即可,因为它们的中间结果还没污染架构状态。
2.4 分支预测:提前赌一把,赌错了就得重来
乱序执行最怕的其实是分支。代码里 if、for、while 到处都是,CPU 在拿到分支结果之前,根本不知道下一条该取哪条指令。如果傻等着分支判断完,流水线又得空转几十个周期。
所以现代 CPU 前端都会有非常激进的分支预测器。它会根据历史执行记录,提前猜一个跳转方向,然后从猜的方向取指令、译码、甚至乱序执行。如果猜对了,这些提前做的工完全免费,等于把分支延迟给藏掉了;如果猜错了,已经进入流水线甚至已经算完的指令全部作废,从正确的分支重新开始。
这里有个连锁反应:分支预测错误的代价,在乱序 CPU 上比顺序 CPU 高得多。因为乱序 CPU 可能已经在错误路径上执行了一大堆指令,这些指令占用了执行单元、占用了 ROB,最后全部白干,还白白烧了功耗。所以现代高性能 CPU 的分支预测器越做越复杂,因为它直接影响乱序执行的效率上限。
把这四个组件连起来走一遍,流程就是:取指 → 译码 → 寄存器重命名 → 分配 ROB → 放入保留站 → 乱序发射执行 → 写回 ROB → 按序提交。
| 组件 | 核心职责 | 通俗类比 |
|---|---|---|
| 保留站/指令窗口 | 监控源操作数,动态调度发射顺序 | 后厨的备菜台,谁的材料齐了谁先下锅 |
| 寄存器重命名 | 消除同名寄存器之间的假依赖 | 同名客人很多,给每个人发不同的桌号 |
| 重排序缓冲 ROB | 乱序执行、按序提交,保证架构状态正确 | 外卖订单乱了没关系,出餐口按订单编号排队出餐 |
| 分支预测 | 提前猜测跳转方向,隐藏分支延迟 | 导航提前猜你走哪条路,走错了再重新规划 |
3. 完整执行流程实操:跟着一条指令走一遍
理论讲完,我们实际推演一次。假设编译器生成了下面这几条汇编指令:
I1: load r2, [r1] // 从内存地址 r1 加载数据到 r2 I2: mul r5, r2, r3 // r5 = r2 * r3,依赖 I1 I3: sub r8, r9, r10 // r8 = r9 - r10,完全独立 I4: add r11, r12, r13 // r11 = r12 + r13,完全独立 I5: store [r14], r8 // 把 r8 写回内存,依赖 I3假设 I1 访问内存发生了 cache miss,需要 80 个周期才能拿到数据。在严格顺序执行的模型下,I2 要等 r2,I3、I4、I5 全都得排在 I2 后面等,至少 80 个周期内,后面的指令一个都跑不了。
但在乱序 CPU 里,推演过程完全不一样。
3.1 前端:取指、译码、按序分配编号
CPU 先从指令缓存里把 I1 到 I5 取出来,逐个译码,并把它们翻译成内部微操作(uop)。在这个阶段,每条指令被发到后端之前,会做一次寄存器重命名。
为了方便观察,假设物理寄存器共有 p0 到 p31 三十二个。原始代码里的 r2、r5、r8 这些逻辑寄存器,会通过映射表指向不同的物理寄存器。比如 I1 要写 r2,硬件从池子里拿一个 p16 分配给 I1;I2 要写 r5,分配 p17;I3 要写 r8,分配 p18;I4 要写 r11,分配 p19。映射表同时更新,以后谁读到 r2,就知道要去读物理寄存器 p16。
每条指令还会在 ROB 里按程序顺序占一个坑位。这里的顺序就是 I1、I2、I3、I4、I5,铁打不动,谁也别想插队。这个坑位用来决定提交顺序,执行顺序可以乱,但提交必须按这个次序来。
分配完 ROB 和物理寄存器之后,指令被派发(dispatch)到保留站里等待执行。
3.2 保留站里发生了什么
现在保留站里有五条指令,每条指令的源操作数状态如下:
- I1:源操作数是内存地址 r1,地址已经算好,可以直接发起访存。
- I2:源操作数 r2 还在等 I1 的结果,来源物理寄存器 p16,等待中。
- I3:源操作数 r9、r10,都已在寄存器堆里准备好了,就绪。
- I4:源操作数 r12、r13,都已就绪。
- I5:源操作数 r8,来源物理寄存器 p18,也就是 I3 的结果,等待中。
调度器(Scheduler)每个周期扫描保留站,选那些所有源操作数都就绪的指令发射(issue)到对应的执行单元。
I1 因为地址早就准备好了,先被发射到访存单元,发出读内存请求,然后进入 80 个周期的等待。这是迫不得已的等待。
紧接着,I3 和 I4 被调度器扫描到,它们没有任何依赖障碍,立刻被发射到整数算术单元。它们几乎在下一个周期就计算完毕,结果写回到自己的 ROB 条目里,同时通过 CDB 广播出去——"p18 的结果是 xx,谁在等 p18?" I5 立刻捕获到这个结果,它的源操作数 r8 就绪了。
I5 也被调度器发射到访存单元去执行存储操作。而 I2 还在等 p16,也就是 I1 的 load 结果。它只能继续蹲在保留站里。
3.3 慢速指令回来了,快速指令已经悄悄干完
等到第 80 个周期,I1 的 load 结果终于从内存返回了。这个结果通过 CDB 广播,I2 捕获到 r2 的值,源操作数就绪,立即被发射到乘法单元执行。
I2 执行完,结果写入 ROB 条目,I1、I2 的前面已没有未提交指令了,按顺序依次退休。I3、I4、I5 的 ROB 记录虽然早就标记为"执行完成",但它们必须等 I1、I2 先提交才能依次提交。最终提交顺序依然是 I1 → I2 → I3 → I4 → I5,和程序原始顺序一模一样。
对比一下总时间:
- 顺序执行:I1 等待 80 周期,I2 随后执行,I3、I4、I5 全在 I2 之后排队,整体完成至少 80 + 3 个周期以上,而且这还是理想化情况。
- 乱序执行:I1 等待的 80 个周期里,I3、I4、I5 已经执行完毕,I1 回来后只需再执行 I2,整体完成只需要 80 + 2 个周期左右。
如果程序后面还有几十条和 I1 无关的指令,乱序执行的收益会更大——那几十条指令全都在 load 等待期间偷偷算完了。这就是"倒着干照样快"的秘密:它不是把慢指令变快了,而是让其他指令不用陪着慢指令一起耗时间。
4. 数据冒险与解决实战:为什么乱序之后还能"对"
很多人刚接触乱序执行时最担心一件事:执行顺序都打乱了,万一某条指令读写同一个寄存器,顺序一变,结果不就错了?
这个担心是对的,乱序执行的正确性全部建立在解决"数据冒险"之上。数据冒险分三种,解法各不相同。
4.1 三类数据冒险,逐个识别
假设有两条指令 A 和 B,A 在程序顺序上排在 B 前面,它们可能产生三类冲突:
| 冒险类型 | 全称 | 冲突场景 | 举例 |
|---|---|---|---|
| RAW | Read After Write | B 要读 A 才写出的值,顺序不能乱 | I1: mul r3, r1, r2;I2: add r5, r3, r4 |
| WAR | Write After Read | B 要写一个 A 正在读的寄存器 | I1: add r3, r4, r5;I2: sub r4, r6, r7 |
| WAW | Write After Write | A、B 都要写同一个寄存器 | I1: mul r3, r1, r2;I2: add r3, r8, r9 |
RAW 是"真依赖",因为第二条指令确实要等第一条的结果,这是运算逻辑本身决定的,没法消掉。RAW 只能通过等待来解决,乱序执行也得乖乖等着。
WAR 和 WAW 则属于"假依赖"。它们不是因为数据真的需要传递,而是因为两条指令碰巧用了同一个寄存器名字。比如 WAR 里,A 要读 r4,B 要写 r4,其实 A 读的是旧值,B 写的是新值,只要把两个值放到不同的物理位置,它们同时进行完全没问题。乱序执行的核心手段——寄存器重命名,就是专门来消灭这两种假依赖的。
4.2 寄存器重命名如何根治 WAR 和 WAW
拿下面三条指令来演示:
I1: add r1, r2, r3 // 第一次写 r1 I2: sub r4, r1, r5 // 读 r1,这是 RAW,必须等 I1 I3: mul r1, r6, r7 // 第二次写 r1,新的 r1如果不做重命名,I2 要读 r1,I3 要写 r1。顺序执行下没问题:I2 先读旧 r1,I3 再写新 r1。但乱序执行下,硬件想提前执行 I3——它和 I1、I2 都没有真依赖——可是如果 I3 直接把 r1 覆盖了,I2 再去读 r1 就读到了新值,结果就错了。这就是 WAR 冒险卡住了调度器。
加上寄存器重命名之后:
- I1 写 r1,硬件分配物理寄存器 p20,映射表变成 r1 → p20。
- I2 读 r1,译码时查映射表,发现 r1 对应 p20,于是它等待的操作数变成"物理寄存器 p20 的结果",跟 r1 这个名字彻底无关了。
- I3 写 r1,硬件从物理寄存器池里另取一块 p21,映射表更新为 r1 → p21。
看,I2 等的是 p20,I3 写的是 p21。两者在不同物理位置上,I3 提前执行根本不影响 I2。WAW 也一样,I1 写 p20,I3 写 p21,两个物理寄存器互不干扰,最后提交时按程序顺序决定架构 r1 的最终值是 p20 还是 p21——I3 在程序顺序上靠后,所以提交时把 p21 作为 r1 的最终值,p20 释放回池子。
这就是重命名的精髓:把"逻辑名冲突"转换为"物理位置隔离",假依赖就此消失。你会发现,真正留下来的就只剩 RAW 真依赖了,而 RAW 是程序逻辑里绕不开的等待,谁来了都得等。
4.3 访存指令的特殊处理:Load/Store 队列与内存消歧
寄存器冒险解决完,还剩一类更麻烦的——内存冒险。寄存器只有一个名字体系,内存地址可复杂多了,两条指令访问的内存地址可能在运行时才能算出来。
举例:
I1: store [r1], r2 // 往前地址 r1 写值 I2: load r3, [r4] // 从地址 r4 读值如果 r1 和 r4 指向同一个内存地址,那么 I2 应该读到 I1 写入的新值,这是 RAW 依赖。乱序执行时,I2 可能被调度器提前发射了,而 I1 还没来得及写内存,于是 I2 读到了旧值,这就错了。
为了解决这个问题,CPU 里有一组 Load/Store 队列,专门跟踪每条访存指令的地址和执行状态。硬件会做"内存消歧"(Memory Disambiguation):在 load 指令被提交之前,检查前面的 store 指令里有没有地址和它相同的,如果有,就先把 store 的数据传给 load,或者干脆强制 load 等 store 执行完再读。这个机制比较隐蔽,但它保证了乱序执行下访存指令之间依然语义正确。
值得一提的是,现代 x86 的强内存模型还会进一步限制 load 的重排程度,所以你能在 CPU 实测数据里看到 load 指令的调度比算术指令保守得多。
4.4 从 Tomasulo 到现代微架构的演进
乱序执行的算法基础,最早来自 IBM 工程师 Robert Tomasulo 在 1967 年提出的 Tomasulo 算法。它在 IBM System/360 Model 91 上实现,核心思想就是:保留站 + 公共数据总线 + 寄存器重命名。今天的乱序 CPU 虽然微架构五花八门,但整体框架仍然是 Tomasulo 的思路。
后来的变化主要集中在窗口大小和调度器设计上:
- Intel 的 Core 架构,ROB 在 100~500 多条不等,具体取决于微架构代号,近几代在 512 条左右。
- AMD 的 Zen 系列,ROB 规模大概在 224~320 条之间,配合多组整数调度器和浮点调度器工作。
- ARM 的高性能核(Cortex-X 系列和 Apple 的 Firestorm 系)也在维持非常庞大的乱序窗口,Apple M 系列芯片的 ROB 能到 600 条以上,调度器条目极多。
窗口越大,越能容忍长延迟事件。一个 500 条 ROB 的 CPU,可以在一次高延迟访存等待期间,最多容纳几百条独立指令在飞行。这就是为什么大乱序窗口的 CPU 在单线程整数性能上特别能打。
| 问题 | 类型 | 解决办法 |
|---|---|---|
| 后面的指令要读前面的结果 | RAW,真依赖 | 只能等,靠保留站和 CDB 精确等待 |
| 后面的指令要写前面的源寄存器 | WAR,假依赖 | 寄存器重命名,隔离到不同物理寄存器 |
| 两条指令写同一个寄存器 | WAW,假依赖 | 寄存器重命名,提交时按序覆盖 |
| 内存读写可能指向同一地址 | 内存冒险 | Load/Store 队列 + 内存消歧 |
5. 现实权衡与性能观察:乱序执行没那么神,也没那么玄
5.1 乱序窗口越大越好吗
理论上,乱序窗口大到天上,IPC 应该无限高。但现实有两堵墙挡着:
第一堵墙是依赖链。程序里真正可并行的独立指令是有限的,如果一个程序本身就是一串强依赖的计算,窗口再大也没用,因为每条指令都得等上一条算完。这种程序叫"延迟敏感"型,乱序执行对它帮助有限。
第二堵墙是功耗和面积。保留站的每一项都需要比较器、标记位、数据缓存;ROB 的每一项都要记录状态和结果;物理寄存器池的端口数量直接决定每周期能重命名多少条指令。这些资源都是真金白银——晶体管烧钱,漏电烧电。所以芯片设计者不会无限放大窗口,而是在性能、功耗、面积之间找个平衡点。
这些年还有一个趋势:乱序窗口继续增大带来的性能收益越来越小,边际效益递减非常明显。与其堆一个大窗口,不如再加一条 SMT 线程去填满空闲槽位,或者做大小核异构,把大乱序核心留给重负载、把小核心留给轻负载。这也是为什么你在桌面 CPU 上看到的多核策略越来越复杂。
5.2 乱序执行与超线程、多核的区别
不少初学者会把"乱序执行"和"超线程"混在一起,其实它们解决的问题完全不同。
- 乱序执行是在一个线程内部找指令级并行。
- 超线程(SMT)是让一个物理核心同时维护多个线程的上下文,让这些线程共享同一套乱序执行硬件。线程 A 的指令在等内存时,调度器可以拉线程 B 的指令来执行。它的核心是提升执行单元利用率,并不是重新发明了一套乱序机制。
- 多核则是每个物理核心各有一套独立的前端、乱序引擎、执行单元和缓存接口,靠的是线程级并行。
简单记:乱序执行是"一条流水线里找并行",超线程是"一条流水线里跑多个程序",多核是"多套流水线同时跑"。现代 CPU 三者叠加使用,这也是为什么单看会懵的原因。
5.3 怎么在系统里观察到乱序执行的效果
理论再多,不如自己看一眼。Linux 下最方便的工具是 perf,它能看 IPC(每周期指令数),这是衡量乱序执行收益最直观的指标。
# 跑一个 5 秒的 CPU 压力测试,统计指令数和周期数 perf stat -e instructions,cycles,ipc stress-ng --cpu 1 --timeout 5如果 IPC 能跑到 3 以上,说明乱序执行和分支预测在拼命工作,平均每个周期退休了好几条指令。如果 IPC 只有 0.5,说明程序几乎每个周期都在等待,很可能是一条长长的依赖链在作祟。
你还可以自己做一个对照实验:写一段大量独立乘法运算的程序,再写一段串行累加的程序,前者依赖编译器帮忙产生大量独立指令,后者天然是一串依赖链。用 perf 统计你会发现,前者的 IPC 明显高于后者。这个实验能直观地让你感受到"乱序窗口救不了真依赖"这句话。
我自己踩过的坑也和依赖链有关。有一段时间调一个数值计算模块,怎么优化都跑不上性能指标,各种缓存优化做了个遍也没用。后来用 perf 看 IPC,才发现隔三差五的跳转方向和一条频繁更新的循环计数器形成了强依赖链,每次迭代都要等上一次迭代的结果。最后靠循环展开(loop unrolling)打破了依赖链,IPC 一下子翻了一倍,问题不了了之。这件事给我最大的教训是:做性能优化时,光想算法复杂度不够,还得看指令之间的依赖结构,乱序执行帮不了你打破逻辑上的先后次序。
5.4 做性能优化时该怎么和乱序执行打交道
了解了乱序执行之后,写代码时你会多几个判断维度。
- 不用手动乱序。千万不要因为 CPU 会乱序执行,就在源码里乱调语句顺序。编译器在生成汇编时已经做了指令调度,硬件在运行时还会再做一次动态调度,你写得再"乱"也帮不上忙,反而可能妨碍矢量化等优化。
- 关注访存局部性就行。乱序执行能藏住首次 miss 的延迟,但藏不住内存带宽的极限。如果你的程序频繁跨缓存行访问,乱序窗口再大也没用,最终还是得回到优化访问模式这条路上。
- 依赖链是最大的敌人。在循环内部,尽量让每轮迭代之间的指令相互独立。循环展开是打破循环进位依赖最直接的手段,配合乱序执行,IPC 会有肉眼可见的提升。
- 分支密集代码要谨慎。如果分支预测频繁出错,乱序窗口里塞满了错误路径的指令,后面的提交流程全被污染。能用分支消除(branchless)处理的条件逻辑,尽量改成算术运算。
说到底,乱序执行是一个高效的"隐藏延迟"机制,它确实能让你在大多数场景下无感获得很高的单核性能,但它的上限始终悬在程序本身的依赖结构之上。
如果你正处于学习体系结构的阶段,我强烈建议不要只看架构图,一定要配合 perf、pmu-tools 这类性能分析工具去看真实程序跑出来的数据。纸上谈兵理解不了 CPU 的设计意图,看到 IPC 从 0.8 变成 3.5 那一刻,你对乱序执行的理解才算真正落地。