【问题标题】:What is the most efficient way to zero all bits below the most significant set bit?将最高有效位以下的所有位归零的最有效方法是什么?
【发布时间】:2019-07-12 09:09:18
【问题描述】:

所以对于以下序列: 0001000111000

期望的结果是: 0001000000000

我完全知道,这是可以通过使用程序集 BSRL(或类似的位旋转 hack)找到 MSB 的索引然后 >> 将数字移位(索引 - 1),然后

【问题讨论】:

  • 定义'不是一点点'。
  • 我想要你的定义。您需要执行位旋转的指令,但您不想要位旋转。你的问题体现了一个矛盾,
  • 1.) 没有这样的说明。 2.)这肯定会被认为是有点玩弄。
  • 是的,它被称为MOV ...带有一个查找表:->
  • 你真正想做什么?

标签: c++ performance assembly x86 bit-manipulation


【解决方案1】:

没有一条指令可以做到这一点。 BMI1 blsi dst,src 可以隔离 lowest 设置位,而不是最高位。即x & -x。如果 x86 有 blsi 的位反转版本,我们可以使用它,但它没有。


但你可以做得比你建议的要好得多。全零输入始终是位扫描和移位的特殊情况。否则,我们的输出恰好设置了 1 位。我是1 << bsr(input)

;; input: x in RDI
;; output: result in RAX
isolate_msb:
    xor   eax, eax           ; tmp = 0
    bsr   rdi, rdi           ; edi = bit index of MSB in input
    jz    .input_was_zero
    bts   rax, rdi           ; rax |= 1<<edi

.input_was_zero:             ; return 0 for input=0
    ret

显然,对于 32 位输入,仅使用 32 位寄存器。如果不可能为零,则省略 JZ。使用 BSR 代替 LZCNT 给了我们一个位索引,而不是 31-bitidx,所以我们可以直接使用它。但 LZCNT 在 AMD 上的速度明显更快。

异或归零偏离关键路径,为 BTS 准备输入。 xor-zero + BTS 是在 Intel CPU 上实现1&lt;&lt;n 的最有效方式。在 AMD 上它是 2 uop 和 2c 延迟,所以 mov rax,1 / shl rax,cl 会更好。但在 Intel 上更糟,因为变量计数移位是 3 微秒,除非你使用 BMI2 shlx

无论如何,这里真正的工作是 BSR + BTS,因此在英特尔 SnB 系列上是 3 个周期 + 1 个周期的延迟。 (https://agner.org/optimize/)


在 C / C++ 中,你可以这样写

unsigned isolate_msb32(unsigned x) {
    unsigned bitidx = BSR32(x);
    //return 1ULL << bitidx;           // if x is definitely non-zero
    return x ? 1U << bitidx : x;
}

unsigned isolate_msb64(uint64_t x) {
    unsigned bitidx = BSR64(x);
    return x ? 1ULL << bitidx : x;
}

BSR32 是根据编译器支持的内在函数定义的。这就是事情变得棘手的地方,尤其是如果您想要 64 位版本。没有单一的可移植内在函数。 GNU C 提供了 count-leading-zeros 内在函数,但 GCC 和 ICC 将 63-__builtin_clzll(x) 优化回只是 BSR。相反,他们否定了两次。 专门用于 BSR 的内置程序,但这些内置程序甚至比 MSVC 和支持 GNU 扩展 (gcc/clang/ICC) 的编译器更特定于编译器。

#include <stdint.h>

// define BSR32() and BSR64()
#if defined(_MSC_VER) || defined(__INTEL_COMPILER)
    #ifdef __INTEL_COMPILER
        typedef unsigned int bsr_idx_t;
    #else
        #include <intrin.h>   // MSVC
        typedef unsigned long bsr_idx_t;
    #endif

    static inline
    unsigned BSR32(unsigned long x){
        bsr_idx_t idx;
        _BitScanReverse(&idx, x); // ignore bool retval
        return idx;
    }
    static inline
    unsigned BSR64(uint64_t x) {
        bsr_idx_t idx;
        _BitScanReverse64(&idx, x); // ignore bool retval
        return idx;
    }
#elif defined(__GNUC__)

  #ifdef __clang__
    static inline unsigned BSR64(uint64_t x) {
        return 63-__builtin_clzll(x);
      // gcc/ICC can't optimize this back to just BSR, but clang can and doesn't provide alternate intrinsics
    }
  #else
    #define BSR64 __builtin_ia32_bsrdi
  #endif

    #include <x86intrin.h>
    #define BSR32(x) _bit_scan_reverse(x)

#endif

On the Godbolt compiler explorer、clang 和 ICC 无分支编译,即使他们不知道 x 不为零。

所有 4 个编译器都无法使用 bts 来实现 1&lt;&lt;bit。 :( 在 Intel 上非常便宜。

# clang7.0 -O3 -march=ivybridge   (for x86-64 System V)
# with -march=haswell and later it uses lzcnt and has to negate.  /sigh.
isolate_msb32(unsigned int):
        bsr     ecx, edi
        mov     eax, 1
        shl     rax, cl
        test    edi, edi
        cmove   eax, edi       # return 1<<bsr(x)  or  x (0) if x was zero
        ret

GCC 和 MSVC 生成分支代码。例如

# gcc8.2 -O3 -march=haswell
    mov     eax, edi
    test    edi, edi
    je      .L6
    bsr     eax, edi
    mov     edi, 1
    shlx    rax, rdi, rax    # BMI2:  1 uop instead of 3 for shl rax,cl
.L6:
    ret

【讨论】:

  • 32 位版本的 MSVC 将发出 BT* 指令。我从未见过 64 位编译器这样做。你实际上可以得到它here。当您重新添加条件时,仍然使用BTS,但不幸的是分支也是如此。好消息是,看起来 MSVC 团队终于修复了 _BitScanReverse 内在代码生成中的错误。
【解决方案2】:

对于你所问的,没有单一的说明,没有。

但是,如果你想避免扭曲变量的位,还有另一种方法:

声明一个与原始变量相同类型的第二个变量,并将第二个变量设置为0。然后从最高位到最低位循环原始变量的位,使用&amp;运算符测试每个位。如果发现某个位设置为 1,则在第二个变量中设置相应的位,然后退出循环。如果需要,将第二个变量分配给原始变量。

【讨论】:

  • 你为什么建议一个比 OP 在问题中已经建议的(显着)更糟糕的替代方案,使用 bsr 来查找最高设置位? (不过,他们对如何使用它的想法低于标准。)
  • @PeterCordes 因为原始发帖人的 BSR 建议是作为 OP 不希望作为答案的比特旋转 hack 的示例给出的。
  • @RossRidge:我不会将bsr + bts 称为 Hackers Delight 或graphics.stanford.edu/~seander/bithacks.html 中的“小技巧”。我发布了一个有效的答案,它创建了一个设置为 1 位(或 0)的新输出,而不是基于对输入的重复操作。
  • @PeterCordes 我说这只是一个替代方案,我没有说这是一个最佳解决方案。如果 OP 求助于编写手动汇编,它应该尽可能使用尽可能少的指令来完成工作,这很可能涉及到操作位,而这是 OP 试图避免的。
猜你喜欢
  • 1970-01-01
  • 2020-09-07
  • 1970-01-01
  • 2015-03-25
  • 1970-01-01
  • 1970-01-01
  • 2011-05-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多