乱序执行与内存重排的边界:在哪交、在哪分、怎么看
承乱序执行总纲 cpu-out-of-order,机制与正确性见 ooo-mechanism / ooo-renaming-retirement。本篇回答全仓库被问得最多的一个混淆:乱序执行和内存重排到底是不是一回事? 答案是——只在"访存投机重排"处擦边,且这个擦边在 x86 上还被 machine-clear 抹平了。最后给观测手段:怎么用
perf看到乱序执行的成效与失效。
一、边界:乱序执行和内存重排到底在哪交、在哪分
回扣 reordering-overview 第一节那张交集图,现在能讲透了:

三块分清:
- 纯核内乱序(左):两个
add的乱序、分支投机——只影响执行效率,从不影响内存可见顺序,和多核内存模型完全无关。这是乱序执行的主体。 - 交集(中):乱序引擎会投机地把 load 提前执行(看起来像 LoadLoad 重排)。但 x86 承诺 TSO——一旦发现"提前读的行在退休前被别核改了",触发 memory-ordering machine clear,把这条投机 load 及后续清空重放(
perf的machine_clears.memory_ordering)。所以 x86 上 OoO 造成的内存重排被硬件自动纠正回来,程序员基本看不到。 - 纯内存重排(右):store buffer 的 StoreLoad——哪怕一个完全顺序执行、没有乱序引擎的核,只要有 store buffer 就会有。这证明内存重排的主因是缓冲区,不是 OoO。
| 判断 | 对/错 | 为什么 |
|---|---|---|
| "有乱序执行就会有内存重排" | ❌ | x86 深度乱序,但把访存乱序用 machine-clear 兜回 TSO |
| "内存重排都是 CPU 乱序执行造成的" | ❌ | 主因是 store buffer/invalidate queue;顺序核也有 StoreLoad |
| "关掉乱序执行就没有内存重排了" | ❌ | store buffer 还在,StoreLoad 照旧 |
| "乱序执行是单核性能优化,单核透明" | ✅ | 顺序退休 + 精确异常保证 |
一句话边界:乱序执行的乱序在退休时被收口在核内(单核透明);内存重排发生在退休之后的缓冲区层(跨核可见)。二者只在"访存投机重排"处擦边,而这一擦边在 x86 上还被 machine-clear 抹平了。所以把它们当同义词,是根本性的概念混淆。
二、和性能观测的关系
乱序执行是"隐形加速",但它的效果和失效都能在 perf 里看到:
# IPC(每拍退休指令数)是乱序执行成效的直接指标:接近端口数说明并行挖得好
perf stat -e cycles,instructions ./app # IPC = instructions/cycles
# 前端/后端停顿:乱序引擎"喂不饱"或"退不动"的瓶颈
perf stat -e cycles,stalled-cycles-frontend,stalled-cycles-backend ./app
# 分支预测失败:投机执行猜错→ROB 清空重放,乱序收益打折
perf stat -e branches,branch-misses ./app
# 内存序 machine clear:第五节"交集"里投机 load 违反 TSO 被拉回的计数
perf stat -e machine_clears.memory_ordering ./app- IPC 高 = 乱序执行成功把多个端口喂满;IPC 低 + backend 停顿高 = 多半卡在 load miss(乱序也掩盖不了太多的内存延迟,这时该优化的是 cache 命中/数据布局,见 mesi 伪共享)。
- branch-misses 高 = 投机猜错多,ROB 频繁清空,乱序收益被吃掉——优化分支可预测性。
判读口诀:IPC 是乱序执行的"成效分",frontend/backend 停顿是"瓶颈定位",branch-misses 是"投机折损",machine_clears 是"访存投机被拉回"——四个指标分别对应乱序执行链条上的四类问题。
三、退休之后的世界:乱序止于 ROB,重排起于 store buffer
顺着 uop-pipeline-lsu-buffers 的视角,把"核内→核外"的分界再推进一步:
- 乱序执行的范围止于 ROB:从重命名到执行,怎么乱都行;退休那一刻,所有结果按程序序对外生效,单核状态机回到"与顺序执行完全一致"。
- 内存重排的范围起于 store buffer:store 指令退休后并没有立刻进内存,而是先进 store buffer 排队,等 LSU 仲裁后异步提交到 L1/L2。真正"被别的核看见"的顺序,由 store buffer 的提交顺序决定,跟 ROB 没关系。
所以两个世界各管一段:ROB 管"单核内部看得对不对",store buffer 管"多核之间看得对不对"。 单核程序里永远观测不到乱序执行;多核程序里观测到的重排,几乎都发生在 store buffer 这一层——这就是为什么 memory-order 讲 fence 时总对着 store buffer 讲,而不是对着 ROB 讲。
四、两个实战场景:别再误判了
场景一:单核自旋锁要不要担心乱序? 不用。自旋锁涉及的单核指令序(写锁→读锁)由 ROB 顺序退休保证,乱序不影响单核视角;跨核的那一层(别核什么时候看到你放锁),靠的是锁前缀指令/LOCK 语义和内存屏障——这是内存重排的范畴,不是乱序执行的范畴。
场景二:生产者-消费者为什么"偶发"读到旧值? 生产者先 store 数据、再 store 标志位;消费者读到标志位为 1 后读数据。在 x86 上,两个 store 本身不会被乱序(store 退休按序、提交按序),但数据 store 可能还堵在 store buffer 里没提交,标志位却先被看到(或反之,取决于缓存行状态)——这是 store buffer 的重排,和乱序执行无关。修法不是"关掉乱序",而是按需用 mfence/原子操作把提交顺序钉死。
场景三:伪共享又怎么算? 两个线程各自写同一 cache line 的不同字段,谁也不会碰到对方的变量——但缓存一致性协议(见 mesi)让这个 cache line 在两个核的缓存间来回奔波,每次写都要先拿独占。表现是:单看每条指令都正常、单核视角毫无问题,多核下却慢得离谱。这既不是乱序执行的错,也不是内存重排的错,而是"缓存行的战争"。排障时若 perf 显示 IPC 低但 backend 停顿不明显,同时确认两个线程在写邻近地址,就优先怀疑它。
判读口诀升级版:凡是单核能稳定复现的,找乱序执行;凡是只有多核才偶发的,找内存重排。 前者被 ROB 收口,后者被 store buffer 放行——两个世界,两套机制,两套工具。
五、怎么验证:把"边界"做成实验
想亲手看到乱序执行和内存重排的分野,两条路:
- 看乱序执行的成效:
perf stat -e cycles,instructions,stalled-cycles-backend跑一个依赖链程序(串行累加)和一个可并行程序(多路累加),前者的 IPC 会被依赖链压到 0.3 左右,后者接近端口上限——乱序执行"挖并行"的边界一目了然。 - 看内存重排的存在:跑 store-load 类 litmus 测试(大量迭代下统计"读到自己新值之前先读到旧值"的比例),x86 上能稳定观测到非零的罕见重排——这是 store buffer 在"作祟"。仓库 demos/ 里有可运行实验可对照,如 compiler-reordering 演示编译器重排,思路类似。
补充一个容易踩的坑:别用
std::atomic的memory_order_relaxed去"复现"乱序执行——relaxed 语义允许的编译器重排是编译期的事(见 compiler-reordering),和 CPU 的乱序执行是两码事。要隔离 CPU 因素,得用原生汇编 + 无优化编译。
两句收尾:乱序执行是"核内的事",perf 看 IPC 和停顿;内存重排是"核间的事",litmus 看罕见重排。工具不同,别混着用。
六、一句话总结
乱序执行和内存重排是正交的两件事,只在"访存投机重排"一处擦边:纯核内乱序(算术/分支)从不影响内存可见顺序;访存投机 load 在 x86 上被 machine-clear 兜回 TSO;纯内存重排(store buffer 的 StoreLoad)哪怕没有乱序引擎也会发生。判断依据一句话:乱序执行的乱序在退休时被 ROB 收口在核内(单核透明),内存重排发生在退休之后的缓冲区层(跨核可见)。观测上,IPC 看乱序成效、frontend/backend 停顿看瓶颈、branch-misses 看投机折损、machine_clears.memory_ordering 看访存投机被拉回的次数——
perf stat一行全搞定。