【问题标题】:Index of lowest order bit最低位索引
【发布时间】:2010-12-01 11:18:45
【问题描述】:

我想找到最快的方法来获取 long long 的最低位的索引。即:

00101001001000 -> 3

涉及循环和移位的解决方案太慢了。即:

int i;
if(bits == 0ULL) {
  i = 64;
} else {
  for(i = 0;!(bits & 1ULL);i++)
    bits >>= 1;
}

编辑:使用信息

使用 ffsll 的函数并不能真正减少它的使用量,但是在这里(当然是简化了)。它只是遍历索引并对它们做一些事情。这个函数可能是我整个应用程序中使用最广泛的函数,尽管它的值有很多缓存。这是我的 alpha-beta 搜索引擎中的合法移动生成器。

while(bits){
  index = ffsll(bits);
  doSomething(index);
  index &= index-1;
}

【问题讨论】:

  • “太慢”是什么意思?它占整个运行时间的百分之几?
  • 这段代码在我的基准测试中运行时间为 7.2 秒,而 ffsll 运行时间为 0.2 秒。减少了 97%。太慢了;)

标签: c binary


【解决方案1】:

英特尔有专门的指令来查找最低或最高位设置位。 BSF 似乎是您所需要的。就在纯 C 中进行操作而言,bit twiddling hacks page 可能满足您的需求。

至少您可以使用半字节或字节表来加快速度。像这样的东西(演示为 int,但可以根据需要轻松更改为 longlong)。

/*
0000 - 0
0001 - 1
0010 - 2
0011 - 1
0100 - 3
0101 - 1
0110 - 2
0111 - 1
1000 - 4
1001 - 1
1010 - 2
1011 - 1
1100 - 3
1101 - 1
1110 - 2
1111 - 1
*/

int ffs(int i) {
    int ret = 0;
    int j = 0;
    static const int _ffs_tab[] = 
        { 0, 1, 2, 1, 3, 1, 2, 1, 4, 1, 2, 1, 3, 1, 2, 1 };

    while((i != 0) && (ret == 0)) {
        ret = _ffs_tab[i & 0x0f];

        if(ret > 0) {
            break;
        }

        i >>= 4;
        j += 4;

        /* technically the sign bit could stay, so we mask it out to be sure */
        i &= INT_MAX;
    }

    if(ret != 0) {
        ret += j;
    }

    return ret;
}

【讨论】:

  • +1 用于小技巧。您可能想要“使用模除法和查找计算右侧的连续零位(尾随)”或“通过二进制搜索计算右侧的连续零位(尾随)”。第一个是常数时间。
  • 对于我的基准 ffs 运行该函数所需时间的 45%,我相信 ffs 是 BSF 的便携式包装器
  • 加上这仅适用于 [0,15] 我需要它用于 [0,0xFFFFFFFFFFFFFFFF] 不太可能
  • 请使用 ffs 发布函数。也许我们可以重新设计函数以减少它的工作量?
  • 你是什么意思[0,15]? BSF 适用于 32 位,而 ffs 适用于 int,它可以是几种不同宽度之一。如果您需要它在 64 位上工作,那么如果还没有找到,您可以通过移位多次执行(类似于我的示例,其中一个表一次只处理一个半字节)。
【解决方案2】:

我发现最快的是string.h中的ffsll(long long)

【讨论】:

  • 注意ffsll()从1开始计算位位置,所以要匹配你需要做的问题ffsll(val) - 1,结果为-1表示没有设置位。
【解决方案3】:

如果使用 Visual Studio,_BitScanForward:

对于 gcc,请尝试 __builtin_ctz__builtin_ffs

与往常一样,应查阅生成的代码以确保生成正确的指令。

【讨论】:

    【解决方案4】:

    您可以使用x & (~x + 1) 隔离最低设置位;这会为您提供最低位值,而不是索引(例如,如果 x = 01101000,则结果为 00001000)。我知道从那里到索引的最快方法可能是 switch 语句:

    switch(x & (~x + 1))
    {
      case     0ULL: index = -1; break;
      case     1ULL: index =  0; break;
      case     2ULL: index =  1; break;
      case     4ULL: index =  2; break;
      ...
      case 9223372036854775808ULL: index = 63; break;
    }
    

    丑陋,但不涉及循环。

    【讨论】:

    • 除了现在你引入了大量的分支......效率也不高
    • log2(x&(~x+1))?但这可能取决于编译器是否对 log2 和 unsigned long long 做了一些聪明的事情。也许看到fckinc.thegerf.net/thinktank/fast_log_two
    • @dharga:是的,我假设一个开关被实现为一个跳转表,但是一个快速的实验表明我的平台为一个开关生成了与 if-else 链相同的代码,所以这个想法是出去。下一个最佳解决方案是查找表,但 64 位值的查找表会有点……大。移位和 8 位或 16 位查找表的组合会更好,但此时您最好转换为双精度并使用 log2。
    【解决方案5】:

    如何实现一种二分搜索?

    查看由位和掩码值产生的低位,该掩码值在低半部分全为 1。如果该值为零,则您知道最小的位在数字的上半部分。

    其他明智的把东西切成两半然后再去。

    【讨论】:

    • 乏味,但我相信用纯 C 语言最快的方法
    • 对于 64 位,二分查找需要 6 次掩码比较;最初发布的代码平均(假设是随机数据)只需要两个(尽管最坏的情况是 64)。
    【解决方案6】:

    这可能适用于 32 位。应该很容易扩展到 64。

    // all bits left of lsb become 1, lsb & right become 0
    y = x ^ (-x);
    
    // XOR a shifted copy recovers a single 1 in the lsb's location
    u = y ^ (y >> 1);
    
    // .. and isolate the bit in log2 of number of bits
    i0 = (u & 0xAAAAAAAA) ?  1 : 0;
    i1 = (u & 0xCCCCCCCC) ?  2 : 0;
    i2 = (u & 0xF0F0F0F0) ?  4 : 0;
    i3 = (u & 0xFF00FF00) ?  8 : 0;
    i4 = (u & 0xFFFF0000) ? 16 : 0;
    index = i4 | i3 | i2 | i1 | i0;
    

    显然,如果有某种方法可以让硬件执行此操作,即,如果有特殊的 CPU 指令可用,那就是可行的方法。

    【讨论】:

      【解决方案7】:

      这样的事情怎么样?它大大减少了循环的数量。

      int shifts = 0;
      if ((bits & 0xFFFFFFFFFFFFULL) == 0) // not in bottom 48 bits
      {
          shifts = 48;
      }
      else if ((bits & 0xFFFFFFFFFFULL == 0) // not in bottom 40 bits
      {
          shifts = 40;
      }
      else
      // etc
      
      bits >>= shifts;  // do all the shifts at once
      
      // this will loop at most 8 times
      for(i = 0;!(bits & 1ULL);i++)
          bits >>= 1;
      
      index = shifts + i;
      

      【讨论】:

      • 如果我要这样做,我会采用另一个答案中提到的二进制搜索想法
      【解决方案8】:

      我写了两个函数,它们返回的结果与 ffsll() 相同。

      int func1( uint64_t n ){
        if( n == 0 ) return 0;
        n ^= n-1;
        int i = 0;
        if( n >= 1ull<<32 ){ n>>=32; i+=32; }
        if( n >= 1ull<<16 ){ n>>=16; i+=16; }
        if( n >= 1ull<< 8 ){ n>>= 8; i+= 8; }
        if( n >= 1ull<< 4 ){ n>>= 4; i+= 4; }
        if( n >= 1ull<< 2 ){ n>>= 2; i+= 2; }
        if( n >= 1ull<< 1 ){         i+= 1; }
        return i+1;
      }
      
      int func2( uint64_t n ){
        return n? ((union ieee754_float)((float)(n^(n-1)))).ieee.exponent-126: 0;
      }
      

      不知道哪个最快:ffsll()、func1() 还是 func2()?

      【讨论】:

        【解决方案9】:

        这里有两个实现,第一个是intrinsic/assembly,第二个是c/c++(索引从0开始)

        unsigned int bsf_asm(unsigned int b)
        {
        
            // b == 0 is undefined
        
        #if defined( \__GNUC__ )
        
            return __builtin_ctz(b);
        
        #else
        
            __asm bsf eax, b;
        
        #endif
        
        }
        
        
        unsigned int bsf(unsigned int b)
        {
        
            // b == 0 is undefined
        
            static const unsigned char btal[] = {0, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0};
        
            int i = 0;
            if(!(b & 0x0000ffff))
            {
                b>>=16;
                i+=16;
            }
        
            if(!(b & 0x000000ff))
            {
                b>>=8;
                i+=8;
            }
        
            if(!(b & 0x0000000f))
            {
                b>>=4;
                i+=4;
            }
        
            return i+btal[b&0x0f];
        
        }
        

        【讨论】:

          【解决方案10】:

          要获得最正确的设置位,可以使用以下表达式

          将变量视为 X

          x & ~(x - 1) 给出一个二进制数,它只包含设置的位,其余全为零

          例子

          x      = 0101
          x-1    = 0100
          ~(x-1) = 1011
          
          x & ~ (x - 1) = 0100
          

          现在不断将这个二进制数向右移动,直到数字为零,并计算移位次数,得出最右边的设置位数。

          【讨论】:

            【解决方案11】:

            如果您的数字是奇数还是偶数,您可以将算法的复杂性减半。 如果是偶数,则最低位是第一个。

            对于奇怪的情况,您可以实现这样的二进制搜索...

            【讨论】:

            • 好吧...你是对的,但这只是一个操作,它不涉及使用循环和移位,它只是一个初步检查,你可以选择行动或不行动!跨度>
            猜你喜欢
            • 2020-04-09
            • 2013-03-22
            • 1970-01-01
            • 2022-05-22
            • 2023-03-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-05-07
            相关资源
            最近更新 更多