問題:git
使用Go實現bitcount函數,統計一個uint64型數值中被設置爲 1 的比特位的數量。github
方案一:golang
最容易想到的實現就是每次右移一位,檢測最後一位是不是1,這樣完成挨個比特檢測後,就能夠得出結果。web
func bitCount1(n uint64)int8{ var count int8 var i uint for i < 64 { if ( (n >> i) & 1) != 0{ count += 1 } i += 1 } return count } var BitCount = bitCount1
實現一個測試函數和一個基準測試函數測試正確性和性能:算法
測試環境:segmentfault
型號名稱: MacBook Pro
處理器名稱: Intel Core i7
處理器速度: 2.5 GHz
處理器數目: 1
核總數: 4
L2 緩存(每一個核): 256 KB
L3 緩存: 6 MB
超線程技術: 已啓用
內存: 16 GB數組
// main_test.go package main import "testing" var tests = []struct{ input uint64 want int8 }{ { 7118255637391829670 , 34 }, { 7064722311543391783 , 25 }, { 4608963400064623015 , 34 }, { 14640564048961355682 , 39 }, { 8527726038136987990 , 27 }, { 9253052485357177493 , 29 }, { 8999835155724014433 , 28 }, { 14841333124033177794 , 35 }, { 1220369398144154468 , 33 }, { 15451339541988045209 , 33 }, { 2516280747705128559 , 28 }, { 4938673901915240208 , 29 }, { 410238832127885933 , 29 }, { 1332323607442058439 , 33 }, { 15877566392368361617 , 30 }, { 3880651382986322995 , 35 }, { 3639402890245875445 , 30 }, { 16428413304724738456 , 39 }, { 14754380477986223775 , 37 }, { 2517156707207435586 , 29 }, { 15317696849870933326 , 30 }, { 6013290537376992905 , 35 }, { 17378274584566732685 , 29 }, { 5420397259425817882 , 31 }, { 11286722219793612146 , 35 }, { 8183954261149622513 , 30 }, { 17190026713975474863 , 41 }, { 379948598362354167 , 34 }, { 3606292518508638567 , 31 }, { 10997458781072603457 , 33 }, { 7601699521132896572 , 31 }, { 16795555978365209258 , 34 }, { 9555709025715093094 , 35 }, { 2957346674371128176 , 29 }, { 6297615394333342337 , 36 }, { 15800332447329707343 , 31 }, { 10989482291558635871 , 36 }, { 10116688196032604814 , 29 }, { 13017684861263524258 , 29 }, { 9721224553709591475 , 35 }, { 7710983100732971068 , 28 }, { 11089894095639460077 , 38 }, { 938751439326355368 , 34 }, { 8732591979705398236 , 33 }, { 5679915963518233779 , 36 }, { 16532909388555451248 , 33 }, { 13248011246533683006 , 31 }, { 1317996811516389703 , 30 }, { 4318476060009242000 , 33 }, { 3082899072464871007 , 34 }, } func TestBitCount(t *testing.T){ for _, test := range tests{ if got := BitCount(test.input); got != test.want{ t.Errorf("BitCount(%q) = %v", test.input, got) } } } func BenchmarkBitCount(b *testing.B) { var input uint64 = 5679915963518233779 for i := 0; i < b.N; i ++{ BitCount(input) } }
命令行執行 go test -bench=.
, 輸出以下:
平均一次執行時間 91.2ns緩存
for循環固定執行了64次,能夠稍做優化,由於輸入數值的第64個比特不必定是1,只要檢測到最高位的那個1就能夠結束了。函數
func bitCount11(n uint64) int8{ var count int8 for n != 0 { if ( n & 1) != 0{ count += 1 } n = n >> 1 } return count } var BitCount = bitCount11
跑一下測試,
性能並無改觀,甚至更糟糕了一點。。。 不過在數值較小的狀況下,執行時間的確短不少,好比輸入是 1 的狀況下:
post
上一版的實現對應是 41.5ns
方案二:
方案一里的實現受限於最高位值位1的比特所在的位數,有沒有辦法只關注值爲1的比特的數量呢?熟悉位操做的話,很容易想到 n = n &(n-1)能夠將 n 的最低一個值爲1的比特位置爲0, 好比 14 & 13 = (0b1110) & (0b1101) = 0b1100 。 這樣數值n中有M個值爲1的比特位,循環檢測的次數就是M次了。
func bitCount2(n uint64)int8{ var count int8 for n != 0{ n = n & ( n - 1 ) count += 1 } return count }
跑一下測試:
時間降到原來1/3的水平了,提高仍是很給力。
若是輸入n是1的話,測試結果:
和初版裏優化過的結果旗鼓至關。
方案三:
上兩個版本的結果都受數值n中值爲1的比特位數量M的影響,有不有常數時間的算法呢?空間換時間的思路很容易想到查表,將一個字節(8位)的全部可能值和對應的值爲1的比特位數M預先寫入表中,而後將n分紅8個字節去查表,將結果相加就好了。
func bitCount31(n uint64)int8{ table := [256]int8{ 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8, }; var count int8 for n != 0 { count += table[n & 0xff] n = n >> 8 } return count }
測試結果以下:
11.6ns ! 只有方案二里的1/3了!
不過在n爲1的時候,表現稍差,達到了9.9ns 。
將表的大小改成16個(即對 4 bit 建表),能夠獲得另外一個有趣的結果:
func bitCount32(n uint64)int8{ table := [16]int8{0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4} var count int8 = 0 for n != 0 { count += table[n & 0xf] n = n >> 4 } return count }
當輸入n爲1的時候,只須要3.06ns,表現要好不少:
實際上,當輸入從0x0增加到0xffffffffffffffff的過程當中(每次左移4位,並將最低4位置爲0xf),前者耗時由8.86ns 線性增加到11.6ns, 後者耗時由2.41ns線性增加到11.5ns,所以用4bit建表效果更好。
方案四:
用分治的思路,一樣能夠常數時間內得出結果。將n的比特位依次按2一組,4一組,8一組,16一組,32個一組,64個一組,計算出1的位數便可。 以 0b1111爲例,兩個一組獲得: 1 1,1 1,將組內的每一個位的值相加,獲得這個組1的個數 :10,10,而後按4個一組,將組內的值相加,獲得 0b10+0b10 = 0b100,對應的十進制值是 4,就是0b1111的1的個數。(這個算法叫 variable-precision SWAR算法,更詳細的介紹能夠看文末的參考連接)
func bitCount41(n uint64)int8 { n = (n & 0x5555555555555555) + ((n >> 1) & 0x5555555555555555) n = (n & 0x3333333333333333) + ((n >> 2) & 0x3333333333333333) n = (n & 0x0f0f0f0f0f0f0f0f) + ((n >> 4) & 0x0f0f0f0f0f0f0f0f) n = (n & 0x00ff00ff00ff00ff) + ((n >> 8) & 0x00ff00ff00ff00ff) n = (n & 0x0000ffff0000ffff) + ((n >> 16) & 0x0000ffff0000ffff) n = (n & 0x00000000ffffffff) + ((n >> 32) & 0x00000000ffffffff) return int8(n & 0x7f) }
只要5.23ns,比查表的實現下降了一半。並且當n爲0或者0xffffffffffffffff,結果穩定在 5.23ns 左右。
uint64最多64個1,64對應的二進制值不會超出一個字節,針對沒必要要的計算,咱們再作一下優化:
func bitCount42(n uint64)int8 { n = n - ((n >> 1) & 0x5555555555555555) n = (n & 0x3333333333333333) + ((n >> 2) & 0x3333333333333333) n = (n & 0x0f0f0f0f0f0f0f0f) + ((n >> 4) & 0x0f0f0f0f0f0f0f0f) n = n + (n >> 8) n = n + (n >> 16) n = n + (n >> 32) return int8(n & 0x7f) }
耗時降到了3.91ns ! 降到接近查表法的 1/3了,降到了初版實現的 3.91/91.2 = 4.3%
附:
查表法的實現,能夠把table放在函數外(做爲全局變量),這樣基準測試數據更好看。table放在函數內的話,每次調用會在棧裏面分配空間放table數組的值,這個過程會比較耗時。table做爲全局變量的話,按4bit建表和按8bit建表的測試結果正好反過來,當n爲0時,兩者耗時都是 2ns,當n = 0xffffffffffffffff,前者耗時仍是約11.5ns,後者耗時約 7ns。當 n = 0xffffff(24位全1)時,後者耗時約4ns,和方案4的耗時至關。所以當輸入大部分分佈在0xffffff之內時,選擇8bit建表的查表法,時間性能更優。
能夠藉助pprof分析一下二者在代碼層面的耗時。
命令行裏依次執行:
$ go test -c main_test.go main.go $ ./main.test -test.bench=. -test.cpuprofile=cpu-profile.prof $ go tool pprof main.test cpu-profile.prof
接着在pprof的輸入:weblist bitCount
table做爲函數的局部變量:
table做爲全局變量:
當n爲0時,耗時:
當n爲0xffffffffffffffff時,耗時:
總結:
在上述實現的迭代優化過程當中,主要思路是下降循環內代碼塊的執行次數,從固定的64降到取決於最高一個1的位置,再降到比特值爲1的比特位的數量,最後經過查表或者分治下降到常數次數操做(按8bit建表時最多查8次,分治固定6次),查表須要執行一次函數棧內分配數組空間和賦值的過程,會耗時較多;而分治耗時低且表現穩定,是生產環境實現中最常採用的算法。
參考:
variable-precision SWAR算法:
https://ivanzz1001.github.io/...
https://segmentfault.com/a/11...
golang的 pprof 使用:
https://blog.golang.org/profi...
CPU分支預測模型: