好的,让我们逐行浏览代码:
第 1 行:
i = i - ((i >> 1) & 0x55555555);
首先,常量的意义0x55555555
在于,使用 Java/GCC 风格的二进制文字表示法编写),
0x55555555 = 0b01010101010101010101010101010101
也就是说,它的所有奇数位(将最低位计算为位 1 = 奇数)是1
,所有偶数位都是0
。
因此,该表达式((i >> 1) & 0x55555555)
将i
右移一位,然后将所有偶数位设置为零。(等效地,我们可以先将 的所有奇数位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
仍然由上表给出,第三位和第四位,第五位和第六位,等等。尤其是:
尽管>> 1
, 的最低两位i - j
不受 的第三位或更高位的影响,因为它们将被;i
屏蔽掉。和j
& 0x55555555
由于 的最低两位j
永远不会有比 更大的数值i
,因此减法永远不会从 的第三位借用i
:因此, 的最低两位i
也不会影响 的第三位或更高位i - j
。
事实上,通过重复相同的论点,我们可以看到这一行的计算实际上将上表应用于并行的 16 个两位块中的每一个。也就是说,在执行这一行之后,新值的最低两位现在将包含在原始值的相应位中设置的位数,接下来的两位也是如此,依此类推。i
i
i
第 2 行:
i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
与第一行相比,这很简单。首先,请注意
0x33333333 = 0b00110011001100110011001100110011
因此,i & 0x33333333
取上面计算的两位计数并每隔一秒丢弃一次,同时在右移两位后(i >> 2) & 0x33333333
做同样的事情。然后我们将结果相加。i
因此,实际上,这一行所做的是将在前一行计算的原始输入的最低两位和倒数第二位的位计数,并将它们加在一起以给出最低四位的位计数输入。而且,它对输入的所有8 个四位块(= 十六进制数字)并行执行此操作。
第 3 行:
return (((i + (i >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
好的,这是怎么回事?
好吧,首先,(i + (i >> 4)) & 0x0F0F0F0F
它与上一行完全相同,只是它将相邻的四位位计数加在一起以给出输入的每个八位块(即字节)的位计数。(在这里,与上一行不同,我们可以避免移动&
外部加法,因为我们知道 8 位的位数永远不会超过 8,因此可以容纳在 4 位内而不会溢出。)
现在我们有一个由四个 8 位字节组成的 32 位数字,每个字节保存原始输入的那个字节中的 1 位的数量。(我们称这些字节为 、 和A
。B
)那么当我们将这个值(我们称它为)乘以 时会发生什么?C
D
k
0x01010101
好吧,因为0x01010101 = (1 << 24) + (1 << 16) + (1 << 8) + 1
,我们有:
k * 0x01010101 = (k << 24) + (k << 16) + (k << 8) + k
因此,结果的最高字节最终是以下各项之和:
- 它的原始价值,由于
k
期限,加上
- 由于
k << 8
术语,下一个低字节的值,加上
- 第二个低字节的值,由于
k << 16
术语,加上
- 由于
k << 24
术语,第四个和最低字节的值。
(一般来说,低字节也可能有进位,但由于我们知道每个字节的值最多为 8,所以我们知道加法永远不会溢出并产生进位。)
也就是说,最高字节k * 0x01010101
最终是输入的所有字节的位数之和,即32位输入数的总位数。然后最后>> 24
简单地将这个值从最高字节向下移动到最低字节。
附言。0x01010101
只需更改to0x0101010101010101
和>> 24
to即可轻松将此代码扩展为 64 位整数>> 56
。事实上,同样的方法甚至适用于 128 位整数;但是,256 位需要添加一个额外的移位/添加/掩码步骤,因为数字 256 不再完全适合 8 位字节。