将32位整数的矢量相乘,只取高32位



我想将16个无符号32位整数的两个512位__m512i向量相乘,并且只从64位相乘结果中取高32位。尽管英特尔内部指南说_mm512_mulhi_epu32存在,但它不会在我的机器上编译。

这里的答案声称_mm512_srli_epi64(_mm512_mul_epu32(a,b),32)会起作用,但事实并非如此——问题似乎是_mm512_mul_epu32只考虑位0…31、64…95等,而忽略奇数位置的值。

如何最快地从32位矢量乘法的结果中提取高32位?

vpmuludq(也称为_mm512_mul_epu32(采用偶数源32位元素(0、2、4等(1。这使得它在每个64位块内高效地执行,将输入的低32位馈送到FP尾数乘法器。这是一个扩展,也就是全乘,而不是高半乘,所以它当然必须忽略一些输入(因为没有SIMD数学指令有两个向量目的地。(

因此,您需要使用它两次来获得所需的所有高半结果:一次用于偶数元素,一次用于位于偶数位置的奇数元素(将两个输入向量右移(。然后,您需要对那些64位元素的高半部分进行交织。

诀窍是有效地做到这一点:AVX-512vpermt2d从2个源向量中挑选32位元素,可以在单个uop中完成任务。所以这很好,尤其是在让编译器提升混洗控制向量常量负载的循环中。其他选项包括_mm512_mask_shuffle_epi32(具有合并掩码的vpshufd(,在k寄存器中给定合并控制的情况下,将高半部分向下复制到1个向量中,并合并到结果的另一个向量中。(其中一个vpmuludq结果具有所需的高半部分,因为输入是右移的(。vmovshdup(_mm512_mask_movehdup_ps(在少1字节的机器代码中执行相同的混洗,不需要立即执行。使用内部函数很不方便,因为您需要使用_mm512_castsi512_ps__m512i强制转换为__m512,但应该具有相同的性能。

甚至存储两次,对第二个存储进行屏蔽,但这可能很糟糕,因为其中一个存储必须错位(因此64字节存储的缓存线交叉(。尽管如此,它确实避免了任何更多的ALU uop。

";明显的";选项(就像您对AVX2所做的那样(将是vpsrld(_mm512_srli_epi64(v,32)(其中之一,然后是vpblendd。但这需要2个单独的ALU uop,并且在当前CPU上使用512位矢量意味着只有2个矢量ALU执行端口可以处理它们。此外,vpblendd没有AVX-512版本;仅存在取CCD_ 21寄存器中的控制操作数的混合。(使用移位/AND和OR合并会更糟,并且仍然需要向量常数(

__m512i mulhi_epu32_512(__m512i a, __m512i b)
{
__m512i evens = _mm512_mul_epu32(a,b);
__m512i odds = _mm512_mul_epu32(_mm512_srli_epi64(a,32), _mm512_srli_epi64(b,32));
return _mm512_mask_shuffle_epi32(odds, 0x5555, evens, _MM_SHUFFLE(3,3,1,1)); 
// _mm512_mask_movehdup_ps may be slightly more efficient, saving 1 byte of code size
}

对于一个独立的函数,clang优化了将屏蔽的shuffle合并到具有来自内存的向量常数的vpermi2d中,而不是mov eax, 0x5555/kmovw k1, eax或其他什么。包含安装程序时会减少uop,但可能会缓存未命中。GCC会按照编写的方式编译它。https://godbolt.org/z/v4M7PK显示两者。对于循环体(挂起设置(,任何一种方式都是单个uop,但合并屏蔽的vpshufd只有1个延迟周期,而车道交叉vpermi2d/vpermt2d只有3个延迟周期。(https://uops.info/和https://agner.org/optimize/)


脚注1:问答;您链接的A要么没有完全描述问题和/或解决方案,要么确实只需要2个数字(在向量的底部?(,而不是2个向量的数字。

最新更新