从四个 16 位的构造一个 64 位掩码寄存器

Construct a 64 bit mask register from four 16 bit ones

从四个 __mmask16 中得到一个 __mmask64 的最佳方法是什么?我只想连接它们。网上好像找不到解决方法。

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

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

或者也许:

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

以上均使用scalar/SSE代码。使用 AVX512 掩码内在函数会更有效(请参阅 以获得更好的解决方案)。

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 compare/test 内在函数的结果,编译器将拥有它们_mm512_cmple_epu32_mask。如果它们来自您之前生成的数组,最好将它们与纯标量组合(参见 Paul 的回答),而不是慢慢地将它们放入 kmov 的掩码寄存器中。 kmov k, mem 是前端的 3 微指令,具有标量整数负载和一个 kmov k, reg 后端微指令,加上一个没有明显原因的额外前端微指令。


__mmask16 只是 unsigned short(在 gcc/clang/ICC/MSVC 中)的类型定义,因此您 可以 像整数一样简单地操作它,编译器将根据需要使用 kmov。 (如果您不小心,这可能会导致代码效率很低,不幸的是,当前的编译器不够智能,无法将 shift/OR 函数编译为使用 kunpckwd。)

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


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

__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,m1,m2,m3);
    return set_mask64_kunpck(m0,m1,m2,m3);
}

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

# gcc 11.1  -O3 -march=skylake-avx512
test(long long __vector(4), 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, k0, k1   # combine masks

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

例如,我本可以将最终掩码结果用于两个输入之间的混合内在函数,但是编译器并没有尝试通过执行 4x kmov(也只有 1 个端口)。

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

MSVC 19.29
        kmovw   ax, k1
        movzx   edx, ax
        ...
        kmovd   k3, edx

这太蠢了,甚至没有使用 kmovw eax, k1 零扩展到 32 位寄存器,更不用说没有意识到下一个 kunpck 只关心它的低位部分无论如何输入,所以根本不需要 kmov 数据 to/from 一个整数寄存器。后来竟然用这个,显然没有意识到kmovd写32位寄存器零扩展到64位寄存器。 (公平地说,GCC 在其 __builtin_popcount 内在优化方面有一些愚蠢的遗漏优化。)

; MSVC 19.29
        kmovd   ecx, k2
        mov     ecx, ecx
        kmovq   k1, rcx

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

__mmask32 _mm512_kunpackw (__mmask32 a, __mmask32 b)

所以这可能是在欺骗 MSVC 通过标量来回手动执行 uint16_t -> uint32_t 转换,因为它显然不知道 vpmovw2m k3, ymm0 已经为零-扩展到完整 k3.