将低于最高有效位的所有位归零的最有效方法是什么?

时间:2019-02-18 22:48:13

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

因此对于以下序列: 0001000111000

所需的结果将是: 0001000000000

我完全意识到,这可以通过以下方式实现:使用程序集BSRL(或类似的比特混乱hack)找到MSB的索引,然后>>将数字移位(index-1),然后<<移位(index- 1), 但是我想知道,具体来说,是否存在汇编指令或具有更好性能的指令序列,而不是可以做到这一点的琐事。

2 个答案:

答案 0 :(得分:5)

没有单个指令可以执行此操作。 BMI1 blsi dst,src可以隔离最低设置位,而不是最高位。即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<<n的最有效方法。在AMD上2微秒的延迟为2c,因此mov rax,1 / shl rax,cl会更好。但是在Intel上更糟,因为除非使用BMI2 shlx,否则可变计数移位为3 oups。

无论如何,这里的实际工作是BSR + BTS,因此Intel 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提供了计数前导零的内在函数,但是GCC和ICC擅长将63-__builtin_clzll(x)优化回BSR。相反,他们两次否定。有专门用于BSR的 内置程序,但它们不仅是MSVC还是支持GNU扩展(gcc / clang / ICC)的编译器,而不仅仅是MSVC。

#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<<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

答案 1 :(得分:-1)

关于您要问的问题,没有单一的说明。

但是,如果要避免使变量的位发生混乱,则有另一种方法:

声明与原始变量相同类型的第二个变量,并将第二个变量设置为0。然后从最高位到最低位循环遍历原始变量的位,并用&测试每一位操作员。如果找到设置为1的位,则在第二个变量中设置相应的位,然后退出循环。如果需要,将第二个变量分配给原始变量。