redis 的 bitcount 可以用来统计一个二进制数组中 1 的总数量。
实现这个命令,最简单的做法是对数组中每个字节都进行位移操作,统计每个位上 1 的数量。这个操作对内内存的需求很小,不过要频繁的位操作,比方数组有 100 万个字节,要执行 800 万次位操作。
进一步的算法是拿空间换时间,一个字节有 255 个值,每个值的 1 的位数是固定的,所以可以通过枚举这些值获取结果;这时候需要使用一个额外的数组保存这些枚举值;为了加快速度,可以将单字节扩展为双字节,甚至更多;不过这是有代价的,因为字节越多,需要的额外空间越大,甚至内存扛不住;即使扛住了,太大的数组也不利于缓存的使用。
汉明算法是专门处理这种需求的一个算法,它的逻辑很简单,就是分组处理,一次性处理多个位而不是 1 位位的处理。这个算法核心的逻辑如下:
int swar(uint32_t val){
val = (val & 0x55555555) + ((val >> 1 ) & 0x55555555);
val = (val & 0x33333333) + ((val >> 2) & 0x33333333);
val = (val & 0x0f0f0f0f) + ((val >> 4) & 0x0f0f0f0f);
val = (val * 0x01010101) >> 24;
return (int)val;
}
整个过程可以分为四步。
第一步
将传入的数字每两位为一组进行分割,先统计每组中低位的 1 的数量;再统计高位中 1 的数量;最后将两者相加,结果保存这两位中,表示这两位 1 的总数;执行最后一步的加法时,每组都不会有进位问题,因为两位的 1 最多为 2,而两位最大能表示的为 3,所以不会有溢出。
第二步
第二步类似于第一步,只是这次分组是按照 4 位来的,这 4 位总的 1 的数量保存在这 4 位上;而且由于 4 位最多 4 个 1,更不会有溢出问题,可以明确知道每组中的最高位一定为 0,即每组的格式都是 0—;
第三步
同样类似于第一步,只是这次按照 8 位分组,由于 最多 8 个 1,因此每个字节的格式都变成了0000----;
第四步
第四步用来汇总最终结果,它的过程是把每个字节的值进行相加,并写入到最高字节上,然后右移 24 位就得到结果了。右移这一步比较好理解,主要的问题在于 i*0x01010101.
上面是这个计算过程, 其实就是小学时候学习的乘法计算;虽然是二进制计算,但依然遵循完全一样的对齐逻辑,所以从数学上说,四个字节会被对齐做一个加法,由于每个字节最高 4 位都是 0,因此不会发生溢出,而低字节的那部分就更不会发生进位了,所以计算的结果中,最高字节正好存储了 1 的总个数,再右移就正好可以读取出来。不得不说这个方法挺神奇的。
一个普通移位操作和 swar 操作的时间对比
#include<stdio.h>
#include<stdint.h>
#include<time.h>
int swar(uint32_t val){
val = (val & 0x55555555) + ((val >> 1 ) & 0x55555555);
val = (val & 0x33333333) + ((val >> 2) & 0x33333333);
val = (val & 0x0f0f0f0f) + ((val >> 4) & 0x0f0f0f0f);
val = (val * 0x01010101) >> 24;
return (int)val;
}
int getbicount(uint32_t val){
int res=0;
uint32_t mask = 1;
uint32_t temp;
for(int i=1; i < 32; i++){
temp = val & mask;
if(temp != 0){
res++;
}
mask <<= 1;
}
return res;
}
int main(){
clock_t start;
start = clock();
for(uint32_t i=0; i < 1000000000; i++){
swar(i);
}
printf("swar time is %ld\n",clock() - start);
start=clock();
for(uint32_t i=0; i < 1000000000; i++){
getbicount(i);
}
printf("get count time is %ld\n",clock() - start);
return 0;
}
因为我对 swar 算法的性能表示怀疑,毕竟它里边还有乘法计算,所以我做了一个对比,代码如上,测试1000000000 个数字。结果如下,能看出来,swar 性能上要不普通的移位算法优秀得多,在这么大数据加成的情况下,性能差距达两个数量级。