我开始在 Linux 上使用 GCC 12 在 x86 上编码 AVX2。一切都按预期进行。除了以下片段:
#include <iostream>
#include <immintrin.h>
__m256i aVector = _mm256_setzero_si256();
_mm256_insert_epi32(aVector, 0x80000000, 0);
_mm256_insert_epi32(aVector, 0x83333333, 3);
_mm256_insert_epi32(aVector, 0x87777777, 7);
alignas(__m256i) uint32_t aArray[8];
_mm256_store_si256((__m256i*)aArray, aVector);
std::cout << aArray[0] << ", " << aArray[1] << ", " << aArray[2] << ", "
<< aArray[3] << ", " << aArray[4] << ", " << aArray[5] << ", "
<< aArray[6] << ", " << aArray[7] << std::endl;
我希望在输出中看到插入的数字。但我得到以下信息:
0, 0, 0, 0, 0, 0, 0, 0
我不知道出了什么问题。我没有收到任何错误或警告。 具有 64 位通道的代码变体具有相同的行为。
为什么插入没有效果?
修改后的向量就是返回值,
v = _mm256_insert_epi32(v, x, 3);
__m256i _mm256_insert_epi32 (__m256i a, __int32 i, const int index)
。
没有任何具有小写名称的英特尔内在函数通过引用修改其参数;小写名称是(或者可以是1)C 函数,并且 C 没有引用参数。如果它们有一个输出,那就是返回值。如果它们有多个输出,则会有一个返回值和一个指针 arg,例如
_addcarry_u64
,它返回进位并有一个 unsigned __int64 * out
arg。 (它通常无法有效编译,但按值进位返回才是问题所在,编译器经常使用 setc
将进位具体化到整数寄存器中。)
有一些全大写命名的内在函数,它们是 CPP 宏,遵循所有大写名称都是宏的通用约定,其他名称不是(除了可能作为实现细节)。最有用的一个是
_MM_SHUFFLE()
,它将四个整数填充到 pshufd
、shufps
、vpermq
等立即数的 2 位字段中。并且至少其中几个会修改其参数,例如 _MM_TRANSPOSE4_PS(__m128, __m128, __m128, __m128)
(指南)
仅供参考,即使对于一个元素,插入常量也不是一种非常有效的方法。没有单一的说明;
vpinsrd
仅存在于零扩展至 256 位的 XMM 目标中。 (或者传统的 SSE pinsrd
,它 会使上半部分保持不变,但会导致某些微架构上的 SSE/AVX 转换停止。编译器不会使用传统的 SSE 形式插入下半部分,即使它会快,例如 -mtune=skylake
或 -mtune=znver1
。)
插入三个常量的一种更快的方法是使用一个
_mm256_blend_epi32
(vpblendd
) 和一个包含要插入元素的向量。希望 clang 将插入优化为混合... Godbolt: 关闭,它在一个混合中完成了低 128 位通道中的两个元素,但留下了高元素进行单独混合。就像它试图节省常量空间一样,但最终仍然使用了 32 字节常量,上半部分有 16 字节零。
__m256i manual_blend(__m256i aVector){
__m256i vconst = _mm256_set_epi32(0x87777777, 0x86666666, 0x85555555, 0x84444444,
0x83333333, 0x82222222, 0x81111111, 0x80000000);
return _mm256_blend_epi32(aVector, vconst, 0b1000'1001);
}
# GCC -O2 -Wall -march=x86-64-v3
manual_blend(long long __vector(4)):
vpblendd ymm0, ymm0, YMMWORD PTR .LC3[rip], 137
ret
对比具有 3 个插入的类似函数,采用向量 arg 并返回修改后的版本(在 YMM0 中)。
# GCC -O2 -Wall -march=x86-64-v3
bar(long long __vector(4)):
mov eax, -2147483648
vpinsrd xmm1, xmm0, eax, 0 # insert into the low half, keeping the orig unmodified in YMM0
mov eax, -2093796557
vextracti128 xmm0, ymm0, 0x1 # get the high half of the original
vpinsrd xmm1, xmm1, eax, 3 # second insert into low half
mov eax, -2022213769
vpinsrd xmm0, xmm0, eax, 3 # insert into the high half
vinserti128 ymm0, ymm1, xmm0, 0x1 # recombine halves
ret
GCC 在这里做得很好,天真地使用
vpinsrd
,跨多个插入进行优化,只提取并放回高半部分一次,而不是在每个插入之间。
# -O2 -Wall -march=x86-64-v3
bar(long long __vector(4)):
vblendps ymm0, ymm0, ymmword ptr [rip + .LCPI1_0], 9 # ymm0 = mem[0],ymm0[1,2],mem[3],ymm0[4,5,6,7]
vbroadcastss ymm1, dword ptr [rip + .LCPI1_1] # ymm1 = [2272753527,2272753527,2272753527,2272753527,2272753527,2272753527,2272753527,2272753527]
vblendps ymm0, ymm0, ymm1, 128 # ymm0 = ymm0[0,1,2,3,4,5,6],ymm1[7]
ret
不幸的是,Clang 即使在整数向量上也使用 FP 混合 (blendps
);如果依赖链的一部分涉及实际的 SIMD 整数指令,如
vpaddd
(
_mm256_add_epi32
),则在 Skylake 等某些 Intel CPU 上,这将花费额外的延迟转发到混合和转发的周期。 (对于具有 SSE1 的非 AVX,
...ps
打包单编码小于等效的
...pd
打包双精度或
p...
整数(
movaps
与
movdqa
),否则它们在机器中的大小相同代码。但通常它不会造成伤害,所以总是这样做就好。对于混合,它确实会造成伤害,但不会节省空间。还可能会损害某些微体系结构上的按位布尔运算的性能,IIRC。就像 Sandybridge 或 Haswell 的吞吐量一样
vandps
与
vpand
。)
always_inline
函数也无法获得常量传播以使参数到 GCC
__builtin_ia32_...
内置实际的编译时常量。但在优化构建中,GCC 标头使用函数定义;有一个 #ifdef 和第二组需要常量的内在函数的定义。