为何添加3个随机字节能让x86-64汇编代码提速10倍?
我正在阅读Agner Fog的《汇编语言子程序优化》手册第9.2节「乱序执行」,并复现其中示例测试性能:
测试案例1:无额外字节的循环
; first case = loop without extra bytes. section .data ; using db instead of dd was a mistake which later led ; me to this question. mem1: db 23 mem3: db 23 section .bss mem2: resd 1 mem4: resd 1 section .text global _start _start: mov rcx, 300000000 loop: ; waste time mov eax, dword [mem1] imul eax, 6 mov [mem2], eax mov edx, dword [mem3] imul edx, 8 mov [mem4], edx sub ecx, 1 jnz loop mov eax, 60 xor edi, edi syscall
编译运行耗时:
% nasm -f elf64 outoforder.asm && ld outoforder.o -o outoforder && time ./outoforder ./outoforder 1.46s user 0.00s system 99% cpu 1.458 total
令我困惑的是:在mem1和mem3周围添加至少3个随机字节后,代码速度提升10倍!
测试案例2:添加随机字节的循环
; second case = loop + at least three random bytes around mem1 and mem3 section .data db 1 mem1: db 23 db 1 mem3: db 23 db 1
编译运行耗时:
% nasm -f elf64 outoforder.asm && ld outoforder.o -o outoforder && time ./outoforder ./outoforder 0.13s user 0.00s system 99% cpu 0.130 total
无论字节位置如何,结果一致。objdump显示地址未对齐,排除对齐因素。
后续测试:
- 使用DX替代EDX时,代码速度介于两者之间,添加字节无影响;
- 使用AX替代DX时,代码速度与案例2一致;
- 单独运行AX相关循环耗时0.31s,替换为EAX后耗时0.06s。
我无法理解:为何添加3个字节会带来如此大的性能提升?AX对EAX高位的依赖为何未拖慢速度?
处理器:11th Gen Intel(R) Core(TM) i7-11700F @ 2.50GHz
系统:Arch Linux 6.6.10-arch1-1
架构:x86-64
一、添加3个字节带来10倍性能提升的原因
核心是重叠内存访问引发的加载-存储转发阻塞。
测试案例1中,mem1和mem3用db定义,内存中仅间隔1字节。执行mov eax, dword [mem1]时会读取从mem1开始的4字节,覆盖mem3所在的内存区域;同理mov edx, dword [mem3]会读取从mem3开始的4字节,覆盖mem1之后的区域。
这种重叠的内存访问会触发CPU存储缓冲区的阻塞:当CPU尝试加载mem3的数据时,发现该区域被之前mem1的4字节读操作覆盖,且后续可能存在未提交的存储操作,被迫等待存储缓冲区刷新,原本可以并行执行的两组mov+imul+mov指令只能串行执行,导致性能暴跌。
添加3个字节后,mem1和mem3的间隔足够大,两个dword加载操作的内存范围不再重叠。此时CPU的乱序执行单元可以同时调度两组指令,充分利用执行单元的并行性,性能自然大幅提升。
二、AX对EAX高位的依赖未拖慢速度的原因
现代Intel CPU的寄存器重命名机制和x86-64的寄存器规则共同导致了这个结果:
- 寄存器重命名的独立性:16位寄存器(如AX)和32位寄存器(如EAX)会被分配独立的物理寄存器,除非存在明确的完整寄存器读写依赖。测试中仅使用AX进行运算和存储,没有读取EAX的完整值,因此AX的操作与EAX高位无依赖关系,乱序执行单元可以独立调度AX相关指令。
- 16位与32位指令的效率差异:单独运行AX循环耗时更高,是因为现代CPU对32位操作的支持更完善,执行延迟和吞吐量都优于16位操作,且32位寄存器的重命名、调度开销更低,因此替换为EAX后性能进一步提升。
内容的提问来源于stack exchange,提问作者MerWay

