【问题标题】:Position of least significant bit that is set设置的最低有效位的位置
【发布时间】:2010-10-19 21:56:49
【问题描述】:

我正在寻找一种有效的方法来确定以整数设置的最低有效位的位置,例如对于 0x0FF0,它将是 4。

一个简单的实现是这样的:

unsigned GetLowestBitPos(unsigned value)
{
   assert(value != 0); // handled separately

   unsigned pos = 0;
   while (!(value & 1))
   {
      value >>= 1;
      ++pos;
   }
   return pos;
}

任何想法如何挤出一些周期?

(注意:这个问题是给喜欢这些东西的人准备的,而不是让人们告诉我 xyzoptimization 是邪恶的。)

[编辑]感谢大家的想法!我也学到了一些其他的东西。酷!

【问题讨论】:

标签: c++ c optimization bit-manipulation


【解决方案1】:

Bit Twiddling Hacks 提供了一个极好的集合,呃,有点小技巧,附有性能/优化讨论。对于您的问题,我最喜欢的解决方案(来自该站点)是«乘法和查找»:

unsigned int v;  // find the number of trailing zeros in 32-bit v 
int r;           // result goes here
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 & -v) * 0x077CB531U)) >> 27];

有用的参考资料:

【讨论】:

  • 为什么投反对票?这可能是最快的实现,取决于乘法的速度。它当然是代码紧凑,并且 (v & -v) 技巧是每个人都应该学习和记住的。
  • +1 非常酷,与 if(X&Y) 操作相比,乘法操作的成本是多少?
  • 有谁知道它的性能与__builtin_ffsl 或ffsl 相比如何?
  • @Jim Balter,但与现代硬件上的乘法相比,模数非常慢。所以我不会称之为更好的解决方案。
  • 在我看来,值 0x01 和 0x00 都会导致数组中的值 0。显然,这个技巧将表明如果传入 0,则设置最低位!
【解决方案2】:

为什么不使用内置的ffs? (我从 Linux 中获取了一个手册页,但它的使用范围更广。)

ffs(3) - Linux 手册页

名称

ffs - 查找单词中的第一位设置

概要

#include <strings.h>
int ffs(int i);
#define _GNU_SOURCE
#include <string.h>
int ffsl(long int i);
int ffsll(long long int i);

说明

ffs() 函数返回在字 i 中设置的第一个(最低有效)位的位置。最低有效位是位置 1 和最高有效位置,例如32 或 64. 函数 ffsll() 和 ffsl() 做同样的事情,但参数大小可能不同。

返回值

这些函数返回第一个位设置的位置,如果 i 中没有设置位,则返回 0。

符合

4.3BSD,POSIX.1-2001。

注意事项

BSD 系统在&lt;string.h&gt; 中有一个原型。

【讨论】:

  • 仅供参考,这将在可用时编译为相应的汇编命令。
【解决方案3】:

有一个 x86 汇编指令 (bsf) 可以做到这一点。 :)

更优化?!

旁注:

此级别的优化本质上是依赖于架构的。今天的处理器过于复杂(在分支预测、缓存未命中、流水线方面),很难预测哪些代码在哪种架构上执行得更快。将操作从 32 减少到 9 或类似的操作甚至可能会降低某些架构的性能。单一架构上的优化代码可能会导致另一个架构上的代码更差。我认为你要么针对特定的 CPU 进行优化,要么保持原样,让编译器选择它认为更好的东西。

【讨论】:

  • @dwc:我明白,但我认为这个条款:“任何想法如何挤出一些周期?”让这样的答案完全可以接受!
  • +1 由于字节顺序,他的答案必然依赖于他的架构,因此下降到汇编指令是一个完全有效的答案。
  • +1 聪明的回答,是的,它不是 C 或 C++,但它是适合这项工作的工具。
  • 等等,没关系。整数的实际值在这里无关紧要。对不起。
  • @Bastian:如果操作数为零,他们设置 ZF=1。
【解决方案4】:

大多数现代架构都会有一些指令用于查找最低设置位或最高设置位的位置,或计算前导零的数量等。

如果你有这门课的任何一条指令,你可以廉价地模仿其他的。

花点时间在纸上完成它,并意识到x &amp; (x-1) 将清除 x 中的最低设置位,而( x &amp; ~(x-1) ) 将只返回最低设置位,而与架构、字长等无关。知道了这一点,如果没有明确的指令,使用硬件计数前导零/最高设置位来查找最低设置位是微不足道的。

如果根本没有相关的硬件支持,则可以将给定here 或Bit Twiddling Hacks 页面上的一个计数前导零的乘法和查找实现简单地转换为给出最低设置位使用上述身份,并具有无分支的优势。

【讨论】:

    【解决方案5】:

    这是一个比较几种解决方案的基准:

    我的机器是 Intel i530 (2.9 GHz),运行 Windows 7 64 位。我用 32 位版本的 MinGW 编译。

    $ gcc --version
    gcc.exe (GCC) 4.7.2
    
    $ gcc bench.c -o bench.exe -std=c99 -Wall -O2
    $ bench
    Naive loop.         Time = 2.91  (Original questioner)
    De Bruijn multiply. Time = 1.16  (Tykhyy)
    Lookup table.       Time = 0.36  (Andrew Grant)
    FFS instruction.    Time = 0.90  (ephemient)
    Branch free mask.   Time = 3.48  (Dan / Jim Balter)
    Double hack.        Time = 3.41  (DocMax)
    
    $ gcc bench.c -o bench.exe -std=c99 -Wall -O2 -march=native
    $ bench
    Naive loop.         Time = 2.92
    De Bruijn multiply. Time = 0.47
    Lookup table.       Time = 0.35
    FFS instruction.    Time = 0.68
    Branch free mask.   Time = 3.49
    Double hack.        Time = 0.92
    

    我的代码:

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    
    #define ARRAY_SIZE 65536
    #define NUM_ITERS 5000  // Number of times to process array
    
    
    int find_first_bits_naive_loop(unsigned nums[ARRAY_SIZE])
    {
        int total = 0; // Prevent compiler from optimizing out the code
        for (int j = 0; j < NUM_ITERS; j++) {
            for (int i = 0; i < ARRAY_SIZE; i++) {
                unsigned value = nums[i];
                if (value == 0)
                    continue;
                unsigned pos = 0;
                while (!(value & 1))
                {
                    value >>= 1;
                    ++pos;
                }
                total += pos + 1;
            }
        }
        
        return total;
    }
    
    
    int find_first_bits_de_bruijn(unsigned nums[ARRAY_SIZE])
    {
        static const int MultiplyDeBruijnBitPosition[32] = 
        {
           1, 2, 29, 3, 30, 15, 25, 4, 31, 23, 21, 16, 26, 18, 5, 9, 
           32, 28, 14, 24, 22, 20, 17, 8, 27, 13, 19, 7, 12, 6, 11, 10
        };
          
        int total = 0; // Prevent compiler from optimizing out the code
        for (int j = 0; j < NUM_ITERS; j++) {
            for (int i = 0; i < ARRAY_SIZE; i++) {
                unsigned int c = nums[i];
                total += MultiplyDeBruijnBitPosition[((unsigned)((c & -c) * 0x077CB531U)) >> 27];
            }
        }
        
        return total;
    }
    
    
    unsigned char lowestBitTable[256];
    int get_lowest_set_bit(unsigned num) {
        unsigned mask = 1;
        for (int cnt = 1; cnt <= 32; cnt++, mask <<= 1) {
            if (num & mask) {
                return cnt;
            }
        }
        
        return 0;
    }
    int find_first_bits_lookup_table(unsigned nums[ARRAY_SIZE])
    {
        int total = 0; // Prevent compiler from optimizing out the code
        for (int j = 0; j < NUM_ITERS; j++) {
            for (int i = 0; i < ARRAY_SIZE; i++) {
                unsigned int value = nums[i];
                // note that order to check indices will depend whether you are on a big 
                // or little endian machine. This is for little-endian
                unsigned char *bytes = (unsigned char *)&value;
                if (bytes[0])
                    total += lowestBitTable[bytes[0]];
                else if (bytes[1])
                  total += lowestBitTable[bytes[1]] + 8;
                else if (bytes[2])
                  total += lowestBitTable[bytes[2]] + 16;
                else
                  total += lowestBitTable[bytes[3]] + 24;
            }
        }
        
        return total;
    }
    
    
    int find_first_bits_ffs_instruction(unsigned nums[ARRAY_SIZE])
    {
        int total = 0; // Prevent compiler from optimizing out the code
        for (int j = 0; j < NUM_ITERS; j++) {
            for (int i = 0; i < ARRAY_SIZE; i++) {
                total +=  __builtin_ffs(nums[i]);
            }
        }
        
        return total;
    }
    
    
    int find_first_bits_branch_free_mask(unsigned nums[ARRAY_SIZE])
    {
        int total = 0; // Prevent compiler from optimizing out the code
        for (int j = 0; j < NUM_ITERS; j++) {
            for (int i = 0; i < ARRAY_SIZE; i++) {
                unsigned value = nums[i];
                int i16 = !(value & 0xffff) << 4;
                value >>= i16;
    
                int i8 = !(value & 0xff) << 3;
                value >>= i8;
    
                int i4 = !(value & 0xf) << 2;
                value >>= i4;
    
                int i2 = !(value & 0x3) << 1;
                value >>= i2;
    
                int i1 = !(value & 0x1);
    
                int i0 = (value >> i1) & 1? 0 : -32;
    
                total += i16 + i8 + i4 + i2 + i1 + i0 + 1;
            }
        }
        
        return total;
    }
    
    
    int find_first_bits_double_hack(unsigned nums[ARRAY_SIZE])
    {
        int total = 0; // Prevent compiler from optimizing out the code
        for (int j = 0; j < NUM_ITERS; j++) {
            for (int i = 0; i < ARRAY_SIZE; i++) {
                unsigned value = nums[i];
                double d = value ^ (value - !!value); 
                total += (((int*)&d)[1]>>20)-1022; 
            }
        }
        
        return total;
    }
    
    
    int main() {
        unsigned nums[ARRAY_SIZE];
        for (int i = 0; i < ARRAY_SIZE; i++) {
            nums[i] = rand() + (rand() << 15);
        }
        
        for (int i = 0; i < 256; i++) {
            lowestBitTable[i] = get_lowest_set_bit(i);
        }
        
        
        clock_t start_time, end_time;
        int result;
        
        start_time = clock();
        result = find_first_bits_naive_loop(nums);
        end_time = clock();
        printf("Naive loop.         Time = %.2f, result = %d\n", 
            (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);
    
        start_time = clock();
        result = find_first_bits_de_bruijn(nums);
        end_time = clock();
        printf("De Bruijn multiply. Time = %.2f, result = %d\n", 
            (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);
    
        start_time = clock();
        result = find_first_bits_lookup_table(nums);
        end_time = clock();
        printf("Lookup table.       Time = %.2f, result = %d\n", 
            (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);
    
        start_time = clock();
        result = find_first_bits_ffs_instruction(nums);
        end_time = clock();
        printf("FFS instruction.    Time = %.2f, result = %d\n", 
            (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);
    
        start_time = clock();
        result = find_first_bits_branch_free_mask(nums);
        end_time = clock();
        printf("Branch free mask.   Time = %.2f, result = %d\n", 
            (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);
    
        start_time = clock();
        result = find_first_bits_double_hack(nums);
        end_time = clock();
        printf("Double hack.        Time = %.2f, result = %d\n", 
            (end_time - start_time) / (double)(CLOCKS_PER_SEC), result);
    }
    

    【讨论】:

    • de Bruijn 和查找的基准可能会产生误导 - 像这样处于紧密循环中,在第一次操作之后,每种类型的查找表将被固定在 L1 缓存中,直到最后一个循环之后。这不太可能与实际使用情况相符。
    • 对于低字节为零的输入,由于指针转换,它通过存储/重新加载而不是移位来获取更高字节。 (完全不必要的顺便说一句,并且使它依赖于字节序,不像移位不会)。无论如何,不​​仅由于热缓存,微基准测试不切实际,而且它还启动了分支预测器并测试了预测非常好的输入并减少了 LUT 的工作量。许多实际用例的结果分布更均匀,而不是输入。
    • 不幸的是,您的 FFS 循环由于 BSF 指令中的错误依赖而减慢了速度,而您的老旧编译器无法避免这种错误(but newer gcc should, same for popcnt/lzcnt/tzcnt.BSF 对其输出有错误依赖(因为input=0 时的实际行为是保持输出不变)。不幸的是,gcc 通过不清除循环迭代之间的寄存器将其变成循环携带的依赖项。因此循环应该每 5 个周期运行一次,在 BSF(3) 上成为瓶颈+ CMOV(2) 延迟。
    • 您的基准测试发现 LUT 的吞吐量几乎是 FFS 方法的两倍,这与我的静态分析预测非常吻合 :)。请注意,您测量的是吞吐量,而不是延迟,因为循环中唯一的串行依赖项是总和。 没有错误的依赖,ffs() 应该有一个每个时钟的吞吐量(3 微指令,1 用于 BSF,2 用于 CMOV,它们可以在不同的端口上运行)。在相同的循环开销下,7 个 ALU 微指令可以(在您的 CPU 上)以每个时钟 3 个运行。开销占主导地位! 来源:agner.org/optimize
    • 是的,如果bsf ecx, [ebx+edx*4] 没有将ecx 视为必须等待的输入,则乱序执行可能会与循环的多次迭代重叠。 (ECX 最后由前一个迭代的 CMOV 编写)。但是 CPU 确实以这种方式运行,以实现“如果源为零,则保持 dest 不变”行为(因此它不像 TZCNT 那样真正是错误的 dep;需要数据依赖性,因为假设没有分支 + 推测执行输入非零)。我们可以通过在bsf 之前添加xor ecx,ecx 来克服它,以打破对ECX 的依赖。
    【解决方案6】:

    最快(非内在/非汇编)解决方案是找到最低字节,然后在 256 条目查找表中使用该字节。这为您提供了 4 条条件指令的最坏情况和 1 条的最佳情况。这不仅是指令数量最少,而且分支数量最少,这在现代硬件上非常重要。

    您的表(256 个 8 位条目)应包含 0-255 范围内每个数字的 LSB 索引。您检查值的每个字节并找到最低的非零字节,然后使用该值查找实际索引。

    这确实需要 256 字节的内存,但如果此功能的速度如此重要,那么 256 字节就值得了,

    例如

    byte lowestBitTable[256] = {
    .... // left as an exercise for the reader to generate
    };
    
    unsigned GetLowestBitPos(unsigned value)
    {
      // note that order to check indices will depend whether you are on a big 
      // or little endian machine. This is for little-endian
      byte* bytes = (byte*)value;
      if (bytes[0])
        return lowestBitTable[bytes[0]];
      else if (bytes[1])
          return lowestBitTable[bytes[1]] + 8;
      else if (bytes[2])
          return lowestBitTable[bytes[2]] + 16;
      else
          return lowestBitTable[bytes[3]] + 24;  
    }
    

    【讨论】:

    • 这实际上是三个条件的最坏情况 :) 但是,是的,这是最快的方法(通常是人们在这样的面试问题中寻找的方法)。
    • 你不想在某个地方添加 +8、+16、+24 吗?
    • 任何查找表都会增加缓存未命中的机会,并可能导致内存访问成本比执行指令高几个数量级。
    • 我什至会使用位移位(每次移动 8 位)。然后可以完全使用寄存器来完成。使用指针,你将不得不访问内存。
    • 合理的解决方案,但是在查找表不在缓存中的可能性(可以解决,正如所指出的)和分支数量(潜在的分支预测错误)之间,我更喜欢乘法- and-lookup 解决方案(没有分支,较小的查找表)。当然,如果您可以使用内在函数或内联汇编,它们可能是更好的选择。不过,这个解决方案还不错。
    【解决方案7】:

    只要你有一个分支,CPU 就必须猜测将采用哪个分支。指令管道加载了引导猜测路径的指令。如果 CPU 猜错了,那么指令管道就会被刷新,并且必须加载另一个分支。

    考虑顶部的简单 while 循环。猜测将留在循环内。当它离开循环时,它至少会出错一次。这将刷新指令管道。这种行为比猜测它会离开循环要好一些,在这种情况下,它会在每次迭代时刷新指令管道。

    从一种处理器类型到另一种处理器,丢失的 CPU 周期数量差异很大。但是您可以预期会丢失 20 到 150 个 CPU 周期。

    下一个更糟糕的组是您认为您将通过将值拆分为更小的部分并添加更多分支来节省一些迭代。这些分支中的每一个都增加了一个额外的机会来刷新指令管道并花费另外 20 到 150 个时钟周期。

    让我们考虑一下在表中查找值时会发生什么。有可能该值当前不在缓存中,至少不是第一次调用您的函数时。这意味着在从缓存加载值时 CPU 会停止。同样,这因一台机器而异。新的英特尔芯片实际上将此作为交换线程的机会,而当前线程正在等待缓存加载完成。这很容易比指令管道刷新更昂贵,但是如果您多次执行此操作,它可能只会发生一次。

    显然,最快的恒定时间解决方案是涉及确定性数学的解决方案。一个纯粹而优雅的解决方案。

    如果这已经涵盖,我深表歉意。

    我使用的每个编译器,除了 XCODE AFAIK,都有编译器内部函数用于正向位扫描和反向位扫描。这些将在大多数硬件上编译成一条汇编指令,没有缓存未命中,没有分支未命中预测,也没有其他程序员产生的绊脚石。

    对于 Microsoft 编译器,请使用 _BitScanForward 和 _BitScanReverse。
    对于 GCC,请使用 __builtin_ffs、__builtin_clz、__builtin_ctz。

    此外,如果您对所讨论的主题没有足够的了解,请不要发布答案并可能误导新人。

    对不起,我完全忘了提供解决方案。这是我在 IPAD 上使用的代码,它没有针对该任务的汇编级指令:

    unsigned BitScanLow_BranchFree(unsigned value)
    {
        bool bwl = (value & 0x0000ffff) == 0;
        unsigned I1 = (bwl * 15);
        value = (value >> I1) & 0x0000ffff;
        
        bool bbl = (value & 0x00ff00ff) == 0;
        unsigned I2 = (bbl * 7);
        value = (value >> I2) & 0x00ff00ff;
    
        bool bnl = (value & 0x0f0f0f0f) == 0;
        unsigned I3 = (bnl * 3);
        value = (value >> I3) & 0x0f0f0f0f;
    
        bool bsl = (value & 0x33333333) == 0;
        unsigned I4 = (bsl * 1);
        value = (value >> I4) & 0x33333333;
    
        unsigned result = value + I1 + I2 + I3 + I4 - 1;
    
        return result;
    }
    

    这里要理解的是,昂贵的不是比较,而是比较之后发生的分支。在这种情况下,比较被强制为 0 或 1 与 .. == 0 的值,并且结果用于组合在分支的任一侧发生的数学运算。

    编辑:

    上面的代码完全被破坏了。这段代码可以工作并且仍然是无分支的(如果优化的话):

    int BitScanLow_BranchFree(ui value)
    {
        int i16 = !(value & 0xffff) << 4;
        value >>= i16;
    
        int i8 = !(value & 0xff) << 3;
        value >>= i8;
    
        int i4 = !(value & 0xf) << 2;
        value >>= i4;
    
        int i2 = !(value & 0x3) << 1;
        value >>= i2;
    
        int i1 = !(value & 0x1);
    
        int i0 = (value >> i1) & 1? 0 : -32;
    
        return i16 + i8 + i4 + i2 + i1 + i0;
    }
    

    如果给定 0,则返回 -1。如果您不关心 0 或乐于为 0 得到 31,请删除 i0 计算,节省大量时间。

    【讨论】:

    • 我为你修好了。请务必测试您发布的内容。
    • 当它包含一个三元运算符时,你怎么能称它为“无分支”?
    • 这是一个有条件的移动。一条汇编语言指令,它将两个可能的值都作为参数,并根据条件的评估执行 mov 操作。因此是“无分支”。没有跳转到另一个未知或可能不正确的地址。
    • FWIW gcc 即使在-O3 godbolt.org/z/gcsUHd 上也会生成分支
    【解决方案8】:

    受this similar post 的启发,我提供了以下内容:

    unsigned GetLowestBitPos(unsigned value)
    {
       double d = value ^ (value - !!value); 
       return (((int*)&d)[1]>>20)-1023; 
    }
    

    优点:

    • 没有循环
    • 没有分支
    • 以恒定时间运行
    • 通过返回一个超出范围的结果来处理 value=0
    • 只有两行代码

    缺点:

    • 假定编码的字节序很小(可以通过更改常量来修复)
    • 假设 double 是一个实数 *8 IEEE 浮点数 (IEEE 754)

    更新: 正如 cmets 中所指出的,联合是一种更简洁的实现(至少对于 C 而言),并且看起来像:

    unsigned GetLowestBitPos(unsigned value)
    {
        union {
            int i[2];
            double d;
        } temp = { .d = value ^ (value - !!value) };
        return (temp.i[1] >> 20) - 1023;
    }
    

    这假定所有内容都使用 32 位整数和 little-endian 存储(想想 x86 处理器)。

    【讨论】:

    • 有趣 - 我仍然害怕在位算术中使用双精度数,但我会记住这一点
    • 使用 frexp() 可能会使它更便携
    • 通过指针转换的类型双关语在 C 或 C++ 中是不安全的。在 C++ 中使用 memcpy,或在 C 中使用联合。(如果您的编译器保证它是安全的,则使用 C++ 中的联合。例如,C++ 的 GNU 扩展(许多编译器支持)确实保证联合类型双关是安全的。)跨度>
    • 较旧的 gcc 还使用联合而不是指针转换来编写更好的代码:它直接从 FP reg (xmm0) 移动到 rax(使用 movq)而不是存储/重新加载。较新的 gcc 和 clang 两种方式都使用 movq。有关联合版本,请参阅 godbolt.org/g/x7JBiL。您是否有意进行 20 的算术移位?您的假设还应该列出 int 是 int32_t,并且有符号右移是算术移位(在 C++ 中它是实现定义的)
    • 另外顺便说一句,Visual Studio(至少 2013 年)也使用 test/setcc/sub 方法。我自己更喜欢 cmp/adc。
    【解决方案9】:

    可以通过少于 32 次操作的最坏情况来完成:

    原理:检查 2 位或更多位与检查 1 位一样有效。

    因此,例如,没有什么可以阻止您首先检查它的哪个分组,然后检查该组中从小到大的每一位。

    所以...
    如果您一次检查 2 位,那么在最坏的情况下,您有 (Nbits/2) + 1 次检查。
    如果您一次检查 3 位,则在最坏的情况下 (Nbits/3) + 2 次检查。
    ...

    最好是 4 人一组进行检查。在最坏的情况下,这需要 11 次操作而不是 32 次。

    如果您使用这种分组思想,最好的情况是从算法的 1 次检查到 2 次检查。但是,在最坏的情况下节省额外的 1 次检查是值得的。

    注意:我将其全部写出来而不是使用循环,因为这样更有效。

    int getLowestBitPos(unsigned int value)
    {
        //Group 1: Bits 0-3
        if(value&0xf)
        {
            if(value&0x1)
                return 0;
            else if(value&0x2)
                return 1;
            else if(value&0x4)
                return 2;
            else
                return 3;
        }
    
        //Group 2: Bits 4-7
        if(value&0xf0)
        {
            if(value&0x10)
                return 4;
            else if(value&0x20)
                return 5;
            else if(value&0x40)
                return 6;
            else
                return 7;
        }
    
        //Group 3: Bits 8-11
        if(value&0xf00)
        {
            if(value&0x100)
                return 8;
            else if(value&0x200)
                return 9;
            else if(value&0x400)
                return 10;
            else
                return 11;
        }
    
        //Group 4: Bits 12-15
        if(value&0xf000)
        {
            if(value&0x1000)
                return 12;
            else if(value&0x2000)
                return 13;
            else if(value&0x4000)
                return 14;
            else
                return 15;
        }
    
        //Group 5: Bits 16-19
        if(value&0xf0000)
        {
            if(value&0x10000)
                return 16;
            else if(value&0x20000)
                return 17;
            else if(value&0x40000)
                return 18;
            else
                return 19;
        }
    
        //Group 6: Bits 20-23
        if(value&0xf00000)
        {
            if(value&0x100000)
                return 20;
            else if(value&0x200000)
                return 21;
            else if(value&0x400000)
                return 22;
            else
                return 23;
        }
    
        //Group 7: Bits 24-27
        if(value&0xf000000)
        {
            if(value&0x1000000)
                return 24;
            else if(value&0x2000000)
                return 25;
            else if(value&0x4000000)
                return 26;
            else
                return 27;
        }
    
        //Group 8: Bits 28-31
        if(value&0xf0000000)
        {
            if(value&0x10000000)
                return 28;
            else if(value&0x20000000)
                return 29;
            else if(value&0x40000000)
                return 30;
            else
                return 31;
        }
    
        return -1;
    }
    

    【讨论】:

    • +1 来自我。这不是最快的,但比原来的要快,这就是重点......
    • @onebyone.livejournal.com:即使代码中存在错误,分组的概念也是我试图理解的重点。实际的代码示例并不重要,可以做得更紧凑但效率更低。
    • 我只是想知道我的答案是否真的有不好的部分,或者人们是否不喜欢我完整地写出来?
    • @onebyone.livejournal.com:当您比较两种算法时,您应该按原样比较它们,而不是假设优化阶段会神奇地改变它们。我也从未声称我的算法“更快”。只是它的操作更少。
    • @onebyone.livejournal.com: ...我不需要分析上面的代码就知道它是更少的操作。我可以清楚地看到这一点。我从未提出任何需要分析的声明。
    【解决方案10】:

    11 年后我们终于有了:countr_zero

    干得好 C++20

    【讨论】:

      【解决方案11】:

      为什么不使用binary search?这将始终在 5 次操作后完成(假设 int 大小为 4 个字节):

      if (0x0000FFFF & value) {
          if (0x000000FF & value) {
              if (0x0000000F & value) {
                  if (0x00000003 & value) {
                      if (0x00000001 & value) {
                          return 1;
                      } else {
                          return 2;
                      }
                  } else {
                      if (0x0000004 & value) {
                          return 3;
                      } else {
                          return 4;
                      }
                  }
              } else { ...
          } else { ...
      } else { ...
      

      【讨论】:

      • +1 这与我的回答非常相似。最佳情况运行时间比我的建议差,但最坏情况运行时间更好。
      【解决方案12】:

      在“编程的艺术,第 4 部分”中使用“魔术面具”发现了这个巧妙的技巧,对于 n 位数,它在 O(log(n)) 时间内完成。 [带有 log(n) 额外空间]。检查设置位的典型解决方案是 O(n) 或需要 O(n) 额外空间来查找表,因此这是一个很好的折衷方案。

      魔术面具:

      m0 = (...............01010101)  
      m1 = (...............00110011)
      m2 = (...............00001111)  
      m3 = (.......0000000011111111)
      ....
      

      关键思想: x = 1 * [(x & m0) = 0] + 2 * [(x & m1) = 0] + 4 * [(x & m2) = 0] + ... 中的尾随零数

      int lastSetBitPos(const uint64_t x) {
          if (x == 0)  return -1;
      
          //For 64 bit number, log2(64)-1, ie; 5 masks needed
          int steps = log2(sizeof(x) * 8); assert(steps == 6);
          //magic masks
          uint64_t m[] = { 0x5555555555555555, //     .... 010101
                           0x3333333333333333, //     .....110011
                           0x0f0f0f0f0f0f0f0f, //     ...00001111
                           0x00ff00ff00ff00ff, //0000000011111111 
                           0x0000ffff0000ffff, 
                           0x00000000ffffffff };
      
          //Firstly extract only the last set bit
          uint64_t y = x & -x;
      
          int trailZeros = 0, i = 0 , factor = 0;
          while (i < steps) {
              factor = ((y & m[i]) == 0 ) ? 1 : 0;
              trailZeros += factor * pow(2,i);
              ++i;
          }
          return (trailZeros+1);
      }
      

      【讨论】:

      • 我看到的一个问题是 log2() 是一项昂贵的操作。使用查找表进行该计算几乎绝对值得。
      【解决方案13】:

      另一种方法(模除法和查找)值得在这里特别提及@anton-tykhyy 提供的link。此方法在性能上与 DeBruijn 乘法和查找方法非常相似,但有细微但重要的区别。

      模除法和查找

       unsigned int v;  // find the number of trailing zeros in v
          int r;           // put the result in r
          static const int Mod37BitPosition[] = // map a bit value mod 37 to its position
          {
            32, 0, 1, 26, 2, 23, 27, 0, 3, 16, 24, 30, 28, 11, 0, 13, 4,
            7, 17, 0, 25, 22, 31, 15, 29, 10, 12, 6, 0, 21, 14, 9, 5,
            20, 8, 19, 18
          };
          r = Mod37BitPosition[(-v & v) % 37];
      

      模除法和查找方法为 v=0x00000000 和 v=FFFFFFFF 返回不同的值,而 DeBruijn 乘法和查找方法在两个输入上都返回零。

      测试:-

      unsigned int n1=0x00000000, n2=0xFFFFFFFF;
      
      MultiplyDeBruijnBitPosition[((unsigned int )((n1 & -n1) * 0x077CB531U)) >> 27]); /* returns 0 */
      MultiplyDeBruijnBitPosition[((unsigned int )((n2 & -n2) * 0x077CB531U)) >> 27]); /* returns 0 */
      Mod37BitPosition[(((-(n1) & (n1))) % 37)]); /* returns 32 */
      Mod37BitPosition[(((-(n2) & (n2))) % 37)]); /* returns 0 */
      

      【讨论】:

      • mod 很慢。相反,您可以使用原始的乘法查找方法并从r 中减去!v 来处理边缘情况。
      • @EitanT 一个优化器很可能将这个 mod 转换成一个快速的乘法,就像黑客们喜欢的那样
      【解决方案14】:

      根据Chess Programming BitScan page 和我自己的测量,减法和异或比求反和掩码更快。

      (请注意,如果您要计算0 中的尾随零,我拥有的方法返回63,而否定和掩码返回0。)

      这是一个 64 位的减法和异或:

      unsigned long v;  // find the number of trailing zeros in 64-bit v 
      int r;            // result goes here
      static const int MultiplyDeBruijnBitPosition[64] = 
      {
        0, 47, 1, 56, 48, 27, 2, 60, 57, 49, 41, 37, 28, 16, 3, 61,
        54, 58, 35, 52, 50, 42, 21, 44, 38, 32, 29, 23, 17, 11, 4, 62,
        46, 55, 26, 59, 40, 36, 15, 53, 34, 51, 20, 43, 31, 22, 10, 45,
        25, 39, 14, 33, 19, 30, 9, 24, 13, 18, 8, 12, 7, 6, 5, 63
      };
      r = MultiplyDeBruijnBitPosition[((uint32_t)((v ^ (v-1)) * 0x03F79D71B4CB0A89U)) >> 58];
      

      作为参考,这里是一个 64 位版本的 negate 和 mask 方法:

      unsigned long v;  // find the number of trailing zeros in 64-bit v 
      int r;            // result goes here
      static const int MultiplyDeBruijnBitPosition[64] = 
      {
        0, 1, 48, 2, 57, 49, 28, 3, 61, 58, 50, 42, 38, 29, 17, 4,
        62, 55, 59, 36, 53, 51, 43, 22, 45, 39, 33, 30, 24, 18, 12, 5,
        63, 47, 56, 27, 60, 41, 37, 16, 54, 35, 52, 21, 44, 32, 23, 11,
        46, 26, 40, 15, 34, 20, 31, 10, 25, 14, 19, 9, 13, 8, 7, 6
      };
      r = MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x03F79D71B4CB0A89U)) >> 58];
      

      【讨论】:

      • 此(v ^ (v-1)) 作品提供v != 0。在v == 0 的情况下,它返回 0xFF....FF 而(v &amp; -v) 给出零(顺便说一句,这也是错误的,但至少它会导致合理的结果)。
      • @CiaPan:这是一个很好的观点,我会提到它。我猜有一个不同的 De Bruijn 数字可以通过将 0 放入第 63 个索引来解决此问题。
      • 呃,这不是问题所在。 0 和 0x8000000000000000 在v ^ (v-1) 之后都会导致 0xFFFFFFFFFFFFFFFF,因此无法区分它们。在我的场景中,永远不会输入零。
      【解决方案15】:
      unsigned GetLowestBitPos(unsigned value)
      {
          if (value & 1) return 1;
          if (value & 2) return 2;
          if (value & 4) return 3;
          if (value & 8) return 4;
          if (value & 16) return 5;
          if (value & 32) return 6;
          if (value & 64) return 7;
          if (value & 128) return 8;
          if (value & 256) return 9;
          if (value & 512) return 10;
          if (value & 1024) return 11;
          if (value & 2048) return 12;
          if (value & 4096) return 13;
          if (value & 8192) return 14;
          if (value & 16384) return 15;
          if (value & 32768) return 16;
          if (value & 65536) return 17;
          if (value & 131072) return 18;
          if (value & 262144) return 19;
          if (value & 524288) return 20;
          if (value & 1048576) return 21;
          if (value & 2097152) return 22;
          if (value & 4194304) return 23;
          if (value & 8388608) return 24;
          if (value & 16777216) return 25;
          if (value & 33554432) return 26;
          if (value & 67108864) return 27;
          if (value & 134217728) return 28;
          if (value & 268435456) return 29;
          if (value & 536870912) return 30;
          return 31;
      }
      

      所有数字的 50% 将在第一行代码返回。

      所有数字的 75% 将在前 2 行代码中返回。

      87% 的数字将在前 3 行代码中返回。

      94% 的数字将在前 4 行代码中返回。

      97% 的数字将在前 5 行代码中返回。

      等等

      我认为那些抱怨这段代码的最坏情况是多么低效的人不明白这种情况会发生多么罕见。

      【讨论】:

      • 还有 32 个分支错误预测的最坏情况 :)
      • 这至少不能做成一个开关吗...?
      • “这不能至少做成一个开关吗……?”在暗示可能之前,您是否尝试过这样做?从什么时候开始你可以对开关的情况进行计算?这是一个查找表,而不是一个类。
      【解决方案16】:

      您可以检查是否设置了任何低位。如果是这样,请查看剩余位的低位。例如:

      32bit int - 检查前 16 个是否已设置。 如果是这样,请检查是否设置了前 8 个中的任何一个。 如果是这样,......

      如果没有,请检查是否设置了任何前16位..

      本质上是二分查找。

      【讨论】:

        【解决方案17】:

        请参阅我的回答 here 以了解如何使用单个 x86 指令执行此操作,除了要找到 最低 有效设置位,您需要 BSF(“位扫描向前” ) 指令而不是那里描述的BSR。

        【讨论】:

          【解决方案18】:

          另一种解决方案,可能不是最快的,但似乎相当不错。
          至少它没有分支。 ;)

          uint32 x = ...;  // 0x00000001  0x0405a0c0  0x00602000
          x |= x <<  1;    // 0x00000003  0x0c0fe1c0  0x00e06000
          x |= x <<  2;    // 0x0000000f  0x3c3fe7c0  0x03e1e000
          x |= x <<  4;    // 0x000000ff  0xffffffc0  0x3fffe000
          x |= x <<  8;    // 0x0000ffff  0xffffffc0  0xffffe000
          x |= x << 16;    // 0xffffffff  0xffffffc0  0xffffe000
          
          // now x is filled with '1' from the least significant '1' to bit 31
          
          x = ~x;          // 0x00000000  0x0000003f  0x00001fff
          
          // now we have 1's below the original least significant 1
          // let's count them
          
          x = x & 0x55555555 + (x >>  1) & 0x55555555;
                           // 0x00000000  0x0000002a  0x00001aaa
          
          x = x & 0x33333333 + (x >>  2) & 0x33333333;
                           // 0x00000000  0x00000024  0x00001444
          
          x = x & 0x0f0f0f0f + (x >>  4) & 0x0f0f0f0f;
                           // 0x00000000  0x00000006  0x00000508
          
          x = x & 0x00ff00ff + (x >>  8) & 0x00ff00ff;
                           // 0x00000000  0x00000006  0x0000000d
          
          x = x & 0x0000ffff + (x >> 16) & 0x0000ffff;
                           // 0x00000000  0x00000006  0x0000000d
          // least sign.bit pos. was:  0           6          13
          

          【讨论】:

          • 要获取从最低有效 1 到 LSB 的所有 1s,请改用 ((x &amp; -x) - 1) &lt;&lt; 1
          • 更快的方法:x ^ (x-1)
          【解决方案19】:

          如果 C++11 可供您使用,编译器有时可以为您完成任务 :)

          constexpr std::uint64_t lssb(const std::uint64_t value)
          {
              return !value ? 0 : (value % 2 ? 1 : lssb(value >> 1) + 1);
          }
          

          结果是从 1 开始的索引。

          【讨论】:

          • 聪明,但是当输入不是编译时常量时,它会编译成灾难性的错误程序集。 godbolt.org/g/7ajMyT。 (使用 gcc 对位进行哑循环,或使用 clang 进行实际递归函数调用。) gcc/clang 可以在编译时评估 ffs(),因此您不需要使用它来进行常量传播工作。 (当然,你必须避免使用 inline-asm。)如果你真的需要像 C++11 constexpr 一样工作的东西,你仍然可以使用 GNU C __builtin_ffs。
          【解决方案20】:

          这是关于@Anton Tykhyy 的回答

          这是我的 C++11 constexpr 实现,通过将 64 位结果截断为 32 位来消除强制转换并删除 VC++17 上的警告:

          constexpr uint32_t DeBruijnSequence[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
          };
          constexpr uint32_t ffs ( uint32_t value )
          {
              return  DeBruijnSequence[ 
                  (( ( value & ( -static_cast<int32_t>(value) ) ) * 0x077CB531ULL ) & 0xFFFFFFFF)
                      >> 27];
          }
          

          要解决 0x1 和 0x0 都返回 0 的问题,您可以这样做:

          constexpr uint32_t ffs ( uint32_t value )
          {
              return (!value) ? 32 : DeBruijnSequence[ 
                  (( ( value & ( -static_cast<int32_t>(value) ) ) * 0x077CB531ULL ) & 0xFFFFFFFF)
                      >> 27];
          }
          

          但如果编译器不能或不会预处理调用,它将为计算添加几个周期。

          最后,如果有兴趣,这里有一个静态断言列表,用于检查代码是否符合预期:

          static_assert (ffs(0x1) == 0, "Find First Bit Set Failure.");
          static_assert (ffs(0x2) == 1, "Find First Bit Set Failure.");
          static_assert (ffs(0x4) == 2, "Find First Bit Set Failure.");
          static_assert (ffs(0x8) == 3, "Find First Bit Set Failure.");
          static_assert (ffs(0x10) == 4, "Find First Bit Set Failure.");
          static_assert (ffs(0x20) == 5, "Find First Bit Set Failure.");
          static_assert (ffs(0x40) == 6, "Find First Bit Set Failure.");
          static_assert (ffs(0x80) == 7, "Find First Bit Set Failure.");
          static_assert (ffs(0x100) == 8, "Find First Bit Set Failure.");
          static_assert (ffs(0x200) == 9, "Find First Bit Set Failure.");
          static_assert (ffs(0x400) == 10, "Find First Bit Set Failure.");
          static_assert (ffs(0x800) == 11, "Find First Bit Set Failure.");
          static_assert (ffs(0x1000) == 12, "Find First Bit Set Failure.");
          static_assert (ffs(0x2000) == 13, "Find First Bit Set Failure.");
          static_assert (ffs(0x4000) == 14, "Find First Bit Set Failure.");
          static_assert (ffs(0x8000) == 15, "Find First Bit Set Failure.");
          static_assert (ffs(0x10000) == 16, "Find First Bit Set Failure.");
          static_assert (ffs(0x20000) == 17, "Find First Bit Set Failure.");
          static_assert (ffs(0x40000) == 18, "Find First Bit Set Failure.");
          static_assert (ffs(0x80000) == 19, "Find First Bit Set Failure.");
          static_assert (ffs(0x100000) == 20, "Find First Bit Set Failure.");
          static_assert (ffs(0x200000) == 21, "Find First Bit Set Failure.");
          static_assert (ffs(0x400000) == 22, "Find First Bit Set Failure.");
          static_assert (ffs(0x800000) == 23, "Find First Bit Set Failure.");
          static_assert (ffs(0x1000000) == 24, "Find First Bit Set Failure.");
          static_assert (ffs(0x2000000) == 25, "Find First Bit Set Failure.");
          static_assert (ffs(0x4000000) == 26, "Find First Bit Set Failure.");
          static_assert (ffs(0x8000000) == 27, "Find First Bit Set Failure.");
          static_assert (ffs(0x10000000) == 28, "Find First Bit Set Failure.");
          static_assert (ffs(0x20000000) == 29, "Find First Bit Set Failure.");
          static_assert (ffs(0x40000000) == 30, "Find First Bit Set Failure.");
          static_assert (ffs(0x80000000) == 31, "Find First Bit Set Failure.");
          

          【讨论】:

            【解决方案21】:

            这是一个简单的替代方案,尽管查找日志的成本有点高。

            if(n == 0)
              return 0;
            return log2(n & -n)+1;   //Assuming the bit index starts from 1
            

            【讨论】:

              【解决方案22】:

              最近看到新加坡总理在facebook上发了一个他写的节目,有一句话要提..

              逻辑就是“值&-值”,假设你有0x0FF0,那么, 0FF0 & (F00F+1) , 等于 0x0010, 表示最低的 1 在第 4 位.. :)

              【讨论】:

              • 这会隔离最低位,但不会为您提供此问题所要求的位置。
              • 我认为这也不适用于查找最后一位。
              • value & ~value 为 0。
              • 哎呀,我的眼睛坏了。我把减号误认为是波浪号。无视我的评论
              【解决方案23】:

              如果你有资源,你可以牺牲内存来提高速度:

              static const unsigned bitPositions[MAX_INT] = { 0, 0, 1, 0, 2, /* ... */ };
              
              unsigned GetLowestBitPos(unsigned value)
              {
                  assert(value != 0); // handled separately
                  return bitPositions[value];
              }
              

              注意:此表至少会消耗 4 GB(如果我们将返回类型保留为 unsigned,则为 16 GB)。这是将一种有限资源 (RAM) 换成另一种(执行速度)的示例。

              如果您的函数需要不惜一切代价保持可移植性并尽可能快地运行,这将是可行的方法。在大多数实际应用程序中,4GB 表是不现实的。

              【讨论】:

              • 输入的范围已经由参数类型指定——“无符号”是一个 32 位的值,所以不,你不好。
              • 嗯...你的神话系统和操作系统有分页内存的概念吗?这要花多少时间?
              • 这是一个非答案。您的解决方案在所有现实世界的应用程序中都是完全不现实的,称其为“权衡”是不诚实的。具有 16GB 内存用于单个功能的神话系统根本不存在。你也应该回答“使用量子计算机”。
              • 为了速度而牺牲内存? 4GB+ 的查找表永远不会适合任何当前现有机器的缓存,所以我想这可能比这里的几乎所有其他答案都慢。
              • 啊。这个可怕的答案一直困扰着我:)@Dan:你对内存缓存是正确的。请参阅上面 Mikeage 的评论。
              猜你喜欢
              • 2013-11-13
              • 1970-01-01
              • 1970-01-01
              • 2015-05-09
              • 1970-01-01
              • 2011-05-05
              • 2016-03-28
              • 2020-02-14
              • 1970-01-01
              相关资源
              最近更新 更多