有没有一种优雅而又快速的方法来测试整数中的1位是否在连续区域中?


85

我需要测试位值1的位置(对于32位整数,从0到31)是否形成连续区域。例如:

00111111000000000000000000000000      is contiguous
00111111000000000000000011000000      is not contiguous

我希望此测试(即某些功能has_contiguous_one_bits(int))具有可移植性。

一种明显的方法是遍历位置以找到第一个置位,然后找到第一个非置位并检查是否还有其他置位。

我想知道是否存在更快的方法?如果有快速的方法来找到最高和最低的设置位(但是从这个问题看来似乎没有任何可移植的位),那么可能的实现方法是

bool has_contiguous_one_bits(int val)
{
    auto h = highest_set_bit(val);
    auto l = lowest_set_bit(val);
    return val == (((1 << (h-l+1))-1)<<l);
}

只是为了好玩,这是带有连续位的前100个整数:

0 1 2 3 4 6 7 8 12 14 15 16 24 28 30 31 32 48 56 60 62 63 64 96 112 120 124 126 127 128 192 224 240 248 252 254 255 256 384 448 480 496 504 508 510 511 512 768 896 960 992 1008 1016 1020 1022 1023 1024 1536 1792 1920 1984 2016 2032 2040 2044 2046 2047 2048 3072 3584 3840 3968 4032 4064 4080 4088 4092 4094 4095 4096 6144 7168 7680 7936 8064 8128 8160 8176 8184 8188 8190 8191 8192 12288 14336 15360 15872 16128 16256 16320

它们(当然)是(1<<m)*(1<<n-1)带有非负m和的形式n


4
@aafulei是的,0x0很紧凑。定义相反的对象(而不是紧凑的对象)更容易:如果有两个置位的位,则它们之间至少有一个未置位的位。
沃尔特

1
@KamilCukh>=l通过的(隐含的)功能highest_set_bit()lowest_set_bit()
瓦尔特


6
OEIS链接说这些数字的二进制数不增加。引用它们的另一种方式是说它们是连续的(或可能是连接的)。对于这位数学家来说,“紧凑”意味着截然不同的东西。
蒂普

1
@Teepeemm我认为这个问题最终出现在网络热点问题上的一个原因恰恰是因为对紧凑一词的误用,这当然是我点击它的原因:我没有想太多,并且想知道定义紧凑性如何有意义那样。显然,这没有任何意义。
没人

Answers:


147
static _Bool IsCompact(unsigned x)
{
    return (x & x + (x & -x)) == 0;
}

简要地:

x & -x给出设置的最低位x(如果为零,x则为零)。

x + (x & -x) 将连续的1的最低字符串转换为单个1(或换为零)。

x & x + (x & -x) 清除那些1位。

(x & x + (x & -x)) == 0 测试是否还有其他1位剩余。

更长:

-x等于~x+1,使用我们假设的二进制补码。将位翻转后~x,加1进位,以便将低1位~x和后0位翻转回去,然后停止。因此,-x直到并包括其前1的低位与的低位相同x,但所有高位均被翻转。(示例:~10011100gives01100011和加1得到01100100,所以低位100相同,但是高位10011被翻转到01100。)然后x & -x给我们两个位中唯一的一位,即最低的1位(00000100)。(如果x为零,x & -x则为零。)

将其加到x会导致所有连续的1进位,将其更改为0。它将在下一个较高的0位留下1(或通过高端传送,而使换行的总数为零)(10100000。)

当此值与AND时x,将1更改为0的位置(以及进位将0更改为1的位置)也将包含0。因此,仅当再高1位时结果也不为零。


23
至少有人知道这本书《黑客的喜悦》。答案请参阅第2-1章。但这已经在SO上得到了多次回答。无论如何:+1
阿明·蒙蒂尼

33
我希望如果您在生产中编写了这样的代码,请在注释中加入解释;)
Polygnome

14
x86 BMI1可以很好地从x & -x一条blsi指令中执行,这在Intel上是1 uop,在AMD Zen上是2 ups。 godbolt.org/z/5zBx-A。但是如果没有BMI1,@ KevinZ的版本将更加高效。
彼得·科德斯

3
@TommyAndersen:_Bool是标准关键字,根据C 2018 6.4.1 1.
Eric Postpischil

1
@沃尔特:嗯?此代码使用unsigned。如果要对带符号的二进制补码执行测试int,最简单的方法是将其简单地传递给此答案中的例程,然后将int其转换为unsigned。那将得到期望的结果。int由于溢出/进位问题,直接将操作显示应用于签名可能会出现问题。(如果您想测试一个人的补码或正负号int,那是另一回事,这些天来大部分只是理论上的兴趣。)
Eric Postpischil

29

实际上,不需要使用任何内部函数。

首先翻转第一个1之前的所有0,然后测试新值是否为mersenne数字。在这种算法中,零映射为true。

bool has_compact_bits( unsigned const x )
{
    // fill up the low order zeroes
    unsigned const y = x | ( x - 1 );
    // test if the 1's is one solid block
    return not ( y & ( y + 1 ) );
}

当然,如果要使用内在函数,请使用以下popcount方法:

bool has_compact_bits( unsigned const x )
{
    size_t const num_bits = CHAR_BIT * sizeof(unsigned);
    size_t const sum = __builtin_ctz(x) + __builtin_popcount(x) + __builtin_clz(z);
    return sum == num_bits;
}

2
如果使用/利用/指令编译,则第一个版本减少到仅4条指令。这将是迄今为止建议的最短版本。不幸的是,几乎没有处理器支持该指令集扩展-mtbmblsfillblcfill
Giovanni Cerretani

19

实际上,您不需要计算前导零。正如pmg在评论中所建议的那样,利用了一个事实,即您正在寻找的数字是序列OEIS A023758的数字,即形式为2 ^ i-2 ^ j且i> = j的数字,您可能只计算结尾的零(即j-1),切换原始值中的那些位(相当于加2 ^ j-1),然后检查该值是否为2 ^ i-1形式。借助GCC / clang内部函数,

bool has_compact_bits(int val) {
    if (val == 0) return true; // __builtin_ctz undefined if argument is zero
    int j = __builtin_ctz(val) + 1;
    val |= (1 << j) - 1; // add 2^j - 1
    val &= (val + 1); // val set to zero if of the form (2^i - 1)
    return val == 0;
}

这个版本您的版本和KamilCuk提出的版本以及Yuri Feldman提出的仅带有popcount的版本要快一些。

如果您使用的是C ++ 20,则可以通过替换__builtin_ctz为来获得可移植的功能std::countr_zero

#include <bit>

bool has_compact_bits(int val) {
    int j = std::countr_zero(static_cast<unsigned>(val)) + 1; // ugly cast
    val |= (1 << j) - 1; // add 2^j - 1
    val &= (val + 1); // val set to zero if of the form (2^i - 1)
    return val == 0;
}

转换很丑陋,但是警告您在处理位时最好使用无符号类型。C ++ 20之前的版本是boost::multiprecision::lsb

编辑:

由于没有为Yuri Feldman版本发出popcount指令,因此删除线链接的基准受到了限制。尝试使用来在我的PC上编译它们-march=westmere,我测量了以下时间的10亿次迭代,这些迭代具有相同的序列std::mt19937

  • 您的版本:5.7秒
  • KamilCuk的第二个版本:4.7秒
  • 我的版本:4.7 s
  • Eric Postpischil的第一个版本:4.3秒
  • 尤里·费尔德曼(Yuri Feldman)版本(显式使用__builtin_popcount):4.1 s

因此,至少在我的体系结构上,最快的似乎是popcount。

编辑2:

我已经使用新的Eric Postpischil版本更新了基准测试。根据评论的要求,可以在此处找到我的测试代码。我添加了一个无操作循环来估计PRNG所需的时间。我还添加了KevinZ的两个版本。代码已在clang上使用-O3 -msse4 -mbmigetpopcntblsi指令进行了编译(感谢Peter Cordes)。

结果:至少在我的体系结构上,Eric Postpischil的版本与Yuri Feldman的版本完全一样快,并且比到目前为止提出的任何其他版本至少快两倍。


我删除了一个操作:return (x & x + (x & -x)) == 0;
埃里克·Postpischil

3
这是对@Eric版本的旧版本进行基准测试,对吗?在当前版本中,埃里克(Eric)的编译指令很少gcc -O3 -march=nehalem(以使popcnt可用),如果BMI1blsi可用于x & -x以下指令,则编译为更少的指令:godbolt.org/z/zuyj_f。而且指令都是简单的单指令,除了popcntYuri版本的指令有3个周期的延迟。(但是我假设您正在提高吞吐量。)我还假设您必须and val从Yuri的服务器中删除了,否则它会变慢。
Peter Cordes

2
另外,您以什么硬件为基准?将您的完整基准代码链接到Godbolt或其他东西上是一个好主意,因此将来的读者可以轻松地测试其C ++实现。
彼得·科德斯

2
您还应该测试@KevinZ的版本;没有BMI1的情况下,它可以编译成更少的指令(至少使用clang; gcc的非内联版本会浪费amov并且无法利用lea): godbolt.org/z/5jeQLQ使用BMI1,Eric的版本在x86-64上仍然更好,至少在Intelblsi是单一uop的情况下,但在AMD上是2 uop。
彼得·科德斯

15

不确定速度快,但可以通过验证val^(val>>1)最多具有2位来进行单线处理。

这仅适用于无符号类型:0必需在顶部移入(逻辑移位),而不是在算术右移中移入符号位的副本。

#include <bitset>
bool has_compact_bits(unsigned val)
{
    return std::bitset<8*sizeof(val)>((val ^ (val>>1))).count() <= 2;
}

要拒绝0(即仅接受具有正好1个连续位组的输入),逻辑与val不为零。关于这个问题的其他答案被0认为是紧凑的。

bool has_compact_bits(unsigned val)
{
    return std::bitset<8*sizeof(val)>((val ^ (val>>1))).count() <= 2 and val;
}

C ++可通过std::bitset::count()在C ++ 20中std::popcount可移植地公开popcount 。C仍然没有一种可移植的方法,可以可靠地将其编译为popcnt或类似目标上的可用指令。


2
到目前为止也是最快的。
Giovanni Cerretani

2
我认为您需要使用无符号类型来确保移入零,而不是符号位的副本。考虑一下11011111。算术右移,它变为11101111,而XOR为00110000。通过逻辑右移(在0顶部移动a ),您可以获取10110000并正确检测多个位组。编辑以解决该问题。
彼得·科德斯

3
这真的很聪明。尽管我不喜欢这种样式(IMO仅使用IMO __builtin_popcount(),但如今每个编译器都具有类似的原始语言),但这是迄今为止最快的(在现代cpu上)。实际上,我将争辩说,表示形式非常重要,因为在没有POPCNT作为单条指令的CPU上,我的实现可能会胜过这一点。因此,如果要使用此实现,则应仅使用内部函数。std::bitset有一个可怕的界面。
KevinZ

9

CPU对此具有专用指令,速度非常快。在PC上,它们是BSR / BSF(于1985年在80386中引入),在ARM上,它们是CLZ / CTZ。

用1查找最低有效位的索引,将整数右移该数量。使用另一个查找最高有效位的索引,将您的整数与(1u <<(bsr + 1))-1比较。

不幸的是,仅仅35年的时间还不足以更新C ++语言以匹配硬件。要使用C ++中的这些指令,您将需要内部函数,这些内部函数不可移植,并且以略有不同的格式返回结果。使用预处理器#ifdef等来检测编译器,然后使用适当的内在函数。在MSVC它们是_BitScanForward_BitScanForward64_BitScanReverse_BitScanReverse64。在GCC和clang中,它们是__builtin_clz__builtin_ctz


2
@ e2-e4为AMD64编译时,Visual Studio不支持内联汇编。这就是为什么我推荐内在函数。
不久

5
从C ++ 20开始,有std::countr_zerostd::countl_zero。如果使用的是Boost,则它具有称为boost::multiprecision::lsb和的可移植包装boost::multiprecision::msb
Giovanni Cerretani

8
这根本没有回答我的问题-我不知道为什么会有任何反对
Walter

3
@Walter您的意思是“不回答”?我已经准确地回答了您应该做什么,先使用预处理程序,然后再使用内部函数。
即将于

2
显然,C ++ 20最终使用位扫描,popcount和Rotate添加了#include <bit> en.cppreference.com/w/cpp/header/bit。可惜地花费了这么长时间才可移植地显示位扫描,但是现在总比没有好。(可通过以下途径获得便携式popcnt std::bitset::count()。)C ++ 20仍缺少Rust提供的某些功能(doc.rust-lang.org/std/primitive.i32.html),例如某些CPU有效提供的位反转和字节序但不是所有的。尽管用户需要知道什么是快速的,但可移植的内置程序对于任何CPU都具有一定意义。
彼得·科德斯

7

用零而不是零进行比较将节省一些操作:

bool has_compact_bits2(int val) {
    if (val == 0) return true;
    int h = __builtin_clz(val);
    // Clear bits to the left
    val = (unsigned)val << h;
    int l = __builtin_ctz(val);
    // Invert
    // >>l - Clear bits to the right
    return (~(unsigned)val)>>l == 0;
}

以下结果使指令比gcc10 -O3x86_64上的指令少,并在符号扩展名上使用:

bool has_compact_bits3(int val) {
    if (val == 0) return true;
    int h = __builtin_clz(val);
    val <<= h;
    int l = __builtin_ctz(val);
    return ~(val>>l) == 0;
}

测试在godbolt


不幸的是,这不是便携式的。我一直很害怕那些移位运算符会误解运算符的优先级-您确定~val<<h>>h>>l == 0自己认为会做什么吗?
沃尔特

4
是的,我敢肯定,还是要编辑并添加大括号。好吧,所以您对便携式解决方案感兴趣?因为我看着there exists a faster way?并假设一切正常。
KamilCuk

5

您可以重新定义要求:

  • 设置N与上一个不同的位数(通过迭代这些位数)
  • 如果N = 2并且第一位或最后一位为0,则答案为是
  • 如果N = 1,则答案为是(因为所有1都在一侧)
  • 如果N = 0则任意位为0,那么您就没有1,如果您认为答案为是或否,则取决于您
  • 其他:答案是否定的

遍历所有位可能看起来像这样:

unsigned int count_bit_changes (uint32_t value) {
  unsigned int bit;
  unsigned int changes = 0;
  uint32_t last_bit = value & 1;
  for (bit = 1; bit < 32; bit++) {
    value = value >> 1;
    if (value & 1 != last_bit  {
      changes++;
      last_bit = value & 1;
    }
  }
  return changes;
}

但这肯定可以优化(例如,通过forvalue达到时放弃循环,0这意味着不再存在值1的更高有效位)。


3

您可以执行以下计算顺序(假设val作为输入):

uint32_t x = val;
x |= x >>  1;
x |= x >>  2;
x |= x >>  4;
x |= x >>  8;
x |= x >> 16;

以获得一个数字,其中所有零以下的最高有效位均1填充有一个零。

您还可以计算y = val & -val去除条中除最低有效1位之外的所有位val(例如7 & -7 == 112 & -12 == 4)。
警告:这将对失败val == INT_MIN,因此您必须单独处理这种情况,但这是立即发生的。

然后右移y一个位置,以使其比的实际最低有效位低一点val,并执行与以下相同的例程x

uint32_t y = (val & -val) >> 1;
y |= y >>  1;
y |= y >>  2;
y |= y >>  4;
y |= y >>  8;
y |= y >> 16;

然后x - yor或x & ~yorx ^ y生成跨越整个长度的“紧凑”位掩码val。只需将其比较val以查看是否val“紧凑”即可。


2

我们可以利用gcc内置说明来检查是否:

设置位数

int __builtin_popcount(无符号int x)
返回x中的1位数字。

等于(a-b):

a:最高设置位的索引(32-CTZ)(32,因为32位是无符号整数)。

int __builtin_clz(无符号int x)
返回x中从最高有效位开始的前导0位的数目。如果x为0,则结果不确定。

b:最低设置位(CLZ)的索引:

int __builtin_clz(无符号int x)
返回x中从最高有效位开始的前导0位的数目。如果x为0,则结果不确定。

例如,如果n = 0b0001100110; 我们将使用popcount获得4,但索引差(a-b)将返回6。

bool has_contiguous_one_bits(unsigned n) {
    return (32 - __builtin_clz(n) - __builtin_ctz(n)) == __builtin_popcount(n);
}

也可以写成:

bool has_contiguous_one_bits(unsigned n) {
    return (__builtin_popcount(n) + __builtin_clz(n) + __builtin_ctz(n)) == 32;
}

我认为它比当前最受好评的答案更优雅或更有效:

return (x & x + (x & -x)) == 0;

具有以下装配:

mov     eax, edi
neg     eax
and     eax, edi
add     eax, edi
test    eax, edi
sete    al

但它可能更容易理解。


1

好的,这是一个遍历位的版本

template<typename Integer>
inline constexpr bool has_compact_bits(Integer val) noexcept
{
    Integer test = 1;
    while(!(test & val) && test) test<<=1; // skip unset bits to find first set bit
    while( (test & val) && test) test<<=1; // skip set bits to find next unset bit
    while(!(test & val) && test) test<<=1; // skip unset bits to find an offending set bit
    return !test;
}

前两个循环找到了第一个紧凑区域。最后的循环检查该区域之外是否还有其他设置位。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.