从四个 16 位的 1

问题描述

从四个 __mmask64 中得到 __mmask16 的最佳方式是什么?我只想连接它们。似乎无法在互联网上找到解决方案。

解决方法

AVX-512 具有用于连接两个掩码寄存器的硬件指令,例如 2x kunpckwd instructions 和一个 kunpckdq 在这里可以解决问题。

(每条指令有4个周期的延迟,只有端口5,在SKX和Ice Lake上。https://uops.info。但至少第一步中的2个独立的可以大部分重叠,分开一个周期开始,受竞争限制用于端口 5。但无论如何它们都不会立即准备好,如果编译器调度生成 4 个掩码的指令,那么一对应该首先准备好,以便它可以开始。)

// compiles nicely with GCC/clang/ICC.  Current MSVC has major pessimizations
inline
__mmask64 set_mask64_kunpck(__mmask16 m0,__mmask16 m1,__mmask16 m2,__mmask16 m3)
{
    __mmask32 md0 = _mm512_kunpackw(m1,m0);  // hi,lo
    __mmask32 md1 = _mm512_kunpackw(m3,m2);
    __mmask64 mq = _mm512_kunpackd(md1,md0);
    return mq;
}

如果您的 __mask16 值实际上位于 k 寄存器中,那么这是您最好的选择,如果它们是 AVX-512 比较/测试内在函数(如 {{1} }.如果它们来自您之前生成的数组,最好将它们与纯标量内容结合起来(参见 Paul 的回答),而不是用 _mm512_cmple_epu32_mask 慢慢地将它们放入掩码寄存器中。 kmov 是前端的 3 uop,带有标量整数加载和 kmov k,mem 后端 uops,加上一个额外的前端 uop,没有明显原因。


kmov k,reg 只是 __mmask16 的 typedef(在 gcc/clang/ICC/MSVC 中),因此您可以像整数一样简单地操作它,编译器将使用 { {1}} 根据需要。 (如果您不小心,这可能会导致代码效率很低,不幸的是,当前的编译器不够聪明,无法将移位/或函数编译为使用 unsigned short。)

内在函数,例如 kmov,但对于将 kunpckwd 实现为 unsigned int _cvtmask16_u32 (__mmask16 a) 的当前编译器来说,它们是可选的。


要在 __mmask16 值在 unsigned short 寄存器中开始的情况下查看编译器输出,有必要编写一个使用内部函数创建掩码值的测试函数。 (或使用内联 asm 约束。)标准的 x86-64 调用约定将 __mmask16 作为标量整数处理,因此作为函数 arg 它已经在整数寄存器中,而不是 k 寄存器中。

__mmask16

使用 GCC 和 clang,编译为 (Godbolt):

k

例如,我本可以将最终掩码结果用于两个输入之间的混合内在函数,但是编译器并没有尝试通过执行 4x __mmask64 test(__m256i v0,__m256i v1,__m256i v2,__m256i v3) { __mmask16 m0 = _mm256_movepi16_mask(v0); // clang can optimize _mm_movepi8_mask into pmovmskb eax,xmm avoiding k regs __mmask16 m1 = _mm256_movepi16_mask(v1); __mmask16 m2 = _mm256_movepi16_mask(v2); __mmask16 m3 = _mm256_movepi16_mask(v3); //return set_mask64_mmx(m0,m1,m2,m3); //return set_mask64_scalar(m0,m3); return set_mask64_kunpck(m0,m3); } (也只有 1端口)。

MSVC 19.29 -O2 -Gv -arch:AVX512 做得很差,将每个掩码提取到内部函数之间的标量整数 regs。喜欢

# gcc 11.1  -O3 -march=skylake-avx512
test(long long __vector(4),long long __vector(4),long long __vector(4)):
        vpmovw2m        k3,ymm0
        vpmovw2m        k1,ymm1
        vpmovw2m        k2,ymm2
        vpmovw2m        k0,ymm3     # create masks

        kunpckwd        k1,k1,k3
        kunpckwd        k0,k0,k2
        kunpckdq        k4,k1   # combine masks

        kmovq   rax,k4              # use mask,in this case by returning as integer
        ret

这非常愚蠢,甚至没有使用 kunpck 将零扩展到 32 位寄存器,更不用说没有意识到下一个 kmov 无论如何只关心其输入的低部分,因此根本不需要将数据移入/移出整数寄存器。后来,它甚至使用了这个,显然没有意识到 MSVC 19.29 kmovw ax,k1 movzx edx,ax ... kmovd k3,edx 写入 32 位寄存器零扩展到 64 位寄存器。 (公平地说,GCC 在其 kmovw eax,k1 内在函数周围有一些愚蠢的遗漏优化。)

kunpck

kmovd 内在函数确实有奇怪的原型,输入与输出一样宽,例如

__builtin_popcount

所以这也许是欺骗 MSVC 通过去标量和返回手动进行 ; MSVC 19.29 kmovd ecx,k2 mov ecx,ecx kmovq k1,rcx -> kunpck 转换,因为它显然不知道 __mmask32 _mm512_kunpackw (__mmask32 a,__mmask32 b) 已经零扩展进入完整的uint16_t

,

您可以将 __mmask16__mmask64 视为 16 位和 64 位整数,例如

__mmask64 set_mask64(__mmask16 m0,__mmask16 m3)
{
    return (((__mmask64)m0) << 0)
         | (((__mmask64)m1) << 16)
         | (((__mmask64)m2) << 32)
         | (((__mmask64)m3) << 48);
}

或者也许:

__mmask64 set_mask64(__mmask16 m0,__mmask16 m3)
{
    return (__mmask64)_mm_set_pi16(m0,m3);
}

以上都使用标量/SSE 代码。使用 AVX512 掩码内在函数会更高效(请参阅 @Peter's answer 以获得更好的解决方案)。