对于具有 BMI2 的 Intel x86 CPU,pext and pdep很快。Zen3 之前的 AMD 微编码 PEXT/PDEP 非常慢 (https://uops.info/)所以要小心这一点;其他选项在 AMD 上可能会更快,甚至可能blsi
在循环中,或者更好地对 popcount 进行二分搜索(见下文)。
只有 Intel 拥有专用的硬件执行单元,用于 pext/pdep 执行的掩码控制打包/解包,使其成为恒定时间:1 uop,3 个周期延迟,只能在端口 1 上运行。
我不知道其他 ISA 具有类似的位打包硬件操作。
pdep
basics: pdep(-1ULL, a) == a
。从第一个操作数中取出低 popcnt(a) 位,并将它们存放在a
已设置位,会给你a
再次回来。
但是,如果您的位源不是全一,而是清除了低 N 位,则前 N 位设置为a
将抓取 0 而不是 1。这正是您想要的。
uint64_t unset_first_n_bits_bmi2(uint64_t a, int n){
return _pdep_u64(-1ULL << n, a);
}
-1ULL << n
适用于 C 中的 n=0..63。 x86 asm 标量移位指令屏蔽了它们的计数(有效地&63
), 所以那是probably更大的 C 未定义行为会发生什么n
。如果您关心,请使用n&63
在源代码中,因此行为在 C 中定义良好,并且它仍然可以编译为直接使用计数的移位指令。
关于上帝之锤使用简单的循环参考实现,表明它们对样本输入产生相同的结果a
and n
.
GCC 和 clang 都以显而易见的方式编译它,如下所示:
# GCC10.2 -O3 -march=skylake
unset_first_n_bits_bmi2(unsigned long, int):
mov rax, -1
shlx rax, rax, rsi
pdep rax, rax, rdi
ret
(SHLX 是单微操作,1 个周期延迟,与更新 FLAGS 的传统变量计数移位不同......除非 CL=0)
所以这有 3 个周期延迟a
->输出(只是pdep)
和 4 个周期延迟n
->输出(shlx,pdep)。
对于前端来说只有 3 uop。
一个半相关的 BMI2 技巧:
pext(a,a)
将把这些位打包在底部, like (1ULL<<popcnt(a)) - 1
但如果所有位均已设置,则不会溢出。
用 AND 掩码清除该值的低 N 位,并用pdep
会工作。但这是一种过于复杂且昂贵的方式来创建具有足够多的高于 N 个零的位源,而这对 pdep 来说才是真正重要的。感谢@harold 在这个答案的第一个版本中发现了这一点。
没有快速 PDEP:也许可以通过二分搜索来找到正确的 popcount
@Nate 的建议二分查找要清除多少个低位可能是 pdep 的一个很好的替代品。
停止时popcount(x>>c) == popcount(x) - N
找出要清除的低位,最好使用无分支更新c
。 (例如。c = foo ? a : b
通常编译为 cmov)。
一旦你完成搜索,x & (-1ULL<<c)
使用该计数,或者只是tmp << c
移回x>>c
结果你已经有了。直接使用右移比生成新掩码并在每次迭代中使用它更便宜。
高性能 popcount 在现代 CPU 上相对广泛地可用。 (虽然notx86-64 的基线;你仍然需要编译-mpopcnt
or -march=native
).
对此进行调整可能涉及选择一个可能的起点,并且可能使用最大初始步长而不是纯二分搜索。通过尝试一些初步猜测来获得一些指令级并行性可能有助于缩短延迟瓶颈。