设置的最低有效位的位置


120

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

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

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

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

有什么想法可以减少一些周期吗?

(注意:这个问题是针对喜欢这种东西的人,而不是告诉我xyzoptimization是邪恶的。)

[edit] 谢谢大家的想法!我也学到了其他一些东西。凉!


while((值_N >>(++ pos))!= 0);
Thomas

Answers:


170

Bit Twiddling Hacks提供了许多错误的Bit twidd 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];

有用的参考资料:


18
为什么要下票?根据乘法的速度,这可能是最快的实现。它肯定是紧凑的代码,(v&-v)技巧是每个人都应该学习和记住的东西。
亚当·戴维斯

2
+1非常酷,与if(X&Y)运算相比,乘法运算有多昂贵?
Brian R. Bondy

4
有人知道它的性能与__builtin_ffslor相比ffsl吗?
史蒂文·卢

2
@Jim Balter,但与现代硬件上的乘法相比,模数很慢。所以我不会称其为更好的解决方案。
2014年

2
在我看来,值0x01和0x00都导致数组中的值为0。显然,如果传递了0,则该技巧将指示最低位已设置!
abelenky

80

为什么不使用内置的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.3 BSD,POSIX.1-2001。

笔记

BSD系统在中有一个原型<string.h>


6
仅供参考,如果可用,将编译为相应的汇编命令。
杰里米

46

有一个x86汇编指令(bsf)可以完成。:)

更优化了吗?

边注:

此级别的优化本质上取决于体系结构。当今的处理器太复杂了(就分支预测,缓存未命中,流水线而言),以至于很难预测哪些代码在哪种架构上执行得更快。将操作从32减少到9或类似的事情,甚至可能会降低某些体系结构的性能。单一体系结构上的优化代码可能会导致其他体系结构中的代码恶化。我认为您可以针对特定的CPU对此进行优化,或者保持不变,然后让编译器选择它认为更好的选择。


20
@dwc:我理解,但我认为这条子句:“有什么想法可以减少一些循环吗?” 使这样的答案完全可以接受!
Mehrdad Afshari

5
+1由于字节顺序,他的答案必然取决于他的体系结构,因此下拉汇编指令是一个完全有效的答案。
克里斯·卢茨

3
+1聪明的答案,是的,它不是C或C ++,但这是完成任务的正确工具。
Andrew Hare

1
等等,没关系。整数的实际值在这里无关紧要。抱歉。
克里斯·卢兹

2
@Bastian:如果操作数为零,则将ZF = 1。
Mehrdad Afshari

43

大多数现代体系结构都将提供一些指令,以查找最低设置位或最高设置位的位置,或计算前导零的数量等。

如果您有此类的任何一条说明,您可以廉价地模仿其他课程。

花一点时间在纸上进行研究,并意识到x & (x-1)将清除x中的最低设置位,并且( x & ~(x-1) )仅返回最低设置位,而无论其结构,字长等如何。知道这一点,使用硬件计数引导是微不足道的-zeroes /最高设置位,用于在没有显式指令的情况下查找最低设置位。

如果根本没有相关的硬件支持,则可以使用上面的标识,轻松地将此处给出的计数超前零的乘法或查找实现或“ Bit Twiddling Hacks”页面上的其中之一之一转换为最低位。具有无分支的优势。


18

杂草,解决方案的负载,而不是一个基准。你们人民应该为自己感到羞耻;-)

我的机器是运行Windows 7 64位的Intel i530(2.9 GHz)。我使用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);
}

8
de Bruijn和lookup的基准可能会产生误导-像这样紧密地循环,在第一次操作之后,每种类型的查找表将固定在L1缓存中,直到最后一个循环之后。这不太可能与实际使用情况相符。
MattW

1
对于低字节为零的输入,由于指针转换,它通过存储/重载而不是移位来获取较高的字节。(完全不必要的BTW,并且使其与尾数相关,而不会像移位那样)。无论如何,不​​仅因为热缓存,微基准测试是不现实的,而且它还对分支预测变量进行了初始化,并测试了可以很好地预测并使LUT减少工作量的输入。许多实际用例的结果分配更为均匀,而不是输入。
彼得·科德斯

2
不幸的是,您的FFS循环由于BSF指令中的错误依赖性而减慢了速度,这是您顽皮的旧编译器无法避免的(但较新的gcc应该与popcnt / lzcnt / tzcnt相同BSF由于其输出具有错误的依赖性(因为实际行为)当input = 0时,输出保持不变。)不幸的是,gcc通过在循环迭代之间不清除寄存器将其转换为循环依赖。因此,循环应每5个周期运行一次,这是BSF(3)+ CMOV的瓶颈(2)延迟
彼得·科德斯

1
您的基准测试发现,LUT的吞吐量几乎是FFS方法的两倍,这与我的静态分析预测非常吻合:)。请注意,您要测量的是吞吐量,而不是等待时间,因为循环中唯一的串行依赖关系是总和。 如果没有错误的依赖关系,ffs()则每个时钟的吞吐量应该为1(3 uops,BSF为1,CMOV为2,它们可以在不同的端口上运行)。在相同的循环开销下,可以在每个时钟上以3个时钟运行7个ALU(在您的CPU上)。开销占主导! 资料来源:agner.org/optimize
Peter Cordes

1
是的,如果bsf ecx, [ebx+edx*4]不按顺序执行,则如果不将其ecx视为必须等待的输入,则可能会使循环的多个迭代重叠。(ECX由上一个iteraton的CMOV最后编写)。但是CPU确实采用这种方式,以实现“如果source为零,则保持dest不变”行为(因此,这并不是像TZCNT那样真正的dep;因为与该假设无关,因此没有分支+投机执行,因此需要数据相关性)输入为非零)。我们可以通过在xor ecx,ecx之前添加来克服它bsf,以打破对ECX的依赖。
彼得·科德斯

17

最快的解决方案(非内部/非汇编程序)是找到最低字节,然后在256项查找表中使用该字节。这为您提供了四个条件指令的最坏情况性能和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;  
}

1
实际上,这是三个条件的最坏情况:)但是,是的,这是最快的方法(通常是人们在这样的面试问题中寻找的东西)。
布赖恩

4
您是否不想在某处放置+ 8,+ 16,+ 24?
Mark Ransom

7
任何查找表都会增加高速缓存未命中的机会,并可能导致内存访问成本的增加,该成本可能比执行指令高几个数量级。
Mehrdad Afshari

1
我什至会使用移位(每次将其移位8)。然后可以完全使用寄存器来完成。使用指针,您将必须访问内存。
Johannes Schaub-litb

1
合理的解决方案,但是在查找表不在缓存中的可能性(可以指出,可以解决)与分支的数量(潜在的分支错误预测)之间,我更喜欢乘法和查找解决方案(没有分支,较小的查询表)。当然,如果可以使用内部函数或内联汇编,则它们可能是更好的选择。不过,这种解决方案也不错。

13

OMG只是螺旋式上升。

这些示例中大多数都缺少对所有硬件如何工作的一点理解。

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

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

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

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

让我们考虑一下在表中查找值时会发生什么。可能是该值当前不在高速缓存中,至少不是在第一次调用函数时。这意味着从缓存中加载值时,CPU会停顿。同样,这从一台机器到另一台机器也有所不同。实际上,新的Intel芯片以此为契机在当前线程等待缓存加载完成时交换线程。这可能比指令管道刷新容易得多,但是,如果您多次执行此操作,则可能只发生一次。

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

如果已经解决此事,我表示歉意。

我使用的每个编译器(除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的值0或1,然后将结果用于合并在分支的任一侧可能发生的数学运算。

编辑:

上面的代码已完全损坏。该代码有效,并且仍然是无分支的(如果已优化):

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或很高兴获得31为0,则删除i0计算,节省大量时间。


3
我帮你修好了 确保测试您发布的内容。
吉姆·巴尔特

5
当其中包含三元运算符时,如何称其为“无分支”?
BoltBait

2
它是有条件的。一条汇编语言指令,它同时使用两个可能的值作为参数,并根据条件的评估执行mov操作。因此是“无分支”。没有跳转到另一个未知或可能不正确的地址。
2013年

FWIW gcc甚至在-O3 godbolt.org/z/gcsUHd
Qix-MONICA已被盗

7

受此类似的涉及搜索设定位的帖子的启发,我提供以下内容:

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

优点:

  • 没有循环
  • 没有分支
  • 持续运行
  • 通过返回超出范围的结果来处理value = 0
  • 只有两行代码

缺点:

  • 假定编码的字节序很少(可以通过更改常量来固定)
  • 假设double是真实的* 8 IEEE float(IEEE 754)

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

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

假设所有内容都具有低端存储的32位整数(请考虑x86处理器)。


1
有趣-我仍然不敢对位算术使用双精度,但我会记住这一点
peterchen

使用frexp()可能会使其更具可移植性
aka.nice 2012年

1
在C或C ++中,通过指针广播进行类型操作并不安全。在C ++中使用memcpy,或在C中使用联合。(如果编译器保证安全,则在C ++中使用联合。例如,C ++的GNU扩展(许多编译器支持)确实保证了联合类型伪造是安全的。)
彼得Cordes

1
较旧的gcc还可以通过并集而不是指针广播来编写更好的代码:它直接从FP reg(xmm0)移至rax(带有movq),而不是存储/重新加载。较新的gcc和clang都使用movq两种方式。有关联合版本,请参见godbolt.org/g/x7JBiL。您是否有意将算术移位20?你的假设也应列出intint32_t,并签署右移是算术移位(在C ++中,它的实现定义)
彼得·柯德斯

1
顺便说一句,Visual Studio(至少2013年)也使用test / setcc / sub方法。我更喜欢cmp / adc。
DocMax

5

最坏的情况是少于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。它不是最快的,但是比原始速度更快,这就是重点……
Andrew Grant

@ onebyone.livejournal.com:即使代码中有错误,分组的概念也是我试图理解的重点。实际的代码示例并不重要,它可以变得更紧凑但效率更低。
布赖恩·邦迪

我只是想知道我的答案中是否存在真正不好的部分,或者人们是否只是喜欢我将其全部写完?
Brian R. Bondy

@ onebyone.livejournal.com:当您比较2种算法时,应按原样进行比较,而不是假设一个算法会在优化阶段神奇地转换。我也从未声称自己的算法“更快”。只是它是较少的操作。
Brian R. Bondy

@ onebyone.livejournal.com:...我不需要分析上面的代码就知道操作较少。我可以清楚地看到。我从未提出过任何需要剖析的声明。
Brian R. Bondy

4

为什么不使用二进制搜索?这将始终在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这与我的答案非常相似。最好的情况下运行时间比我的建议差,但最坏的情况下运行时间更好。
Brian R. Bondy

2

@ anton-tykhyy提供的同一链接在这里值得特别提及的另一种方法(模除法和查找)。该方法的性能与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 */

1
mod是慢的。相反,您可以使用原始的乘法和查找方法并!v从中减去r以处理边缘情况。
Eitan T

3
@EitanT优化器很可能会像黑客一样高兴地将该mod转换为快速乘法
phuclv 2014年

2

根据国际象棋编程BitScan页和我自己的测量,减法和异或比求反和掩码更快。

(请注意,如果您要计算尾随零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位版本:

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 & -v)0(顺便说一句也是错误的,至少buf会导致合理的结果)。
CiaPan 2014年

@CiaPan:好点,我会提一下。我猜想有一个不同的De Bruijn编号可以通过在第63个索引中放入0来解决此问题。
jnm2 2014年

嗯,那不是问题所在。0和0x8000000000000000都在之后产生0xFFFFFFFFFFFFFFFFFF v ^ (v-1),因此没有区别。在我的情况下,永远不会输入零。
jnm2

1

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

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

如果没有,检查是否设置了鞋帮16。

本质上是二进制搜索。


1

有关如何使用单个x86指令执行此操作的信息,请参见此处的答案,除了要找到最低有效位之外,您将需要BSF(“位向前扫描”)指令而不是在BSR那里进行描述。


1

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

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((x & -x) - 1) << 1
要使

更快的方法:x ^ (x-1)
phuclv 2014年

1
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行中返回。

等等

我认为有人抱怨此代码在最坏情况下的效率低下,无法理解这种情况会多么罕见。


3
最坏的情况是32个分支预测错误:)

1
难道这至少不能做成开关...吗?
史蒂文·卢

“这至少不能做成一个开关吗……?” 在暗示可能之前,您是否尝试过这样做?从什么时候开始可以对开关的情况进行计算?这是一个查找表,而不是一个类。
j riv

1

在“编程的艺术,第4部分”中使用“魔术蒙版”发现了这个巧妙的技巧,它在O(log(n))时间内完成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);
}

1

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

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

结果是从1开始的索引。


1
聪明,但是当输入不是编译时常量时,它将编译为灾难性的糟糕汇编。 godbolt.org/g/7ajMyT。(使用gcc进行位的愚蠢循环,或使用clang进行实际的递归函数调用。)gcc / clang可以ffs()在编译时求值,因此您无需使用它进行常量传播即可工作。(当然,您必须避免使用inline-asm。)如果确实需要使用C ++ 11的产品constexpr,则仍然可以使用GNUC __builtin_ffs
彼得·科德斯

0

这是关于@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.");


-3

最近,我看到新加坡总理发布了一个他在Facebook上写的程序,其中有一行提到它。

逻辑简单地是“ value&-value”,假设您有0x0FF0,然后是0FF0&(F00F + 1),等于0x0010,这意味着最低的1在第4位。.::


1
这会隔离最低位,但不会给您其位置,这正是这个问题所要的。
桥本2015年

我也不认为这对找到最后一点有用。
yyny

value&
〜value

哎呀,我的眼睛快坏了。我误认为是负号。忽略我的评论
khw

-8

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

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(如果将返回类型保留为,则将为16 GB unsigned)。这是将一个有限资源(RAM)换成另一个(执行速度)的示例。

如果您的功能需要保持可移植性,并且不惜一切代价使它尽可能快地运行,那将是您的最佳选择。在大多数实际应用中,4GB表是不现实的。


1
输入的范围已经由参数类型指定-'unsigned'是32位值,所以不,您不满意。
布赖恩

3
嗯...您的神话般的系统和操作系统是否具有分页内存的概念?那要花多少时间?
Mikeage

14
这是无答案的。您的解决方案在所有实际应用程序中都是完全不现实的,称其为“折衷”是不明智的。您拥有16GB RAM专门用于单个功能的神话系统根本不存在。您也会回答“使用量子计算机”。
布赖恩

3
牺牲记忆以提高速度?4GB +的查询表将永远无法容纳在任何现有计算机上的缓存中,因此我想这可能比这里几乎所有其他答案都要慢。

1
啊 这个可怕的答案一直困扰着我:)@Dan:您对内存缓存是正确的。请参阅上面的Mikeage评论。
e.James 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.