標籤:多線程 高效能 無鎖雜湊表
無鎖雜湊表(Lock-Free Hash Table)是多線程編程中的理想資料結構,但是實現以及使用都需要一定的技巧。作者對此做了一個巧妙的設計實現,在現代X86平台上能取得千萬次每秒的並發尋找/增加/刪除操作。
通過考察各種基於CAS原子操作的無鎖資料結構實現,目前公認可實現無鎖安全的資料結構是數組和單向隊列。其他實現都一定程度上受到ABA問題的威脅。數組的實現相對於單向隊列要簡單,所以無鎖hash table理想的選擇是數組,對於衝突不拉鏈。但是如何解決hash衝突呢?基本思想依然是開放定址探測法。為了同時支援增加/尋找/刪除三種操作,業界各種開放定址探測的演算法,都為一次探測做了最佳化且照顧了其他探測次數以及最壞情況,但是這個照顧動作對於無鎖設計實在是不太完美。為了達到O(1)的操作效能,開放定址應該限制探測的次數為固定有限次,超過這個數目的探測應該通過演算法降低為0,但同時用一個很小的數組(遍曆模式)收容最壞情況(這裡稱之為保險數組)。由此可以實現,所有的操作都是對數組做無鎖設計。
為了保證固定有限的探測次數極為有效(也就是夠用),應該通過演算法保證衝突率夠低。降低衝突,一般不外乎兩種措施:
一,增加hash表長度。此措施受限於記憶體,所以對每個bucket要盡量節省記憶體。一般用指標作為bucket單元就是很節省記憶體了。但64bit系統指標是8個位元組,我認為4個位元組作為bucket單元,就可以支援40億buckets了,同樣的記憶體帶來雙倍hash表長度,對降低hash衝突率有很大的改進。不用指標,怎麼去找hash node呢?你自然會想到“記憶體池”。對,4個位元組作為hash node在記憶體池的索引。
二,增加hash函數的分散效能,盡量接近理論極限。業界對於hash函數的討論和實現已經有很多了,高計算效能分散性極佳的已有很多,比如murmur hash,city hash。
但是,好像上面兩種辦法並沒有什麼特別的,跟大家常用的降低衝突率沒啥改進啊。這裡增加了第三種措施:固定有限的探測位置,如果盡量彼此獨立(也就是不會導致二次卷積),將會從理論上降低hash衝突機率。那麼如何做到彼此獨立呢?作者的設計就是利用hash函數的輸出 -- 選用輸出128位結果的hash函數,分割成四個32位整數(128位是隨機的因而四個整數是獨立的),每個32bit整數對hash table長度求模,可以得到4個獨立的位置,這樣將大大減少衝突率。
但是,這樣一般還不夠好。原因是我們想盡量提hash table的裝載率(load factor)且仍然能夠處理衝突增加的傾向。為此,增加第二個數組,用上面四個32bit整數同樣地映射出第二個數組內的4個位置,這樣對一個key我們就有8個獨立的位置了。8個位置都探測過了仍然衝突的怎麼辦?扔到保險數組裡面去。
至此我們有三個最佳化目標:1,進入保險數組的機率應該極低;2,總的bucket利用率(效能牆)盡量高;3,兩個數組佔用的記憶體之和應該最小。通過計算(此略),可以限定1,得到2和3的最佳配置。在作者的實現中建立了三個數組:一個高load factor的大數組作為主hash表,一個低load factor的小數組作為輔hash表,和一個極其微小(64或128足夠)的數組作為保險。
這個最佳化目標需要一個前提,就是基於機率計算的前提是可靠的,也就是對於任何key,選用hash函數輸出要足夠隨機。這個前提,對於key足夠長,city hash和murmurhash函數已經做得足夠好。但是對於key小於12位元組,就變差了。這個也是理論上沒辦法的事情。使用者應該明白並避讓這一點。
本文出自 “歪歪斜斜” 部落格,轉載請與作者聯絡!
一個高效能無鎖雜湊表的設計和實現