Golang - Map 內部實現原理解析
一.前言
- Golang中Map存儲的是kv鍵值對,采用哈希表作為底層實現,用拉鏈法解決hash沖突
本文Go版本:gov1.14.4,源碼位於src/runtime/map.go
二.Map的內存模型
在源碼中,表示map的結構體是hmap,是hashmap的縮寫
const (
// 一個桶(bucket)內 可容納kv鍵值對 的最大數量
bucketCntBits = 3
bucketCnt = 1 << bucketCntBits
)
// map的底層結構
type hmap struct {
count int // map中kv鍵值對的數量
flags uint8 // 狀態標識符,比如正在被寫,buckets和oldbuckets正在被遍歷或擴容
B uint8 // 2^B=len(buckets)
noverflow uint16 // 溢出桶的大概數量,當B小於16時是准確值,大於等於16時是大概的值
hash0 uint32 // hash因子
buckets unsafe.Pointer // 指針,指向一個[]bmap類型的數組,數組大小為2^B,我們將一個bmap叫做一個桶,buckets字段我們稱之為正常桶,正常桶存滿8個元素后,正常桶指向的下一個桶,我們將其叫做溢出桶(拉鏈法)
oldbuckets unsafe.Pointer // 類型同上,用途不同,用於在擴容時存放之前的buckets
nevacuate uintptr // 計數器,表示擴容進度
extra *mapextra // 用於gc,指向所有的溢出桶,正常桶里面某個bmap存滿了,會使用這里面的內存空間存放鍵值對
}
// 溢出桶結構
type mapextra struct {
overflow *[]*bmap // 指針數組,指向所有溢出桶
oldoverflow *[]*bmap // 指針數組,發生擴容時,指向所有舊的溢出桶
nextOverflow *bmap // 指向 所有溢出桶中 下一個可以使用的溢出桶
}
// 桶結構
type bmap struct {
tophash [bucketCnt]uint8 // 存放key哈希值的高8位,用於決定kv鍵值對放在桶內的哪個位置
// 以下屬性,編譯時動態生成,在源碼中不存在
keys [bucketCnt]keytype // 存放key的數組
values [bucketCnt]valuetype // 存放value的數組
pad uintptr // 用於對齊內存
overflow uintptr // 指向下一個桶,即溢出桶,拉鏈法
}
用圖表示一下map底層的內存模型:
解析:
-
map的內存模型中,其實總共就三種結構,hmap,bmap,mapextra
-
hmap表示整個map,bmap表示hmap中的一個桶,map底層其實是由很多個桶組成的
-
當一個桶存滿之后,指向的下一個桶,就叫做溢出桶,溢出桶就是拉鏈法的具體表現
-
mapextra表示所有的溢出桶,之所以還要重新的指向,目的是為了用於gc,避免gc時掃描整個map,僅掃描所有溢出桶就足夠了
-
桶結構的很多字段得在編譯時才會動態生成,比如key和values等
-
桶結構中,之所以所有的key放一起,所有的value放一起,而不是key/value一對對的一起存放,目的便是在某些情況下可以省去pad字段,節省內存空間
-
golang中的map使用的內存是不會收縮的,只會越用越多。
三.Map的設計原理
1.hash值的使用
通過哈希函數,key可以得到一個唯一值,map將這個唯一值,分成高8位和低8位,分別有不同的用途
- 低8位:用於尋找當前key屬於哪個bucket
- 高8位:用於尋找當前key在bucket中的位置,bucket有個tohash字段,便是存儲的高8位的值,用來聲明當前bucket中有哪些key,這樣搜索查找時就不用遍歷bucket中的每個key,只要先看看tohash數組值即可,提高搜索查找效率
map其使用的hash算法會根據硬件選擇,比如如果cpu是否支持aes,那么采用aes哈希,並且將hash值映射到bucket時,會采用位運算來規避mod的開銷
2.桶的細節設計
bmap結構,即桶,是map中最重要的底層實現之一,其設計要點如下:
-
桶是map中最小的掛載粒度:map中不是每一個key都申請一個結構通過鏈表串聯,而是每8個kv鍵值對存放在一個桶中,然后桶再通以鏈表的形式串聯起來,這樣做的原因就是減少對象的數量,減輕gc的負擔。
-
桶串聯實現拉鏈法:當某個桶數量滿了,會申請一個新桶,掛在這個桶后面形成鏈表,新桶優先使用預分配的桶。
-
哈希高8位優化桶查找key : 將key哈希值的高8位存儲在桶的tohash數組中,這樣查找時不用比較完整的key就能過濾掉不符合要求的key,tohash中的值相等,再去比較key值
-
桶中key/value分開存放 : 桶中所有的key存一起,所有的value存一起,目的是為了方便內存對齊
-
根據k/v大小存儲不同值 : 當k或v大於128字節時,其存儲的字段為指針,指向k或v的實際內容,小於等於128字節,其存儲的字段為原值
-
桶的搬遷狀態 : 可以根據tohash字段的值,是否小於minTopHash,來表示桶是否處於搬遷狀態
3.map的擴容與搬遷策略
map底層擴容策略如下:
- map的擴容策略是新分配一個更大的數組,然后在插入和刪除key的時候,將對應桶中的數據遷移到新分配的桶中去
map的搬遷策略如下:
- 由於map擴容需要將原有的kv鍵值對搬遷到新的內存地址,直接一下子全部搬完會非常的影響性能
- 采用漸進式的搬遷策略,將搬遷的O(N)開銷均攤到O(1)的賦值和刪除操作上
以下兩種情況時,會進行擴容:
-
當裝載因子超過6.5時,擴容一倍,屬於增量擴容
-
當使用的溢出桶過多時間,重新分配一樣大的內存空間,屬於等量擴容,實際上沒有擴容,主要是為了回收空閑的溢出桶
裝載因子等於 map中元素的個數 / map的容量,即len(map) / 2^B
- 裝載因子用來表示空閑位置的情況,裝載因子越大,表明空閑位置越少,沖突也越多
- 隨着裝載因子的增大,哈希表線性探測的平均用時就會增加,這會影響哈希表的性能,當裝載因子大於70%,哈希表的性能就會急劇下降,當裝載因子達到100%,整個哈希表就會完全失效,這個時候,查找和插入任意元素的復雜度都是O(N),因為需要遍歷所有元素.
為什么會出現以上兩種情況?
-
情況1:確實是數據量越來越多,撐不住了
-
情況2:比較特殊,歸根結底還是map刪除的特性導致的,當我們不斷向哈希表中插入數據,並且將他們又全部刪除時,其內存占用並不會減少,因為刪除只是將桶對應位置的tohash置nil而已,這種情況下,就會不斷的積累溢出桶造成內存泄露。為了解決這種情況,采用了等量擴容的機制,一旦哈希表中出現了過多的溢出桶,她會創建新桶保存數據,gc會清理掉老的溢出桶,從而避免內存泄露。
如何定義溢出桶是否太多需要等量擴容呢?兩種情況:
- 當B小於15時,溢出桶的數量超過2^B,屬於溢出桶數量太多,需要等量擴容
- 當B大於等於15時,溢出桶數量超過2^B,屬於溢出桶數量太多,需要等量擴容
4.map泛型的實現
- golang並沒有實現泛型,為了支持map的泛型,底層定義了一個maptype類型,maptype定義了這類key使用什么hash函數,定義了bucket的大小,bucket如何比較。
type maptype struct {
typ _type
key *_type // key類型
elem *_type // value類型
bucket *_type // 桶內部使用的類型
hasher func(unsafe.Pointer, uintptr) uintptr // 哈希函數
keysize uint8 // key大小
elemsize uint8 // value大小
bucketsize uint16 // bucket大小
flags uint32
}
四.Map的源碼實現
1.創建map
- 創建map,主要是創建hmap這個結構,以及對hmap的初始化
// 創建map
func makemap(t *maptype, hint int, h *hmap) *hmap {
// 參數校驗,計算哈希占用的內存是否溢出或者超出能分配的最大值
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
// 初始化 hmap
if h == nil {
h = new(hmap)
}
// 獲取一個隨機的哈希種子
h.hash0 = fastrand()
// 確定B的大小
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
// 分配桶
if h.B != 0 {
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil {
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
makeBucketArray函數是給buckets字段分配桶空間的,知道大致功能就ok了
- 默認會創建2^B個bucket,如果b大於等於4,會預先創建一些溢出桶,b小於4的情況可能用不到溢出桶,沒必要預先創建
2.map中賦值元素
- mapassign函數,從非常宏觀的角度,拋開並發安全和擴容等操作不談,大致可以分成下面五個步驟
// 往map中添加元素/修改元素值
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if h == nil {
panic(plainError("assignment to entry in nil map"))
}
if raceenabled {
callerpc := getcallerpc()
pc := funcPC(mapassign)
racewritepc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
if msanenabled {
msanread(key, t.key.size)
}
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
// 第一部分: 確認哈希值
hash := t.hasher(key, uintptr(h.hash0))
h.flags ^= hashWriting
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
// 第二部分: 根據hash值確認key所屬的桶
bucket := hash & bucketMask(h.B)
if h.growing() {
growWork(t, h, bucket)
}
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
top := tophash(hash)
var inserti *uint8
var insertk unsafe.Pointer
var elem unsafe.Pointer
bucketloop:
// 第三部分: 遍歷所屬桶和此桶串聯的溢出桶,尋找key(通過桶的tohash字段和key值)
for {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
if isEmpty(b.tophash[i]) && inserti == nil {
inserti = &b.tophash[i]
insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
}
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
if !t.key.equal(key, k) {
continue
}
if t.needkeyupdate() {
typedmemmove(t.key, k, key)
}
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
goto done
}
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again
}
// 第四部分: 當前鏈上所有桶都滿了,創建一個新的溢出桶,串聯在末尾,然后更新相關字段
if inserti == nil {
newb := h.newoverflow(t, b)
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
elem = add(insertk, bucketCnt*uintptr(t.keysize))
}
// 第五部分 根據key是否存在,在桶中更新或者新增key/value值
if t.indirectkey() {
kmem := newobject(t.key)
*(*unsafe.Pointer)(insertk) = kmem
insertk = kmem
}
if t.indirectelem() {
vmem := newobject(t.elem)
*(*unsafe.Pointer)(elem) = vmem
}
typedmemmove(t.key, insertk, key)
*inserti = top
h.count++
done:
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
h.flags &^= hashWriting
if t.indirectelem() {
elem = *((*unsafe.Pointer)(elem))
}
return elem
}
3.map中刪除元素
- mapdelete函數,大致可以分為以下六步
// map中刪除元素
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
if raceenabled && h != nil {
callerpc := getcallerpc()
pc := funcPC(mapdelete)
racewritepc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
if msanenabled && h != nil {
msanread(key, t.key.size)
}
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.hasher(key, 0) // see issue 23734
}
return
}
// 第一部分: 寫保護
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
// 第二部分: 獲取hash值
hash := t.hasher(key, uintptr(h.hash0))
// Set hashWriting after calling t.hasher, since t.hasher may panic,
// in which case we have not actually done a write (delete).
h.flags ^= hashWriting
// 第三部分: 根據hash值確定桶,並看是否需要擴容
bucket := hash & bucketMask(h.B)
if h.growing() {
growWork(t, h, bucket)
}
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
bOrig := b
top := tophash(hash)
// 第四部分:遍歷桶和桶串聯的溢出桶
search:
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
// 快速試錯
if b.tophash[i] == emptyRest {
break search
}
continue
}
// 第五部分: 找到key,然后將桶的該key的tohash值置空,相當於刪除值了
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
k2 := k
if t.indirectkey() {
k2 = *((*unsafe.Pointer)(k2))
}
if !t.key.equal(key, k2) {
continue
}
// Only clear key if there are pointers in it.
if t.indirectkey() {
*(*unsafe.Pointer)(k) = nil
} else if t.key.ptrdata != 0 {
memclrHasPointers(k, t.key.size)
}
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
if t.indirectelem() {
*(*unsafe.Pointer)(e) = nil
} else if t.elem.ptrdata != 0 {
memclrHasPointers(e, t.elem.size)
} else {
memclrNoHeapPointers(e, t.elem.size)
}
b.tophash[i] = emptyOne
// If the bucket now ends in a bunch of emptyOne states,
// change those to emptyRest states.
// It would be nice to make this a separate function, but
// for loops are not currently inlineable.
if i == bucketCnt-1 {
if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
goto notLast
}
} else {
if b.tophash[i+1] != emptyRest {
goto notLast
}
}
for {
b.tophash[i] = emptyRest
if i == 0 {
if b == bOrig {
break // beginning of initial bucket, we're done.
}
// Find previous bucket, continue at its last entry.
c := b
for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
}
i = bucketCnt - 1
} else {
i--
}
if b.tophash[i] != emptyOne {
break
}
}
notLast:
h.count--
break search
}
}
// 第六部分: 解除寫保護
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
h.flags &^= hashWriting
}
需要注意的是:
- 刪除key僅僅只是將其對應的tohash值置空,如果kv存儲的是指針,那么會清理指針指向的內存,否則不會真正回收內存,內存占用並不會減少
- 如果正在擴容,並且操作的bucket沒有搬遷完,那么會搬遷bucket
4.map中查詢元素
- mapaccess1函數
// map中查找元素
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if raceenabled && h != nil {
callerpc := getcallerpc()
pc := funcPC(mapaccess1)
racereadpc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
if msanenabled && h != nil {
msanread(key, t.key.size)
}
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.hasher(key, 0) // see issue 23734
}
return unsafe.Pointer(&zeroVal[0])
}
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}
// 第一部分:計算hash值並根據hash值找到桶
hash := t.hasher(key, uintptr(h.hash0))
m := bucketMask(h.B)
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
if c := h.oldbuckets; c != nil {
if !h.sameSizeGrow() {
// There used to be half as many buckets; mask down one more power of two.
m >>= 1
}
oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
if !evacuated(oldb) {
b = oldb
}
}
top := tophash(hash)
// 第二部分:遍歷桶和桶串聯的溢出桶,尋找key
bucketloop:
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
if t.key.equal(key, k) {
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
if t.indirectelem() {
e = *((*unsafe.Pointer)(e))
}
return e
}
}
}
return unsafe.Pointer(&zeroVal[0])
}
需要注意的地方:
- 如果根據hash值定位到桶正在進行搬遷,並且這個bucket還沒有搬遷到新哈希表中,那么就從老的哈希表中找。
- 在bucket中進行順序查找,使用高八位進行快速過濾,高八位相等,再比較key是否相等,找到就返回value。如果當前bucket找不到,就往下找溢出桶,都沒有就返回零值。
5.map的擴容與搬遷
- 通過上述的map賦值和刪除流程,我們知道,觸發擴容操作的是map的賦值和刪除操作
- 擴容操作的要點其實在於搬遷
// 擴容
func growWork(t *maptype, h *hmap, bucket uintptr) {
// 搬遷正在使用的舊 bucket
evacuate(t, h, bucket&h.oldbucketmask())
// 再搬遷一個 bucket,以加快搬遷進程
if h.growing() {
evacuate(t, h, h.nevacuate)
}
}
// 是否需要擴容
func (h *hmap) growing() bool {
return h.oldbuckets != nil
}
// 搬遷bucket
func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
// 定位老的 bucket 地址
b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
// 計算容量 結果是 2^B,如 B = 5,結果為32
newbit := h.noldbuckets()
// 如果 b 沒有被搬遷過
if !evacuated(b) {
// 默認是等 size 擴容,前后 bucket 序號不變
var xy [2]evacDst
// 使用 x 來進行搬遷
x := &xy[0]
x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))
x.k = add(unsafe.Pointer(x.b), dataOffset)
x.v = add(x.k, bucketCnt*uintptr(t.keysize))
// 如果不是等 size 擴容,前后 bucket 序號有變
if !h.sameSizeGrow() {
// 使用 y 來進行搬遷
y := &xy[1]
// y 代表的 bucket 序號增加了 2^B
y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))
y.k = add(unsafe.Pointer(y.b), dataOffset)
y.v = add(y.k, bucketCnt*uintptr(t.keysize))
}
// 遍歷所有的 bucket,包括 overflow buckets b 是老的 bucket 地址
for ; b != nil; b = b.overflow(t) {
k := add(unsafe.Pointer(b), dataOffset)
v := add(k, bucketCnt*uintptr(t.keysize))
// 遍歷 bucket 中的所有 cell
for i := 0; i < bucketCnt; i, k, v = i+1, add(k, uintptr(t.keysize)), add(v, uintptr(t.valuesize)) {
// 當前 cell 的 top hash 值
top := b.tophash[i]
// 如果 cell 為空,即沒有 key
if top == empty {
// 那就標志它被"搬遷"過
b.tophash[i] = evacuatedEmpty
continue
}
// 正常不會出現這種情況
// 未被搬遷的 cell 只可能是 empty 或是
// 正常的 top hash(大於 minTopHash)
if top < minTopHash {
throw("bad map state")
}
// 如果 key 是指針,則解引用
k2 := k
if t.indirectkey {
k2 = *((*unsafe.Pointer)(k2))
}
var useY uint8
// 如果不是等量擴容
if !h.sameSizeGrow() {
// 計算 hash 值,和 key 第一次寫入時一樣
hash := t.key.alg.hash(k2, uintptr(h.hash0))
// 如果有協程正在遍歷 map 如果出現 相同的 key 值,算出來的 hash 值不同
if h.flags&iterator != 0 && !t.reflexivekey && !t.key.alg.equal(k2, k2) {
// useY =1 使用位置Y
useY = top & 1
top = tophash(hash)
} else {
// 第 B 位置 不是 0
if hash&newbit != 0 {
//使用位置Y
useY = 1
}
}
}
if evacuatedX+1 != evacuatedY {
throw("bad evacuatedN")
}
//決定key是裂變到 X 還是 Y
b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY
dst := &xy[useY] // evacuation destination
// 如果 xi 等於 8,說明要溢出了
if dst.i == bucketCnt {
// 新建一個 bucket
dst.b = h.newoverflow(t, dst.b)
// xi 從 0 開始計數
dst.i = 0
//key移動的位置
dst.k = add(unsafe.Pointer(dst.b), dataOffset)
//value 移動的位置
dst.v = add(dst.k, bucketCnt*uintptr(t.keysize))
}
// 設置 top hash 值
dst.b.tophash[dst.i&(bucketCnt-1)] = top // mask dst.i as an optimization, to avoid a bounds check
// key 是指針
if t.indirectkey {
// 將原 key(是指針)復制到新位置
*(*unsafe.Pointer)(dst.k) = k2 // copy pointer
} else {
// 將原 key(是值)復制到新位置
typedmemmove(t.key, dst.k, k) // copy value
}
//value同上
if t.indirectvalue {
*(*unsafe.Pointer)(dst.v) = *(*unsafe.Pointer)(v)
} else {
typedmemmove(t.elem, dst.v, v)
}
// 定位到下一個 cell
dst.i++
dst.k = add(dst.k, uintptr(t.keysize))
dst.v = add(dst.v, uintptr(t.valuesize))
}
}
// Unlink the overflow buckets & clear key/value to help GC.
// bucket搬遷完畢 如果沒有協程在使用老的 buckets,就把老 buckets 清除掉,幫助gc
if h.flags&oldIterator == 0 && t.bucket.kind&kindNoPointers == 0 {
b := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))
ptr := add(b, dataOffset)
n := uintptr(t.bucketsize) - dataOffset
memclrHasPointers(ptr, n)
}
}
// 更新搬遷進度
if oldbucket == h.nevacuate {
advanceEvacuationMark(h, t, newbit)
}
}
五.FQA
1.為什么map遍歷是無序的?
- 因為map底層的擴容與搬遷
- map在擴容后,會發生key的搬遷,原來在同一個桶的key,搬遷后,有可能就不處於同一個桶了,而遍歷map的過程,就是遍歷這些桶,桶里的元素發生了變化,那么map遍歷當然就是無序的啦
2.map並發訪問安全嗎?
- 不安全
- 有兩個解決方法:
- 加鎖
- 使用golang自帶的sync.map
3.map元素為何無法取地址?
- 因為擴容后map元素的地址會發生變化,歸根結底還是map底層的擴容與搬遷
六.小結
- Golang中,通過哈希表實現map,用拉鏈法解決哈希沖突
- 通過將key的哈希值散落到不同桶中,每個桶中8個cell,哈希值的低8位決定在哪個桶,哈希值的高八位決定在桶的的哪個位置
- 擴容分為等量擴容和2倍增量擴容
- 當向桶中添加了很多key,造成溢出桶太多,會觸發等量擴容,擴容后,原來一個桶中的key會一分為二,重新分配到兩個桶中
- 擴容過程是漸進式的,主要是防止一次擴容要搬遷的元素太多引發性能問題
- 觸發擴容的時間是在新增元素,搬遷的時間是賦值和刪除操作期間,每次最多搬遷兩個bucket
- 查找,賦值,刪除這些操作一個很核心的內容都是如何定位key的位置
七.參考文章
- https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/#刪除
- https://golang.design/go-questions/map/principal/
- https://juejin.cn/post/6972535873971847204#heading-11
- https://i6448038.github.io/2018/08/26/map-secret/
- https://segmentfault.com/a/1190000039101378
- https://aimuke.github.io/go/2019/05/16/map/#42-查詢mapaccess1
- https://www.helloworld.net/p/3714029944
- https://yangxikun.com/golang/2019/10/07/golang-map.html