首页 > 解决方案 > 乘以 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 位?

标签: c++intrinsicslow-levelavx512

解决方案


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

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

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

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

更“明显”的选项(就像您对 AVX2 所做的那样)是vpsrld( _mm512_srli_epi64(v,32)) 其中一个,然后是vpblendd. 但这需要 2 个单独的 ALU 微指令,并且在当前 CPU 上使用 512 位向量意味着只有 2 个向量 ALU 执行端口可以处理它们。此外,vpblendd没有 AVX-512 版本;只有混合将控制操作数放在k寄存器中。(使用 shift / 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带有来自内存的向量常量的 a,而不是mov eax, 0x5555/kmovw k1, eax或其他。包含设置时的微指令更少,但可能缓存未命中。GCC 将其编译为书面形式。 https://godbolt.org/z/v4M7PK显示两者。对于循环体(已提升设置),任何一种方式都是单个 uop,但合并屏蔽vpshufd只有 1 个延迟周期,而车道交叉vpermi2d/则为 3 个vpermt2d。(https://uops.info/https://agner.org/optimize/


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


推荐阅读