我有一个跨体系结构的代码,它通过索引查找混洗,用于在向量中移动uint32_t元素。每次混洗都需要一个完整的向量常量,但只有4个字节的非冗余信息。(或者实际上是4x2比特的信息,但解包的成本更高。)
在SSSE3-SSE4.2上,我使用的是_mm_shuffle_epi8,而在arm上,我使用的是table。
但是,现在我存储了整个混洗掩码,也就是控制向量,例如,对于int的标识,我将存储:0x0f0e0d0c0b0a09080706050403020100
我只想存储0x03020100,每个唯一的shuffle控制元素都存储在一个字节/ uint8_t中。
有没有从一个到另一个的有效方法?convert + multiply看起来有点笨重。
发布于 2021-09-28 21:43:15
存储您打包的LUT,每个字节保存起始字节号,这样您就不需要扩展它们。
将每个控制索引广播到相应元素的字节中(1个固定的混洗),然后添加一个常量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完成的,在这种情况下,您不必担心执行严格别名安全的dword加载。对此的内部支持是不确定的,但movd的_mm_cvtsi32_si128很容易使用。它接受一个值(不是地址),所以在C语言中,内存访问是在纯C中发生的。编译器仍然可以将加载合并到movd的内存操作数中。
顺便说一句,我假设你说的是SSE4.2,因为AVX1有_mm_permutevar_ps (vpermilps),所以_mm_cvtepu8_epi32 (pmovzxbd)可以解压4字节的加载,而不需要进一步的修改。使用双字索引,而不是字节索引,因此您可以将标识无序存储为0x03020100。
不幸的是,让编译器从内部代码发出内存源vpmovzxbd xmm0, [rdi]指令对于除了clang之外的编译器来说是一件痛苦的事情。它们通常无法将movd或movq load内部函数合并到内存源操作数中,但如果您不想在调试构建中超过缓冲区的末尾,则必须使用它而不是完整的__m128i load。有关几年前的实际编译器结果,请参阅Loading 8 chars from memory into an __m256 variable as packed single precision floats。
将AVX2或BMI2+AVX打包到单个字节中
每个混洗索引实际上只有2比特的信息,所以4个索引可以打包成1个字节(uint8_t)。
解包的方法是使用BMI2整数pdep。即_pdep_u32(lut[i], 0x03030303。然后是vmovd / vpmovzxbd / vpermilps。由于vpermilps只关心每个双字的低2位,因此甚至可以用乘数常量替换pdep。
但在Zen3之前,pext在AMD上的速度非常慢。即使在Intel上,首先加载到整数中也会有很大的延迟。
另一种选择是使用AVX2变量移位将适当的2位带到每个双字元素的底部。从字节的广播加载开始。或者更有效地在大多数情况下(除了高速缓存线分割),CPU可以在加载端口中“免费”进行的双字广播,不需要单独的ALU混洗操作。(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,也就是_mm256_permutexvar_epi32。但是,您需要8 x 3位的混洗控制数据=3字节,而不是1。然后,可能仍然有太多的可能性来进行LUT。)
相关的还有:
_mm_movemask_epi8结果而不是直接使用它来索引混洗矢量的65536 x __m128i (1MiB)表来压缩混洗控制矢量的查找表。https://stackoverflow.com/questions/69368358
复制相似问题