Replacing a 32-bit loop counter with 64-bit introduces crazy performance deviations with _mm_popcnt_u64 on Intel CPUs
Replacing a 32-bit loop counter with 64-bit introduces crazy performance deviations with _mm_popcnt_u64 on Intel CPUs
技术背景
在使用 _mm_popcnt_u64
指令时,将 32 位循环计数器替换为 64 位会在 Intel CPU 上引入疯狂的性能偏差。这主要是由于 popcnt
指令存在虚假数据依赖问题,而编译器可能并未意识到这一点。
实现步骤
1. 确认虚假数据依赖
在 Sandy/Ivy Bridge、Haswell 和 Skylake 等处理器上,popcnt src, dest
指令对目标寄存器 dest
存在虚假依赖。尽管该指令只进行写入操作,但它会等待 dest
准备好后才执行。Intel 已将此问题记录为勘误 [HSD146 (Haswell)][1] 和 [SKL029 (Skylake)][2]。
2. 测试不同寄存器使用情况
通过内联汇编绕过编译器,测试不同寄存器使用情况对性能的影响。
1 |
|
核心代码解释
- 不同寄存器使用情况:
- 不同寄存器:每个
popcnt
指令使用不同的寄存器,避免了虚假依赖,性能较高。 - 相同寄存器:所有
popcnt
指令使用相同的寄存器,引入了虚假依赖,性能较低。 - 相同寄存器但打破依赖链:在每次迭代开始时将寄存器清零,打破了跨迭代的依赖,性能有所提升。
- 不同寄存器:每个
最佳实践
- 使用
__builtin
内联函数:使用__builtin_popcountll
可以避免因虚假依赖导致的意外长循环依赖。
1 |
|
- 拆分计数变量:将计数结果累加到不同的变量中,最后再求和,避免虚假依赖。
常见问题
- 编译器未意识到虚假依赖:大多数编译器(如 Clang、MSVC 和 Intel 的 ICC)尚未意识到
popcnt
指令的虚假依赖问题,不会生成补偿代码。 - CPU 存在虚假依赖的原因:
popcnt
与bsf
/bsr
在同一执行单元运行,而bsf
/bsr
存在输出依赖,可能是为了方便硬件设计而导致popcnt
也存在虚假依赖。 static
变量改变性能的原因:推测是由于static
变量存储在全局数据空间,而普通局部变量存储在栈上,编译器对栈上内存位置的引用方式可能导致性能差异。
性能评估建议
- 估算峰值性能:参考 Intel 架构优化手册,查看
POPCNT
指令的延迟和吞吐量,计算最佳可能带宽。 - 分析编译器生成的代码:查看循环中所有指令的吞吐量,评估生成代码的最佳性能。
- 考虑数据依赖:分析指令之间的数据依赖关系,避免因数据依赖导致的延迟。
总结
在使用 _mm_popcnt_u64
指令时,要注意 popcnt
指令的虚假依赖问题。通过合理使用寄存器、拆分计数变量和使用 __builtin
内联函数等方法,可以避免虚假依赖,提高性能。同时,不同编译器对该问题的处理能力不同,在选择编译器时需要考虑这一点。此外,在进行性能评估时,要综合考虑指令的延迟、吞吐量和数据依赖等因素。
Replacing a 32-bit loop counter with 64-bit introduces crazy performance deviations with _mm_popcnt_u64 on Intel CPUs
https://119291.xyz/posts/replacing-32-bit-loop-counter-with-64-bit-performance-deviations/