contributed by < jimmylu890303
>
在版本一中,會透過 log2(N)
去尋找 N 的最高位元,再從高位逐步去尋找滿足 (result + a) * (result + a) <= N
的位元。
以輸入 N = 36 為例, msb = 5 且 a = 100000
a | result | (result + a) * (result + a) <= N |
---|---|---|
100000(32) | 0 | X |
10000(16) | 0 | X |
1000(8) | 0 | x |
100(4) | 0 | O |
10(2) | 4 | O |
1(1) | 6 | X |
所以 result 的值為 6 ,在此版本中使用到 log2 ,所以需要在編譯時多新增編譯選項 -lm
在版本二中,去改善版本一使其不依賴 log2 並保持原本的函式原型 (prototype) 和精度 (precision),而改善的作法就是使用 shift 的方式去找出最高位的位元。
在版本三中使用 __builtin_clz(x)
的巨集來找出最高位在哪個位元算出 leading zero 個數。
x | m | z | b | z(after shift) | x >= b | x-=b | z+=m |
---|---|---|---|---|---|---|---|
36 | 10000(16) | 0 | 16 | 0 | O | 20 | 16 |
20 | 100(4) | 16 | 20 | 8 | O | 0 | 12 |
0 | 1(1) | 12 | 13 | 6 | X | 0 | 6 |
針對正整數在相鄰敘述進行 mod 10 和 div 10 操作,如何減少運算成本?
採用 bitwise operation 來實作除法
先考慮除以 10 的情況,除以 10 相當於乘以
但由於 無法以二進位的方式去表示,所以只能透過一個近似的值去替代
而 ,因為
所以 x 除以 10 相當於
更進一步拆解上述的式子
考慮以下式子,相當於
由於往右作 shift 3個位元會造成最右邊三個位元資訊損失,所以在往左 shift 後要將最右邊三個位元加回來。所以以下操作值會等於
所以最後往右 shift 7個位元相當於除上128,這樣就會得到 的結果
而 tmp 取 10 的餘數則利用餘數定理
q 為商數,則以下操作為 ,即可算出餘數 r
以上的概念可以包裝成以下程式碼
在以下操作中,
而這邊的操作是將 0.79 逼近至 0.8
考慮以下操作
q >> 3
為商數
而 q & ~0x7
將最低三位清零,相當於商數往左 shift 3位,即為商數的 8 倍
*div << 1
為商數的兩倍
((q & ~0x7) + (*div << 1))
所以這裡相當於商數乘上 10
在以下程式碼中,使用右移操作來計算 log 值
以 8 為例
round | log | i | i >>= 1 | log++ |
---|---|---|---|---|
1 | -1 | 1000 | 100 | 0 |
2 | 0 | 100 | 10 | 1 |
3 | 1 | 10 | 1 | 2 |
4 | 2 | 1 | 0 | 3 |
以 7 為例
round | log | i | i >>= 1 | log++ |
---|---|---|---|---|
1 | -1 | 0111 | 011 | 0 |
2 | 0 | 011 | 01 | 1 |
3 | 1 | 01 | 0 | 2 |
由以上觀察可發現 shift 操作次數是由 msb 在第幾個位元而定,所以這個實作也可以藉由 counting leading zero 來完成
以下程式碼是將版本一改善,假設輸入資料為 65536 ,在版本一中的 while 迴圈次數需要 65536+1 次,而在版本二中只需要 1 次的迴圈,對於輸入資料很大時,可以減少所需的時間。
使用 GNU extension __builtin_clz
來計算最高位前方有幾個 0 ,在透過 31 - 0 個個數即為最高位元的位置,此值即為 log 的值。
EWMA(指數加權移動平均) 是種取平均的統計手法,並且使經過時間越久的歷史資料的權重也會越低,以下為 EWMA 的數學定義:
以下為 ewma 的結構, internal 為 ,factor 為縮放因子, weight 為權重
考慮以下實作,
若 avg->internal 為 0 ,則為
若 avg->internal 不為 0 ,則為
以下程式碼計算
以下用於判別 msb 在左邊 16 位元或右 16 位元,若在左 16 位元則 x > 0xFFFF
條件成立,則將 x shift 16位元。
以下用於判別 msb 在這 16 位元中的左 8 位元或右 8 位元,r 用於紀錄 shift 幾個 bits
以此類推即可求出 之值
第一個為 x=0 時,做 x– 會導致 x 值變成 0xffffffff
,所以作了以下改進
x -= (!!x)
會使 x > 0 時才會作減法的動作。
第二個為 x=1 時,應該要回傳 0 的值,但是在原本的程式中會回傳 1 ,所以去檢查 !(r | shift | x > 0)
有無成立,若該條件成立代表 x = 0 或者 x = 1 的狀態,再將回傳值清零。
在最初的 popcount實作中,是使用迴圈將 LSB 的值清 0 ,
這邊使用特別操作的手法為 v &= (v-1)
, 此外 v &= (v-1)
的操作也可以用於判斷是否為 2 的冪次方。
n = -(~n)
為 n++ 的操作, 為二的補數操作,則
根據以下公式,我們可以計算 popcount(x)
以下操作為計算
0x77777777
的設定是因為這邊是以 4 個 bits 為單位 (nibble) ,為了防止另外一組的 nibble 值 shift 過來。
所以操作完後的值會結果如下(只考慮最右邊的 nibble),這個 nibble 即代表這 4 個 bits 的 popcount 之值 :
假設 為 nibble ,所以以下操作可以看出它的結果
最後在透過 v *= 0x01010101
將各個 nibble 相加
可以發現 A6+A4+A2+A0 是我們期望的結果,所以透過 v >> 24 取得。
使用 XOR 運算 可以得出兩數字的差異,在利用巨集 __builtin_popcount
即可以算出兩者的 Hamming Distance ,而 Total Hamming distance 就是每個數字都跟其他數字相比,所以時間複雜度會被限制在 。
假設輸入資料為 4, 14, 4 三筆資料,我們從最低位元到最高位元將每個數字的該位元加總,可以發現以下規律 : 該位元的加總 * (數字個數 - 該位元的加總) = 該位元的 Total Hamming distance ,並且時間複雜度為