golang map實現原理淺析


總體來說golang的maphashmap,是使用數組+鏈表的形式實現的,使用拉鏈法消除hash沖突


map的內存模型


我的go源碼版本是:go1.17.2

map的源碼在Go_SDK\go1.17.2\src\runtime\map.go中。


首先我們來看一下map最重要的兩個結構:

hmap:

// A header for a Go map.
type hmap struct {
	// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
	// Make sure this stays in sync with the compiler's definition.
	count     int // # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8
	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
	hash0     uint32 // hash seed

	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
	oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

	extra *mapextra // optional fields
}

bmap:(bucket桶)

// A bucket for a Go map.
type bmap struct {
	// tophash generally contains the top byte of the hash value
	// for each key in this bucket. If tophash[0] < minTopHash,
	// tophash[0] is a bucket evacuation state instead.
	tophash [bucketCnt]uint8
	// Followed by bucketCnt keys and then bucketCnt elems.
	// NOTE: packing all the keys together and then all the elems together makes the
	// code a bit more complicated than alternating key/elem/key/elem/... but it allows
	// us to eliminate padding which would be needed for, e.g., map[int64]int8.
	// Followed by an overflow pointer.
}

實際上在golang runtime時,編譯器會動態為bmap創建一個新結構:

type bmap struct {
    topbits  [8]uint8
    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr
    overflow uintptr
}

Golang中map的底層實現是一個哈希表,因此實現map的過程實際上就是實現哈希表的過程。在這個哈希表中,主要出現的結構體有兩個,一個叫hmap(a header for a go map),一個叫bmap(a bucket for a Go map,通常叫其bucket)。這兩種結構的樣子分別如下所示:


hmap:

在這里插入圖片描述
圖中有很多字段,但是便於理解map的架構,你只需要關心的只有一個,就是標紅的字段:buckets數組。Golang的map中用於存儲的結構是bucket數組。


bucket:
在這里插入圖片描述
標紅的字段依然是“核心”,map中的key和value就存儲在這里。“高位哈希值”數組記錄的是當前bucket中key相關的“索引”,稍后會詳細敘述。還有一個字段是一個指向擴容后的bucket的指針,使得bucket會形成一個鏈表結構。例如下圖:

在這里插入圖片描述
由此看出hmapbucket的關系是這樣的:

在這里插入圖片描述
而bucket又是一個鏈表,所以整體的結構應該是這樣的:

在這里插入圖片描述
哈希表的特點是會有一個哈希函數,對傳進來的key進行哈希運算,得到唯一的值,一般情況下都是一個數值。Golang的map中也有這么一個哈希函數,也會算出唯一的值,對於這個值的使用:

Golang把求得的值按照用途一分為二:高位和低位。

在這里插入圖片描述
如圖所示,藍色為高位,紅色為低位。 然后低位用於尋找當前key屬於hmap中的哪個bucket,而高位用於尋找bucket中的哪個key。上文中提到:bucket中有個屬性字段是“高位哈希值”數組,這里存的就是藍色的高位值,用來聲明當前bucket中有哪些“key”,便於搜索查找。 需要特別指出的一點是:我們map中的key/value值都是存到同一個數組中的。數組中的順序是這樣的:
在這里插入圖片描述
並不是key0/value0/key1/value1的形式,這樣做的好處是:在key和value的長度不同的時候,可以消除padding(內存對齊)帶來的空間浪費。


現在,我們可以得到Go語言map的整個的結構圖了:(hash結果的低位用於選擇把KV放在bmap數組中的哪一個bucket中,高位用於key的快速預覽,用於快速試錯)

在這里插入圖片描述

map的擴容


負載因子


判斷擴充的條件,就是哈希表中的負載因子(即loadFactor)。
每個哈希表的都會有一個負載因子,數值超過負載因子就會為哈希表擴容。
Golang的map的加載因子的公式是:map長度 / 2^B(這是代表bmap數組的長度,B是取的低位的位數)閾值是6.5。其中B可以理解為已擴容的次數。


漸進式擴容


需要擴容時就要分配更多的桶(Bucket),它們就是新桶。需要把舊桶里儲存的鍵值對都遷移到新桶里。如果哈希表存儲的鍵值對較多,一次性遷移所有桶所花費的時間就比較顯著。
所以通常會在哈希表擴容時,先分配足夠多的新桶,然后用一個字段(oldbuckets)記錄舊桶的位置。
再增加一個字段(nevacuate),記錄舊桶遷移的進度。例如記錄下一個要遷移的舊桶編號。
在哈希表每次進行讀寫操作時,如果檢測到當前處於擴容階段,就完成一部分鍵值對遷移任務,直到所有的舊桶遷移完成,舊桶不再使用,才算真正完成一次哈希表的擴容。
像這樣把鍵值對遷移的時間分攤到多次哈希表操作中的方式,就是漸進式擴容,可以避免一次性擴容帶來的性能瞬時抖動。

在這里插入圖片描述


擴容規則


bmap結構體的最后一個字段是一個bmap型指針,指向一個溢出桶。溢出桶的內存布局與常規桶相同,是為了減少擴容次數而引入的。
當一個桶存滿了,還有可用的溢出桶時,就會在后面鏈一個溢出桶,繼續往這里面存。

在這里插入圖片描述
實際上如果哈希表要分配的桶數目大於2 ^ 4,就認為要使用到溢出桶的幾率較大,就會預分配2 ^ (B - 4)個溢出桶備用。
這些溢出桶與常規桶在內存中是連續的,只是前2 ^ B個用做常規桶,后面的用作溢出桶。

hmap結構體最后有一個extra字段,指向一個mapextra結構體。里面記錄的都是溢出桶相關的信息。nextoverflow指向下一個空閑溢出桶。
overflow是一個slice,記錄目前已經被使用的溢出桶的地址。noverflower記錄使用的溢出桶數量。oldoverflower用於在擴容階段儲存舊桶用到的那些溢出桶的地址。

在這里插入圖片描述


翻倍擴容


當負載因子 count / (2 ^ B) > 6.5 ,就會發生翻倍擴容(hmap.B++),分配新桶的數量是舊桶的兩倍。
buckets指向新分配的兩個桶,oldbuckets指向舊桶。nevacuate為0,表示接下來要遷移編號為0的舊桶。
每個舊桶的鍵值對都會分流到兩個新桶中。

在這里插入圖片描述


等量擴容


如果負載因子沒有超標,但是使用的溢出桶較多,也會出發擴容,不過這一次是等量擴容

那么用多少溢出桶算多了呢?

  • 如果常規桶的數目不大於 2 ^ 15 ,那么使用溢出桶的數目超過常規桶就算是多了。
  • 如果常規桶的數目大於 2 ^ 15 ,那么使用溢出桶的數目一旦超過 2 ^ 15 ,就算是多了。

所謂等量擴容,就是創建和舊桶數目一樣多的新桶。然后把原來的鍵值對遷移到新桶中,但是既然是等量,那來回遷移的又有什么用呢?
什么情況下,桶的負載因子沒有超過上限值,卻偏偏使用了很多溢出桶呢?自然是有很多鍵值對被刪除的情況。同樣數目的鍵值對,遷移到新桶中,能夠排列的更加緊湊,從而減少溢出桶的使用。這就是等量擴容的意義所在。


參考博客:
https://www.cnblogs.com/maji233/p/11070853.html

https://www.bilibili.com/video/BV1Sp4y1U7dJ?spm_id_from=333.999.0.0


免責聲明!

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



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