- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言 —— 在 1 个周期与 1,980 亿个周期之间
- 规则造就的东西 —— 什么才算「一条指令」
- 第一区段 —— 核心内部微码介入的地方
- 第二区段 —— 一致性与平台介入的地方
- 第三区段 —— 走出芯片的那一刻
- 第一名的策略证明了什么
- 这张表在讲你的基准测试
- 实际会绊倒你的三件事
- 参考资料
引言 —— 在 1 个周期与 1,980 亿个周期之间
指令延迟表是做优化的人的基本工具。乘法 3 个周期、除法 20 个周期,就这样记下来再写代码。
2026 年 8 月公开的 Assembly Hall of Shame 走的是反方向。副标题是「Racing to the bottom of CPU performance」。这是一份把单条指令跑到最慢的比赛排行榜。
垫底的第 27 名是 nop,1 个周期。第一名是 fxrstor64,1,980 亿个周期,换算成时间是 62 秒。
如果同一类东西之间能差出 2,000 亿倍,那你首先该怀疑的是「用一把尺子去量这一类东西」这个前提。本文从下往上读这份榜单,看是什么造成了这种差距,以及它对你的代码和基准测试意味着什么。
规则造就的东西 —— 什么才算「一条指令」
在这类比赛里,规则本身就是论点。仓库里写下的规则是这样的。
- 准备阶段用什么都可以,但计分的只有单独一条指令。
- 对于会陷入陷阱、被模拟或被虚拟化的指令,只计到陷阱为止,不计处理程序。
- 指令不得是可中断的。像
rep movs或pause这样的直接判负。 - 时间按 CPU 基础时钟归一化。
- 所有平台必须是出厂设置,禁止改动硬件。
第三条规则让这场比赛变得有意义。如果允许可中断的指令,用带重复前缀的字符串指令就能造出任意数字。要求不可中断,数的才是流水线真正被这一条指令拴住的时间。
第二条规则同理。允许会陷入陷阱的指令,测的对象就不再是指令,而是操作系统的处理程序。
正因为有这两条规则,榜单才不是一场杂耍,而成了一份微架构观察记录。
第一区段 —— 核心内部微码介入的地方
下半段全都是发生在核心内部的事。测量大多在 Intel Core i7-8559U 上进行。
| 名次 | 指令 | 周期 | 发生了什么 |
|---|---|---|---|
| 27 | nop | 1 | 什么都没发生 |
| 26 | nop16 | 20 | 加了七个 data16 前缀的长 nop |
| 25 | rdtsc | 49 | 基准点 |
| 24 | idiv | 77 | 用 128 位被除数走完除法器最长的路径 |
| 23 | enter | 112 | 用最大嵌套深度触发微码的 display 遍历 |
| 22 | fldl | 133 | 加载非规格化数进入 FP 微码辅助 |
| 20 | fsin | 257 | 指数为 0x7ff 的特殊值处理路径 |
| 17 | fadd | 677 | 非规格化操作数导致的 FP 微码辅助 |
| 15 | fdiv | 883 | 非规格化除数 |
| 14 | cpuid | 1,248 | 选取延迟最大的那个 leaf |
这一区段的共同模式已经看得出来。给硬件快速路径一个它处理不了的输入,控制权就会交给微码。
fadd 这一条尤其有教益。同一条指令,仅仅因为操作数是非规格化数就要花 677 个周期。正常的浮点加法只有几个周期。指令没变,只有数据变了,倍率就多出了两个数量级。
enter 这一条也很有意思。把嵌套深度给到最大值 31,就会走上加载并压入 30 个 display 指针的微码路径。指令编码上是允许的,但现代编译器绝不会生成这种形式。
第二区段 —— 一致性与平台介入的地方
从中段开始,事情就不止于一个核心了。
| 名次 | 指令 | 周期 | 发生了什么 |
|---|---|---|---|
| 21 | clflush | 165 | 驱逐脏行 |
| 19 | mfence | 326 | 用 16 条 movnti 把写合并缓冲填满后全部排空 |
| 18 | mov cr3 | 352 | TLB 全量失效 |
| 16 | split lock | 865 | 跨缓存行的 lock xaddl 触发外部总线锁 |
| 13 | rdrand | 5,579 | 硬件熵池枯竭后等待恢复 |
| 12 | wrmsr | 34,304 | 写 Zen 的 MCG_CTL。推测会同步到片外单元 |
| 11 | out | 49,857 | 跨 NIC 寄存器边界的端口写入使 TX DMA 停摆 |
| 9 | wbinvd | 1,616,480 | 把整个缓存层级填成脏数据后回写到 DRAM |
第 16 名的 split lock 是实务中最常遇到的一条。当原子操作的操作数跨越缓存行边界时,CPU 无法使用快速的 MESI 缓存一致性路径,只能加外部总线锁。它要花 865 个周期,期间其他核心也会受影响。
第 9 名的 wbinvd 高达 160 万个周期,也值得留意。这不是因为指令复杂,而是因为缓存里所有的脏数据都必须被推到 DRAM。也就是说,这条指令的成本不是由指令决定的,而是由那一刻缓存的状态决定的。
第 12 名 wrmsr 条目里的推测也值得引用。仓库写道,这看起来是微码在停止并同步分布于多个硬件单元的机器检查库,其中一部分位于片外,因此似乎需要的不是简单的本地寄存器写入,而是总线级别的通信。作者本人已表明这是推测,因此原样搬过来。
第三区段 —— 走出芯片的那一刻
上半段的性质完全不同。
| 名次 | 指令 | 周期 | 时间 |
|---|---|---|---|
| 8 | inl(ACPI PM 端口) | 12,524,415 | 3.92 毫秒 |
| 7 | movl(MMIO GPU 寄存器) | 443,937,696 | 139 毫秒 |
| 6 | movq(8 字节 MMIO) | 887,716,864 | 278 毫秒 |
| 5 | vmovdqu xmm(16 字节) | 1,774,555,776 | 556 毫秒 |
| 4 | vmovdqu ymm(32 字节) | 3,549,079,296 | 1.111 秒 |
| 3 | vmovdqu ymm(非对齐 32 字节) | 4,453,212,256 | 1.394 秒 |
这里的全都是 mov。除了搬数据什么也不做的指令,跑了一秒多。
原因在地址上。这些地址不是 DRAM,而是PCIe 总线另一侧的设备寄存器。作者用另一个叫 mmiotic 的工具翻遍 MMIO 空间,找出了响应最慢的区域。
而从第 6 名到第 3 名的增长模式,把这一区段的原理直接摊了出来。把访问宽度从 8 字节加到 16 字节再加到 32 字节,时间大致每次翻倍。按仓库的说明,这是因为一次 8 字节的 MMIO 读取会被拆成两次双字寄存器访问。32 字节就是八次,弄成非对齐就是九次。
这里的关键是这一点。MMIO 读取是非投递式事务。 写入可以发出去就忘掉,读取却必须等到响应。所以往返时间就直接变成了指令的执行时间。关于第 3 名,仓库写道,非对齐的 32 字节 MMIO 读取「技术上并不被允许,但反正它能跑」。
第一名的策略证明了什么
现在轮到第一名。
; CPU 0 - 被测量的指令
movl $0xfcc68830, %rsi
fxrstor64 %rsi
; CPU 1..N - 敲打另一个高延迟位置的锤击循环
movl 0xfcc68858, %eax
fxrstor64 是从内存恢复 512 字节 FPU/MMX/XMM 状态的指令。让它从最慢的 MMIO 区域去读这 512 字节,就成了上面第 3 名那套手法的加强版。仅此一项就是 74,584,168,512 个周期、23.35 秒。
第一名在这上面又叠了一层。在那次加载进行的同时,其余的核心以 4 字节为单位不断敲打另一个高延迟的 MMIO 寄存器。 PCIe 根复合体与端点被非投递式事务塞满,CPU 0 的那次 512 字节加载只能排在后面。
结果是 198,002,498,236 个周期、62 秒。也就是说,一条指令的执行时间,因为其他核心在做什么而变化了两倍以上。
这个实验所证明的命题很清楚。「这条指令是几个周期」这个问题,不把周边状态固定住就没有答案。 表里写的数字不是指令的属性,而是指令与系统状态这个组合的观测值。
顺带一提,这套手法也带来了实用的结果。仓库写道,第 3 名那条非对齐的 ymm 加载被用来打破 System Management Mode 的根本设计,并链接了另一个仓库。只要能把中断任意拉长,以原子性为前提的设计就会被打破。
这张表在讲你的基准测试
榜单看起来像是极端的玩闹,但结论可以原封不动地套用到平常的性能工作上。
微基准测试测的是系统状态,不是指令。 同一段代码,在缓存温热与冰冷时、其他核心空闲与繁忙时、数据是规格化数与非规格化数时,会给出完全不同的数字。这就是引用基准测试结果时必须一并写明条件的原因。
最坏情况不是平均值的倍数。 上面表格里的各个区段并不连续,而是被悬崖分开的。平均延迟再漂亮,只要踩中一次悬崖,那一个请求就慢一百倍。处理尾延迟时,找出并消除悬崖比改善平均值更有效。
同样的指令流也会受其他核心活动的影响。 第一名的案例是极端情形,但原理相同。共享资源一路从末级缓存延伸到内存控制器、互连,直到 I/O 总线。这就是单独运行的基准测试预测不了生产环境性能的原因。
实际会绊倒你的三件事
最后,把这张表里与实务直接相关的三条整理出来。
非规格化数。 表里的第 22、17、15 名全是这个。在信号处理或物理仿真这类数值会缓慢收敛到 0 的代码里,它是真会出现的。症状是「同一段代码会因输入数据不同而突然变慢」。应对方式因硬件和语言而异(x86 的 flush-to-zero 与 denormals-are-zero 模式是代表性做法),需要各自查阅平台文档。这里重要的是:只有知道这道悬崖存在,你才找得到原因。
split lock。 表里的第 16 名。Linux 内核提供了检测这一现象的功能。内核文档把 split lock 定义为「操作数跨越两个缓存行的所有原子操作」,把总线锁定义为「对回写内存的 split lock 访问,或对非回写内存的所有加锁访问」。启动参数 split_lock_detect 接受 off、warn、fatal、ratelimit:N 这些值,文档中记载的默认值是 warn。如果内核日志里出现了相关警告,那很可能就是一个真实的性能问题。
把 MMIO 当内存来用的代码。 表的上半段整个都在讲这件事。语法只是一条 mov,成本却可能是 DRAM 访问的一百万倍。如果驱动或嵌入式代码在循环里读设备寄存器,那个循环就不是在计算,而是在反复做 I/O 往返。值得考察一下:能不能把状态轮询换成中断,能不能一次读多个寄存器,能不能把读改成写。
仓库表示目前只填了 x86 的榜单,ARM 与 RISC-V 还在准备中。等其他架构的清单填上来,哪些悬崖是 x86 独有的、哪些是现代 SoC 的普遍性质,就会更清楚了。
参考资料
- Assembly Hall of Shame — GitHub(所有周期数值与策略说明都搬自这个仓库的 README)
- x86 Bus Lock Detection and Handling — Linux 内核文档
- mmiotic —— 面向未文档化硬件的延迟探测工具
这份榜单里的任何一条我都没有亲自运行过。其中相当一部分是会让系统停摆或让设备不稳定的操作,而仓库里的实测值是针对特定硬件(主要是 Intel Core i7-8559U 与 AMD Ryzen 7 5800H)的数值。