【问题标题】:How to unset N right-most set bits如何取消设置 N 最右边的设置位
【发布时间】:2021-04-25 06:50:49
【问题描述】:

有一个相对著名的技巧可以取消设置最右边的单个位:

y = x & (x - 1) // 0b001011100 & 0b001011011 = 0b001011000 :)

我发现自己有一个紧密的循环来清除最右边的 n 位,但是有没有更简单的代数技巧?

假设 n 相对较大(对于 64 位整数,n 必须

// x = 0b001011100 n=2
for (auto i=0; i<n; i++) x &= x - 1;
// x = 0b001010000

我翻阅了我的 TAOCP Vol4 几次,但找不到任何灵感。

也许有一些硬件支持?

【问题讨论】:

  • 您关心任何特定的 ISA 以获得硬件支持?我认为 x86 pext / pdep 可以使设置的位连续,以允许使用 AND 清除它们。
  • 有趣 - 我简单地查看了pext/pdep,但看来我需要提前计算掩码,对吧?我不能保证输入变量中的 n 位是连续的。
  • 我还没有测试过这个,但我认为pext(a,a) 会在底部打包位:选择哪些位的所需掩码输入数字,因为你想要所有的设置位而不是清除位。
  • 随机想法:屏蔽掉一些任意数量的位(比如(64+n)/2),使用popcount 来查看您清除了多少位,然后进行二分查找直到正确为止。最多应该进行 6 次迭代,但不可预测的分支可能会成为杀手,除非有聪明的无分支方法。
  • 在支持popcount 但不支持pdep 的硬件上,这是个好主意。我对@PeterCordes 的回答感到非常高兴(我自己)——它有效,而且一旦我阅读了 BMI2 手册,我实际上能够优化更多的地方;我仍然很好奇是否有办法在更受限制的硬件上加速它!在此期间,我会在几天内接受它。让我们征集非 BMI2 的替代品!

标签: bit-manipulation intrinsics integer-arithmetic


【解决方案1】:

对于具有 BMI2 的 Intel x86 CPU,pextpdep 速度很快。 Zen3 之前的 AMD 微编码 PEXT/PDEP 非常慢 (https://uops.info/) 所以要小心这个;其他选项在 AMD 上可能更快,甚至可能在循环中使用 blsi,或者更好地对 popcount 进行二进制搜索(见下文)。
只有 Intel 为 pext/pdep 所做的掩码控制打包/解包提供专用硬件执行单元,使其具有恒定时间:1 uop,3 个周期延迟,只能在端口 1 上运行。

我不知道其他 ISA 具有类似的位打包硬件操作。


pdep 基础知识pdep(-1ULL, a) == a。从第一个操作数中取出低 popcnt(a) 位,并将它们存放在 a 已设置位的位置,将再次返回 a

但是,如果您的位源清除了低 N 位,而不是全 1,则 a 中的前 N ​​个设置位将获取 0 而不是 1。这正是您想要的。

uint64_t unset_first_n_bits_bmi2(uint64_t a, int n){
    return _pdep_u64(-1ULL << n, a);
}

-1ULL &lt;&lt; n 在 C 中适用于 n=0..63。x86 asm 标量移位指令掩盖了它们的计数(实际上是 &amp;63),所以 可能对于 C 未定义-较大的n 的行为。如果您愿意,请在源代码中使用n&amp;63,这样该行为在 C 中定义良好,并且仍然可以编译为直接使用计数的移位指令。

On Godbolt 带有一个简单的循环参考实现,表明它们对样本输入 an 产生相同的结果。

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 是单 uop,1 个周期延迟,与更新 FLAGS 的传统可变计数移位不同...除非 CL=0)

所以这从a->输出有 3 个周期延迟(只是 pdep)
以及来自n->输出(shlx、pdep)的 4 个周期延迟。

而且前端只有 3 微秒。


一个半相关的 BMI2 技巧:

pext(a,a) 将打包底部的位,与(1ULL&lt;&lt;popcnt(a)) - 1 类似,但如果所有位都已设置,则不会溢出。

使用 AND 掩码清除其低 N 位,并使用 pdep 扩展将起作用。但这是一种过于复杂且昂贵的方式来创建具有足够多的大于 N 个零的位源,这对 pdep 来说实际上很重要。感谢@harold 在此答案的第一个版本中发现了这一点。


没有快速 PDEP:可能对正确的 popcount 进行二分搜索

@Nate 建议二进制搜索要清除多少低位可能是 pdep 的一个很好的替代方案。

popcount(x&gt;&gt;c) == popcount(x) - N 时停止以找出要清除多少低位,最好使用c 的无分支更新。 (例如c = foo ? a : b 经常编译为 cmov)。

完成搜索后,x &amp; (-1ULL&lt;&lt;c) 会使用该计数,或者只使用tmp &lt;&lt; c 将您已有的x&gt;&gt;c 结果移回。直接使用右移比生成一个新的掩码并在每次迭代中都使用它更便宜。

高性能 popcount 在现代 CPU 上相对广泛可用。 (虽然 不是 x86-64 的基线;您仍然需要使用 -mpopcnt-march=native 进行编译。

调整这可能涉及选择一个可能的起点,并且可能使用最大初始步长而不是纯二进制搜索。从尝试一些初步猜测中获得一些指令级并行性可能有助于缩短延迟瓶颈。

【讨论】:

  • 这不能用pdep(-1ULL &lt;&lt; n, a)来完成吗?
  • @harold:已更新,谢谢。我正在考虑像 AVX512 vpcompressd 这样的问题,其中每个输入元素都有一个标识,但 1 位只是 1 位,并且不必来自原始输入。
猜你喜欢
  • 2011-06-09
  • 2021-01-08
  • 2015-10-02
  • 1970-01-01
  • 2014-05-11
  • 1970-01-01
  • 1970-01-01
  • 2016-02-05
  • 1970-01-01
相关资源
最近更新 更多