存储打包的 LUT,每个字节都包含起始字节编号,因此您无需放大它们。
将每个控制索引广播到对应元素的字节中(1个固定shuffle),然后添加一个常量set1_epi32(0x03020100)来偏移它们。
__m128i v = _mm_cvtsi32_si128(shuffle_lut[i]);
v = _mm_shuffle_epi8(v, _mm_set1_epi32(0x03030303, 0x02020202, 0x01010101, 0x00000000)); // broadcast each byte into a dword
v = _mm_add_epi8(v, _mm_set1_epi32(0x03020100)); // offset the byte indices
// v is your shuffle-control vector, usable with another pshufb
// as if you'd just unpacked lut[i]>>2 to dwords for vpermilps
身份洗牌存储为0x0c080400。 0x0c + 0x03 = 0x0f 在顶部元素的顶部字节中。
我猜你在 C 中的 LUT 实际上是作为 uint32_t shuffle_lut 完成的,在这种情况下,你不必担心执行严格混叠安全的双字加载。对此的内在支持是冒险的,但_mm_cvtsi32_si128 的movd 易于使用。它需要一个值(不是地址),因此在 C 语言中,内存访问发生在纯 C 中。编译器仍然可以将负载折叠到 movd 的内存操作数中。
顺便说一句,我假设您说的是 SSE4.2,因为 AVX1 有 _mm_permutevar_ps (vpermilps),所以 _mm_cvtepu8_epi32 (pmovzxbd) 可以解压 4 字节负载而无需进一步修改。使用双字索引,而不是字节索引,因此您会将身份洗牌存储为0x03020100。
不幸的是,让编译器从内部代码发出内存源 vpmovzxbd xmm0, [rdi] 指令对于除了 clang 之外的编译器来说是一件痛苦的事情。他们经常无法将 movd 或 movq 加载内在函数折叠到内存源操作数中,但如果您不想超过缓冲区的末尾,则必须使用它而不是完整的 __m128i 加载。调试构建。几年前的实际编译结果见Loading 8 chars from memory into an __m256 variable as packed single precision floats。
AVX2 或 BMI2+AVX 打包成一个字节
每个 shuffle 索引实际上只有 2 位信息,因此可以将四个索引打包成 1 个字节 (uint8_t)。
解压方式是BMI2整数pdep。即_pdep_u32(lut[i], 0x03030303。然后vmovd/vpmovzxbd/vpermilps。也许pdep 甚至可以用乘数常数代替,因为vpermilps 只关心每个 dword 的低 2 位。
但是pext 在 Zen3 之前的 AMD 上非常慢。即使在 Intel 上,首先加载到整数也会有很大的延迟。
另一个选项是使用 AVX2 可变移位将适当的 2 位带到每个 dword 元素的底部。从字节的广播加载开始。或者在大多数情况下更有效(缓存行拆分除外),CPU 可以在加载端口中“免费”执行的双字广播,不需要单独的 ALU shuffle uop。 (https://uops.info/)
为此避免严格混叠 UB 是很痛苦的,例如_mm_set1_epi32( *(uint32_t*) &lut[i] ) 不安全。但是有一个内部函数需要一个指针,_mm_broadcast_ss。
// make sure LUT[] doesn't end right at the end of a page
// so we can broadcast-load 4 bytes starting at any byte offset in it.
// i.e. pad it by 3 bytes if needed.
__m128i v = _mm_castps_si128( _mm_broadcast_ss( (const float*)&LUT[i] ));
// alternative: __m128i v = _mm_set1_epi8( LUT[i] ); // vpbroadcastb is an extra shuffle uop, but narrower load
v = _mm_srlv_epi32(v, _mm_set_epi32(6, 4, 2, 0));
// ready for _mm_permutevar_ps
// low 2 bits of each 32-bit element of v are correct
不需要_mm_and_si128; vpermilps 不关心控制向量元素中的高垃圾。
请注意,AVX2 vpermd 没有 XMM 版本,因此即使有可用的 AVX2,vpermilps 仍然是使用 32 位粒度的可变控制随机播放的最佳选择。
(除非您想将整个算法扩展到 __m256i 中的 8 个元素,否则使用车道交叉 vpermd aka _mm256_permutexvar_epi32。但是您需要 8 x 3 位的随机控制数据 = 3字节不是 1。然后可能仍然有太多可能为其制作 LUT。)
也相关: