我想将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个向量的数字。