【问题标题】:How to find if there are n consecutive set bits in a 32 bit buffer?如何查找 32 位缓冲区中是否有 n 个连续的设置位?
【发布时间】:2022-04-16 14:52:29
【问题描述】:

也许您可以帮助我解决以下问题,以帮助我加快我正在考虑的内存管理器(我不确定是否存在解决方案 - 我没有找到)。

我有一个 32 位的寄存器,我需要找出其中是否有 n 个连续的设置位,如果有,它们的偏移量是多少。例如,如果寄存器保存以下值 111100000000000000000001111111000 并且 n 等于 4 - 接受以下任何答案(偏移量从 0 开始):

3、4、5、6、28

我拥有的原子操作都是常规的按位操作(&、|、~、...),并且还找到了最低有效位偏移量(上面寄存器中的 3)。该算法(假设存在一个)——应该不超过 5 个原子操作。

【问题讨论】:

  • 制作一个设置了最后一个n 位的掩码,然后将其移位并匹配。容易。
  • @daa - 直到最近我才知道如何接受答案。我只尝试了循环,但没有它不知道怎么做。
  • @Qnan - 远远超过 5 次操作.. 最好循环
  • 我知道一种更好的方法,它只查看 1 的运行,但它仍然超过 5 次操作,并且当位在 0 和 1 之间交替时,它的行为非常糟糕。
  • 这不是作业,我可以做一个简单的循环并找到所需的偏移量。但我真的不知道它是否可以在 O(1) 内完成

标签: algorithm bit-manipulation


【解决方案1】:

如果有一种算法可以做到这一点,那么最坏情况的复杂度至少是O(m-n),其中m 是寄存器中的位数,n 是您连续设置的位数寻找。这很容易看出,因为如果设置了所有位,您的算法将必须准确输出 m-n 项,因此它的复杂性不能再低了。

编辑

这里有一个类似问题的优雅解决方案Looping through bits in an integer, ruby,找到 longes 1 序列的长度。

如果您提前知道要查找的运行的长度n,则此算法将只需要n 步骤。然后可以在大约 5 个以上的步骤中从算法的最后一步中的尾随零的数量中恢复偏移量。这不是非常有效,但可能比循环解决方案更好,尤其是对于一个小的n

编辑 2

如果n 是预先知道的,我们可以为它计算出一系列必要的转变。例如。如果我们正在寻找 7 位运行,那么我们必须这样做

x &= x >> 1
x &= x >> 3
x &= x >> 1
x &= x >> 1

关键是,如果n 是偶数,我们将向右移动n/2 位,如果n 是奇数,则向右移动1,然后按照@harold 的建议相应地更新nn = n - 1 or n = n / 2)。动态估计这些值会很昂贵,但如果我们预先计算它们,那么它会非常有效。

编辑 3

更好的是,对于任何n,只要在floor(n/2)2^floor(log(2,n-1)) 之间,无论我们采取哪种转变,都需要精确的ceil(log(2,n)) 步骤。请参阅下面的 cmets。

【讨论】:

  • 它只需要输出一项 - 问题是 any 这些偏移都可以,而不是必须找到所有偏移。
  • 但是找到一个偏移量就足够了-我不需要所有偏移量-找到一个偏移量的那一刻-它将打印出来,仅此而已..
  • n==7 的示例已损坏,例如在63 上尝试。可以优化的长度是那些不是素数的长度。
  • @salva 检查更新。我认为这是关于奇数与偶数,而不是素数或合数。想想 9。我想应该是 1,4,2,1。或 11,对应 1,5,1,2,1。
  • @Qnan,我认为最佳关系比寻找素数或奇偶数更复杂。我认为找到两个小于 n 的p = (1<<k) 的最大幂然后计算x & (x >> (n - p)) 更重要。例如在 n=7 的情况下,最佳值是x &= x >> 1; x &= x >> 2; x &= x >> 3。对于 n=9,它是 x &= x >> 1; x &= x >> 2; x &= x >> 4; x &= x >> 1
【解决方案2】:

对于每个可能的字节值 (0-255),计算开头的位数、结尾的位数和字节内最长的连续位数以及此序列的偏移量。比如0b11011101,开头有2位,结尾有1位,其中有3个连续位的序列。

将此值存储在 4 个数组中,例如 startendlongestlongest_offset

然后,将 32 位数字视为 4 字节数组,并按如下方式遍历这些字节:

int search_bit_sequence(uint32 num, int desired) {
  unsigned char *bytes = (unsigned char *)#
  int i, acu;
  for (acu = i = 0; i < 4; i++) {
    int byte = bytes[i];
    acu += start[byte];
    if (acu >= desired)
      return (i * 8 - (acu - start[byte]));

    if (longest[byte] >= desired)
      return ( i * 8 + longest_offset[byte]);

    if (longest[byte] < 8)
      acu = end[byte];
  }
  return -1; /* not found */
}

更新:注意你的 CPU 的字节序可能需要改变循环方向。

【讨论】:

  • 这对于搜索更长的位数组来说是可以的,但我认为如果你只关心一个寄存器,这是一个真正的过冲
  • 你的意思是0b11011101吗?十六进制 0x11011101 没有任何连续的设置位,只有一些 0001 半字节组 (0x1) 和一些 0000 组 (0x0)。
  • @PeterCordes,是的,你是对的!
【解决方案3】:

Qnan 发布的链接显示了针对一般情况的优雅解决方案。

对于特定的 m 值,可以进一步优化。

例如,对于 m == 4,您可以这样做:

x &= (x >> 1);
x &= (x >> 2);
// at this point, the first bit set in x indicates a 4 bit set sequence.

对于 m == 6:

x &= (x >> 1);
x &= (x >> 1);
x &= (x >> 3);

最后,这只是简化为 m。

更新

另请注意,对于较高的值,实际上只检查每个可能位置的位序列可能更便宜。

例如,对于 m = 23,模式只能从 0 到 9 的位置开始。

【讨论】:

  • 我认为你混淆了顺序,应该是先 x >> 2,然后 x >> 1 为 4。
  • @Qnan:我认为顺序根本不重要。
【解决方案4】:

我检查了this question and answers 并提出了以下想法。

int i = n-1;
uint32_t y = x;
while(y && i--) {
    y = y & (y << 1);
};

经过上述操作,如果有n 连续设置位,y 非零。接下来要做的是找到在那里设置的最不重要的值。以下代码将删除除最低有效位之外的所有设置位。

z = y - (y & (y-1));

现在我们只设置了一个位,我们需要找到位的位置。 32 种情况下我们可以使用 switch 语句。

static inline int get_set_position(const uint32_t z) {
    switch(z) {
        case 0x1:
            return 0;
        case 0x2:
            return 1;
        ....
        .... // upto (1<<31) total 32 times.
    }
    return -1;
}

最后为了得到结果,我们需要减少n-1。所以整个过程如下。

static inline int get_set_n_position(const uint32_t x, const uint8_t n) {
    if(!n) return -1;
    int i = n-1;
    uint32_t y = x;
    while(y && i--) {
        y = y & (y << 1);
    };
    if(!y) return -1;
    uint32_t z = y - (y & (y-1));
    if(!z) return -1;
    int pos = get_set_position(z);
    if(pos < 0) return -1;
    assert(pos >= (n-1));
    return pos - (n-1);
}

现在人们担心大端。我想我只需要为大端更改 get_set_position() 即可使其工作(假设连续设置位定义基于端序进行更改)。

让我分享一个使用gcc提供的builtin_ctzl的测试代码。

OPP_INLINE int get_set_n_position(BITSTRING_TYPE x, const uint8_t n) {
    if(!n || n > BIT_PER_STRING) return -1;
    int i = n-1;
    while(x && i--) {
        x = x & (x << 1);
    };
    if(!x) return -1;
    int pos = __builtin_ctzl(x);
    return pos - (n-1);
}

代码在 O(1) 时间内工作,因为 32 是恒定的(正如@Qnan 注意到的那样)。如果寄存器的大小不同,它再次在 O(n) 中工作。

注意:感谢 cmets 和单元测试,我修复了这些错误。

【讨论】:

  • 您最初的断言是错误的。如果x = 11011011n=3 那么x &lt;&lt; (n-1)0110110011011011 &amp; 01101100 = 01001000。结果非零,但没有 3 个连续的设置位。
  • @JimMischel 谢谢,我已经修复了这个错误并更新了答案。
  • 那也行不通。 x = 11011011x &lt;&lt; n = 11011000 按位与的结果是11011000
  • @JimMischel 谢谢,这是一个错误。现在我修复并测试了代码。
猜你喜欢
  • 1970-01-01
  • 2015-10-14
  • 2020-02-14
  • 2017-11-19
  • 1970-01-01
  • 1970-01-01
  • 2018-04-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多