C++ 将32位整数的向量相乘,只取高32位

C++ 将32位整数的向量相乘,只取高32位,c++,intrinsics,low-level,avx512,C++,Intrinsics,Low Level,Avx512,我想将16个无符号32位整数的两个512位\uuuum512i向量相乘,并仅从64位乘法结果中获取高32位。尽管《英特尔intrinsics指南》中说存在\u mm512\u mulhi\u epu32,但它不会在我的机器上编译 答案声称\u mm512\u srli\u epi64(\u mm512\u mul\u epu32(a,b),32)会起作用,但它不会起作用-问题似乎是\u mm512\u mul\u epu32只考虑位0…31,64…95等,而忽略奇数位置的值 如何最快地从32位向

我想将16个无符号32位整数的两个512位
\uuuum512i
向量相乘,并仅从64位乘法结果中获取高32位。尽管《英特尔intrinsics指南》中说存在
\u mm512\u mulhi\u epu32
,但它不会在我的机器上编译

答案声称
\u mm512\u srli\u epi64(\u mm512\u mul\u epu32(a,b),32)
会起作用,但它不会起作用-问题似乎是
\u mm512\u mul\u epu32
只考虑位0…31,64…95等,而忽略奇数位置的值

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

aka
\u mm512\u mul\u epu32
获取偶数源32位元素(0、2、4等)1。这使它能够在每个64位块内高效执行,将输入的低32位馈送到FP尾数乘法器。这是一个全乘法,而不是高半乘法,因此它当然必须忽略一些输入(因为没有SIMD数学指令有两个向量目标)

因此,您需要使用它两次以获得所需的所有高半部结果:一次使用偶数元素,一次使用偶数位置的奇数元素(将两个输入向量右移)。然后,您需要从这些64位元素中交错高半部

诀窍是有效地做到这一点:AVX-512
vpermt2d
从两个源向量中拾取32位元素,可以在单个uop中完成任务。这很好,特别是在一个循环中,编译器可以提升shuffle控制向量常量的负载。其他选项包括
\u mm512\u mask\u shuffle\u epi32
(带合并掩码)在
k
寄存器中给定合并控件的情况下,在1个向量中向下复制高半部,并合并到另一个结果向量中。(其中一个
vpmuludq
结果在您想要的地方有高半部,因为输入是右移的)。(
\u mm512\u mask\u movehdup\u ps
)在机器代码的1个字节内执行相同的洗牌,无需立即执行。使用内部函数很不方便,因为您需要使用
\u mm512\u castsi512\u ps
\uuuuum512i
强制转换为
\uuuuum512
,但应该具有相同的性能

甚至存储两次,对第二个存储进行屏蔽,但这可能很糟糕,因为其中一个存储必须未对齐(因此64字节存储的缓存线交叉)。不过,它确实避免了更多的ALU UOP

更“明显”的选择(就像您使用AVX2一样)是
vpsrld
\u mm512\u srli\u epi64(v,32)
)其中一个,然后
vpblendd
。但这需要2个单独的ALU UOP,在当前CPU上使用512位向量意味着只有2个向量ALU执行端口可以处理它们。而且,
vpblendd
没有AVX-512版本;只有使用
k
寄存器中的控制操作数的混合。(使用shift/AND/OR进行合并会更加糟糕,并且仍然需要一个向量常量)

对于一个独立函数,clang优化了将屏蔽混洗合并到内存中向量常量的
vpermi2d
,而不是
mov-eax、0x5555
/
kmovw-k1、eax
或其他任何函数。包含安装程序时UOP更少,但可能会缓存未命中。GCC按照编写的方式编译它。两者都有。对于环体(安装已提升),任何一种方式都是单个uop,但合并屏蔽的
vpshufd
只有1个延迟周期,而车道交叉
vpermi2d
/
vpermt2d
只有3个延迟周期。(及)



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

只是澄清一下,
\u mm512\u mulhi\u epi32
存在,但仅适用于骑士角体系结构(Xeon Phi零件系列的一部分)。任何支持AVX512的CPU都不支持它。@JasonR:
vpmuludq
()采用偶数源32位元素(0、2、4等),而不是低半部分。它完全在通道中,实际上在每个64位元素中。(因此可以通过FP尾数乘法器路由整数数据来实现,而无需洗牌)。但是是的,合并两半的想法是正确的。您可以利用带有合并掩蔽的32位随机洗牌来避免单独的
vpor
vpblendd
@PeterCordes您当然是对的。我误读了《内在指南》的内容。我将删除其他注释以避免混淆。或者
vpermt2d
从给定控制向量的2x512位向量中选择元素。然后,您只需要2x
vpsrlq
(移动每个输入的奇数元素)和2x
vpmuludq
来输入
vpermt2d
。这非常有效!我不知道为什么,但是在一些易变的编译器上,你需要用
\u MM\u SHUFFLE(3,3,1,1)
替换
\u MM\u PERM\u DDBB
来编译函数。@Baingcow:什么编译器不能处理英特尔的
\u MM\u SHUFFLE
宏?这已经存在了十多年了,可能要追溯到SSE1,甚至是MMX pshufw intrinsics。当然,如果需要与具有错误的
immintrin.h
的编译器兼容,您可以使用
0xf5
__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
}