深入理解 Go Map


文章參考:Go語言設計與實現3.3 哈希表

哈希表的意義不言而喻,它能提供 O(1) 復雜度的讀寫性能,所以主流編程語言中都內置有哈希表。

哈希表的關鍵在於哈希函數, 好的哈希函數能減少哈希碰撞,提供最優秀的讀寫性能。

哈希碰撞

因為沒有完美的哈希函數, 所以哈希碰撞不可避免,一般有開放尋址法和拉鏈法,其中拉鏈法是主流

  • 開放尋址法:當向哈希表寫入新的數據時,如果發生了沖突,就會將鍵值對寫入到下一個索引不為空的位置

    image-20210706223505867

  • 拉鏈法:拉鏈法一般使用數組和鏈表組成,數據經過哈希函數得到一個桶時,先遍歷桶中的鏈表,存在相同的鍵值對,則更新,不存在則在鏈表末尾追加新鍵值對

    image-20210706223857753

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
}

image-20210706224402496

哈希表 hmap 的桶是 bmap,每個 bmap 都能存儲 8 個鍵值對,單個桶裝滿時會使用 nextOverflow 桶存儲溢出的數據

type bmap struct {
	// 存儲了鍵的哈希的高 8 位
  // 通過比較不同鍵的哈希的高 8 位可以減少訪問鍵值對次數以提高性能
  tophash [bucketCnt]uint8
}

訪問 map 中的數據

image-20210706225253558

如上圖所示,每一個桶都是一整片的內存空間,當發現桶中的 tophash 與傳入鍵的 tophash 匹配之后,我們會通過指針和偏移量獲取哈希中存儲的鍵 keys[0] 並與 key 比較,如果兩者相同就會獲取目標值的指針 values[0] 並返回

向 map 寫入數據

image-20210706225431579

函數會根據傳入的鍵拿到對應的哈希和桶,通過遍歷比較桶中存儲的 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 上,新計算舊桶內元素的哈希到新桶上,

在擴容期間訪問哈希表時會使用舊桶,整個期間元素再分配的過程也是在調用寫操作時增量進行的,不會造成性能的瞬時巨大抖動


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM