【问题标题】:Determine which single bit in the byte is set确定字节中的哪个单个位被设置
【发布时间】:2013-01-20 21:41:18
【问题描述】:

我有一个byte 用于位标志。我知道byte 中的只有一个 位在任何时候都被设置。

例如:unsigned char b = 0x20; //(00100000) 6th most bit set

我目前使用以下循环来确定设置了哪个位:

int getSetBitLocation(unsigned char b) {
  int i=0;
  while( !((b >> i++) & 0x01) ) { ; }
  return i;
}

如何最有效地确定设置位的位置?我可以在没有迭代的情况下做到这一点吗?

【问题讨论】:

  • 假设一个 8 位字节,使用查找表可能会有所帮助,除非您可以使用“计数前导零”原语/指令。
  • "我可以不用迭代就做到这一点吗?"使用查找表或 switch 语句。
  • 我希望得到一个有点小技巧的破解,而不是明显的switch 声明
  • 您可以进行三个比较和一些数学运算。它实际上会比这个循环更快吗?很难说。
  • 如果这是您的瓶颈,那么您将无法击败查找表。我猜您实际上并没有进行任何分析,并且在没有受益的情况下进行了优化。

标签: c optimization bit-manipulation bitflags


【解决方案1】:

我可以在不迭代的情况下做到这一点吗?

确实有可能。

如何最有效地确定设置位的位置?

你可以试试这个算法。它将字符分成两半以搜索最高位,每次移动到低半:

int getTopSetBit(unsigned char b) {
  int res = 0;
  if(b>15){
    b = b >> 4;
    res = res + 4;
  }
  if(b>3){
    b = b >> 2;
    res = res + 2;
  }

  //thanks @JasonD
  return res + (b>>1);
}

它使用两个比较(三个用于uint16s,四个用于uint32s...)。它可能比你的循环更快。绝对不会更短。


基于 Anton Kovalenko 的想法(散列查找)和 6502 的评论(除法很慢),我还建议这种实现(8 位 => 使用 de-Bruijn 序列的 3 位散列)

int[] lookup = {7, 0, 5, 1, 6, 4, 3, 2};

int getBitPosition(unsigned char b) {
  // return lookup[(b | (b>>1) | (b>>2) | (b>>4)) & 0x7];
  return lookup[((b * 0x1D) >> 4) & 0x7];
}

或(更大的 LUT,但只使用三个而不是四个)

int[] lookup = {0xFF, 0, 1, 4, 2, 0xFF, 5, 0xFF, 7, 3, 0xFF, 0xFF, 6, 0xFF, 0xFF, 0xFF};

int getBitPosition(unsigned char b) {
  return lookup[(b | (b>>3) | (b>>4)) & 0xF];
}

【讨论】:

  • 您可以删除最后一个 if() 并添加 b-1
  • @JasonD 不是b-1,而是b>>1b 可以是 23
  • @JanDvorak 聪明,这可能更有效率
  • @JanDvorak 他说只设置了 1 位,所以应该只有 1 或 2。不过你的选择更通用。
  • @JanDvorak 在检查唯一设置位的位置时,您的哈希查找表 lookup[((b * 0x1D) >> 4) & 0x7]; 通过了我的 8 个字节测试用例 { 0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80 }。十分优雅!我可以问你从哪里得到哈希?你能链接到 De-Brujin 序列吗?
【解决方案2】:

查找表很简单,如果值集稀疏,您可以减小它的大小。让我们尝试使用 11 个元素而不是 128 个元素:

unsigned char expt2mod11_bits[11]={0xFF,0,1,0xFF,2,4,0xFF,7,3,6,5};
unsigned char pos = expt2mod11_bits[b%11];
assert(pos < 8);
assert(1<<pos == b);

当然,它不一定更有效,尤其是对于 8 位,但同样的技巧可以用于更大的尺寸,其中完整的查找表会非常大。让我们看看:

unsigned int w; 
....
unsigned char expt2mod19_bits[19]={0xFF,0,1,13,2,0xFF,14,6,3,8,0xFF,12,15,5,7,11,4,10,9};
unsigned char pos = expt2mod19_bits[w%19];
assert(pos < 16);
assert(1<<pos == w);

【讨论】:

  • 大尺寸的好主意...但对于 8 位,我认为模运算将是一个问题。
  • 在 x86 上,我会尝试使用 BSF 进行内联汇编。
  • @AntonKovalenko 除了 mod-11 与平台无关,而 BSF 不是。
  • @JanDvorak:你有点太乐观了。除法总是慢得令人讨厌……太慢了,以至于在设计第一个奔腾处理器时,他们甚至试图偷工减料;-)。如果您需要商,在许多情况下可以将除法换成乘法,但如果您需要余数,则该技巧不起作用。在我使用的任何处理器上,甚至整数乘法 IIRC 都不是 2 个周期。
  • @JanDvorak 放不下评论区,但它以乘以 780903145 开头。注意这个数字正好是 0x200000003 / 11
【解决方案3】:

对于使用 64 位表示位置的国际象棋程序来说,这是一个相当普遍的问题(即,一个 64 位数字存储所有白棋在哪里,另一个存储所有黑棋在哪里等等)。

使用这种表示有时需要找到第一个或最后一个设置位的索引 0...63,并且有几种可能的方法:

  1. 像你一样做一个循环
  2. 使用二分法搜索(即如果 x &amp; 0x00000000ffffffffULL 为零,则无需检查低 32 位)
  3. 如果处理器上可用,则使用特殊指令(例如 x86 上的 bsfbsr
  4. 使用查找表(当然不是针对整个 64 位值,而是针对 8 位或 16 位)

什么更快,但实际上取决于您的硬件和实际用例。 对于仅 8 位和现代处理器,我认为可能具有 256 个条目的查找表是最佳选择...

但是你真的确定这是你算法的瓶颈吗?

【讨论】:

  • ...FULL 的意思是“unsigned long long”?
  • ULL 是后缀,F 只是最后一个十六进制数字,我将对其进行编辑以使其更清晰
  • 我的意思是,它实际上是可编译的,还是只是一个占位符/省略号:-)
【解决方案4】:
unsigned getSetBitLocation(unsigned char b) {
  unsigned pos=0;
  pos = (b & 0xf0) ? 4 : 0; b |= b >>4;
  pos += (b & 0xc) ? 2 : 0; b |= b >>2;
  pos += (b & 0x2) ? 1 : 0; 
  return pos; 
}

很难做到无跳跃。也许与 Bruin 序列有关?

【讨论】:

    【解决方案5】:

    基于Find the log base 2 of an N-bit integer in O(lg(N)) operations中的log2计算:

    int getSetBitLocation(unsigned char c) {
      // c is in {1, 2, 4, 8, 16, 32, 64, 128}, returned values are {0, 1, ..., 7}
      return (((c & 0xAA) != 0) |
              (((c & 0xCC) != 0) << 1) |
              (((c & 0xF0) != 0) << 2));
    }
    

    【讨论】:

    • 这取决于!= 返回01 的事实。这是规范保证的吗?我也猜想由于!= 的实现方式,它并不能真正避免分支。
    • @JanDvorak:是的,它是在 c99 中添加的。 (我用三元向下兼容c89/c90)而且它确实看起来不是jumpfree的。 (但现在有一些奇怪的指令)
    • @JanDvorak:yes, it is guaranteed even before c99。此外,我在代码中看不到任何分支。你可以see its assembly online。你能指出分支指令吗?
    • @J.F.Sebastian 没有优化,包裹在int main(char c)中,三个!=s转为set if not equal在线0x14(令人印象深刻),0x25: je 0x2E / 0x2C: jmp 0x33(哎呀,一个分支指令) ,并且相同的组合出现在0x40。启用优化后,jes 变为subtract with borrows(令人印象深刻)ARM 使用move if not equal / move if equals 组合(很好的指令)。感谢编译器的链接。
    • 请注意,基于 deBrujin 的解决方案只需要五个操作:MOVZB, IMUL, SAR, AND, MOV
    【解决方案6】:

    最简单的事情是创建一个查找表。最简单的一种是稀疏的(有 256 个元素),但从技术上讲它会避免迭代。

    这里的评论在技术上避免了迭代,但我们在开玩笑,它仍然在做同样数量的检查:How to write log base(2) in c/c++

    封闭形式是 log2(), a la, log2() + 1 但我不确定它的效率如何 - 可能 CPU 有一条指令用于以 2 为底的对数?

    【讨论】:

    • FYL2X 指令v/s位移?不确定。
    • 具有 255 个元素的稀疏查找表的内存需求不会超过较小的性能优化。
    • 当 CHAR_BIT == 16 或 CHAR_BIT == 32 时,稀疏查找表会有多少个元素?
    • “可能 CPU 有一条以 2 为底的对数指令?”在大多数处理器中都有一条指令可以计算前导零。
    【解决方案7】:

    如果你定义了

    const char bytes[]={1,2,4,8,16,32,64,128}
    

    并使用

    struct byte{
    char data;
    int pos;
    }
    void assign(struct byte b,int i){
    
    b.data=bytes[i];
    b.pos=i
    }
    

    你不需要确定设置位的位置

    【讨论】:

    • you don't need to determine the position of the set bit -- 我想提出异议
    • 他正在与 OPOSITE 问题作斗争...鉴于找到索引的值。扫描你的表或多或少就像检查位一样。
    【解决方案8】:

    当 CHAR_BIT == 8 时,查找表既快速又简单,但在某些系统上,CHAR_BIT == 16 或 32 并且查找表变得异常庞大。如果您正在考虑使用查找表,我建议将其包装起来;改为“查找表函数”,以便在需要优化时交换逻辑。

    使用分治法,通过对排序数组执行二进制搜索,涉及基于 log2 CHAR_BIT 的比较。该代码更复杂,涉及初始化 unsigned char 数组以用作开始的查找表。一旦你初始化了这样的数组,你可以使用bsearch 来搜索它,例如:

    #include <stdio.h>
    #include <stdlib.h>
    void uchar_bit_init(unsigned char *table) {
        for (size_t x = 0; x < CHAR_BIT; x++) {
            table[x] = 1U << x;
        }
    }
    int uchar_compare(void const *x, void const *y) {
        char const *X = x, *Y = y;
        return (*X > *Y) - (*X < *Y);
    }
    size_t uchar_bit_lookup(unsigned char *table, unsigned char value) {
        unsigned char *position = bsearch(lookup, c, sizeof lookup, 1, char_compare);
        return position ? position - table + 1 : 0;
    }
    int main(void) {
        unsigned char lookup[CHAR_BIT];
        uchar_bit_init(lookup);
        for (;;) {
            int c = getchar();
            if (c == EOF) { break; }
            printf("Bit for %c found at %zu\n", c, uchar_bit_lookup(lookup, c));
        }
    }
    

    附:这听起来像是微优化。完成您的解决方案(将所需的操作抽象到这些函数中),然后担心基于您的分析的优化。如果您要专注于微优化,请确保您的分析针对您的解决方案将运行的系统,因为微优化的效率差异很大,因为硬件甚至略有不同......通常购买一个更好的主意更快的 PC ;)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-07-15
      • 1970-01-01
      • 1970-01-01
      • 2021-03-28
      相关资源
      最近更新 更多