go中map的資料結構理解

水墨先生發表於2021-10-06

這篇文章想說一下go中map的資料結構和我對它的理解,感興趣的可以往下接著看哈

Golang的map使用雜湊表作為底層實現,一個雜湊表裡可以有多個雜湊表節點,也即bucket,而每個bucket就儲存了map中的一個或一組鍵值對,看過map的底層資料結構應該知道它在runtime/map.go/hmap 定義:

type hmap struct {
    count int
    ...
    B uint8
    ...
    buckets unsafe.Pointer
    ...
}

給大家看一下擁有4個bucket的map:

Go

  • 上圖中,hmap(B=2),hmap.buckets長度是2^B為4. 元素經過雜湊運算後會落到某個bucket中進行儲存。查詢過程類似。

bucket資料結構由 runtime/map.go/bmap 定義,每個bucket可以儲存8個鍵值對:

type bmap struct {
    tophash [8]uint8 //儲存雜湊值的高8位
    data byte[1] //key value資料:key/key/key/.../value/value/value...
    overflow *bmap //溢位bucket的地址 
}
  • tophash是個長度為8的陣列,雜湊值相同的鍵(準確的說是雜湊值低位相同的鍵)存入當前bucket時會將雜湊值的高位儲存在該陣列中,以方便後續匹配。
  • data區存放的是key-value資料,存放順序是key/key/key/…value/value/value,如此存放是為了節省位元組對齊帶來的空間浪費。
  • overflow 指標指向的是下一個bucket,據此將所有衝突的鍵連線起來。
  • 上面程式中的data和overflow並不是在結構體中顯示定義的,而是直接通過指標運算進行訪問的。

下圖的bucket存放8個key-value對:

Go

來了解一下map中什麼是雜湊衝突

  • 當有兩個或以上數量的鍵被雜湊到了同一個bucket時,我們稱這些鍵發生了衝突。Go使用鏈地址法來解決鍵衝突。由於每個bucket可以存放8個鍵值對,所以同一個bucket存放超過8個鍵值對時就會再建立一個鍵值對,用類似連結串列的方式將bucket連線起來。

來看看發生衝突後的map是怎樣的,看下圖:
Go

  • bucket資料結構指示下一個bucket的指標稱為overflow bucket,意為當前bucket裝不下而溢位的部分。事實上
  • 雜湊衝突並不是好事情,它降低了存取效率,好的雜湊演算法可以保證雜湊值的隨機性,但衝突過多也是要控制的

瞭解了什麼是雜湊衝突,接著一起了解下什麼是負載因子

  • 負載因子用於衡量一個雜湊表衝突情況,公式為:
  • 負載因子 = 鍵數量/bucket數量
  • 對於一個bucket數量為4,包含4個鍵值對的雜湊表來說,這個雜湊表的負載因子為1
  • 雜湊表需要將負載因子控制在合適的大小,超過其閥值需要進行rehash,也即鍵值對重新組織:
    1. 雜湊因子過小,說明空間利用率低
    2. 雜湊因子過大,說明衝突嚴重,存取效率低
  • 每個雜湊表的實現對負載因子容忍程度不同,比如Redis實現中負載因子大於1時就會觸發rehash,而Go則在在負載因子達到6.5時才會觸發rehash,因為Redis的每個bucket只能存1個鍵值對,而Go的bucket可能存8個鍵值對,所以Go可以容忍更高的負載因子。

瞭解完負載因子後接著一起map是怎麼擴容的吧

  • 為了保證訪問效率,當新元素將要新增進map時,都會檢查是否需要擴容,擴容實際上是以空間換時間的手段。觸發擴容的條件有二個:

    1. 負載因子>6.5時,也即平均每個bucket儲存的鍵值對達到6.5個。
    2. overflow數量>2^15時,也即overflow數量超過32768時。
  • 當負載因子過大時,就新建一個bucket,新的bucket長度是原來的2倍,然後舊bucket資料搬遷到新的bucket。考慮到如果map儲存了數以億計的key-value,一次性搬遷將會造成比較大的延時,Go採用逐步搬遷策略,即每次訪問map時都會觸發一次搬遷,每次搬遷2個鍵值對

  • 下圖展示了包含一個bucket滿載的map:

Go

  • 上圖可以看出map儲存了7個鍵值對,只有1個bucket。時地負載因子為7。再次插入資料時將會觸發擴容操作,擴容之後再將新插入鍵寫入新的bucket。當第8個鍵值對插入時,將會觸發擴容,擴容後示意圖如下:

Go

  • hmap資料結構中oldbuckets成員指身原bucket,而buckets指向了新申請的bucket。新的鍵值對被插入新的bucket中。後續對map的訪問操作會觸發遷移,將oldbuckets中的鍵值對逐步的搬遷過來。當oldbuckets中的鍵值對全部搬遷完畢後,刪除oldbuckets,搬遷完成後的示意圖如下:

Go

map的資料結構就分享到這裡了,它的結構很複雜,還有很多是我沒有了解到的,如果有特別想要完全瞭解map的,也只能自己一步步深挖go的原始碼啦

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章