从四个 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
.
从四个 __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
.