文章參考:Go語言設計與實現3.3 雜湊表
雜湊表的意義不言而喻,它能提供 O(1) 複雜度的讀寫效能,所以主流程式語言中都內建有雜湊表。
雜湊表的關鍵在於雜湊函式, 好的雜湊函式能減少雜湊碰撞,提供最優秀的讀寫效能。
雜湊碰撞
因為沒有完美的雜湊函式, 所以雜湊碰撞不可避免,一般有開放定址法和拉鍊法,其中拉鍊法是主流
-
開放定址法:當向雜湊表寫入新的資料時,如果發生了衝突,就會將鍵值對寫入到下一個索引不為空的位置
-
拉鍊法:拉鍊法一般使用陣列和連結串列組成,資料經過雜湊函式得到一個桶時,先遍歷桶中的連結串列,存在相同的鍵值對,則更新,不存在則在連結串列末尾追加新鍵值對
Go 表示雜湊表的資料結構
type hmap struct {
// 表示雜湊表中元素的數量
count int
flags uint8
// 表示雜湊表中桶的數量, len(buckets) = 2^B
B uint8
noverflow uint16
// hash函式的種子
hash0 uint32
buckets unsafe.Pointer
// 用於在擴容時儲存之前 buckets
// 因為每次擴容都是2的倍數,所以 bucket = 2oldbuckets
oldbuckets unsafe.Pointer
nevacuate uintptr
extra *mapextra
}
type mapextra struct {
overflow *[]*bmap
oldoverflow *[]*bmap
nextOverflow *bmap
}
雜湊表 hmap 的桶是 bmap,每個 bmap 都能儲存 8 個鍵值對,單個桶裝滿時會使用 nextOverflow 桶儲存溢位的資料
type bmap struct {
// 儲存了鍵的雜湊的高 8 位
// 通過比較不同鍵的雜湊的高 8 位可以減少訪問鍵值對次數以提高效能
tophash [bucketCnt]uint8
}
訪問 map 中的資料
如上圖所示,每一個桶都是一整片的記憶體空間,當發現桶中的 tophash
與傳入鍵的 tophash
匹配之後,我們會通過指標和偏移量獲取雜湊中儲存的鍵 keys[0]
並與 key
比較,如果兩者相同就會獲取目標值的指標 values[0]
並返回
向 map 寫入資料
函式會根據傳入的鍵拿到對應的雜湊和桶,通過遍歷比較桶中儲存的 tophash
和鍵的雜湊,如果找到了相同結果就會返回目標位置的地址,獲得目標地址之後會通過算術計算定址獲得鍵值對 k 和 val, 如果當前鍵值對在雜湊中不存在,雜湊會為新鍵值對規劃儲存的記憶體地址,這期間只會返回記憶體地址,真正的賦值操作是在編譯期間插入的。
00018 (+5) CALL runtime.mapassign_fast64(SB)
00020 (5) MOVQ 24(SP), DI ;; DI = &value
00026 (5) LEAQ go.string."88"(SB), AX ;; AX = &"88"
00027 (5) MOVQ AX, (DI) ;; *DI = AX
我們通過 LEAQ
指令將字串的地址儲存到暫存器 AX
中,MOVQ
指令將字串 "88"
儲存到了目標地址上完成了這次雜湊的寫入
擴容
隨著雜湊表中元素的逐漸增加,雜湊表的效能會逐漸惡化,當裝載因子 > 6.5 時, 或者 雜湊表建立了太多的溢位桶, 會觸發擴容
裝載因子 = 元素數量 / 桶數量
雜湊表在擴容的過程中會建立一組新桶和溢位桶,隨後將原油的桶陣列設定到 oldbuckets 上,將新桶設定到 buckets 上,新計算舊桶內元素的雜湊到新桶上,
在擴容期間訪問雜湊表時會使用舊桶,整個期間元素再分配的過程也是在呼叫寫操作時增量進行的,不會造成效能的瞬時巨大抖動