AVX2 或更早版本中没有单一指令。 (AVX512可以直接使用位图形式的掩码,and有一条将掩码扩展为向量的指令)。
-
4 位 -> 4 个 qword在 YMM 寄存器中:这个答案:LUT 很好,ALU 也很好
-
8 位 -> 8 个双字在 YMM 寄存器中:这个答案(或这个没有 AVX2 https://stackoverflow.com/questions/48811369/how-to-use-bits-in-a-byte-to-set-dwords-in-ymm-register-without-avx2-inverse-o). ALU.
-
16 位 -> 16 个字:这个答案与
vpbroadcastw
/ vpand
/ vpcmpeqw
-
32 位 -> 32 字节:
如何执行 _mm256_movemask_epi8 (VPMOVMSKB) 的逆运算? https://stackoverflow.com/questions/21622212/how-to-perform-the-inverse-of-mm256-movemask-epi8-vpmovmskb
Also 将 32 位解压缩为 32 字节 SIMD 向量的最快方法 https://stackoverflow.com/questions/24225786/fastest-way-to-unpack-32-bits-to-a-32-byte-simd-vector.
-
8 位 -> 8 个字节或字(无 AVX2):如何使用 x86 SIMD 将 8 位位图高效转换为 0/1 整数数组 https://stackoverflow.com/questions/52098873/how-to-efficiently-convert-an-8-bit-bitmap-to-array-of-0-1-integers-with-x86-sim/52105856#52105856尽管没有 SSSE3 的掩码的 8 位或 16 位广播可能会花费多次洗牌,但相当便宜。
注意使用技巧_mm_min_epu8(v, _mm_set1_epi8(1))
instead of _mm_cmpeq_epi8
得到 0/1而不是 0/FF。
-
16 位 -> 16 个字节(带 SSE2 或 SSSE3),或AVX-512: 将 16 位掩码转换为 16 字节掩码 https://stackoverflow.com/questions/67201469/convert-16-bits-mask-to-16-bytes-mask/67203617#67203617.
(还有 BMI2 为unsigned __int128
、纯 C++ 乘法 bithack 和获取 0/1 而不是 0/-1 的 AVX-512 示例)
-
8 位 -> 8 字节:如果您一次只需要 8 位,标量乘法技巧可能会更好:如何从 8 个布尔值中创建一个字节(反之亦然)? https://stackoverflow.com/questions/8461126/how-to-create-a-byte-out-of-8-bool-values-and-vice-versa/51750902#51750902.
对于您的情况,如果您从内存加载位图,则将其直接加载到 ALU 策略的向量寄存器中应该可以很好地工作,即使对于 4 位掩码也是如此。
如果您将位图作为计算结果,那么它将位于整数寄存器中,您可以轻松地将其用作 LUT 索引,因此如果您的目标是 64 位元素,那么这是一个不错的选择。否则,可能仍然会使用 ALU 来处理 32 位或更小的元素,而不是使用巨大的 LUT 或执行多个块。
在从整数位掩码到矢量掩码的廉价转换成为可能之前,我们必须等待 AVX-512 的掩码寄存器。 (和kmovw k1, r/m16
,编译器隐式生成int => __mmask16
)。有一个 AVX512 insn 用于从掩码设置向量(VPMOVM2D zmm1, k1
, ,以及不同元素大小的其他版本),但您通常不需要它,因为过去使用掩码向量的所有内容现在都使用掩码寄存器。也许如果您想计算满足某些比较条件的元素? (你会在哪里使用pcmpeqd
/ psubd
生成并累加 0 或 -1 个元素的向量)。但标量popcnt
在面具结果上会是一个更好的选择。
但请注意vpmovm2d
要求面罩位于 AVX512 中k0..7
掩码寄存器。获取它需要额外的指令,除非它来自向量比较结果,并且移动到掩码寄存器的指令需要 Intel Skylake-X 和类似 CPU 上端口 5 的微指令,因此这可能是一个瓶颈(特别是如果您进行任何洗牌) )。特别是如果它在内存中启动(加载位图)并且您只需要每个元素的高位,那么即使 256 位和 512 位 AVX512 指令可用,您也可能最好使用广播加载 + 变量移位。
也可能(对于 0/1 结果而不是 0/-1)是来自常量的零屏蔽负载,例如_mm_maskz_mov_epi8(mask16, _mm_set1_epi8(1))
. https://godbolt.org/z/1sM8hY8Tj https://godbolt.org/z/1sM8hY8Tj
对于 64 位元素,掩码只有 4 位,因此查找表是合理的。您可以通过加载来压缩 LUTVPMOVSXBQ ymm1, xmm2/m32. (_mm256_cvtepi8_epi64) http://felixcloutier.com/x86/PMOVSX.html。这将为您提供 (1pmovsx
不方便用作具有内在函数的窄负载.
特别是如果您已经将位图存储在整数寄存器(而不是内存)中,则vpmovsxbq
LUT 在 64 位元素的内部循环中应该表现出色。或者,如果指令吞吐量或洗牌吞吐量是瓶颈,请使用未压缩的 LUT。这可以让您(或编译器)使用掩码向量作为其他内容的内存操作数,而不需要单独的指令来加载它。
32 位元素的 LUT:可能不是最佳的,但您可以这样做
对于 32 位元素,8 位掩码可为您提供 256 个可能的向量,每个向量有 8 个元素长。 256 * 8B = 2048 字节,即使对于压缩版本来说,这也是相当大的缓存占用空间(加载vpmovsxbd ymm, m64
).
要解决此问题,您可以将 LUT 拆分为 4 位块。大约需要 3 条整数指令才能将一个 8 位整数拆分为两个 4 位整数(mov/and/shr
)。然后使用 128b 向量的未压缩 LUT(对于 32 位元素大小),vmovdqa
低半部分和vinserti128
高的一半。您仍然可以压缩 LUT,但我不会推荐它,因为您需要vmovd
/ vpinsrd
/ vpmovsxbd
,这是 2 次洗牌(因此您可能会成为 uop 吞吐量的瓶颈)。
Or 2x vpmovsxbd xmm, [lut + rsi*4]
+ vinserti128
英特尔的情况可能更糟。
ALU 替代方案:适用于 16/32/64 位元素
当整个位图适合每个元素时:广播它,并使用选择器掩码,并针对同一常量(可以在循环中多次使用此常量时保留在寄存器中)。
vpbroadcastd ymm0, dword [mask] ; _mm256_set1_epi32
vpand ymm0, ymm0, setr_epi32(1<<0, 1<<1, 1<<2, 1<<3, ..., 1<<7)
vpcmpeqd ymm0, ymm0, [same constant] ; _mm256_cmpeq_epi32
; ymm0 = (mask & bit) == bit
; where bit = 1<<element_number
掩码可以来自带有 vmovd + vpbroadcastd 的整数寄存器,但是如果广播负载已经在内存中,那么广播负载就很便宜,例如从掩码数组应用于元素数组。我们实际上只关心该双字的低 8 位,因为 8x 32 位元素 = 32 个字节。 (例如,您从vmovmaskps
)。对于 16x 16 位元素的 16 位掩码,您需要vpbroadcastw
。要首先从 16 位整数向量中获取这样的掩码,您可以vpacksswb
两个向量在一起(保留每个元素的符号位),vpermq
将元素按车道内打包后的顺序排列,然后vpmovmskb
.
对于 8 位元素,您需要vpshufb
the vpbroadcastd
结果将相关位放入每个字节中。看如何执行 _mm256_movemask_epi8 (VPMOVMSKB) 的逆运算? https://stackoverflow.com/questions/21622212/how-to-perform-the-inverse-of-mm256-movemask-epi8-vpmovmskb。但对于 16 位及更宽的元素,元素数量
vpbroadcastd/q
甚至不花费任何 ALU 微指令,它是在加载端口中完成的。 (b
and w
是加载+随机播放)。即使您的掩码打包在一起(32 或 64 位元素的每个字节一个),它可能仍然更有效vpbroadcastd
代替vpbroadcastb
. The x & mask == mask
check 不关心广播后每个元素的高字节中的垃圾。唯一担心的是缓存行/页面分割。
如果您只需要符号位,则可变移位(Skylake 更便宜)
变量混合和屏蔽加载/存储仅关心屏蔽元素的符号位。
一旦您将 8 位掩码广播到双字元素,这只是 1 uop(在 Skylake 上)。
vpbroadcastd ymm0, dword [mask]
vpsllvd ymm0, ymm0, [vec of 24, 25, 26, 27, 28, 29, 30, 31] ; high bit of each element = corresponding bit of the mask
;vpsrad ymm0, ymm0, 31 ; broadcast the sign bit of each element to the whole element
;vpsllvd + vpsrad has no advantage over vpand / vpcmpeqb, so don't use this if you need all the bits set.
vpbroadcastd
与从内存加载一样便宜(Intel CPU 和 Ryzen 上根本没有 ALU uop)。 (更窄的广播,例如vpbroadcastb y,mem
在 Intel 上采用 ALU shuffle uop,但在 Ryzen 上可能不行。)
Haswell/Broadwell 上的可变移位稍微昂贵(3 uops,有限的执行端口),但与 Skylake 上的立即计数移位一样便宜! (端口 0 或 1 上有 1 个 uop。)在 Zen 3 之前的 AMD 上,它们不会花费额外的 uop,但速度很慢(3c 延迟和正常移位 uop 吞吐量的 1/4)。在 Zen 1 上,这是特别糟糕的,因为 256 位操作通常以 2 uop 运行。但这并不是一场灾难,特别是如果其他微指令可以在同一端口上使用其他执行单元,同时它们占用额外的周期(如果可能的话,我不知道)。在 Zen 3 及更高版本上,它们的性能与 Skylake 上一样,延迟为 1c,吞吐量为 0.5c。
See the x86 /questions/tagged/x86标记 wiki 以获取性能信息,尤其是Agner Fog 的 insn 表 https://agner.org/optimize/ and https://uops.info/ https://uops.info/.
对于 64 位元素,请注意算术右移仅适用于 16 位和 32 位元素大小。如果您希望将 4 位 -> 64 位元素的整个元素设置为全零/全一,请使用不同的策略。
使用内在函数:
// AVX2, most efficient on Skylake and Zen 3 and later
// if you just need the MSBs set. Otherwise still use and/cmpeq
__m256i bitmap2vecmask(int m) {
const __m256i vshift_count = _mm256_set_epi32(24, 25, 26, 27, 28, 29, 30, 31);
__m256i bcast = _mm256_set1_epi32(m);
__m256i shifted = _mm256_sllv_epi32(bcast, vshift_count); // high bit of each element = corresponding bit of the mask
return shifted;
// use _mm256_and and _mm256_cmpeq if you need all bits set, not two shifts.
// would work but not worth it: return _mm256_srai_epi32(shifted, 31); // broadcast the sign bit to the whole element
}
在循环内部,LUT 可能值得缓存占用空间,具体取决于循环中的指令组合。特别是对于 64 位元素大小,其缓存占用空间不大,但甚至对于 32 位也可能如此。
另一种选择,而不是变量移位,是使用 BMI2 将每个位解压缩为一个字节,并在高位中使用该掩码元素,然后vpmovsx
:
; 8bit mask bitmap in eax, constant in rdi
pdep rax, rax, rdi ; rdi = 0b1000000010000000... repeating
vmovq xmm0, rax
vpmovsxbd ymm0, xmm0 ; each element = 0xffffff80 or 0
; optional
;vpsrad ymm0, ymm0, 8 ; arithmetic shift to get -1 or 0
如果您在整数寄存器中已经有掩码(您必须在其中vmovq
/ vpbroadcastd
无论如何,单独),那么即使在可变计数移位便宜的 Skylake 上,这种方式也可能更好。
如果您的掩码在内存中开始,则另一个 ALU 方法(vpbroadcastd
直接进入向量)可能更好,因为广播负载非常便宜。
注意pdep
Zen 1 和 Zen 2 上有 6 个依赖的 uop(18c 延迟、18c 吞吐量,或更糟,具体取决于位),因此即使您的掩码确实以整数寄存器开始,这种方法在 Ryzen 上也很糟糕。 Zen 3及更高版本有专用pext
/pdep
硬件并像英特尔一样高效地运行它们,作为单个微指令。
(未来的读者,请随意编辑此内容的内在函数版本。编写 asm 更容易,因为输入量少得多,并且 asm 助记符更易于阅读(不傻)_mm256_
到处乱七八糟)。)