如果您要一次迭代一个位隔离掩码,则一次生成一个掩码是有效的;请参阅@harold 的回答。
但如果你真的只想要所有的掩码,x86 和 AVX512F 可以有效地并行化这个。(至少可能有用,具体取决于周围的代码。更有可能这只是应用 AVX512 和对大多数用例没有用处)。
关键的构建块是AVX512F vpcompressd:给定一个掩码(例如来自 SIMD 比较),它会将选定的 dword 元素打乱为向量底部的连续元素。
一个 AVX512 ZMM / __m512i 向量保存 16 个 32 位整数,因此我们只需要 2 个向量来保存每个可能的单位掩码。 我们的输入数字是一个掩码,它选择哪些元素应该成为输出的一部分。(无需将其广播到向量和vptestmd 或类似的东西中; 我们可以把kmov 放到一个掩码寄存器中直接使用。)
另见我在AVX2 what is the most efficient way to pack left based on a mask? 上的 AVX512 答案
#include <stdint.h>
#include <immintrin.h>
// suggest 64-byte alignment for out_array
// returns count of set bits = length stored
unsigned bit_isolate_avx512(uint32_t out_array[32], uint32_t x)
{
const __m512i bitmasks_lo = _mm512_set_epi32(
1UL << 15, 1UL << 14, 1UL << 13, 1UL << 12,
1UL << 11, 1UL << 10, 1UL << 9, 1UL << 8,
1UL << 7, 1UL << 6, 1UL << 5, 1UL << 4,
1UL << 3, 1UL << 2, 1UL << 1, 1UL << 0
);
const __m512i bitmasks_hi = _mm512_slli_epi32(bitmasks_lo, 16); // compilers actually do constprop and load another 64-byte constant, but this is more readable in the source.
__mmask16 set_lo = x;
__mmask16 set_hi = x>>16;
int count_lo = _mm_popcnt_u32(set_lo); // doesn't actually cost a kmov, __mask16 is really just uint16_t
_mm512_mask_compressstoreu_epi32(out_array, set_lo, bitmasks_lo);
_mm512_mask_compressstoreu_epi32(out_array+count_lo, set_hi, bitmasks_hi);
return _mm_popcnt_u32(x);
}
使用 clang on Godbolt 和 gcc 可以很好地编译,而不是使用 mov、movzx 和 popcnt 的几个次优选择,并且无缘无故地制作一个帧指针。 (它也可以用-march=knl编译;它不依赖于AVX512BW或DQ。)
# clang9.0 -O3 -march=skylake-avx512
bit_isolate_avx512(unsigned int*, unsigned int):
movzx ecx, si
popcnt eax, esi
shr esi, 16
popcnt edx, ecx
kmovd k1, ecx
vmovdqa64 zmm0, zmmword ptr [rip + .LCPI0_0] # zmm0 = [1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768]
vpcompressd zmmword ptr [rdi] {k1}, zmm0
kmovd k1, esi
vmovdqa64 zmm0, zmmword ptr [rip + .LCPI0_1] # zmm0 = [65536,131072,262144,524288,1048576,2097152,4194304,8388608,16777216,33554432,67108864,134217728,268435456,536870912,1073741824,2147483648]
vpcompressd zmmword ptr [rdi + 4*rdx] {k1}, zmm0
vzeroupper
ret
在 Skylake-AVX512 上,vpcompressd zmm{k1}, zmm 是端口 5 的 2 微秒。输入向量 -> 输出的延迟为 3 个周期,但输入掩码 -> 输出的延迟为 6 个周期。 (https://www.uops.info/table.html/https://www.uops.info/html-instr/VPCOMPRESSD_ZMM_K_ZMM.html)。内存目标版本is 4 uops: 2p5 + 通常的存储地址和存储数据微指令,它们在较大指令的一部分时不能微熔。
最好压缩成 ZMM reg 然后存储,至少在第一次压缩时,以节省总 uops。第二个可能仍应利用 vpcompressd [mem]{k1} 的屏蔽存储功能,因此输出数组不需要填充即可踩到。 IDK 如果这有助于缓存行拆分,即掩码是否可以避免在第二个缓存行中为具有全零掩码的部分重放存储 uop。
在 KNL 上,vpcompressd zmm{k1} 只是一个微指令。 Agner Fog 没有使用内存目标 (https://agner.org/optimize/) 对其进行测试。
这是 Skylake-X 前端的 14 个融合域微指令,用于实际工作(例如,在内联到多个 x 值的循环后,因此我们可以将 vmovdqa64 负载提升出循环. 否则那是另外 2 微秒)。所以前端瓶颈 = 14 / 4 = 3.5 个周期。
后端端口压力:端口 5 6 uop(2x kmov(1) + 2x vpcompressd(2)):每 6 个周期 1 次迭代。 (即使在 IceLake (instlatx64) 上,vpcompressd 仍然是 2c 吞吐量,不幸的是,显然 ICL 的额外 shuffle 端口不能处理这些微指令。kmovw k, r32 仍然是 1/clock,所以大概仍然是端口 5也是。)
(其他端口都很好:popcnt 在端口 1 上运行,当 512 位微指令在运行时,该端口的向量 ALU 被关闭。但它的标量 ALU 不是,它是唯一一个处理 3 周期延迟整数指令的。 movzx dword, word 无法消除,只有 movzx dword, byte 可以做到,但它可以在任何端口上运行。)
延迟:整数结果只是一个popcnt(3 个周期)。记忆结果的第一部分在掩码准备好后大约 7 个周期存储。 (kmov -> vpcompressd)。 vpcompressd 的向量源是一个常量,因此 OoO exec 可以尽早准备好它,除非它在缓存中丢失。
通过移位构建 1<<0..15 常量是可能的,但可能不值得。例如使用vpmovzxbd 加载16 字节_mm_setr_epi8(0..15),然后在set1(1) 的向量上使用vpsllvd(您可以从广播中获取或使用vpternlogd+shift 即时生成)。但这可能不值得,即使你用 asm 手工编写(所以它是你的选择,而不是编译器),因为这已经使用了很多 shuffle,并且常量生成至少需要 3 或 4 条指令(每个至少有 6 个字节长;仅 EVEX 前缀每个就是 4 个字节)。
不过,我会生成 hi 部分,并从 lo 转移,而不是单独加载它。除非周围的代码在端口 0 上遇到严重瓶颈,否则 ALU uop 并不比加载 uop 差。一个 64 字节的常量填满了整个缓存行。
您可以使用vpmovzxwd 负载压缩 lo 常量:每个元素适合 16 位。值得考虑是否可以将其提升到循环之外,这样每次操作就不会花费额外的随机播放。
如果您希望将结果存储在 SIMD 向量中而不是存储到内存中,您可以将 2x vpcompressd 放入寄存器中,并且可以使用 count_lo 来查找 vpermt2d 的随机播放控制向量。可能来自数组上的滑动窗口而不是 16x 64 字节向量?但结果不能保证适合一个向量,除非您知道您的输入设置了 16 位或更少的位。
64 位整数的情况更糟 8x 64 位元素意味着我们需要 8 个向量。因此,与标量相比,它可能不值得,除非您的输入设置了很多位。
不过,您可以在循环中执行此操作,使用 vpslld by 8 来移动向量元素中的位。你会认为kshiftrq 会很好,但是有 4 个周期的延迟,这是一个很长的循环承载的 dep 链。无论如何,您都需要每个 8 位块的标量 popcnt 来调整指针。所以你的循环应该使用shr/kmov和movzx/popcnt。 (使用计数器 += 8 和 bzhi 来喂 popcnt 会花费更多的微指令)。
循环携带的依赖项都很短(并且循环只运行 8 次迭代以覆盖 64 位掩码),因此乱序 exec 应该能够很好地重叠工作以进行多次迭代。特别是如果我们展开 2,那么向量和掩码依赖项可以在指针更新之前。
- 向量:
vpslld 立即数,从向量常数开始
- 掩码:
shr r64, 8 以x 开头。 (在移出所有位后,当它变为 0 时可能会停止循环。这个 1 周期的 dep 链足够短,OoO exec 可以在它发生时快速穿过它并隐藏大部分的错误预测惩罚。)
- 指针:
lea rdi, [rdi + rax*4],其中 RAX 保存 popcnt 结果。
其余的工作在迭代中都是独立的。根据周围的代码,我们可能会在端口 5 上出现瓶颈,vpcompressd shuffles 和 kmov