汇编耻辱堂:CPU 单指令性能下限大比拼
指令延迟分析通常关注的是性能优化——让代码跑得尽可能快。Assembly Hall of Shame(汇编耻辱堂)则反其道而行:寻找单条指令性能的绝对下限。
🏆 当前冠军 🏆
x86:fxrstor64
策略:使用 fxrstor64 从 PCIe 总线中一个高延迟的 MMIO 区域加载 512 字节的 FPU/MMX/XMM 状态,然后在加载期间让总线"挨饿"——一队"打桩核"用紧凑的 4 字节读操作猛敲另一个高延迟 MMIO 寄存器,用未发布(non-posted)事务把 PCIe 根复合体和终端设备彻底打满,这样 CPU 0 的 512 字节 fxrstor64 就只能排在这些争用流量的后面慢慢等。
挑战者:AMD Ryzen 7 5800H
; CPU 0 — 被计时的指令 movl $0xfcc68830, %rsi fxrstor64 %rsi ; CPU 1..N — 针对另一个高延迟地址的打桩循环 movl 0xfcc68858, %eax
🏆 得分:198,002,498,236 周期
🏆 耗时:62 秒
荣誉提名
在 smiiiiiiiiiiiiiiii 中,有人利用一条违反规范的 未对齐 ymm0 加载,迫使 GPU 寄存器产生未发布的 dword 事务,从而突破了系统管理模式(SMM)的根本设计。
vmovdqu 0xfcc003b1, %ymm0
规则
- 指令可以使用任何必要的准备,但只有单条指令本身计入得分。
- 对于陷入/模拟/虚拟化的指令,只能计入陷入本身的时间,不算处理器部分。
- 指令必须不可中断。
rep movs、pause等一律取消资格。 - 时间按 CPU 基础时钟频率归一化。
- 所有平台必须保持出厂默认配置,不得改装硬件。
x86 排行榜
27. nop
策略:nop 啥也不干。它也就只能在排行榜上垫底了。
挑战者:Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
nop
得分:1 周期
耗时:0 纳秒
26. nop16
策略:普通的 nop 太短了,但怎么让"什么都不做"花更长时间呢?试试超长 nop。
参赛选手:Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
data16 data16 data16 data16 data16 data16 data16 nopl 0x00000000(%%eax,%%eax,1)
得分:20 个周期
耗时:7 纳秒
25. rdtsc
策略:只是一条参考指令,用来校准基准。
参赛选手:Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
rdtsc
得分:49 个周期
耗时:18 纳秒
24. idiv
策略:用 128 位被除数(rdx:rax=2:0)配以小除数,使商超过符号扩展所设定的上限,从而走完除法器微码的最长路径。
参赛选手:Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
xorq %rax, %rax ; rax = 0 (低 64 位被除数) movq $2, %rdx ; rdx = 2 (高 64 位:完整被除数 = 2^65) movq $5, %rbx ; 除数 → 商 = 2^65/5 ≈ 7.4×10^18 idivq %rbx
得分:77 个周期
耗时:28 纳秒
23. enter
策略:使用最大嵌套深度(31),强制走 30 次显示指针加载与压入的微码遍历路径。
参赛选手:Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
enter $0, $31 ; 分配 0 字节,嵌套深度 31(最大值)
得分:112 个周期
耗时:41 纳秒
22. fldl
策略:试一个小非规格化数,触发浮点微码辅助例程。
参赛选手:Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
movabsq $0x0000000000000001, %rax
movq %rax, -8(%rsp)
fldl -8(%rsp)
得分:133 个周期
耗时:49 纳秒
21. clflush
策略:只要确保缓存行是脏的就行。
参赛选手:Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
clflush (%rax) ; rax -> 驻留在 L3 中的脏缓存行
得分:165 个周期
耗时:60 纳秒
20. fsin
策略:用指数 0x7ff 触发微码中的"特殊值"处理路径;正负号、NaN/inf 似乎没区别,直接用 QNaN。
参赛选手:Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
movabsq $0x7fffffffffffffff, %rax
movq %rax, -8(%rsp)
fldl -8(%rsp)
fsin
得分:257 个周期
耗时:94 纳秒
19. mfence
策略:用 movnti 向不同的缓存行写入,把所有写合并行填充缓冲区(LFB)打满,迫使 mfence 在执行完毕前必须把整个 LFB 写路径排空到 uncore。
参赛选手:Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
movnti %r9, 0*64(%rdi) ; ×16 条不同的缓存行 —— 打满写合并 LFB ; … movnti %r9, 15*64(%rdi) mfence ; 执行完毕前必须排空所有挂起的 LFB 写入
得分:326 个周期
耗时:120 纳秒
18. mov cr3
策略:暂时没什么花招,就是看看让 TLB 失效要花多久。
参赛选手:AMD Ryzen 7 5800H with Radeon Graphics(Trigkey S5)
mov %rax, %cr3
得分:352 个周期
耗时:110 纳秒
17. fadd
策略:用非规格化源操作数触发 x87 浮点的微码辅助路径。
参赛选手:Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
fldl subnorm ; 1e-310:小于 DBL_MIN 的值,偏移指数为 0 faddl subnorm ; 源操作数是次正规数 → 触发 FP 微码辅助
得分:677 个周期
耗时:249 纳秒
16. split lock
策略:让带 lock 前缀的操作数跨越缓存行边界,迫使 CPU 走外部总线锁定而非 MESI 缓存一致性快速路径。
选手:Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
; split_ptr % 64 == 63 — dword 跨越第 N 行的字节 63 与第 N+1 行的字节 64–66 lock xaddl %r9d, (%rdi)
得分:865 个周期
耗时:319 纳秒
15. fdiv -
策略:使用次正规数作为除数,硬件将控制权交给微码辅助例程,辅助例程先对操作数进行规范化,再执行除法,最后恢复架构状态。
选手:Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
movabsq $0x3ff0000000000000, %rax ; 1.0(正规被除数)
movq %rax, -8(%rsp)
fldl -8(%rsp) ; ST(0) = 1.0
movabsq $0x0000002000000000, %rax ; 6.79e-313(次正规除数)
movq %rax, -8(%rsp)
fdivl -8(%rsp) ; ST(0) = 1.0 / 次正规数 → FP 辅助
得分:883 个周期
耗时:325 纳秒
14. cpuid
策略:用 rakefield 找出延迟最高的 CPUID leaf。
选手:Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
movl $6, %eax
cpuid
得分:1248 个周期
耗时:460 纳秒
13. rdrand
策略:在紧凑循环中持续调用 rdrand,以比硬件熵池补充更快的速度耗尽熵源,迫使后续调用阻塞等待熵源恢复。
选手:Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz
rdrand %rax
得分:5,579 个周期
耗时:2.057 微秒
12. wrmsr
策略:用 project:nightshyft 找出高延迟的 MSR。Zen 上的 MCG_CTL 看起来很有戏:可能是微码静默并跨硬件单元(其中一些可能跨晶粒)同步 MCA 错误组,需要走 fabric 级通信,而不是一次简单的本地寄存器写。
参赛选手:AMD Ryzen 7 5800H with Radeon Graphics(Trigkey S5)
movl $0x17b, %ecx ; MCG_CTL wrmsr
得分:34,304 周期
耗时:10.742 微秒
11. out
策略:瞄准一个跨越网卡设备寄存器边界的 I/O 端口,让设备每次写入时都静默其 TX DMA 引擎。
参赛选手:AMD Ryzen 7 5800H with Radeon Graphics(Trigkey S5)
mov $0xf019, %dx outl %eax, %dx
得分:49,857 周期
耗时:15.580 微秒
10. rdmsr
策略:用 project:nightshyft 找出高延迟的 MSR:VIA 有一个未公开的寄存器 0x133,响应时间长得离谱,也不知道它是干什么的。
参赛选手:VIA Eden Processor 800MHz
movl $0x133, %ecx ; undocumented MSR rdmsr
得分:161,602 周期
耗时:202.004 微秒
9. wbinvd
策略:把 L1/L2/L3 缓存全部填满脏行,强制把整条缓存层级写回 DRAM。
参赛选手:AMD Ryzen 7 5800H with Radeon Graphics(Trigkey S5)
wbinvd
得分:1,616,480 周期
耗时:506.165 微秒
8. in
策略:瞄准映射到 ACPI PM 块的 I/O 端口,那里的一次未对齐 4 字节读会被解码成多次 non-posted 读,源地址取决于这个端口最终走到哪里。
参赛选手:AMD Ryzen 7 5800H 集成 Radeon 显卡(Trigkey S5)
mov $0x0413, %dx inl %dx, %eax
得分:12,524,415 个时钟周期
耗时:3.921769 毫秒
7. mov
策略:用 mmiotic 找出 PCIe 互联中延迟最高的空闲区域,访问一个未知的 GPU 寄存器。
参赛选手:AMD Ryzen 7 5800H 集成 Radeon 显卡(Trigkey S5)
movl 0xfcc003b0, %esi
得分:443,937,696 个时钟周期
耗时:139.010268 毫秒
6. mov rax -
策略:在 MMIO 地址空间里搜索 PCIe 互联中最慢的寄存器,访问一个未知的 GPU 寄存器,并用 8 字节 MMIO 读取来触发两次 dword 寄存器访问——严格来说这是不允许的,但确实能用。
参赛选手:AMD Ryzen 7 5800H 集成 Radeon 显卡(Trigkey S5)
movq 0xfcc003b0, %rax
得分:887,716,864 个时钟周期
耗时:277.971228 毫秒
5. vmovdqu xmm -
策略:在 MMIO 地址空间里搜索 PCIe 互联中最慢的寄存器,访问一个未知的 GPU 寄存器,并用 16 字节 MMIO 读取来触发四次 dword 寄存器访问——严格来说这是不允许的,但确实能用。
参赛选手:AMD Ryzen 7 5800H 集成 Radeon 显卡(Trigkey S5)
vmovdqu 0xfcc003b0, %xmm0
得分:1,774,555,776 个时钟周期
耗时:555.664133 毫秒
4. vmovdqu ymm -
策略:在 MMIO 地址空间里搜索 PCIe 互联中最慢的寄存器,访问一个未知的 GPU 寄存器,并用 32 字节 MMIO 读取来触发八次 dword 寄存器访问——依然严格来说不允许,但确实能用。
参赛选手:AMD Ryzen 7 5800H 集成 Radeon 显卡(Trigkey S5)
vmovdqu 0xfcc003b0, %ymm0
得分:3,549,079,296 个时钟周期
耗时:1.111345034 秒
3. vmovdqu ymm(未对齐) -
策略:在 PCIe 互联中搜索最慢的 MMIO 寄存器空间,命中某个未知的 GPU 寄存器,用 32 字节未对齐的 MMIO 读操作触发九次双字寄存器访问——比起对齐版本更加"不合规",但照常能跑。
参赛选手:AMD Ryzen 7 5800H with Radeon Graphics(Trigkey S5)
vmovdqu 0xfcc003b1, %ymm0
得分:4,453,212,256 个时钟周期
耗时:1.394428818 秒
2. fxrstor64(基线版本) -
策略:用 mmiotic 定位 PCIe 互联中的高延迟死区,隔离出 0 附近的某段区域并偏移状态以避免 MXCSR 被破坏(可能是 VGA 缓冲区?),然后用 fxrstor64 从 MMIO 加载 512 字节的 FPU/MMX/XMM 状态,强制 CPU 透过最慢的内存窗口处理 512 字节的 I/O 事务。
参赛选手:AMD Ryzen 7 5800H
movl $0xfcc68830, %rsi fxrstor64 %rsi
得分:74,584,168,512 个时钟周期
耗时:23.354502677 秒
1. 🏆 fxrstor64 🏆
策略:在 fxrstor64(基线版本) 的基础上,在 fxrstor64 加载过程中让互联陷入"饥饿"——一组"打手"核心对着另一个高延迟 MMIO 寄存器疯狂发起紧凑的 4 字节读,把 PCIe 根联合体和终端设备用未发布事务(non-posted transaction)淹没,让 CPU 0 的 512 字节 fxrstor64 不得不排在这堆竞争流量后面。
参赛选手:AMD Ryzen 7 5800H with Radeon Graphics(Trigkey S5)
; CPU 0 — 计时指令 movl $0xfcc68830, %rsi fxrstor64 %rsi ; CPUs 1..N — 对另一个高延迟位置的打手循环 movl 0xfcc68858, %eax
🏆 得分:198,002,498,236 个时钟周期
🏆 耗时:62 秒
??. xrstor64(AMX,MMIO)
策略:在 Sapphire Rapids 上结合 fxrstor64 的 MMIO 思路,启用扩展 AVX 状态:xsave 的状态区域为 8KB,对比 512 字节大了 16 倍 → 1,000,000,000,000 个时钟周期
参赛选手:TODO
; XCR0 必须启用 AMX 组件(位 17-18);状态区域约 8KB xrstor64 (%rsi) ; rsi -> MMIO 区域,与 fxrstor64 手法相同
ARM 排行榜
- 待定。
RISC-V 排行榜
- 待定。
作者
汇编耻辱堂由 Christopher Domas(@xoreaxeaxeax)发起研究。