Golang - Map 內部實現原理解析

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的位置

七.參考文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章