【问题标题】:Fastest way to count consecutive 1 bits. C++计算连续 1 位的最快方法。 C++
【发布时间】:2014-07-16 11:15:23
【问题描述】:

基本上我只需要知道 int 或 unsigned int 中最高 1 位的位置。喜欢:

00001111=4;
00011111=5;
11111111=8;

因为我确信我得到的任何数字都会有连续的 1 位。 0...0000011...1 不会有 ..00010011... 什么的。所以方法可以找到最高的 1 或只计算 1。不管。

这是我能做到的最好的:

Uint32 number;
int shift=16; int segment=8;
while (segment) 
{
if (number>>shift!=0) shift+=segment; 
else shift-=segment;
segment>>1; // /2
}

【问题讨论】:

标签: c++ count bit-manipulation bit


【解决方案1】:

复制/粘贴我的函数:

size_t FirstSetBit(unsigned int v) const
{
#if defined(_MSC_VER)
    unsigned long ul;
    // Just 10% faster than MultiplyDeBruijnBitPosition method, on Core i7
    _BitScanForward(&ul, v);
    return ul;
#elif defined(__GNUC__) || defined(__clang__)
    return 31 - __builtin_clz(v);
#else // integer fallback for non-x64
    #warning You may be able to optimise this code for your compiler/processor

    int r;
    static const int MultiplyDeBruijnBitPosition[32] =
    {
        0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
        31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
    };

    r = MultiplyDeBruijnBitPosition[(uint32_t((v & -int(v)) * 0x077CB531U)) >> 27];
return r;
#endif
}

【讨论】:

  • clz 表示计算前导零,因此这不会返回第一个设置的位。您需要将解决方案修改为 32-clz 之类的东西。 BitScanReverse 计算最重要的位,BitScanForward 计算最不重要的位吗?
  • 是的,我刚刚测试过。 MSVC 应该是 BitScanReverse,GCC 应该是 31 - clz。
  • 对不起,你是对的,FirstSetBit() 就像它的名字所说的那样;它按顺序从位 0 扫描到位 31。你要求相反;从第 31 位扫描到第 0 位。因此您需要使用 _BitScanReverse() 并将 __builtin_clz() 更改为反向。
  • 你的代码还是错的。除了 MSVC 部分,它不是 FirstBitSet。 GCC 和后备部分实际上是 BitScanReverse。做一些测试。
【解决方案2】:

假设您知道相关 int 的大小(例如 32 位),您可以很容易地使用二进制搜索来查找设置的最高位:

int bit_pos(unsigned value) { 
    static const std::vector<unsigned> masks = {
        0xffff0000, 
        0xff00ff00, 
        0xf0f0f0f0, 
        0xcccccccc, 
        0xaaaaaaaa
    };

    if (!value)
        return 0;

    int position = 0;

    int val = 16;

    for (unsigned mask : masks) {
        if (value & mask) {
            position += val;
            value &= mask;
        }
        val /= 2;
    }

    return position + 1;
}

为了(可能)以更大的模糊性为代价获得一点额外的速度,您可以在前面做一点额外的摆弄,只设置一个位,然后找到它的位置:

unsigned bit_pos2(unsigned int value) {
    unsigned int position = 32;
    value = ~value;
    value &= -signed(value);
    if (value) --position;
    if (value & 0x0000ffff) position -= 16;
    if (value & 0x00ff00ff) position -= 8;
    if (value & 0x0f0f0f0f) position -= 4;
    if (value & 0x33333333) position -= 2;
    if (value & 0x55555555) position -= 1;
    return position;
}

对于 64 位整数,数字会变大,但我们只需要再添加一次迭代即可:

unsigned bit_pos64(unsigned long long value) {
    value = ~value;
    unsigned position = 64;
    value &= -(long long)value;
    if (value) --position;
    if (value & 0x00000000ffffffff) position -= 32;
    if (value & 0x0000ffff0000ffff) position -= 16;
    if (value & 0x00ff00ff00ff00ff) position -= 8;
    if (value & 0x0f0f0f0f0f0f0f0f) position -= 4;
    if (value & 0x3333333333333333) position -= 2;
    if (value & 0x5555555555555555) position -= 1;
    return position;
}

通过只设置一个位,我们避免了循环迭代之间的依赖关系,因此迭代可以并行执行。手动展开循环(如上)可能有助于降低发生这种情况的几率,至少会略微降低。这也只需要每次迭代而不是 2 次操作,因此即使没有任何并行执行它也可能更快。

【讨论】:

    【解决方案3】:

    其他人给出了各种各样的答案,但可能值得一提的是,有一整本书充满了这类东西——Hacker's Delight by Henry S. Warren Jr., ISBN 978-0-201-91465-8,其中也有a web page。

    还值得强调的是,一些微处理器对其中一些东西有特殊的支持。上面 Lasse Reinhold 的回答利用了这一事实,但并未引起读者的注意。一般来说,除了非常简单的情况(如位旋转指令),编译器无法将“位旋转”算法优化为单个机器指令,所以如果你知道你在一台机器上,例如位扫描前向或人口计数指令或类似指令,并且您可以使用它(通过编译器内在函数或asm 语句),您可能想要这样做。

    最后,由于问题开始时说已知数字的形式为 0...000111...1,我将添加另一个选项,基于计算(并行)组的总和位数:

    uint32_t count_set_bits(uint32_t x) {
      x = ((x >> 1) & 0x55555555) + (x & 0x55555555);
      x = ((x >> 2) & 0x33333333) + (x & 0x33333333);
      x = ((x >> 4) & 0x0f0f0f0f) + (x & 0x0f0f0f0f);
      x = ((x >> 8) + x) & 0x00ff00ff);
      return (x >> 16) + x) & 0x0000ffff;
    }
    

    【讨论】:

      【解决方案4】:

      您所做的称为bit scan。在您的情况下,它被称为位扫描反向。也是对数基二算法的底。

      x86 指令集有一个指令,bsr = 位扫描反向,自 Intel 386 以来。所以你应该尝试使用一个函数来调用该指令。对于 MSVC,您希望使用 _BitScanReverse、GCC 31 -__builtin_clz(x) 和 ICC _bit_scan_reverse。

      我查看了来自 MSVC 和 GCC 的这些内在函数/内置函数的汇编输出,它们都生成了 bsr 指令。

      Intel Haswell 处理器添加了lzcnt 指令(AMD 在巴塞罗那很早就添加了它)。这会计算前导零。它与 31 - bsr 相同(或等于 bsr - 请参阅下面的警告)。您可以使用带有 MSVC 的 __lzcnt 调用它。但是,应该警告您,如果您在不支持 lzcnt 的处理器上执行此操作will not crash and instead will use the bsr instruction

      lzcnt 的编码与 bsr 非常相似,如果 lzcnt 在不支持它的 CPU 上执行,例如 Haswell 之前的 Intel CPU,它将执行 bsr 操作,而不是引发无效指令错误。

      如果您想在软件中执行 BitScanReverse,有几种不同的方法。请参阅“查找 N 位整数的对数基数 2”部分 http://graphics.stanford.edu/~seander/bithacks.html#IntegerLogDeBruijn 和 http://chessprogramming.wikispaces.com/BitScan

      【讨论】:

      【解决方案5】:
      int getHighestOne(unsigned int num) {
          int count = 0;
          while(num >>= 1) ++count;
          return count;
      }
      

      从0开始返回最高的位置,如果没有则返回-1。

      getHighestOne(0) 将返回 -1
      getHighestOne(1) 将返回 0
      getHighestOne(10) 将返回 3

      编辑: 这是一些快速日志方法的link。

      【讨论】:

      • 但这是“最快”的方式吗?
      • 不知道,不过应该够快了吧?
      • 我希望有更快的速度,比如最多 5 次操作。
      • @Quiz Canners:现在只有 4 次操作。有关更多方法和解释,请参阅我的编辑中的链接。
      • 基于循环的解决方案不是最好的方法。请参阅下面的其他答案。
      【解决方案6】:

      (1)

      你能数出你必须将 unsigned int 移位多少次直到它为零?

      见What are bitwise shift (bit-shift) operators and how do they work?

      或

      (2)

      例子

      号码:0111

      右移一位:0011,使用按位 x-or 与原始数字 0111 ^ 0011 = 0100

      在 cpp 中:

      unsigned int num = 3;
      
      unsigned int answer = ((num >> 1) ^ (num)); 
      
      cout << answer << '\n';
      

      【讨论】:

      • 但是 OP 要求的是“最快”的方法,而不仅仅是“任何”。没有?
      • 感谢您的评论,我添加了第二种方法。我觉得这样好一点……
      【解决方案7】:

      以下代码可能有助于计算连续的 1 位。

      int count_consecutive_ones(int in) {
          int count = 0;
          while (in) {
              in = (in & (in << 1));
              count++;
          }
          return count;
      }
      

      【讨论】:

        【解决方案8】:
        int count_bit(unsigned int num) {
            // Assume 0<= num < 256, i.e. 8 bit
            // It can be easily support larger word size by successive calling.
            if ( num >= 256 ) return -1;
            return count[ num ];
        }
        

        是的,最快的方法是查找表。

        【讨论】:

        • 当且仅当整个表已经在缓存中时才会如此。在现代处理器上,CPU 操作比内存读取快 很多。
        • 如果操作引起大量依赖和其他停顿,CPU 操作不需要更快。在这样的水平上,有很多因素,在你衡量具体的使用模式之前,没有绝对最快的方法。无论如何,如果 OP 关心性能,我假设这个操作被称为 A LOT time,这样的小表应该保留在缓存中。
        • 所涉及的 CPU 操作不需要很多依赖项,因此停顿的可能性非常小。即使充其量,对于一个 64 位整数,您也需要调用 8 次并将中间结果相加——这已经比我答案中的代码(举一个明显的例子)更多的操作。
        猜你喜欢
        • 2020-02-18
        • 1970-01-01
        • 2010-09-07
        • 2011-02-09
        • 1970-01-01
        • 2013-12-31
        • 1970-01-01
        • 2013-01-17
        相关资源
        最近更新 更多