Intel提供了一个名为_mm256_madd_epi16的C风格函数,它基本上
__m256i_mm256_madd_epi16(__m256i a,__m256i B)
将a和b中的压缩有符号16位整数相乘,产生中间有符号32位整数。将相邻的32位中间整数对水平相加,并将结果打包到dst中。
现在我有两个__m256i变量,每个变量都有32个8位int。
我想实现与_mm256_madd_epi16
相同的功能,但结果__m256i中的每个int32_t元素都是有符号char的四个乘积的**和,而不是两对有符号int16_t
。**每个32位块中四个int8_t
元素的点积。
我可以在一个标量循环中做到这一点:
alignas(32) uint32_t res[8] = {0};
for (int i = 0; i < 32; ++i)
res[i / 4] += _mm256_extract_epi8(a, i) * _mm256_extract_epi8(b, i);
return _mm256_load_si256((__m256i*)res);
请注意,乘法结果在加法之前被 sign-扩展为int
,_mm256_extract_epi8
助手函数1返回有符号的__int8
。不要介意总数是uint32_t
而不是int32_t
;它不能溢出,因为只有四个8x8 => 16位数字要添加。
它看起来非常丑陋,并且不能有效地运行,除非编译器使用SIMD进行一些魔法,而不是像编写标量提取那样进行编译。
脚注1:_mm256_extract_epi8
不是一个内在的。vpextrb
只适用于256位向量的低通道,并且此helper函数可能允许不是编译时常量的索引。
1条答案
按热度按时间7bsow1i61#
pmaddubsw
:如果至少有一个输入是非负的(因此可以被视为无符号),则可用如果已知其中一个输入总是非负的,则可以将其用作
pmaddubsw
的无符号输入;pmaddwd
的8->16位等价物。它添加了u8*i8 -> i16
乘积对,带符号饱和度为16位。但饱和是不可能的一个输入是最多127而不是255。(127*-128 = -0x3f80
,所以两倍仍然适合i16。在
pmaddubsw
之后,使用pmaddwd
对_mm256_set1_epi16(1)
对元素对进行求和,并正确处理符号。(这通常比手动将16位元素符号扩展到32位以添加它们更有效。(
pmaddwd
对于4字节元素内的对的水平16=>32位和,在某些CPU上的延迟比移位/和/加法更高,但对于符号扩展到32位,将两个输入都视为有符号。而且它只有一个uop,所以这对吞吐量很有好处,特别是如果周围的代码在相同的执行端口上没有瓶颈。一般情况(两个输入都可以为负)
最近对_mm512_dpbusd_epi32 AVX-512 VNNI指令的AVX-512 BW仿真的回答提出了一个很好的技巧,将一个输入分为MSB和低7位,因此可以使用
vpmaddubsw
(_mm256_maddubs_epi16
)而不会溢出。我们可以借用这个技巧,在hsumming时取反,因为MSB的位值是-2^7
,而不是vpmaddubsw
的无符号输入所处理的2^7
。这避免了有符号饱和:一侧的最大乘数是128(MSB被设置并被视为无符号)。
128 * -128
=-16384,两倍即-32768 = -0x8000 =位模式0x 8000。或者128 * 127 * 2
= 0x 7 f00作为最高阳性结果。这是7个uop(乘法单元为4个)与以下版本的9个uop(4个移位+ 2个乘法)。
AVX-512 VNNI
_mm256_dpbusd_epi32
(或512),或AVX_VNNI_mm256_dpbusd_avx_epi32
(VPDPBUSD
)类似于vpmaddubsw
(u8*i8
产品),但添加到现有的总和,并在单个指令的一个字节内对4个产品求和。(i32 += four u8 * i8
)。同样的拆分技巧也可以,_mm256_sub_epi32(low7_prods, msb_prods)
,但我们可以跳过madd_epi16
(vpmaddwd
)i16到i32的水平求和步骤。(其他VNNI指令包括
vpdpbusds
(与vpdpbusd
相同,但具有带符号饱和而不是环绕)。无论哪种方式,饱和度都是i32,而不是像vpmaddubsw
那样的i16,因此只有当累加器输入为非零时才会饱和。如果一个输入是非负的,因此可以被视为无符号的,则这在一个指令中完成整个作业而不拆分。和vpdpwssd[s]
,有符号字的MAC,有或没有饱和,像vpmaddwd
,但有累加器操作数。)没有AVX-512 VNNI的AVX-512可以使用AVX 2版本不变,或扩大到512。或者可以通过移位将符号位转换为掩码(
vptestmb
)并将4字节块的水平和的输入(零掩码vpmovdqu8
)的一些字节归零为32位元素(vdbpsadbw
对零,具有标识混洗控制)来应用符号位。但是不,这不会在添加它们之前对8位输入进行符号扩展,因为它是无符号差异。也许首先将范围移位到无符号(例如与0x80
的零掩码异或),然后添加4*128
?无论如何,msb = _mm256_slli_epi32(dword_hsums_of_input_b, 7)
将以与上面的代码使用其msb
变量相同的方式使用。如果这甚至工作,IDK如果它节省uops。欢迎反馈,或发布AVX-512 BW答案。另一种方式:解包和符号扩展到16位
显而易见的解决方案是将输入字节解压缩为带有零或符号扩展的16位元素。然后,您可以使用
pmaddwd
两次,并将结果相加。如果您的输入来自内存,那么使用
vpmovsxbw
加载它们可能是有意义的。例如但是现在你有4个字节,你想分散在 * 两个 * 双字,所以你必须 Shuffle 的结果一个
_mm256_madd_epi16(a,b)
。您可以使用vphaddd
来进行 Shuffle ,并将两个256位的乘积向量添加到一个256位的结果向量中,但这需要大量的 Shuffle 。因此,我认为我们应该从每个256位输入向量生成两个256位向量:一个具有每个字符号扩展到16的高字节,另一个具有扩展的低字节符号。我们可以用3个移位(对于每个输入)来做到这一点
作为
vplldq
的一个替代方案,vpsllw
的8位__m256i a_low = _mm256_slli_epi16(a, 8);
是更“明显”的方式,在每个字内从低到高移位,如果周围的代码在shuffles上出现瓶颈,可能会更好。但通常情况下情况更糟,因为 this 代码在shift + vec-int乘法上严重瓶颈。在KNL上,你可以使用AVX 512
vprold z,z,i
(Agner Fog没有显示AVX 512vpslld z,z,i
的时序),因为它与你在每个字的低字节中移位或 Shuffle 无关;这只是为算术右移而设置的。执行端口瓶颈:
Haswell只在端口0上运行向量移位和向量整数乘法,因此这严重阻碍了这一点。(Skylake更好:p0/p1). http://agner.org/optimize/ .
我们可以使用shuffle(端口5)代替左移作为算术右移的设置。这提高了吞吐量,甚至通过减少资源冲突来减少延迟。
但是我们可以通过使用
vpslldq
进行向量字节移位来避免shuffle控制向量。它仍然是一个通道内的 Shuffle (在每个通道的末尾移位零),因此它仍然具有单周期延迟。(我的第一个想法是vpshufb
和一个像14,14, 12,12, 10,10, ...
这样的控制向量,然后是vpalignr
,然后我记得简单的旧pslldq
有一个AVX 2版本。同一条指令有两个名称。我喜欢_mm256_bslli_epi128
,因为字节移位的b
将其区分为shuffle,不像元素内的位移位。我没有检查哪个编译器支持128位或256位版本的intrinsic的名称。这也有助于AMD Zen 1。向量移位只能在一个执行单元(P2)上运行,但混洗可以在P1或P2上运行。
我还没有看过AMD Ryzen执行端口冲突,但我很确定这不会在任何CPU上更糟(除了KNL Xeon Phi,其中AVX 2在小于dword的元素上的操作都非常慢)。移位和通道内混洗是相同数量的uop和相同的延迟。
如果任何元素已知为非负,则sign-extend = zero-extend
(Or最好使用
pmaddubsw
,如第一部分所示。)零扩展比手动扩展符号更便宜,并且避免了端口瓶颈。
a_low
和/或b_low
可以用_mm256_and_si256(a, _mm256_set1_epi16(0x00ff))
创建。a_high
和/或b_high
可以用shuffle而不是shift创建。(pshufb
在混洗控制向量的高位被设置时将元素置零)。在主流Intel上,Shuffle吞吐量也被限制为每时钟1次,因此如果您过度使用,shuffle可能会成为瓶颈。但至少它和multiply不是同一个端口。如果只有高字节是已知的非负,用
vpshufb
替换vpsra/lw
可能会有帮助。不对齐的加载,使那些高字节是低字节可能会更有帮助,为a_low
和/或b_low
设置vpand
。