为何使用多寄存器组会导致x86-64循环性能骤降?
一、性能骤降的核心原因
两组代码性能差异的关键在于Coffee Lake架构的硬件资源瓶颈与部分寄存器操作的额外开销:
1. 加载端口资源竞争
Core i9-9900K(Coffee Lake-S)仅配备3个加载执行端口(P2/P3/P4),每个Block包含3次加载操作:
movzx r9, byte ptr[r10](加载s[i])movzx rax, byte ptr[r11](加载s[j])movzx rax, byte ptr[rsp + r9](加载s[tmp])
第一种方案是单Block连续展开256次,乱序执行窗口内同时调度的加载操作数量不会超过端口承载能力;而第二种方案将两个Block交错,单次循环内包含6次加载操作,乱序执行会尝试并行调度所有加载指令,超过3个加载端口的处理上限,导致大量指令排队等待,直接砍半吞吐量。
2. 8位寄存器操作的部分寄存器重命名开销
第二种方案中使用add r10b, 2、add r13b, 2这类8位寄存器修改操作,会触发处理器的部分寄存器合并微操作:当后续用整个64位寄存器(r10/r13)作为内存地址时,处理器需要将8位更新与寄存器高位合并,额外增加微操作数和延迟。而第一种方案的inc r10b虽然也是8位操作,但连续重复的相同模式会被硬件优化,开销远低于交错的多寄存器8位操作。
二、Block1的优化建议
针对核心逻辑,可从以下方向优化指令效率与硬件利用率:
1. 替换交换逻辑,减少加载/存储指令
原代码中s[i]与s[j]的交换逻辑可直接用xchg指令替代,减少指令冗余:
; 原交换逻辑 movzx rax, byte ptr[r11] ; s[i] = s[j] mov byte ptr[r10], al ; mov byte ptr[r11], r9b ; s[j] = tmp ; 替换为 xchg byte ptr[r10], byte ptr[r11]
注:无lock前缀的xchg并非原子操作,微操作数与原逻辑一致,但指令数更少,有助于降低指令缓存压力。
2. 改用64位寄存器操作,消除部分寄存器开销
将所有8位寄存器操作替换为64位操作,避免部分寄存器重命名的额外延迟:
add r11b, r9b→add r11, r9(r9是movzx生成,高56位为0,结果等价)inc r10b→inc r10add r10b, 2→add r10, 2
3. 简化栈地址加载操作
movzx rax, byte ptr[rsp + r9]中,r9经movzx零扩展后高56位为0,可直接用mov al, byte ptr[rsp + r9]替代,省去零扩展的微操作(后续仅使用al参与异或,不影响结果)。
4. 调整循环展开深度,匹配乱序窗口
Core i9-9900K的乱序执行窗口为224微操作,256次展开的总微操作数(256×9=2304)远超过窗口容量,会导致前端指令供给压力。可尝试将展开次数调整为128或64,平衡指令缓存命中率与乱序执行效率。
5. 增加软件预取,优化缓存访问
在Block开头添加预取指令,提前将后续要访问的s数组数据载入缓存:
prefetchnta [r10 + 64] ; 预取64字节外的数据,适配L1缓存行大小
附:原循环终止代码
add rcx, UNROLL_CNT ; data += UNROLL_CNT dec rbx ; if --num_unrolled_loops jnz some_loop ; continue
内容的提问来源于stack exchange,提问作者yopyop

