该算法如何计算32位整数中的设置位数?

时间:2014-02-27 22:16:37

标签: c++ c algorithm hammingweight

int SWAR(unsigned int i)
{
    i = i - ((i >> 1) & 0x55555555);
    i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
    return (((i + (i >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
}

我已经看到这个代码在32位整数中计算了等于1的位数,我注意到它的性能优于__builtin_popcount但我无法理解它的方式的工作原理。

有人可以详细解释这段代码的工作原理吗?

3 个答案:

答案 0 :(得分:34)

好的,让我们逐行完成代码:

第1行:

i = i - ((i >> 1) & 0x55555555);

首先,常量0x55555555的意义在于,使用Java / GCC样式binary literal notation编写的),

0x55555555 = 0b01010101010101010101010101010101

即,所有奇数位(将最低位计为位1 =奇数)为1,所有偶数位为0

表达式((i >> 1) & 0x55555555)因此将i的位右移1,然后将所有偶数位设置为零。 (等效地,我们可以首先将i的所有奇数位设置为零,& 0xAAAAAAAA然后将结果右移一位。)为方便起见,我们称之为中间值j

当我们从原始j中减去此i时会发生什么?好吧,让我们看看如果i只有两个位会发生什么:

    i           j         i - j
----------------------------------
0 = 0b00    0 = 0b00    0 = 0b00
1 = 0b01    0 = 0b00    1 = 0b01
2 = 0b10    1 = 0b01    1 = 0b01
3 = 0b11    1 = 0b01    2 = 0b10

喂!我们设法计算了两位数的位数!

好的,但如果i设置了两位以上怎么办?事实上,很容易检查上面的表格仍会给出i - j的最低两位,,第三和第四位以及第五和第六位也是如此等等。特别是:

  • 尽管>> 1i - j的最低两位不受i的第三位或更高位的影响,因为它们将被j屏蔽掉。 1 {}由& 0x55555555;以及

  • 因为j的最低两位永远不会比i的数值更大,所以减法永远不会从i的第三位借用:因此,i的最低两位也不会影响i - j的第三位或更高位。

事实上,通过重复相同的参数,我们可以看到该行的计算实际上将上表应用于{{1}中16个两位块的每个 } 并行。也就是说,在执行此行之后,新值i的最低两位现在将包含原始值{{1}中的相应位中设置的数字位接下来的两位也是如此,等等。

第2行:

i

与第一行相比,这个很简单。首先,请注意

i

因此,i = (i & 0x33333333) + ((i >> 2) & 0x33333333); 采用上面计算的两位数计算并抛弃其中的每一位计数,而0x33333333 = 0b00110011001100110011001100110011 在转移i & 0x33333333之后执行相同的两位。然后我们将结果一起添加。

因此,实际上,这一行的作用是获取原始输入的最低两位和第二低两位的bitcounts,在前一行计算,并将它们加在一起得到最低的bitcount < em>输入的四个位。而且,它同样为所有输入的8个四位块(=十六进制数字)并行执行此操作。

第3行:

(i >> 2) & 0x33333333

好的,这里发生了什么?

嗯,首先,i与前一行完全相同,不同之处在于它将相邻的四位 bitcounts一起添加到每个八的bitcounts中-bit 输入的块(即字节)。 (这里,与前一行不同,我们可以在添加之外移动return (((i + (i >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24; ,因为我们知道8位bitcount永远不会超过8,因此将适合四位而不会溢出。)

现在我们有一个由4个8位字节组成的32位数字,每个字节保存原始输入字节中的1位数。 (我们称这些字节为(i + (i >> 4)) & 0x0F0F0F0F&AB。)那么当我们将此值(我们称之为C)乘以{时会发生什么? {1}}?

好吧,自D以来,我们有:

k

因此,结果的最高字节最终成为:

的总和
  • 原始值,由于0x01010101字词加上
  • 由于0x01010101 = (1 << 24) + (1 << 16) + (1 << 8) + 1项加上
  • 而导致的下一个较低字节的值
  • 第二个较低字节的值,由于k * 0x01010101 = (k << 24) + (k << 16) + (k << 8) + k 项,加上
  • 由于k术语而导致的第四个和最低字节的值。

(一般情况下,也可能有较低字节的进位,但由于我们知道每个字节的值最多为8,因此我们知道加法永远不会溢出并创建进位。)

也就是说,k << 8的最高字节最终是输入的所有字节的bitcounts的总和,即32位输入数的总bitcount。最后k << 16然后只是将此值从最高字节向下移动到最低字节。

Ps。只需将k << 24更改为k * 0x01010101,将>> 24更改为{{1>即可轻松将此代码扩展为64位整数}}。实际上,相同的方法甚至适用于128位整数;然而,256位需要添加一个额外的移位/添加/屏蔽步骤,因为数字256不再完全适合8位字节。

答案 1 :(得分:10)

我更喜欢这个,它更容易理解。

x = (x & 0x55555555) + ((x >> 1) & 0x55555555);
x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
x = (x & 0x0f0f0f0f) + ((x >> 4) & 0x0f0f0f0f);
x = (x & 0x00ff00ff) + ((x >> 8) & 0x00ff00ff);
x = (x & 0x0000ffff) + ((x >> 16) &0x0000ffff);

答案 2 :(得分:1)

这是对Ilamari's answer的评论。 由于格式问题,我把它作为答案:

第1行:

i = i - ((i >> 1) & 0x55555555);  // (1)

此行源自此easier to understand line

i = (i & 0x55555555) + ((i >> 1) & 0x55555555);  // (2)

如果我们打电话

i = input value
j0 = i & 0x55555555
j1 = (i >> 1) & 0x55555555
k = output value

我们可以重写(1)和(2)以使解释更清楚:

k =  i - j1; // (3)
k = j0 + j1; // (4)

我们想证明(3)可以从(4)得出。

i可以写为偶数和奇数位的加法(将最低位计为第1位=奇数):

i = iodd + ieven =
  = (i & 0x55555555) + (i & 0xAAAAAAAA) =
  = (i & modd) + (i & meven)

由于meven掩码清除i的最后一位, 最后的等式可以这样写:

i = (i & modd) + ((i >> 1) & modd) << 1 =
  = j0 + 2*j1

那是:

j0 = i - 2*j1    (5)

最后,将(5)替换为(4)我们实现(3):

k = j0 + j1 = i - 2*j1 + j1 = i - j1