本文写于2017年,其时读《Redis设计与实现》,发觉其中关于bitcount一节的内容很有趣,后通过网络搜索深究了一番,期间收获良多。全文由5个问题贯穿起来,这些问题都是我在深究一番时提出,如有谬误,请不吝赐教。
1 什么是bitcount?
这是Redis的一个命令,作用是统计给定字符串的二进制表示中1的个数。比如字符串“A”的二进制表示为01000001,那么bitcount命令会返回2。
2 什么是汉明重量?
要回答什么是汉明重量,先得解释什么是汉明距离。汉明距离是指两个等长字符串,在对应位有不同字符的个数。比如字符串“zang”和“wang”,都是长为4的字符串,他们在第1位的字符分别是z和w,而第2位至第4位的字符都是相同的,因此他们之间的汉明距离为1。类似的“abc”和“abb”汉明距离为1,“abc”和“xyz”汉明距离为3。而汉明重量是指,给定字符串与一个全位“0”的等长字符串的汉明距离,可见它是汉明距离的一种特例。比如“zang”的汉明重量,就是在说“zang”与“0000”的汉明距离,结果是4;而“a00”的汉明重量,就是“a00”与“000”的汉明距离,结果是1。
bitcount命令的作用是统计给定字符串的二进制表示中1的个数,换句话说就是求这个二进制串的汉明重量。由于二进制串每一位可能的字符要么是0要么是1,因此求二进制串的汉明距离其实就是统计其中1的个数。汉明距离是以美国数学家理查德·卫斯里·汉明的名字命名的,汉明距离在信息论、密码学等领域有着广泛应用。题外话:另一个非常著名的以其名字命名的概念叫汉明码,相信很多人都听过。
3 如何计算汉明重量?
简单说有3种算法:
1)遍历法,简单到不必说了。
2)字典法,就是事先构造一个字典,字典的key是字符串,值是这个字符串的汉明重量,时间复杂度O(1),空间复杂度O(n)。
3)variable-precision SWAR算法,目前已知的效率最高的计算汉明重量的算法。
这里详细说一下SWAR算法,为了简化说明,我们假设要求的是一个8位01串“10110101”的汉明重量,计算过程如下:
10 11 01 01 //这是原始串,为了表述方便,这里用空格分割成2位一组
01 10 01 01 //分组统计1的个数,结果用二进制表示,存储在该组所在位上
0011 0010 //对相邻两组的二进制值求和,结果用二进制表示,存储在该两组所在位上
3+2=5 //相邻两组求和,得出汉明重量\
再举一例,看下16位串“1011010101001001”的计算过程:
10 11 01 01 01 00 10 01 //原始串
01 10 01 01 01 00 01 01 //分组统计1的个数,结果用二进制表示,存储在该组所在位上
0011 0010 0001 0010 //对相邻两组的二进制值求和,结果用二进制表示,存储在该两组所在位上
0101 0011 //对相邻两组的二进制值求和,结果用二进制表示,存储在该两组所在位上
5+3=8 //相邻两组求和,得出汉明重量\
如果将上述过程用图像画表示,你会得到一棵二叉树,原始串二叉树的过程,树根就是汉明重量。举一反三,你可以列出32、64位等长度为2的幂次方的二进制串的汉明重量,你只需log2(n)步就可以计算出结果。上述过程用程序来表达就是(java的Integer.bitCount方法源码):\
int bitCount(int i) {
i = i - ((i >>> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
i = (i + (i >>> 4)) & 0x0f0f0f0f;
i = i + (i >>> 8);
i = i + (i >>> 16);
return i & 0x3f;
}
Redis的bitcount命令的实现综合了字典法和variable-precision SWAR算法,长度不超过8位的串用字典法。那么还有没有更快的计算汉明重量的方法(注意是方法,不是算法)呢?答案是有。
4 什么是popcnt?
popcnt缩写自population count,是x86架构CPU提供的一个指令,属于SSE指令集。popcnt指令可以在一个指令周期内计算出一个字长(对于64位机器就是64位)的二进制串的汉明重量。显然这就是那个更快的计算汉明重量的方法。那么如何在程序中使用popcnt指令呢?在Windows系统,可使用c/c++编译器内建函数__popcnt64()|AMD或_mm_popcnt_u64|Intel,它们都有对应的16和32位版。在Linux系统,可使用gcc内建函数__builtin_popcount()或__builtin_popcountl()。这些内建函数都是机器依赖的,所以实际使用时你需要考虑跨平台移植问题。
5 什么是算法?
这里主要说一说我的理解。算法字面含义就是计算的方法咯,但是方法是个抽象的概念,这么说等于没说,所以我倾向于这么解释算法:算法是完成一项任务的动作序列。 这么理解的话设计算法就是编排动作,这里的动作可以是:
一行java或c#代码;
一条jvm或clr指令;
一条机器指令;
一条微指令。
可以看出,上述四种动作处于不同的级别,做个不太恰当的类比,从代码到微指令就好比从四维空间降到了一维空间。有了上面的理解,当我在设计或实现一个算法的时候,我必须清楚地知道自身正处于几维空间里,如果我还能清楚地知道我的代码被各级转换器(编译器、解释器、微程序控制器)如何转换成一维空间的微指令,将会对衡量程序的效率很有帮助。现在回过头看下采用popcnt计算汉明重量的方法,用一句时髦语来说,这简直就是降维打击嘛。
参考资料
- 汉明距离
zh.wikipedia.org/wiki/汉明距离\ - 一个popcnt硬件实现专利
www.google.com/patents/US8…\ - 编译器内建函数
msdn.microsoft.com/zh-cn/libra…\ - GCC内建函数
gcc.gnu.org/onlinedocs/…\ - POPCNT Instruction Adventure
www.privacore.com/2016/03/17/…\ - 算法
zh.wikipedia.org/wiki/算法
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。