创建具有N个最低有效位集的掩码

时间:2018-09-29 23:46:19

标签: c performance bit-manipulation bitmask

我想创建一个宏或函数 1 mask(n),给定一个数字n会返回一个无符号整数,该整数的n最低有效位已设置。尽管这似乎应该是一个基本的原语,并且要进行大量讨论并实现有效编译,但事实并非如此。

当然,对于诸如unsigned int之类的原始整数类型,各种实现可能具有不同的大小,因此,为了具体起见,我们假设正在讨论返回uint64_t,尽管当然是可以接受的解决方案适用于任何无符号整数类型(具有不同的定义)。特别是,当返回的类型等于或小于平台的本机宽度时,该解决方案应该是有效的。

至关重要的是,这必须适用于[0,64]中的所有n。特别是mask(0) == 0mask(64) == (uint64_t)-1。对于这两种情况之一,许多“显而易见的”解决方案都不起作用。

最重要的标准是正确性:只有不依赖未定义行为的正确解决方案才有意义。

第二个最重要的标准是性能:理想情况下,习惯用法应该编译为在通用平台上执行此操作的最有效的特定于平台的方式。

以性能为代价牺牲简单性的解决方案,例如在不同平台上使用不同的实现方案,就可以了。


1 最一般的情况是一个函数,但理想情况下它也可以作为宏运行,而不必多次重新评估其任何参数。

6 个答案:

答案 0 :(得分:5)

另一个没有分支的解决方案

unsigned long long mask(unsigned n)
{
    return ((1ULL << (n & 0x3F)) & -(n != 64)) - 1;
}

n & 0x3F保持最大移位量为63,以避免UB。实际上,大多数现代体系结构只会获取移位量的低位,因此为此不需要 and指令

可以将64的检查条件更改为-(n < 64),以使其返回n⩾64的所有值,这等效于_bzhi_u64(-1ULL, (uint8_t)n)

The output from Clang looks better than gcc。发生这种情况时,gcc会针对MIPS64和ARM64发出条件指令,但不会针对x86-64发出条件指令,从而导致输出更长


如果n = 64,则条件也可以简化为n >> 6。我们可以从结果中减去条件,而不用像上面那样创建掩码

return (1ULL << (n & 0x3F)) - (n == 64) - 1; // n >= 64
return (1ULL << (n & 0x3F)) - (n >> 6) - 1;

gcc将后者编译为

mov     eax, 1
shlx    rax, rax, rdi
shr     edi, 6
dec     rax
sub     rax, rdi
ret

更多替代品

return ~((~0ULL << (n & 0x3F)) << (n == 64));
return ((1ULL << (n & 0x3F)) - 1) | (((uint64_t)n >> 6) << 63);

答案 1 :(得分:4)

这里是便携式且无条件的:

unsigned long long mask(unsigned n)
{
    assert (n <= sizeof(unsigned long long) * CHAR_BIT);
    return (1ULL << (n/2) << (n-(n/2))) - 1;
}

答案 2 :(得分:3)

尝试

unsigned long long mask(const unsigned n)
{
  assert(n <= 64);
  return (n == 64) ? 0xFFFFFFFFFFFFFFFFULL :
     (1ULL << n) - 1ULL;
}

有几个很好的,聪明的答案可以避免使用条件,但是现代的编译器可以为此生成不会分支的代码。

您的编译器可能会发现可以内联,但是您可以使用inline或在C ++中使用constexpr来提示。

保证unsigned long long int类型至少为64位宽,并且在每种实现中都存在,而uint64_t不是。

如果您需要一个宏(因为您需要一个可以用作编译时常量的东西),则可能是:

#define mask(n) ((64U == (n)) ? 0xFFFFFFFFFFFFFFFFULL : (1ULL << (unsigned)(n)) - 1ULL)

正如几个人在评论中正确提醒我的那样,1ULL << 64U是潜在的不确定行为!因此,请为该特殊情况插入支票。

如果重要的是要在大于64位的实现上支持该类型的全部范围,则可以用64U替换CHAR_BITS*sizeof(unsigned long long)

您可以类似地从无符号右移生成此内容,但是由于特殊情况,由于类型的右移是未定义的行为,因此您仍然需要检查n == 64

ETA:

The relevant portion of the (N1570 Draft) standard说到左右位移:

  

如果右操作数的值为负或大于或等于提升的左操作数的宽度,则行为不确定。

这使我绊倒了。再次感谢所有评论我的代码并向我指出错误的人。

答案 3 :(得分:2)

这不是确切问题的答案。仅当0不是必需的输出时才有效,但是效率更高。

2 n + 1 -1计算无溢出。即设置了n低位的整数,n = 0 .. all_bits

可能在cmov的三元数中使用它可能是解决问题中整个问题的更有效解决方案。可能是基于a left-rotate个具有最高有效位的数字,而不是1的左移,以解决此问题与pow2的问题的计数差异计算。

// defined for n=0 .. sizeof(unsigned long long)*CHAR_BIT
unsigned long long setbits_upto(unsigned n) {
    unsigned long long pow2 = 1ULL << n;
    return pow2*2 - 1;                  // one more shift, and subtract 1.
}

编译器输出建议使用备用版本,如果您不使用gcc / clang(已经做到这一点),则适用于某些ISA:进行额外的移位计数,以便初始移位可以移出所有位,将0 - 1 =的所有位都保留下来。

unsigned long long setbits_upto2(unsigned n) {
    unsigned long long pow2 = 2ULL << n;      // bake in the extra shift count
    return pow2 - 1;
}

此功能的32位版本的输入/输出表为:

 n   ->  1<<n        ->    *2 - 1
0    ->    1         ->   1        = 2 - 1
1    ->    2         ->   3        = 4 - 1
2    ->    4         ->   7        = 8 - 1
3    ->    8         ->  15        = 16 - 1
...
30   ->  0x40000000  ->  0x7FFFFFFF  = 0x80000000 - 1
31   ->  0x80000000  ->  0xFFFFFFFF  = 0 - 1

您可以在cmov后面打一巴掌,或通过其他方式处理必须产生零的输入。


在x86上,我们可以efficiently compute this with 3 single-uop instructions :(对于Ryzen上的BTS为2微秒)。

xor  eax, eax
bts  rax, rdi               ; rax = 1<<(n&63)
lea  rax, [rax + rax - 1]   ; one more left shift, and subtract

(三分量LEA在Intel上具有3个周期的延迟,但是我认为这对于uop计数和吞吐量在许多情况下是最佳的。)


在C中,它可以很好地针对除x86 Intel SnB系列之外的所有64位ISA进行编译

不幸的是,即使针对不带BMI2(其中bts为3 uops)的Intel CPU进行调优,C编译器也无法使用shl reg,cl傻瓜和错过。

例如gcc和clang都这样做(用dec或加-1),on Godbolt

# gcc9.1 -O3 -mtune=haswell
setbits_upto(unsigned int):
    mov     ecx, edi
    mov     eax, 2       ; bake in the extra shift by 1.
    sal     rax, cl
    dec     rax
    ret

由于Windows x64的调用约定,MSVC在ECX中以n开头,但是以模为单位,它和ICC做相同的事情:

# ICC19
setbits_upto(unsigned int):
    mov       eax, 1                                        #3.21
    mov       ecx, edi                                      #2.39
    shl       rax, cl                                       #2.39
    lea       rax, QWORD PTR [-1+rax+rax]                   #3.21
    ret                                                     #3.21

使用BMI2(-march=haswell),我们可以使用-march=haswell从gcc / clang获得适用于AMD的代码

    mov     eax, 2
    shlx    rax, rax, rdi
    add     rax, -1

ICC仍使用3分量LEA,因此,如果您以MSVC或ICC为目标,则无论是否启用BMI2都在源中使用2ULL << n版本,因为您都无法获得BTS。这避免了两个世界的最坏情况。慢LEA和可变计数移位而不是BTS。


在非x86 ISA(大概是可变计数移位是有效的)上,因为它们没有x86税制,如果计数恰好为零,则不更改标志,并且可以使用任何寄存器作为计数),这样编译就可以了。

例如AArch64。当然,这可以提升常数2以便在不同的n上重用,就像x86可以与BMI2 shlx一起使用。

setbits_upto(unsigned int):
    mov     x1, 2
    lsl     x0, x1, x0
    sub     x0, x0, #1
    ret

在PowerPC,RISC-V等上基本相同。

答案 4 :(得分:1)

#include <stdint.h>

uint64_t mask_n_bits(const unsigned n){
  uint64_t ret = n < 64;
  ret <<= n&63; //the &63 is typically optimized away
  ret -= 1;
  return ret;
}

结果:

mask_n_bits:
    xor     eax, eax
    cmp     edi, 63
    setbe   al
    shlx    rax, rax, rdi
    dec     rax
    ret

返回预期结果,如果传递了恒定值,它将被优化为clang和gcc以及-O2(而不是-Os)处icc的恒定掩码。

说明:

&63进行了优化,但确保偏移为<= 64。

对于小于64的值,仅使用(1<<n)-1设置前n位。 1<<n设置第n位(等效pow(2,n)),然后从2的幂中减去1会设置所有小于该位的位。

通过使用条件将初始的1设置为移位,不会创建任何分支,但对于所有值== 64的值,您都会得到0,因为向左移位0始终会产生0。因此,当我们减去1时,获取所有设置为64或更大的值的位(因为2表示-1的补码表示法)。

注意事项:

  • 补码系统必须死掉-如果有补码系统,则需要特殊的套管
  • 某些编译器可能无法优化&63远

答案 5 :(得分:0)

当输入N在1到64之间时,我们可以使用-uint64_t(1) >> (64-N & 63)
常数-1有64个置位,我们将其中的64-N移开了,所以剩下N个置位。

当N = 0时,我们可以在移位之前使常数为零:

uint64_t mask(unsigned N)
{
    return -uint64_t(N != 0) >> (64-N & 63);
}

这将在x64 clang中编译为五个指令。 neg instruction将进位标志设置为N != 0,而sbb instruction将进位标志变为0或-1。移位长度64-N & 63已优化为-Nshr instruction已具有隐式shift_length & 63

mov rcx,rdi
neg rcx
sbb rax,rax
shr rax,cl
ret

有了BMI2扩展名,它只有4条指令(移位长度可以留在 rdi 中):

neg edi
sbb rax,rax
shrx rax,rax,rdi
ret