JDK中Integer.bitCount解析

使用過Redis的人可能知道,Redis中給我們提供了統計二進制位數爲1的位數量的指令bitcount,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方法在代碼所耗費的指令上已經作了極盡的優化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章