你提供的这段代码是 popcount64c,是用于计算64位整数汉明权重(1的个数)的分治法经典实现。它常被称为SWAR算法(SIMD within a register),在常数时间内完成计算,且不依赖硬件POPCNT指令。下面我将逐行详细解析,并附上完整的实现。
完整代码及掩码定义
// 常量掩码
const uint64_t m1 = 0x5555555555555555; // 01010101...
const uint64_t m2 = 0x3333333333333333; // 00110011...
const uint64_t m4 = 0x0f0f0f0f0f0f0f0f; // 00001111...
const uint64_t h01 = 0x0101010101010101; // 用于求和
int popcount64c(uint64_t x)
{
// 步骤1: 每2位统计其中的1的个数,结果存储在原来的2位中
x -= (x >> 1) & m1; // 2位分组计数
// 步骤2: 每4位统计其中的1的个数,结果存储在原来的4位中
x = (x & m2) + ((x >> 2) & m2); // 4位分组计数
// 步骤3: 每8位统计其中的1的个数,结果存储在原来的8位中
x = (x + (x >> 4)) & m4; // 8位分组计数
// 步骤4: 将8个8位的计数相加,结果在最高8位中
return (x * h01) >> 56; // 最终求和
}详细解析
前提与思路
- 基本思想是分而治之:将64位数划分为多个小组,先统计小组内的1的个数,然后合并结果。
- 每一步都将分组大小加倍,最终得到总数。
步骤1: 2位分组统计
x -= (x >> 1) & m1;目的:每2位(bit pair)独立计算其中1的个数,将结果(0, 1, 2)存储在这2位中。
掩码 m1 = 0x5555... = 01010101...
- 作用:屏蔽奇数位(最低位开始第1、3、5…位)。
运算分析:
x >> 1:右移1位,将每对bit的高位移动到低位。(x >> 1) & m1:保留原数偶数位的值(对应每对bit的高位)。x - ...:相当于从原始2位值中减去高位值。
数学上:对于一个2位组 [b1 b0](b1高位,b0低位):
- 原值 =
2*b1 + b0 - 减去的高位值 =
b1 - 结果 =
(2*b1 + b0) - b1 = b1 + b0← 正好是1的个数
结果范围:每2位存储0、1或2(二进制00、01或10)。
示例:
初始: x = 0110 1101 ... (按2位分组)
分组: |01|10|11|01|...
计数: 1 2 2 1 ... (以2位二进制存储)
步骤2: 4位分组统计
x = (x & m2) + ((x >> 2) & m2);目的:将相邻的2个2位计数相加,得到4位组内的总计数。
掩码 m2 = 0x3333... = 00110011...
- 作用:每4位中,屏蔽低2位或高2位。
运算分析:
x & m2:获取每个4位组的低2位计数。(x >> 2) & m2:获取每个4位组的高2位计数(右移到低位)。- 两者相加:将相邻的2位计数合并为4位计数。
结果范围:每4位存储0-4(二进制0000到0100)。
步骤3: 8位分组统计
x = (x + (x >> 4)) & m4;目的:将相邻的2个4位计数相加,得到8位组内的总计数。
掩码 m4 = 0x0F0F... = 00001111...
- 作用:每8位中,屏蔽低4位(保留低4位结果)。
运算分析:
x + (x >> 4):将每个8位组的高4位计数加到低4位上。& m4:屏蔽高4位(因为低4位已包含完整计数,高4位可能产生进位干扰)。
结果范围:每8位存储0-8(二进制0000 0000到0000 1000)。
步骤4: 合并所有8位组
return (x * h01) >> 56;目的:将8个8位组的计数全部相加,得到最终的1的总数。
魔法数 h01 = 0x0101010101010101
- 每个字节(8位)都是
0x01(二进制00000001)。
乘法原理:
- 将
x视为一个由8个字节组成的数:[B7 B6 B5 B4 B3 B2 B1 B0] - 乘以
h01相当于:x * 0x0101010101010101 = x + (x << 8) + (x << 16) + ... + (x << 56) - 结果最高字节(56-63位)包含:
因为每个字节最大为8(<16),加法不会跨字节进位。B7 + (B6 + B7) + (B5 + B6 + B7) + ... + (B0 + B1 + ... + B7) 但实际效果是:B7 + B6 + B5 + B4 + B3 + B2 + B1 + B0
右移56位:取出最高字节,即为总和。
示例:
x = 0x0302050004010007 (每字节是一个8位组的计数)
乘法后最高字节 = 3+2+5+0+4+1+0+7 = 22
算法特点
优点
- 无分支:避免分支预测失败,性能稳定。
- 常数时间:无论1的个数多少,固定执行这些操作。
- 纯算术运算:仅使用移位、与、加、减、乘,不需要查表。
- 并行性:在单个寄存器内隐式并行处理所有分组。
性能
- 在现代CPU上,通常比朴素的位循环快得多。
- 但通常不如硬件
POPCNT指令(单周期)。 - 在需要跨平台、保证无特定指令集支持时是最优选择之一。
变体与扩展
- 32位版本:将掩码改为32位,最后使用
(x * 0x01010101) >> 24。 - 可扩展至更宽的字长,原理相同。
总结
这段代码是分治法计算汉明权重的优雅实现,通过巧妙的掩码和移位操作,在O(1)时间内完成计算。它展示了位级并行计算的经典技巧,是高效位操作算法的典范。