qdo*_*dot 5 bit-manipulation intrinsics integer-arithmetic
有一个相对知名的技巧可以取消设置最右边的一个位:
y = x & (x - 1) // 0b001011100 & 0b001011011 = 0b001011000 :)
Run Code Online (Sandbox Code Playgroud)
我发现自己有一个紧密的循环来清除最右边的 n 位,但是有更简单的代数技巧吗?
假设 n 相对较大(对于 64 位整数,n 必须小于 64,但通常约为 20-30)。
// x = 0b001011100 n=2
for (auto i=0; i<n; i++) x &= x - 1;
// x = 0b001010000
Run Code Online (Sandbox Code Playgroud)
我翻阅了 TAOCP Vol4 几次,但找不到任何灵感。
也许有一些硬件支持?
适用于具有 BMI2 的 Intel x86 CPU,pext并且pdep速度很快。 Zen3 之前的 AMD 的微编码 PEXT/PDEP ( https://uops.info/ )非常慢,所以要小心这一点;其他选项在 AMD 上可能会更快,甚至可能在循环中,或者更好的是在 popcount 上进行二进制搜索(见下文)。
只有 Intel 拥有专用的硬件执行单元,用于 pext/pdep 执行的掩码控制打包/解包,使其成为恒定时间:1 uop,3 个周期延迟,只能在端口 1 上运行。blsi
我不知道其他 ISA 具有类似的位打包硬件操作。
(对于简单得多的低位问题n,x & (-1ULL<<n)如果您不需要处理 n=64。否则可能bzhi反对-1/ andn。)
pdep基本:pdep(-1ULL, a) == a。从第一个操作数中取出低 popcnt(a) 位,并将它们存放在a已设置位的位置,将a再次返回。
但是,如果您的位源不是全 1,而是清除了低 N 位,则前 N 个设置位a将获取 0 而不是 1。这正是您想要的。
uint64_t unset_first_n_bits_bmi2(uint64_t a, int n){
return _pdep_u64(-1ULL << n, a);
}
Run Code Online (Sandbox Code Playgroud)
-1ULL << n适用于 C 中的 n=0..63。 x86 asm 标量移位指令屏蔽了它们的计数(有效&63),因此这可能是较大 .x86 asm 标量移位指令的 C 未定义行为会发生的情况n。如果您关心,请n&63在源代码中使用,以便在 C 中明确定义行为,并且它仍然可以编译为直接使用计数的移位指令。
在 Godbolt 上有一个简单的循环参考实现,表明它们对于样本输入a和产生相同的结果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
Run Code Online (Sandbox Code Playgroud)
(SHLX 是单微指令,1 个周期延迟,与更新 FLAGS 的传统变量计数移位不同...除非 CL=0。uops.info 报告 Alder Lake 有 3 个周期延迟shlx,但这没有多大意义,与 InstLatx64 测量不一致,所以我很确定 SHLX/SHRX 延迟仍然是 1 个周期。)
因此,从a-> 输出(仅 pdep)有 3 个周期延迟
,从n-> 输出(shlx、pdep)有 4 个周期延迟。
对于前端来说只有 3 uop。
一个半相关的 BMI2 技巧:
pext(a,a)将把这些位打包在底部,就像(1ULL<<popcnt(a)) - 1设置所有位一样但不会溢出。
使用 AND 掩码清除该值的低 N 位,然后使用 进行扩展pdep即可。但这是一种过于复杂且昂贵的方式来创建具有足够多的高于 N 个零的位源,而这对 pdep 来说才是真正重要的。感谢@harold 在这个答案的第一个版本中发现了这一点。
@Nate 建议对要清除的低位进行二分搜索,这可能是 pdep 的一个很好的替代方案。
何时停止popcount(x>>c) == popcount(x) - N以找出要清除多少低位,最好使用 的无分支更新c。(例如c = foo ? a : b通常编译为 cmov)。
完成搜索后,x & (-1ULL<<c)使用该计数,或者只是将已有的结果tmp << c移回原来的位置。x>>c直接使用右移比生成新掩码并在每次迭代中使用它更便宜。
高性能 popcount 在现代 CPU 上相对广泛地可用。(虽然不是x86-64 的基准;您仍然需要使用-mpopcnt或进行编译-march=native)。
对此进行调整可能涉及选择一个可能的起点,并且可能使用最大初始步长而不是纯二分搜索。通过尝试一些初始猜测来获得一些指令级并行性可能有助于缩短延迟瓶颈。