golang map源码分析
2019獨(dú)角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
1. map數(shù)據(jù)結(jié)構(gòu)
Golang的map使用哈希表作為底層實(shí)現(xiàn),一個(gè)哈希表里可以有多個(gè)哈希表節(jié)點(diǎn),也即bucket,而每個(gè)bucket就保存了map中的一個(gè)或一組鍵值對(duì)。
map數(shù)據(jù)結(jié)構(gòu)由runtime/map.go/hmap定義:
type hmap struct {count int // 當(dāng)前保存的元素個(gè)數(shù)...B uint8 // 指示bucket數(shù)組的大小...buckets unsafe.Pointer // bucket數(shù)組指針,數(shù)組的大小為2^B... }下圖展示一個(gè)擁有4個(gè)bucket的map:
本例中,?hmap.B=2, 而hmap.buckets長(zhǎng)度是2^B為4. 元素經(jīng)過(guò)哈希運(yùn)算后會(huì)落到某個(gè)bucket中進(jìn)行存儲(chǔ)。查找過(guò)程類(lèi)似。
bucket很多時(shí)候被翻譯為桶,所謂的哈希桶實(shí)際上就是bucket。
2. bucket數(shù)據(jù)結(jié)構(gòu)
bucket數(shù)據(jù)結(jié)構(gòu)由runtime/map.go/bmap定義:
type bmap struct {tophash [8]uint8 //存儲(chǔ)哈希值的高8位data byte[1] //key value數(shù)據(jù):key/key/key/.../value/value/value...overflow *bmap //溢出bucket的地址 }每個(gè)bucket可以存儲(chǔ)8個(gè)鍵值對(duì)。
- tophash是個(gè)長(zhǎng)度為8的數(shù)組,哈希值相同的鍵(準(zhǔn)確的說(shuō)是哈希值低位相同的鍵)存入當(dāng)前bucket時(shí)會(huì)將哈希值的高位存儲(chǔ)在該數(shù)組中,以方便后續(xù)匹配。
- data區(qū)存放的是key-value數(shù)據(jù),存放順序是key/key/key/...value/value/value,如此存放是為了節(jié)省字節(jié)對(duì)齊帶來(lái)的空間浪費(fèi)。
- overflow 指針指向的是下一個(gè)bucket,據(jù)此將所有沖突的鍵連接起來(lái)。
注意:上述中data和overflow并不是在結(jié)構(gòu)體中顯示定義的,而是直接通過(guò)指針運(yùn)算進(jìn)行訪問(wèn)的。
下圖展示bucket存放8個(gè)key-value對(duì):
3. 哈希沖突
當(dāng)有兩個(gè)或以上數(shù)量的鍵被哈希到了同一個(gè)bucket時(shí),我們稱(chēng)這些鍵發(fā)生了沖突。Go使用鏈地址法來(lái)解決鍵沖突。
由于每個(gè)bucket可以存放8個(gè)鍵值對(duì),所以同一個(gè)bucket存放超過(guò)8個(gè)鍵值對(duì)時(shí)就會(huì)再創(chuàng)建一個(gè)鍵值對(duì),用類(lèi)似鏈表的方式將bucket連接起來(lái)。
下圖展示產(chǎn)生沖突后的map:?
bucket數(shù)據(jù)結(jié)構(gòu)指示下一個(gè)bucket的指針?lè)Q為overflow bucket,意為當(dāng)前bucket盛不下而溢出的部分。事實(shí)上哈希沖突并不是好事情,它降低了存取效率,好的哈希算法可以保證哈希值的隨機(jī)性,但沖突過(guò)多也是要控制的,后面會(huì)再詳細(xì)介紹。
4. 負(fù)載因子
負(fù)載因子用于衡量一個(gè)哈希表沖突情況,公式為:
負(fù)載因子 = 鍵數(shù)量/bucket數(shù)量例如,對(duì)于一個(gè)bucket數(shù)量為4,包含4個(gè)鍵值對(duì)的哈希表來(lái)說(shuō),這個(gè)哈希表的負(fù)載因子為1.
哈希表需要將負(fù)載因子控制在合適的大小,超過(guò)其閥值需要進(jìn)行rehash,也即鍵值對(duì)重新組織:
- 哈希因子過(guò)小,說(shuō)明空間利用率低
- 哈希因子過(guò)大,說(shuō)明沖突嚴(yán)重,存取效率低
每個(gè)哈希表的實(shí)現(xiàn)對(duì)負(fù)載因子容忍程度不同,比如Redis實(shí)現(xiàn)中負(fù)載因子大于1時(shí)就會(huì)觸發(fā)rehash,而Go則在在負(fù)載因子達(dá)到6.5時(shí)才會(huì)觸發(fā)rehash,因?yàn)镽edis的每個(gè)bucket只能存1個(gè)鍵值對(duì),而Go的bucket可能存8個(gè)鍵值對(duì),所以Go可以容忍更高的負(fù)載因子。
5. 漸進(jìn)式擴(kuò)容
5.1 擴(kuò)容的前提條件
為了保證訪問(wèn)效率,當(dāng)新元素將要添加進(jìn)map時(shí),都會(huì)檢查是否需要擴(kuò)容,擴(kuò)容實(shí)際上是以空間換時(shí)間的手段。
觸發(fā)擴(kuò)容的條件有二個(gè):
5.2 增量擴(kuò)容
當(dāng)負(fù)載因子過(guò)大時(shí),就新建一個(gè)bucket,新的bucket長(zhǎng)度是原來(lái)的2倍,然后舊bucket數(shù)據(jù)搬遷到新的bucket。
考慮到如果map存儲(chǔ)了數(shù)以億計(jì)的key-value,一次性搬遷將會(huì)造成比較大的延時(shí),Go采用逐步搬遷策略,即每次訪問(wèn)map時(shí)都會(huì)觸發(fā)一次搬遷,每次搬遷2個(gè)鍵值對(duì)。
下圖展示了包含一個(gè)bucket滿載的map(為了描述方便,圖中bucket省略了value區(qū)域):
?當(dāng)前map存儲(chǔ)了7個(gè)鍵值對(duì),只有1個(gè)bucket。此地負(fù)載因子為7。再次插入數(shù)據(jù)時(shí)將會(huì)觸發(fā)擴(kuò)容操作,擴(kuò)容之后再將新插入鍵寫(xiě)入新的bucket。
當(dāng)?shù)?個(gè)鍵值對(duì)插入時(shí),將會(huì)觸發(fā)擴(kuò)容,擴(kuò)容后示意圖如下:?
hmap數(shù)據(jù)結(jié)構(gòu)中oldbuckets成員指身原bucket,而buckets指向了新申請(qǐng)的bucket。新的鍵值對(duì)被插入新的bucket中。 后續(xù)對(duì)map的訪問(wèn)操作會(huì)觸發(fā)遷移,將oldbuckets中的鍵值對(duì)逐步的搬遷過(guò)來(lái)。當(dāng)oldbuckets中的鍵值對(duì)全部搬遷完畢后,刪除oldbuckets。
搬遷完成后的示意圖如下:
數(shù)據(jù)搬遷過(guò)程中原bucket中的鍵值對(duì)將存在于新bucket的前面,新插入的鍵值對(duì)將存在于新bucket的后面。 實(shí)際搬遷過(guò)程中比較復(fù)雜,將在后續(xù)源碼分析中詳細(xì)介紹。
5.3 等量擴(kuò)容
所謂等量擴(kuò)容,實(shí)際上并不是擴(kuò)大容量,buckets數(shù)量不變,重新做一遍類(lèi)似增量擴(kuò)容的搬遷動(dòng)作,把松散的鍵值對(duì)重新排列一次,以使bucket的使用率更高,進(jìn)而保證更快的存取。
在極端場(chǎng)景下,比如不斷的增刪,而鍵值對(duì)正好集中在一小部分的bucket,這樣會(huì)造成overflow的bucket數(shù)量增多,但負(fù)載因子又不高,從而無(wú)法執(zhí)行增量搬遷的情況,如下圖所示:
上圖可見(jiàn),overflow的buckt中大部分是空的,訪問(wèn)效率會(huì)很差。此時(shí)進(jìn)行一次等量擴(kuò)容,即buckets數(shù)量不變,經(jīng)過(guò)重新組織后overflow的bucket數(shù)量會(huì)減少,即節(jié)省了空間又會(huì)提高訪問(wèn)效率。
6. 查找過(guò)程
查找過(guò)程如下:
注:如果查找不到,也不會(huì)返回空值,而是返回相應(yīng)類(lèi)型的0值。
7. 插入過(guò)程
新員素插入過(guò)程如下:
轉(zhuǎn)載于:https://my.oschina.net/tantexian/blog/3037976
總結(jié)
以上是生活随笔為你收集整理的golang map源码分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: C#流程控制语句--跳转语句(break
- 下一篇: 热门的“挑战视频”,凭什么火起来的?