目的: 檢驗學員對 bitops 及數值系統的認知
1
延伸〈從模除偏差談亂數分布〉和第三份作業提到的「資訊理論和亂數產生器」,以下利用 select 系統呼叫開發類似 traceroute 的程式。
在高鐵列車上測試:
補完 traceroute.c (部分遮蔽),使其運作符合預期。書寫規範:
x + 1
,而非 x + 2
延伸閱讀: How to Read a Traceroute
延伸問題:
2
求 的值,其中 為已知實數。將這個問題轉換成求指數形式:
也就是要知道 能拆成多少個 的冪相乘,只要把這些冪相加即可。
下面是可能做法:
把 不斷除以 2,直到除不盡時剩下一個 :
其中 比 2 小,該怎麼把它表示成以 2 為底的冪呢?
因為指數函數 的增量會以 的冪形式跳動,故 可以看成 。同理,我們可觀察 若大於 2,便可繼續遞迴。亦即:
架設 ,那就跟當初拆 一樣,可將 拆開:
同樣地
再設 ,繼續跟 相同的拆法,依此類推。這用遞迴概念去計算對數,不用查表,也不需要任何高等數學知識。
若底數不是 2,而是假設某個 ,做法也一樣:
然後依照 的方式處理。接著將 再遞迴拆解即可。
概念程式碼如下,可計算任意底數的對數: (部分遮蔽)
參考執行輸出:
作答規範:
;
或 ?
或 :
延伸問題:
3
第二週教材系統軟體開發思維提到 Bloom filter,這利用雜湊函數,在不用走訪全部元素的前提,「預測」特定字串是否存於資料結構中。因此時間複雜度是 ,而非傳統逐一搜尋的 。
建構:
0
到 n - 1
) ,所以有 k 個雜湊函數,就該有 k 個 index ,然後將該 table[index]
上的位元 set (即指定為 1
)此後若要檢查該字串 s 是否存在時,只需要再執行前述 k 個雜湊函數,並檢查是否所有的 k 個位元都是 set 即可
但該做法存在錯誤率,例如原本沒有在資料結構中的字串 s1 經過雜湊函數轉換後得出的位元的位置和另一個存在於資料結構中的字串 s2 經過雜湊函數轉換後的結果相同,如此一來,先前不存在的 s1 便會被認為存在,這就是 false positive (指進行實用測試後,測試結果可能無法反映出真正的面貌或狀況)。
Bloom filter 一類手法的應用很常見。例如在社群網站 Facebook,在搜尋欄鍵入名字時,能夠在 20 億個註冊使用者 (2017 年統計資料) 中很快找到結果,甚至是根據與使用者的關聯度排序。
延伸閱讀:
首先,建立一個 n 個位元構成的 table,並將每個位元初始化為 0。
我們將所有的字串構成的集合 (set) 表示為 S = { x1, x2, x3, … ,xn },Bloom Filter 會使用 k 個不同的雜湊函數,每個雜湊函數轉換後的範圍都是 0 到 n-1 (為了能夠對應上面建立的 n 個位元構成的 table)。而對每個 S 內的 element xi,都需要經過 k 個雜湊函數,一一轉換成 k 個 index。轉換過後,table 上的這 k 個 index 的值就必須設定為 1
。
注意: 可能會有同一個 index 被多次設定為
1
的狀況
Bloom Filter 這樣的機制,存在錯誤率。若今天想要找一個字串 x 在不在 S 中,這樣的資料結構只能保證某個 x1 一定不在 S 中,但沒辦法 100% 確定某個 x2一定在 S 中。因為會有誤判 (false positive) 的可能。
此外,資料只能夠新增,而不能夠刪除,試想今天有二個字串 x1, x2 經過某個雜湊函數 hi 轉換後的結果 hi(x1) = hi(x2),若今天要刪除 x1 而把 table 中 set 的 1 改為 0,豈不是連 x2 都受到影響?
首先假設所有字串集合 S 裡面有 n 個字串,雜湊函數總共有 k 個,Bloom Filter 的 table 總共 m 位元。我們會判斷一個字串存在於 S 內,是看經過轉換後的每個位元都被 set 了,我們就會說可能這個字串在 S 內。但試想若是其實這個字串不在 S 內,但是其他的 a b c 等等字串經過轉換後的 index ,剛好涵蓋目標字串轉換後的 index,就造成了誤判這個字串在 S 內的情況。
如上述,Bloom Filter 存有錯誤機率,程式開發應顧及回報錯誤機率給使用者,以下分析錯誤率。
當我們把 S 內的所有字串,每一個由 k 個雜湊函數轉換成 index 並把 table[index]
設為 1,而全部轉換完畢時,table 中某個位元仍然是 0 的機率是 。
其中 是每次雜湊函數轉換後,table 的某個位元仍然是 0 的機率。因為我們把雜湊函數轉換後到每個 index (而這個 index 會被 set) 的機率視為相等 (每單位是 ),所以用 1 減掉即為不會被 set 的機率。我們總共需要做 kn 次雜湊運算,所以就得到 。
由 特性,可知
這就是當全部字串轉換完畢時,某個位元還沒有被 set 的機率。
因此誤判的機率等同於全部經由 k 個雜湊轉完後的 k 位元已被其他人 set 的機率:
轉完後某個位元被 set 機率是: ,因此某 k 位元被 set 機率為:
為確保錯誤率最小,也就是讓 的值是最小。先把原式改寫成 ,我們只要使 最小,原式就是最小值。可以看出當 時,會達到最小值。因此 即能達到最小值。
Bloom Filter calculator 和 Bloom Filter calculator 2 這二個網站可以透過設定自己要的錯誤率與資料的多寡,得到 k
和 m
值。
縱軸是雜湊函數的數量、橫軸是 table size,並以錯誤率做為深淺來製圖,畫面中紫色線條為 ,其中 93827 為測試用的字典中項目數量。
我們想要最小的空間及執行速度,並符合可預測的錯誤率,於是期望的數值在可行域的左下角。
為了讓圖片不明顯的部分,能被看得清楚,將圖片轉為灰階後做直方圖均衡化
把此圖視為高線圖,可以發現山谷都在 上,而在這裡實際的意義就是錯誤率最小值發生在 與推導的結果相符。透過開四次方根把錯誤率之間的差距加大,再製圖,可更明顯看出上述的內容。
bloom.[ch] 是上述 Bloom filter 的實作,並利用 Check 進行測試,預期的執行輸出如下: (假設 malloc 不會遇到任何錯誤)
安裝 Check 套件:
Bloom filter 程式碼 (部分遮蔽)
編譯和測試:
population count 簡稱 popcount 或叫 sideways sum,是計算數值的二進位表示中,有多少位元是 1
,在一些場合下很有用,例如計算 0-1 稀疏矩陣 (sparse matrix)或 bit array 中非 0
元素個數、計算兩個字串的 Hamming distance。Intel 在 2008 年 11 月 Nehalem 架構的處理器 Core i7 引入 SSE4.2 指令集,其中就有 CRC32
和 popcount
指令,popcount
可處理 16-bit, 32-bit, 64-bit 整數。
對應到 C 程式的實作:
函式 popcount_naive()
利用不斷清除 LSB 直到輸入的數值 v
為 0。
v &= (v - 1)
的作用是將 LSB 設為 0 (reset LSB) 舉例來說,假設輸入數值為 20
:
類似的操作還有
x & -x
,將x
的 LSB 取出 (isolate LSB)
n = -(~n)
等同 n++
,因為在二補數系統中,
因此 popcount_naive()
的執行時間取決於輸入數值中 1 (set bit) 的個數。可改寫為以下常數時間複雜度的實作:
對於一個 32 bit 的無號整數,popcount 可以寫成以下數學式:
假設 ,先看看 4 個位元,用以上公式可以計算得:
相當於 C 表達式中
x >> 1
稍微改寫可得到:
因此 popcount 的一般式可改寫為:
因為 ,只要對應的 為 1,這個 bit 就會在 popcount 的總和中加一,剛好對應 popcount_naive()
,因此映證上述數學式確實可計算出 population count。
且一個 32 位元無號整數最多有 32 個 1 (set bit),剛好可用一個 byte 表示,所以可分成幾個區塊平行計算,最後再全部加總到一個 byte 中,進行避免檢查 32 次。
popcount_branchless
實作一開始以每 4 個位元 (nibble) 為一個單位計算 1 的個數,利用最初的公式計算
關鍵的程式碼,逐行解釋:
n = (v >> 1) & 0x77777777
: 將輸入數值 v
除以 2,得到
v -= n
: 計算結果相當於 n = (n >> 1) & 0x77777777
: 再對 n
除以 2,得到 v -= n
: 計算出 最後這段結束後計算出 ,得到每 4 個位元為一個單位中 set bit 的個數
v = (v + (v >> 4)) & 0x0F0F0F0F
: 將每 4 個位元中 set bit 的個數加到 byte 中:
0x0F0F0F0F
做 mask 可得
v *= 0x01010101
: 在最後一道敘述中,將 v
乘上 0x01010101
。寫成直式如下
我們可發現期望輸出就在原本 的位置 (),因此將 v
右移 24 bits 即為所求,剩下的位數會 overflow ,右移後不影響結果。
return v >> 24
: 最後得到的結果會放在 Most significant byte 中,因此向右位移 24 位元,即為所求的 popcount 值。
GCC 提供對應的內建函式:
__builtin_popcount(x)
: 計算 x 的二進位表示中,總共有幾個1
使用示範:
兩個整數間的 Hamming distance 為其二進位的每個位元的差
Example :
1 (0 0 0 1)
4 (0 1 0 0)
hamming distance = 2
A B | B = 0 | B = 1 |
---|---|---|
A = 0 | 0 | 1 |
A = 1 | 1 | 0 |
A ^ B
就為二進位中兩數字的每個位元的差,再透過 popcount() 就可以得到答案
作答規範:
%
(modulo) 運算子,變數應當在 integer literal 前出現;
字元),應包含 popcount
,小括號對 (即 (
和 )
構成的 pair) 的數量僅為 1,也就是函式呼叫所用由於 Bloom filter 的限制,後來出現一系列改進的替代選擇,例如 Meta 發展的 Ribbon Filter (用於 RocksDB)、空間使用率更高且支援 delete 操作的 Cuckoo filter、空間使用率較 Cuckoo filter 高的 Xor filter,以及進一步改良的 Binary Fuse Filters。
延伸問題: