现代处理器具有强大的矢量指令,允许您一次加载多个值,并(在一条指令中)对所有这些值进行操作。同样,它们允许您拥有向量常量。因此,如果您想向大型数组中的所有整数添加一些整数(比如 10001),您可能首先加载一个值为 10001 的 8 倍的常量,然后您将从数组中加载元素,8 个元素乘以 8 个元素,添加矢量常量(因此一次做 8 次加法),然后存储结果。在其他条件相同的情况下,这可能会快 8 倍。
优化编译器甚至可以为您进行此优化(称为“自动矢量化”的过程)。但是,对于更复杂的代码,您可能需要使用“内部”函数(例如,_mm256_loadu_si256、_mm256_add_epi32 等)手动完成。
让我们考虑一下我描述的简单情况,但是我们同时处理两个数组……使用相同的常量:
#包含< x86intrin.h > #包含< stdint.h > void process_avx2 ( const uint32_t * in1 , const uint32_t * in2 , size_t len ) { // 定义常量,8 x 10001 __m256i c = _mm256_set1_epi32 ( 10001 ) ; const uint32_t * finalin1 = in1 + len ; const uint32_t * finalin2 = in2 + len ; 对于( ; in1 + 8 < = finalin1 ; in1 + = 8 ) { // 将 8 个整数加载到一个 32 字节的寄存器中 __m256i x = _mm256_loadu_si256 ( ( __m256i * ) in1 ) ; // 将刚刚加载的 8 个整数与 8 个常量整数相加 x = _mm256_add_epi32 ( c , x ) ; // 存储修改后的 8 个整数 _mm256_storeu_si256 ( ( __m256i * ) in1 , x ) ; } ; 对于( ; in2 + 8 < = finalin2 ; in2 + = 8 ) { // 将 8 个整数加载到一个 32 字节的寄存器中 __m256i x = _mm256_loadu_si256 ( ( __m256i * ) in2 ) ; // 将刚刚加载的 8 个整数与 8 个常量整数相加 x = _mm256_add_epi32 ( c , x ) ; // 存储修改后的 8 个整数 _mm256_storeu_si256 ( ( __m256i * ) in2 , x ) ; } }
直到最近,我的期望是优化编译器会将常量保存在寄存器中,并且永远不会加载它两次。他们为什么会这样?
然而,您可以检查GCC 是否加载常量两次。
在这种情况下,其他编译器(如 LLVM)做得更好。然而,在其他情况下,LLVM 和 GCC 都愉快地加载常量不止一次。只有英特尔编译器 (ICC) 似乎能够以一定的一致性避免此问题。
有关系吗?在大多数情况下,这种效果对性能的影响应该很小。几乎可以肯定的是,每个函数的开销只有几条指令。但是,在规模上,它会增加包含许多常量的代码。 AVX-512 引入了新的掩模类型,它们也受到这种影响。
能够指示编译器不要重新加载常量会很有趣。您可能认为 static 关键字会有所帮助,但是对于 LLVM,静态向量变量可能会受到锁的保护,这可能会使您的代码更加繁重。
原文: https://lemire.me/blog/2022/12/06/optimizing-compilers-reload-vector-constants-needlessly/