Go中的Map实现机制
Map大合集
- 1. 原理
- 2.1 哈希沖突
- 2.2 Map底層原理剖析
- 2.2.1 初始化
- 2.2.2 寫入數據
- 2.2.3 查找數據
- 2.2.4 擴容
- 2.2.5 遷移
- 翻倍擴容
- 等量擴容
- 2.3 map不安全問題
1. 原理
Go中的map原理是 :
將多個鍵 / 值 (key / value)對分散的存儲在hashBuckets(哈希桶)中
給定一個鍵, 通過特定的哈希算法會計算出鍵值對的哈希值, 然后再用哈希值 % array_size, 就得了對應的存儲下標 index
hash表:哈希值會確定其鍵應該映射到哪一個桶。而一個好的哈希函數,應當盡量少的出現哈希沖突,以此保證操作哈希表的時間復雜度
2.1 哈希沖突
哈希函數在實際中遇到的最常見的問題就是哈希碰撞, 即不同的鍵通過哈希函數可能產生相同的哈希值, 則他們會被分配到同一個桶中
主要有兩種策略
拉鏈法是將同一個桶中的元素通過鏈表的形式連接起來.
好處就是 : 不用預先申請空間, 可以不斷的鏈接新的元素
缺點就是 : 需要額外的指針來連接元素, 增加了哈希表的大小, 同時如果同一個桶中的連接元素太多了, 那么對于查找來說就不嚴格符合O(1), 當一條鏈表過長時, 我們需要改變這種情況, 讓這個鏈表減少元素 , 更改哈希函數, 或者擴容.
所有元素存儲在桶的數組中, 當插入新元素時, 將按照某種探測策略操作, 直到找到未使用的數組插槽為止
go中采用的是開放尋址法中的線性探測策略, 每次順序加1
2.2 Map底層原理剖析
Golang中的Map有自己的一套實現原理,其核心是由hmap和bmap兩個結構體實現。
2.2.1 初始化
// 初始化一個可容納10個元素的map info = make(map[string]string,10)-
第一步:創建一個hmap結構體對象。
-
第二步:生成一個哈希因子hash0 并賦值到hmap對象中(用于后續為key創建哈希值)。
-
第三步:根據hint=10,并根據算法規則來創建 B,當前B應該為1。
hint B 0~8 0 9~13 1 14~26 2 ... -
第四步:根據B去創建去創建桶(bmap對象)并存放在buckets數組中,當前bmap的數量應為2.
- 當B<4時,根據B創建桶的個數的規則為:2B2^B2B(標準桶)
- 當B>=4時,根據B創建桶的個數的規則為:2B2^B2B + 2B?42^{B-4}2B?4(標準桶+溢出桶)
注意:每個bmap中可以存儲8個鍵值對,當不夠存儲時需要使用溢出桶,并將當前bmap中的overflow字段指向溢出桶的位置。
2.2.2 寫入數據
info["name"] = "王嘉豪"在map中寫入數據時,內部的執行流程為:
-
第一步:結合哈希因子和鍵 name生成哈希值 011011100011111110111011011。
-
第二步:獲取哈希值的后B位,并根據后B位的值來決定將此鍵值對存放到那個桶中(bmap)。
將哈希值和桶掩碼(B個為1的二進制)進行 & 運算,最終得到哈希值的后B位的值。假設當B為1時,其結果為 0 : 哈希值:011011100011111110111011010 桶掩碼:000000000000000000000000001 結果: 000000000000000000000000000 = 0通過示例你會發現,找桶的原則實際上是根據后B為的位運算計算出 索引位置,然后再去buckets數組中根據索引找到目標桶(bmap)。 -
第三步:在上一步確定桶之后,接下來就在桶中寫入數據。
獲取哈希值的tophash(即:哈希值的`高8位`),將tophash、key、value分別寫入到桶中的三個數組中。 如果桶已滿,則通過overflow找到溢出桶,并在溢出桶中繼續寫入。注意:以后在桶中查找數據時,會基于tophash來找(tophash相同則再去比較key)。 -
第四步:hmap的個數count++(map中的元素個數+1)
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Cp491mQI-1639726855036)(assets/image-20200919191800912.png)]
2.2.3 查找數據
value := info["name"]在map中讀取數據時,內部的執行流程為:
-
第一步:結合哈希引子和鍵 name生成哈希值。
-
第二步:獲取哈希值的后B位,并根據后B為的值來決定將此鍵值對存放到那個桶中(bmap)。
-
第三步:確定桶之后,再根據key的哈希值計算出tophash(高8位),根據tophash和key去桶中查找數據。
當前桶如果沒找到,則根據overflow再去溢出桶中找,均未找到則表示key不存在, 返回value類型的零值
2.2.4 擴容
在向map中添加數據時,當達到某個條件,則會引發字典擴容。
擴容條件:
- map中數據總個數 / 桶個數 > 6.5 ,引發翻倍擴容。
- 使用了太多的溢出桶時(溢出桶使用的太多會導致map處理速度降低)。
- B <=15,已使用的溢出桶個數 >= 2B2^B2B 時,引發等量擴容。
- B > 15,已使用的溢出桶個數 >= 2152^{15}215 時,引發等量擴容。
當擴容之后:
- 第一步:B會根據擴容后新桶的個數進行增加(翻倍擴容新B=舊B+1,等量擴容 新B=舊B)。
- 第二步:oldbuckets指向原來的桶(舊桶)。
- 第三步:buckets指向新創建的桶(新桶中暫時還沒有數據)。
- 第四步:nevacuate設置為0,表示如果數據遷移的話,應該從原桶(舊桶)中的第0個位置開始遷移。
- 第五步:noverflow設置為0,擴容后新桶中已使用的溢出桶為0。
- 第六步:extra.oldoverflow設置為原桶(舊桶)已使用的所有溢出桶。即:h.extra.oldoverflow = h.extra.overflow
- 第七步:extra.overflow設置為nil,因為新桶中還未使用溢出桶。
- 第八步:extra.nextOverflow設置為新創建的桶中的第一個溢出桶的位置。
2.2.5 遷移
擴容之后,必然要伴隨著數據的遷移,即:將舊桶中的數據要遷移到新桶中。
翻倍擴容
如果是翻倍擴容,那么遷移規就是將舊桶中的數據分流至新的兩個桶中(比例不定),并且桶編號的位置為:同編號位置 和 翻倍后對應編號位置。
那么問題來了,如何實現的這種遷移呢?
首先,我們要知道如果翻倍擴容(數據總個數 / 桶個數 > 6.5),則新桶個數是舊桶的2倍,即:map中的B的值要+1(因為桶的個數等于2B2^B2B,而翻倍之后新桶的個數就是2B2^B2B * 2 ,也就是2B+12^{B+1}2B+1,所以 新桶的B的值=原桶B + 1 )。
遷移時會遍歷某個舊桶中所有的key(包括溢出桶),并根據key重新生成哈希值,根據哈希值的 底B位 來決定將此鍵值對分流道那個新桶中。
擴容后,B的值在原來的基礎上已加1,也就意味著通過多1位來計算此鍵值對要分流到新桶位置,如上圖:
- 當新增的位(紅色)的值為 0,則數據會遷移到與舊桶編號一致的位置。
- 當新增的位(紅色)的值為 1,則數據會遷移到翻倍后對應編號位置。
例如:
舊桶個數為32個,翻倍后新桶的個數為64。 在重新計算舊桶中的所有key哈希值時,紅色位只能是0或1,所以桶中的所有數據的后B位只能是以下兩種情況:- 000111【7】,意味著要遷移到與舊桶編號一致的位置。- 100111【39】,意味著會遷移到翻倍后對應編號位置。特別提醒:同一個桶中key的哈希值的低B位一定是相同的,不然不會放在同一個桶中,所以同一個桶中黃色標記的位都是相同的。等量擴容
如果是等量擴容(溢出桶太多引發的擴容),那么數據遷移機制就會比較簡單,就是將舊桶(含溢出桶)中的值遷移到新桶中。
這種擴容和遷移的意義在于:當溢出桶比較多而每個桶中的數據又不多時,可以通過等量擴容和遷移讓數據更緊湊,從而減少溢出桶。
2.3 map不安全問題
map 不是線程安全的。
在查找、賦值、遍歷、刪除的過程中都會檢測寫標志,一旦發現寫標志置位(等于1),則直接 panic。賦值和刪除函數在檢測完寫標志是復位之后,先將寫標志位置位,才會進行之后的操作。
if h.flags&hashWriting ==0{throw("concurrent map writes") }如果要使用線程安全的map, 就可以去了解一下sync.Map
這個就是加了讀寫鎖的map, 但是一但加鎖, 對于性能必定有影響. 各種協程, 對于鎖的競爭是很激烈的.
總結
以上是生活随笔為你收集整理的Go中的Map实现机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Go中切片扩容原理
- 下一篇: Go语言defer详解