Golang中,通過哈希查找實現hash,通過鏈表解決hash沖突
map的內存模型
type hmap struct { count int // map 中的元素個數,必須放在 struct 的第一個位置,因為 內置的 len 函數會從這里讀取 flags uint8 B uint8 // log_2 of # of buckets (最多可以放 loadFactor * 2^B 個元素,再多就要 hashGrow 了) noverflow uint16 // overflow 的 bucket 的近似數 hash0 uint32 // hash 種子,為hash函數結果引入隨機性 buckets unsafe.Pointer // 2^B 大小的數組,如果 count == 0 的話,可能是 nil oldbuckets unsafe.Pointer // 一半大小的之前的 bucket 數組,只有在擴容過程中是非 nil nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) extra *mapextra // 當 key 和 value 都可以 inline 的時候,就會用這個字段 }
map中更小的單元桶,每一個桶會裝8個key,通過hash結果的高8位決定在桶里具體的位置,由hash結果的低B位決定落在哪個桶
bmap內存結構
// bucket 本體 type bmap struct { // tophash 是 hash 值的高 8 位 tophash [bucketCnt]uint8 // keys // values // overflow pointer }
bmap是存具體key-value的地方,進一步觀察bmap底層, 將key,value分開存儲可以避免在key、value不是同一種結構時出現的內存碎片,比如map[int64]int8,在每一個key-value后面都要padding7個字節,分開存儲就只需要在最后加padding
當一個bmap滿了以后,會通過overflow連接一個溢出桶,實際會將每一個bmap中的overflow統一遷移到hmap的extra中,主要是為了避免gc時掃描整個hmap,所以不在單獨的桶里設置指針,而是直接讓其指向hmap.extra.overflow數組
mapextra底層結構
type mapextra struct { // 如果 key 和 value 都不包含指針,並且可以被 inline(<=128 字節) // 使用 extra 來存儲 overflow bucket,這樣可以避免 GC 掃描整個 map // 然而 bmap.overflow 也是個指針。這時候我們只能把這些 overflow 的指針 // 都放在 hmap.extra.overflow 和 hmap.extra.oldoverflow 中了 // overflow 包含的是 hmap.buckets 的 overflow 的 bucket // oldoverflow 包含擴容時的 hmap.oldbuckets 的 overflow 的 bucket overflow *[]*bmap oldoverflow *[]*bmap // 指向空閑的 overflow bucket 的指針 nextOverflow *bmap }
map的初始化
hashTable := make(map[k]v, hint),當hint >= 4時后面會追加2^(hint-4)個桶,之后再內存頁幀對齊又追加了若干個桶
// make(map[k]v, hint), hint即預分配大小 // 不傳hint時,如用new創建個預設容量為0的map時,makemap只初始化hmap結構,不分配hash數組 func makemap(t *maptype, hint int, h *hmap) *hmap { // 省略部分代碼 // 隨機hash種子 h.hash0 = fastrand() // 2^h.B 為大於hint*6.5(擴容因子)的最小的2的冪 B := uint8(0) // overLoadFactor(hint, B)只有一行代碼:return hint > bucketCnt && uintptr(hint) > loadFactorNum*(bucketShift(B)/loadFactorDen) // 即B的大小應滿足 hint <= (2^B) * 6.5 // 一個桶能存8對key-value,所以這就表示B的初始值是保證這個map不需要擴容即可存下hint個元素對的最小的B值 for overLoadFactor(hint, B) { B++ } h.B = B // 這里分配hash數組 if h.B != 0 { var nextOverflow *bmap h.buckets, nextOverflow = makeBucketArray(t, h.B, nil) // makeBucketArray()會在hash數組后面預分配一些溢出桶, // h.extra.nextOverflow用來保存上述溢出桶的首地址 if nextOverflow != nil { h.extra = new(mapextra) h.extra.nextOverflow = nextOverflow } } return h }
分配hash數組
// 分配hash數組 func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) { base := bucketShift(b) // base代表用戶預期的桶的數量,即hash數組的真實大小 nbuckets := base // nbuckets表示實際分配的桶的數量,>= base,這就可能會追加一些溢出桶作為溢出的預留 if b >= 4 { // 這里追加一定數量的桶,並做內存對齊 nbuckets += bucketShift(b - 4) sz := t.bucket.size * nbuckets up := roundupsize(sz) if up != sz { nbuckets = up / t.bucket.size } } // 后面的代碼就是申請內存空間了,此處省略 // 這里大家可以思考下這個數組空間要怎么分配,其實就是n*sizeof(桶),所以: // 每個桶前面是8字節的tophash數組,然后是8個key,再是8個value,最后放一個溢出指針 // sizeof(桶) = 8 + 8*sizeof(key) + 8*sizeof(value) + 8 return buckets, nextOverflow }
map的讀寫
value, ok := map[key] #可以通過ok判斷map中是否存在這個key
valuePtr := map[key] #返回value對應類型的空值,對於key和value在map中存儲的是否為指針,需要根據key,value的大小來判斷,當大於128字節時,key和value存儲的都為指針
將map作為函數參數時,會傳遞一個指針副本,對該副本操作,也同樣會影響原始map對象中的值
對於訪問key,通過key低B位確定哪個桶,通過桶高8位去與桶里每個tophash比較,只查找時,比較未找到就繼續去overflow溢出桶里找,如果是插入沒有找到,就插入第一個hashtop為empty的位置
map的key可以是除了slice,map,func 的任意類型,value可以是任意類型。
map並不是一個線程安全的數據結構,在邊遍歷邊刪除是一個同時讀寫的行為,被檢測到會直接報panic
map的刪除
通過delete(map, key)函數刪除key,相應的hashtop被置為empty,並沒有立即釋放內存,在指針沒有引用時會被系統gc
map擴容,擴容的兩個臨界點
1.由裝載因子確定,默認6.5 、即元素個數 > = 桶個數 * 6.5,表示大部分桶已滿 (B+1,將hmap的bucket數組擴容一倍)
2.由overflow的桶個數決定,當overflow溢出桶太多,代表可能是一邊插入一邊刪除,導致大量桶出現空洞,此時值存儲的非常稀疏,當bucket總數<2^15 時,overflow的bucket總數>=bucket總數;
bucket總數>=2^15,overflow的bucket >= 2^15,即認為溢出桶太多(移動bucket內容,使其更加緊密進而提高bucket利用率)縮容
當map中擁有大量hash沖突,也可能會導致overflow溢出桶數量過多,但這只是一種理論可能,golang中map中的hash隨機種子幾乎可以避免這種情況。
擴容過程:
由hmap內存結構知道了,buckets指向新的內存地址,oldbuckets仍然指向老的內存地址,golang中map的擴容是一種漸進式擴容,原有key/value不會一次性全部遷移完畢,每次只會遷移2個buckets,每一次搬遷以buckets作為單元,包括hash桶和這個桶的溢出鏈表。
對於2的縮容並不是真正的縮容,hash的容量沒有發生變化,只是讓數據更加緊湊,如果要做到真正的縮容就需要重新創建一個map,再復制。
參考:
1.https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/
2.https://segmentfault.com/a/1190000023879178