在与GCC __builtin__popcount(int)一样快的整数上计数位1

时间:2018-07-17 18:21:09

标签: c bit-manipulation built-in

我编写了一种算法(取自“ C编程语言”),可以非常快速地计算1位的数量:

int countBit1Fast(int n)
{
    int c = 0;
    for (; n; ++c)
        n &= n - 1;
    return c;
}

但是一个朋友告诉我__builtin__popcount(int)快很多,但便携性较差。我尝试一下,速度快了很多倍!为什么这么快?我想尽可能快地计算位数,但又不依赖于特定的编译器。

编辑:我可能会在PIC微控制器以及非英特尔处理器上使用它,因此我需要最大的可移植性。

3 个答案:

答案 0 :(得分:1)

正如其他人所提到的,__buildin__popcount()很快,因为它使用单个x86指令。

如果您想要比不使用任何处理器或编译器的东西快得多的东西,则可以创建包含256个条目的查找表:

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

然后使用它来获取每个字节的位数:

int countBit1Fast(int n) 
{
    int i, count = 0;
    unsigned char *ptr = (unsigned char *)n;
    for (i=0;i<sizeof(int);i++) {
        count += bitcount[ptr[i]];
    }
    return count;
}

答案 1 :(得分:1)

  

我编写了一种算法(取自“ C编程语言”),可以非常快速地计算1位的数量:

我不明白为什么有人会把您的方法描述为“非常快”。这有点聪明,并且平均而言应该比幼稚的替代方法更快。它也不取决于int表示形式的宽度,这是一个加号。我观察到它对于否定参数具有不确定的行为,但这是按位运算符和函数的共同主题。

让我们分析一下,假设一个非负参数:

int c = 0;
for (; n; ++c)
    n &= n - 1;
  • 执行了多少次循环迭代?

    值的二进制表示形式中的每1位

    1,而不管值中每个位所在的何处

  • 每个迭代执行多少工作

    • 每增加c
    • n与零进行比较(在退出循环时再加上其中的一个)
    • n递减1
    • 一个按位“和”

    这将忽略读取和存储,通过将操作数保存在寄存器中,很可能可以使它们变得免费或特别便宜。如果我们假设每种方法的成本均等,则每个迭代要进行四个操作。对于随机的32位整数,平均将进行16次迭代,平均总共进行 65次操作。 (最好的情况是一个操作,但是最坏的情况是129,这并不比单纯的实现好。)

另一方面,

__builtin__popcount()使用一条指令,而不管支持该指令的平台(例如您的指令)上的输入如何。但是,即使在那些没有针对性的指令的情况下,它也可以更快地完成(平均而言)。

@dbush提出了一种这样的机制,它具有与您提出的机制类似的优点。特别是,它不依赖于预先选择的整数宽度,尽管它确实依赖于1位所驻留的表示形式中的 where ,但是对于某些参数(较小的),它的运行速度确实比其他参数快。如果我计算正确的话,那么在32位随机输入中,平均大约20次操作:在四个循环迭代中,每个迭代要进行5次(只有0.4%的随机输入需要少于四个迭代)。我在计算每次迭代读取的一张表,我认为可以从缓存中获取该表,但它可能仍然不如对寄存器中已保存的值进行算术运算的速度快。

严格计算的是:

int countBit1Fast(uint32_t n) {
    n = (n & 0x55555555u) + ((n >> 1) & 0x55555555u);
    n = (n & 0x33333333u) + ((n >> 2) & 0x33333333u);
    n = (n & 0x0f0f0f0fu) + ((n >> 4) & 0x0f0f0f0fu);
    n = (n & 0x00ff00ffu) + ((n >> 8) & 0x00ff00ffu);
    n = (n & 0x0000ffffu) + ((n >>16) & 0x0000ffffu);
    return n;
}

这很容易计算:五个加法,五个移位和十个按位“与”运算,以及5个常数负载,每个输入总共进行 25个运算(并且仅上升)对于64位输入,则为30,尽管现在是64位操作,而不是32位操作)。但是,此版本本质上取决于输入数据类型的特定大小。

答案 2 :(得分:0)

__buildin__popcount()之所以如此之快,是因为它的gcc扩展利用了内置的硬件指令。如果您愿意用体系结构的可移植性来换取编译器的可移植性,请研究快速的intel内在函数,特别是:

_mm_popcnt_u64()

_mm_popcnt_u32()

然后必须包括<mmintrin.h>头文件才能使用这些内在函数,但是它们将与非gcc编译器一起使用。您可能还必须提供目标体系结构,才能使用-march=native之类的东西来内联功能(这是严格要求的)。