JDK中Integer.bitCount解析

/ JAVA / 0 条评论 / 369浏览 / 自动同步于GITHUB

使用过Redis的人可能知道,Redis中给我们提供了统计二进制位数为1的位数量的指令bitcout,JDK中Integer类同样也给我们提供了该功能的方法Integer.bigCount,得益于此,我们很容易就能一窥该方法的实现

    public static int bitCount(int i) {
        // HD, Figure 5-2
        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;
    }

上述代码作为Integer类中比较有意思的一个方法,该方法利用了一个技巧:通过分割分配二进制位的方式,CPU可以实现一个指令同时计算多个数值。该方法的前四行都利用了该技巧。

NOTE:CPU要通过分配字节位的方式同时计算多个数值对,需要有一定的前提:由于每个数分配的字节位的长度有限,这就要求计算结果的二进制表示不能超出分配的位数。在当前问题上,显而易见是成立的:相加的两个数的最大值所占的二进制位数只有分配的二进制位的一半,结果值需要的二进制位必然不会超过分配的二进制位数。

案例解析

为了利于问题的解决,对计算二进制位1的数量这个问题,做一个等价转换:计算二进制位上每一位值的和。

以数字‭1823425321‬为例,二进制数值为

‭0 1 1 0  1 1 0 0  1 0 1 0  1 1 1 1  0 1 0 0  0 0 1 1  0 0 1 0  1 0 0 1‬

1. 方法第一行

将二进制的每1位都视为一个单独的数字,从左往右两个两个数字配对,形成16组二进制数相加,得到16个数值(2位二进制)。为了使结果是2位二进制数,相加前还需先给每个数前面补零。计算过程如下:

‭0 1  1 0  1 1  0 0  1 0  1 0  1 1  1 1  0 1  0 0  0 0  1 1  0 0  1 0  1 0  0 1‬     
                                      ⇩  
00   01   01   00   01   01   01   01   00   00   00   01   00   01   01   00
                                      +
01   00   01   00   00   00   01   01   01   00   00   01   00   00   00   01
                                      ⇩
01   01   10   00   01   01   10   10   01   00   00   10   00   01   01   01

上述计算过程用代码表示如下(0x55555555的二进制值是0b_01010101010101010101010101010101)

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

公式中,i >>> 1将偶数位变为了奇数位,& 0x55555555则清空偶数位,结合起来之后(i & 0x55555555)((i >>> 1) & 0x55555555)就分别提取了奇数位和偶数位的值,分别以奇偶位为基构建了两组2位二进制数数组,两组数组相加,完成第1步计算。

认真的可能就会发现,不对啊,代码里分明是减法啊。实际上,第一行代码,用了另一个公式替代:对于2位二进制数n,1的个数c可用公式c = n - (n >>> 1)计算得到。这个公式也很容易证明:

假设:2位二进制n = b1 * (2 ^ 1) + b0 * (2 ^ 0),显然b1是n的第2位数,b0是n的第1位数
那么:所证问题等价于证明 b1 + b0 = n - b1
因为
    n - b1 = b1 * (2 ^ 1) + b0 * (2 ^ 0) - b1
           = b1 * 2 + b0 - b1
           = b1 + b0
所以问题得证

新的公式的代码在计算指令上比旧的代码减少了一个指令。所以新的代码就变为了

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

2. 方法第二行

将二进制数每2位视为一个单独的数字,从左往右两个两个数字配对,形成8组二进制数相加,得到8个数值(4位二进制)。同样的,为了使结果是4位二进制数,相加前还得给每个数前面补零。计算过程如下:

01  01    10  00    01  01    10  10    01  00    00  10    00  01    01   01
                                     ⇩  
0001      0010      0001      0010      0001      0000      0000      0001
                                     +
0001      0000      0001      0010      0000      0010      0001      0001
                                     ⇩
0010      0010      0010      0100      0001      0010      0001      0010

上述计算过程用代码表示如下(0x33333333的二进制值是0b_0011001100110011001100110011‬0011)

i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);

同样的,公式中,通过(i>>>2) & 0x33333333i & 0x33333333i分为了相加的两部分,0x33333333起到了清空高2位数据的作用

3. 方法第三行

将二进制数的每4位视为一个单独的数字,从左往右两个两个数字配对,形成4组二进制数相加,得到4个数值(8位二进制)。同上所述,前面补零。计算过程如下:

0010   0010   0010   0100   0001   0010   0001   0010
                          ⇩
00000010      00000010      00000001      00000001
                          +
00000010      00000100      00000010      00000010
                          ⇩
00000100      00000110      00000011      00000011

上述计算过程用代码表示如下(0x0f0f0f0f的二进制值是0b_00001111000011110000111100001111‬)

i = (i & 0x0f0f0f0f) + ((i >>> 4) & 0x0f0f0f0f);

在8位二进制数中,值为1的位数最大为8,仅需4个二进制位就能表示(PS:实际上,由n < 2 ^ (n / 2)n > 4时均成立,可以得出:在n大于4时,n位二进制数的值为1的位数值m,只需不超过n / 2个二进制位即可表示)。这意味着相加后的结果值也不会超过4个二进制位,所以在计算中可以先不考虑高4位会对结果造成影响,清空高4位值的计算& 0x0f0f0f0f可以在加法完成之后再进行,代码就可以简化成如下所示代码(比原来的代码少了一个指令)。

i = (i + (i >>> 4)) & 0x0f0f0f0f;

4. 方法第四行

与前面一样,每8位视为一个单独的数字数字,相加之后得到2个16位二进制数值。计算过程如下:

00000100    00000110     00000011    00000011
                      ⇩
0000000000000100         0000000000000011
                      +
0000000000000110         0000000000000011
                      ⇩
0000000000001010         0000000000000110

代码表示如下(0x00ff00ff的二进制值是0b_00000000111111110000000011111111)

i = (i & 0x00ff00ff) + ((i >>> 8) & 0x00ff00ff);

同理于第3步,加法计算中也可以先不用担心高8位对结果造成的影响直接计算即可,代码优化为

i = (i + (i >>> 8)) & 0x00ff00ff;

此外,对于32位的Integer,值为1的位数最大为32,也就是说最终结果仅需6个二进制位即可表示。而当前每个加数都已经达到8个二进制位,这种情况下,相加后的和的高8位的值即使不清空也不会影响最终结果的低6位的值。所以,可以将高位清空的任务留到所有计算完成后一并处理,省略& 0x00ff00ff后代码简化为

i = i + (i >>> 8);

而省略& 0x00ff00ff后实际的计算过程是

00000100    00000110     00000011    00000011
                      ⇩
0000000000000100         0000011000000011
                      +
0000010000000110         0000001100000011
                      ⇩
0000010000001010         0000100100000110

5. 方法第五行

同理于第4步,计算过程如下(以第4步中理论结果值为例)

0000000000001010         0000000000000110
                    ⇩
00000000000000000000000000001010
                    +
00000000000000000000000000000110
                    ⇩
00000000000000000000000000010000

代码表示如下(0x0000ffff的二进制值是0b_00000000000000001111111111111111)

i = (i & 0x0000ffff) + ((i >>> 16) & 0x0000ffff);

同理于第4步的优化,优化后代码如下

i = i + (i >>> 16);

同第4步一样的,贴出实际的计算过程如下

0000010000001010         0000100100000110
                    ⇩
00000100000010100000100100000110
                    +
00000000000000000000010000001010
                    ⇩
00000100000010100000110100010000

6. 方法第六行

前面第4步说过,为了精简代码的指令,将高位清空的任务留到所有计算完成后一并处理。在第4步和第5步中均遗留了未处理的高位数据,所以第6步将完成前面未完成的高位清空工作。第4步中已经分析过了,最终结果仅需6个二进制位即可表示,所以最后清空高26位的数据,计算过程如下

00000100000010100000110100010000
                ⇩
00000000000000000000000000010000

代码表示如下(0x0000003f的二进制值是0b_00000000000000000000000000111111)

i = i & 0x0000003f;

所以最终的结果是0b_10000即16

可以看出来,Integer.bitCount方法在代码所耗费的指令上已经作了极尽的优化。