Go 笔记六 map简单剖析

前言

结构体中一个 map 字段,函数调用传递下去后,并发将结构体作为参数调用,而后 panic了,原因很简单,并发读写了~难得浮生片刻闲,好好解读下 go 中 map。笔者
本文所示源码基于 go1.17 版本。如果想理解,得多一些耐心,不过阅读完以后还是有些收获。

map 基础介绍

底层结构

Python 类似结构叫 dict,貌似高级点的语言都有这种结构,怎么去理解 map 呢?看一下 wiki map,不做过多赘述,普遍认知就是 “哈希表(hash table)”,首先看 Go 中 map 是怎么定义。
Go map 初始化使用

var a, b map[int]int
a = make(map[int]int, 5)

这中 a 可以进行map 的元素操作了,b则不可以,b还是 nil。原因 map 的 零值是 nil,可是为啥呢?
这个得从map这种结构底层存储看下,我们打开 runtime/map.go

// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {
	...
}

可见,make map 返回的底层结构就是 *hmap,所以声明map时候,指针的零值是 nil。我们先了解下 hmap 结构体的字段,这个就是 map结构的底层存储,所有map 的增删改查都是围绕着 hamp 这个结构体的操作。

// A header for a Go map.
type hmap struct {
	// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
	// Make sure this stays in sync with the compiler's definition.
	count     int // # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8
	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
	hash0     uint32 // hash seed

	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
	oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

	extra *mapextra // optional fields
}
字段说明
countmap当前元素个数,平时 len(mapObject) 得到的值
flags标记,对应 const 中 // flags 下的几个状态
Bbuckets 的数量的log2 值,对应 buckets = 2^B
noverflowoverflow 的 bukects 的数量
buckets桶数组,用来存储实际 key-value 的单元
oldbuckets旧桶数组,map 出现扩容时会用到
nevacuate扩容已搬迁的桶数量,比此数低的 bucket 都已完成了扩容搬迁

hmap 内部另一个核心结构就是 bucket,实际 hash table 的元素落入单元 – bucket【桶】,bucket结构呢?

// A bucket for a Go map.
type bmap struct {
	// tophash generally contains the top byte of the hash value
	// for each key in this bucket. If tophash[0] < minTopHash,
	// tophash[0] is a bucket evacuation state instead.
	tophash [bucketCnt]uint8
	// Followed by bucketCnt keys and then bucketCnt elems.
	// NOTE: packing all the keys together and then all the elems together makes the
	// code a bit more complicated than alternating key/elem/key/elem/... but it allows
	// us to eliminate padding which would be needed for, e.g., map[int64]int8.
	// Followed by an overflow pointer.
} 

实际运行时会对 bmap 结构动态改变,最终的结构会是:

type bmap struct {
    tophash  [8]uint8
    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr
    overflow uintptr
}

可以参考:https://www.infoq.cn/article/occ9isefi4pwcxxmifrx。bmap 中是有 8个 Cell 的,可以存放 8 组 key-value
整个哈希表的结构基本就有了,哈希表解决哈希冲突基本就是两类方法,开放地址 or 拉链,Go hmap就是在 bmap 上拉链解决哈希冲突。
借用 Google 图库的 Go map 结构图:
在这里插入图片描述
由上面结构再仔细看下 bmap 的字段

字段说明
tophash当前桶中数据的 hash 值的高8位组成的长度为 8 的数组
keys存放对应 tophash 数组的 key
values存放对应 tophash 数组的 value
overflowoverflow 的 bmap

这两个底层的结构是理解 map 后续操作的基础,可以用 dlv 工具调试一个 map 数据如下图:
在这里插入图片描述

map 初始化

func makemap(t *maptype, hint int, h *hmap) *hmap {
	// 根据 map 指定容量估算 map 所需内存,如果溢出或者内存超过最大限制把 map 容量置 0
	mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
	if overflow || mem > maxAlloc {
		hint = 0
	}

	// 如果当前 hmap 为空,则new hmap,并且设置此 hmap 的哈希计算种子随机数 hash0
	if h == nil {
		h = new(hmap)
	}
	h.hash0 = fastrand()
	
	// 根据负载因子计算合理 B 值,LoadFactor 是用来衡量 map 容量和当前元素数量之间的关系的一个参数
	// 当其超越某个界值表示map需要进行扩容,后文详解。持续对 B 进行负载因子计算,得出B合理的值
	B := uint8(0)
	for overLoadFactor(hint, B) {
		B++
	}
	h.B = B

	// B值对应的 bucket 的数量为 2^B,对 hmap 的 桶进行分配初始化
	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
}

map 读/写

参考 曹大 github 的 map 原图
在这里插入图片描述

map key 定位

计算 key 的 hash 值,64位机器得到一个 长度为 64bit 的二进制码,其中 低 B 位用来确定落在 hmap 的哪个 bucket 中(前文已说明 hmapbucket 个数为 2^B )
而后在 bmap 中,定义一个 key 用的是 key 的 hash 值 高8位,bmap 结构的 tophash 就是有 8个 8bit 的组成

map 读

由上面 map key 定位方式可知,读取 key 的过程就是先 定位 bucket,再在 bmap 中对逐个比较,包括 bmap 后接的 overflow 的 bmap,看下源码

func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
	// build 时候开启 race 时会记录检测
	if raceenabled && h != nil {
		callerpc := getcallerpc()
		pc := funcPC(mapaccess1)
		racereadpc(unsafe.Pointer(h), callerpc, pc)
		raceReadObjectPC(t.key, key, callerpc, pc)
	}
	// build 时候开启内存消毒器互操作
	if msanenabled && h != nil {
		msanread(key, t.key.size)
	}
	// 真正获取key的操作开始
	// 判定当前 map 是否为 nil or 空,则默认返回对应 value 的 0 值
	if h == nil || h.count == 0 {
		if t.hashMightPanic() {
			t.hasher(key, 0) // see issue 23734
		}
		return unsafe.Pointer(&zeroVal[0])
	}
	// 如果 h.flags 做正在进行写操作的比对
	if h.flags&hashWriting != 0 {
		throw("concurrent map read and map write")
	}
	// 利用h初始化时确定 h.hash0计算key 的hash值,并且取 低 B 位作 bucket 的偏移量
	hash := t.hasher(key, uintptr(h.hash0))
	m := bucketMask(h.B)
	// 通过 h.buckets 和 偏移量(hash&m)*单个(bucket)大小得出当前 bucket - bmap
	b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
	// 可能当前map正处在扩容搬迁的流程,确定是否需要去 oldbuckets 中定位 bucket,暂且不考虑这段逻辑,就当 map 是不涉及扩容逻辑
	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
		}
	}
	// 计算当前key高8位 hash值,值得注意其中有一些保留值,所以会对高 8 位做最小minTopHash 判定运算
	top := tophash(hash)
// 开启双层遍历,外层会遍历 bmap 以及 bmap后链的 overflow 的 bmap
bucketloop:
	for ; b != nil; b = b.overflow(t) {
		// bmap 逐个 cell 遍历,即 8 个 tophash 的
		for i := uintptr(0); i < bucketCnt; i++ {
			// 当前 cell top值不匹配
			if b.tophash[i] != top {
				// 判定特殊标记,是否是一个全空 bmap 如果是则直接终止循环
				if b.tophash[i] == emptyRest {
					break bucketloop // 终止整个双层循环
				}
				continue
			}
			// 通过bmap地址 + dataOffset段偏移量 + key偏移量i * key大小,定位到k
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
			// 如果k是指针,对 k 解引用
			if t.indirectkey() {
				k = *((*unsafe.Pointer)(k))
			}
			// 对 k 和 寻找对象 key 进行类型的 equal 等值判定,如果相等则代表在 map 中寻找到了 key 
			if t.key.equal(key, k) {
				// 寻找 value 通过
				// bmap地址 + dataOffset段偏移量 + 8个key偏移量 + i号value相对偏移量,定位到 value
				e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
				// 如果 value 也是指针则解引用
				if t.indirectelem() {
					e = *((*unsafe.Pointer)(e))
				}
				return e
			}
		}
	}
	return unsafe.Pointer(&zeroVal[0])
}

其中 dataOffset 是一个常量

// data offset should be the size of the bmap struct, but needs to be
// aligned correctly. For amd64p32 this means 64-bit alignment
// even though pointers are 32 bit.
dataOffset = unsafe.Offsetof(struct {
	b bmap
	v int64
}{}.v)

在理解 map 代码时对于给定的 常量含义需要有一定了解,否则阅读代码不太明白判定,譬如 emptyRest 核心就是为了加速遍历 bucket

理解了map的构造和 key 的定位去阅读 map 的 access 还是比较容易。Go 对于 map access 提供了很多方法,还有一个 mapaccess2 对应我们在使用map时附加获取存在判定

// 底层调用 mapaccess2
a, exist := targetMap["my_key"]

map 写

写操作是相对复杂,其中包括在了正常key的写入,并且在 map 容量不够时触发扩容,先忽略扩容,只看正常的写入。map的写入实现在mapassign 中,详情如下:

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
	// 对于没有make 初始化的,还是nil的 map写操作直接抛出 panic
	if h == nil {
		panic(plainError("assignment to entry in nil map"))
	}
	// 类似 access
	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)
	}
	// 当前map 是否有正在执行写操作的,如果有已经在写的,则出现了并发写,直接异常退出
	if h.flags&hashWriting != 0 {
		throw("concurrent map writes")
	}
	// 计算当前 assigen key 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.
	h.flags ^= hashWriting // map flag 置为"hashWriting"标记,对应前面的判定

	// 如果make map 没有指定容量,初开始map 的buckets数组是空,此处初始化h.buckets 
	if h.buckets == nil {
		h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
	}

again:
	// 利用B值,确定当前需要写入的 bucket 编号
	bucket := hash & bucketMask(h.B)
	if h.growing() { // 判定map是否在扩容,如果在扩容开始执行扩容的数据搬迁,后续详细介绍
		growWork(t, h, bucket)
	}
	// 计算具体 bucket 对象地址 bmap
	b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
	top := tophash(hash)  // key 高8位,后续写入b对应bmap的tophash数组中的value值即为 top

	var inserti *uint8 // 需要写入tophash 数组的index
	var insertk unsafe.Pointer // 写入 key 对象指针
	var elem unsafe.Pointer // 写入key 对应 elem 对象指针
// 开启双层遍历,外层遍历 bukect到overflow bucket,内部遍历每个bucket cell,直到找到第一个非空的cell
bucketloop:
	for {
		for i := uintptr(0); i < bucketCnt; i++ {
			if b.tophash[i] != top { // 当前 cell 与 key 的top值不同
				if isEmpty(b.tophash[i]) && inserti == nil { // 当前cell以为空标记且尚未确定 inserti 
					// 开始设置 inserti, insertk,elem,以当前i为偏移量计算对应存储地址
					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 { // map扩容搬迁逻辑,暂时忽略
					break bucketloop
				}
				continue
			}
			// 取出当前i位置的k
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
			if t.indirectkey() { // 如果map的key是指针,则还原该指针对象
				k = *((*unsafe.Pointer)(k))
			}
			// 判定当前k与需要assign的key 是否一致,如果不一致,说明当前 tophash 的高8位值与另一个k冲突了,需要继续循环,找寻可以插入当前key 的 bucket及i
			if !t.key.equal(key, k) { 
				continue
			}
			// already have a mapping for key. Update it.
			if t.needkeyupdate() { // 需要用 key 覆盖 k 
				typedmemmove(t.key, k, key)
			}
			// 循环开始条件没命中,i符合条件,对 elem计算地址值
			elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
			goto done // 寻找对应value放入的元素elem结束,转入done处执行
		}
		// 如果 bucket 没有找到,寻找当前bucket的overflow bucket
		ovf := b.overflow(t)
		if ovf == nil {
			break
		}
		b = ovf
	}

	// Did not find mapping for key. Allocate new cell & add entry.

	// If we hit the max load factor or we have too many overflow buckets,
	// and we're not already in the middle of growing, start growing.
	if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
		hashGrow(t, h)
		goto again // Growing the table invalidates everything, so try again
	}

	if inserti == nil {
		// The current bucket and all the overflow buckets connected to it are full, allocate a new one.
		newb := h.newoverflow(t, b)
		inserti = &newb.tophash[0]
		insertk = add(unsafe.Pointer(newb), dataOffset)
		elem = add(insertk, bucketCnt*uintptr(t.keysize))
	}

	// store new key/elem at insert position
	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:
	// 再次对flags和hashWriting进行标志判定,如果与hashWriting不一致则退出程序
	if h.flags&hashWriting == 0 {
		throw("concurrent map writes")
	}
	// flags 对 hashWriting 按位置0,"&^" 表示按右边的hashWriting 二进制,为1的位置,置0
	h.flags &^= hashWriting
	if t.indirectelem() { // 如果 elem 是指针对象,解对象
		elem = *((*unsafe.Pointer)(elem))
	}
	return elem
}

elem 地址return 外层后,会进行value 的赋值操作。
这中间留意 flags 的置位和判定操作
上述的代码解析,暂且搁置了 扩容的阶段判定。主体map写入,就是上述路径。

中间有很多逻辑值得反复思考:

  • bucketloop 的循环,建议把其中 if 的分支都思考一下
  • flags 的比对,怎么去判定map并行读写的
  • 其中的 map 扩容后的key寻取,等扩容逻辑分析后,可以再详细看

map 扩容

map 容量拓展

极致情况,如果 hmap 的 B=0,则 bucket 的数组长度为1,那么持续装入元素则变成 bucket后续接overflow bucket,再接overflow bucket,退化成了数组。失去了 map 的寻取特性。
golang中如何衡量一个 map 的容量?这其中有一个参数 loadFactor -- 负载因子
loadFactor 计算方式 loadFactor = count / (2^B)
默认值 loadFactorNum/loadFactorDen = 13/2 = 6.5 默认值是 6.5

// 当 map 不在扩容中,并且后续两个条件满足其一即扩容,这两个条件的含义
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
		hashGrow(t, h)
		goto again // Growing the table invalidates everything, so try again
	}

// overLoadFactor reports whether count items placed in 1<<B buckets is over loadFactor. 计算loadFactor观测是否大于默认 6.5
func overLoadFactor(count int, B uint8) bool {
	return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}

// overflow bucket数量太多判定,当 B < 15 时,overflow 的 bucket > 2^B
// 当 B >= 15 时,overflow 的 bucket > 2^15
// 这两种都算是 overflow bucket 过多
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
	// If the threshold is too low, we do extraneous work.
	// If the threshold is too high, maps that grow and shrink can hold on to lots of unused memory.
	// "too many" means (approximately) as many overflow buckets as regular buckets.
	// See incrnoverflow for more details.
	if B > 15 {
		B = 15
	}
	// The compiler doesn't see here that B < 16; mask B to generate shorter shift code.
	return noverflow >= uint16(1)<<(B&15)
}

对应上面两种判定,也会有两种扩容逻辑,如果是 loadFactor 超阈值,则说明 hmap 需要增加 buckets 的长度,但是buckets增加伴随着 B的变大,对应key hash值的低位长度变长,值也会发生变化,那么原来的 buckets 中的 value会有一个区分高低位搬移的过程,
另外的 overflow buckets过多,则不需要增加buckets数量,但是需要把原来的bmap 变的更紧凑,避免过多 overflow buckets,需要同位置bucket紧凑化搬移的过程,因为 B 没有变化

func hashGrow(t *maptype, h *hmap) {
	// If we've hit the load factor, get bigger.
	// Otherwise, there are too many overflow buckets,
	// so keep the same number of buckets and "grow" laterally.
	bigger := uint8(1) // 默认设置需要更多buckets
	if !overLoadFactor(h.count+1, h.B) { // 如果不是因为 loadFactor 过高导致扩容,则不需要 bigger,并且hmap 标记为同尺寸扩容
		bigger = 0
		h.flags |= sameSizeGrow
	}
	oldbuckets := h.buckets // 将 bucket留到 oldbuckets
	// 生成新的 bucket,bigger为0,则 len(newbuckets) == len(oldbuckets),否则len(newbuckets) = 2len(oldbuckets)
	newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)
	// 将 h.flags的 iterator | oldIterator 置0取值得flags
	flags := h.flags &^ (iterator | oldIterator)
	if h.flags&iterator != 0 { // h正在迭代
		flags |= oldIterator // 将 flags的 oldIterator 置位
	}
	// commit the grow (atomic wrt gc)
	h.B += bigger
	h.flags = flags
	h.oldbuckets = oldbuckets
	h.buckets = newbuckets
	h.nevacuate = 0
	h.noverflow = 0

	// 对 extra中的 overflow 和 oldoverflow 调整,将原先的overflow 调整为 oldoverflow
	if h.extra != nil && h.extra.overflow != nil {
		// Promote current overflow buckets to the old generation.
		if h.extra.oldoverflow != nil {
			throw("oldoverflow is not nil")
		}
		h.extra.oldoverflow = h.extra.overflow
		h.extra.overflow = nil
	}
	// 新生成的 nextOverflow 不为 nil 时,设置 h.extra.nextOverflow
	if nextOverflow != nil {
		if h.extra == nil {
			h.extra = new(mapextra)
		}
		h.extra.nextOverflow = nextOverflow
	}

	// the actual copying of the hash table data is done incrementally
	// by growWork() and evacuate().
}

整个上述的扩容逻辑是完成了额map容量的调整,并未对数据调整,容量调整的核心在 makeBucketArray 中

func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
	// 计算本次基础容量长度,2^b,并且 nbuckets 设置为 base
	base := bucketShift(b)
	nbuckets := base
	// For small b, overflow buckets are unlikely.
	// Avoid the overhead of the calculation.
	if b >= 4 { // 对于小 map,没有必要设置overflow buckets,而所谓的 "小",就是 b < 4
		// Add on the estimated number of overflow buckets
		// required to insert the median number of elements
		// used with this value of b.
		nbuckets += bucketShift(b - 4) // 计算当前需要总 nbuckets 的数量
		sz := t.bucket.size * nbuckets
		up := roundupsize(sz)
		if up != sz {
			nbuckets = up / t.bucket.size
		}
	}

	if dirtyalloc == nil { // 开启分配新的 nbuckets 长度 buckets
		buckets = newarray(t.bucket, int(nbuckets))
	} else {
		// dirtyalloc was previously generated by
		// the above newarray(t.bucket, int(nbuckets))
		// but may not be empty.
		buckets = dirtyalloc
		size := t.bucket.size * nbuckets
		if t.bucket.ptrdata != 0 {
			memclrHasPointers(buckets, size)
		} else {
			memclrNoHeapPointers(buckets, size)
		}
	}

	// 当 base != nbuckets  说明此时map的 nbuckets 是大于 2^b,多出来的则是 nextOverflow,条件段中计算 nextOverflow 的起点
	if base != nbuckets {
		// We preallocated some overflow buckets.
		// To keep the overhead of tracking these overflow buckets to a minimum,
		// we use the convention that if a preallocated overflow bucket's overflow
		// pointer is nil, then there are more available by bumping the pointer.
		// We need a safe non-nil pointer for the last overflow bucket; just use buckets.
		nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
		last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
		last.setoverflow(t, (*bmap)(buckets))
	}
	return buckets, nextOverflow
}

所以,可以看出实际中 map的 buckets 和 overflow bucket 是在一个 bmap 的数组中的,对于hmap的buckets只会用到前面的 2^B 个,后续的是 overflow bucket,并且一个小map是没有 overflow bucket。因为 map 扩容首先进行的是上面的 hmap 的容量拓展,而另一个大开销在于值的迁移,这是一个逐步完成的过程。

map扩容后值迁移

上文 hashGrow 最后注释也说明map的迁移是在 growWork() and evacuate() 中,回归 map 的赋值会判定 h 是否正在 growing,相应会进行搬迁工作。

func growWork(t *maptype, h *hmap, bucket uintptr) {
	// make sure we evacuate the oldbucket corresponding
	// to the bucket we're about to use
	evacuate(t, h, bucket&h.oldbucketmask())  // 实际执行对 bucket号的bucket搬迁

	// evacuate one more oldbucket to make progress on growing
	if h.growing() {
		evacuate(t, h, h.nevacuate) 对 h.nevacuate 号 bucket 搬迁
	}
}

最核心的 evacuate 操作,比较复杂,一切基础都是对 map 结构的理解,得铭记上面描述的 map 结构。

假设我们现在 hmap,B=5,则 hmap 的 buckets 长度为 32,如果进行负载因子过载过扩容,这容量翻倍,B=6,buckets 长度为 64。那么同样一个 key 的 hash值低
在原始的 5位值 10011 现在多 拓展一位,?10011,对于这其中的值,如果

  • ? 为0,则对应搬迁还是把旧的 10011 号搬到 B=6 时新长度为 64 的 buckets 的 010011
  • ? 为1,则对应搬迁还是把旧的 10011 号搬到 B=6 时新长度为 64 的 buckets 的 110011,原始编号高位偏移 2^5,就是 旧B 的掩码搬迁
    如果是另外一种扩容模式,则新的B还是5,那么只是把原先 overflow过多的bucket进行紧凑化,原先对应 10011 号的 bucket 下所有元素还在这个编号下。

再阅读源码

func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
	// 定位旧的编号为 oldbucket的bmap
	b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
	newbit := h.noldbuckets() // 计算 oldbucket 的长度或者说 新B 的最高位掩码
	if !evacuated(b) { // 如果 b 没有搬迁完开启搬迁
		// TODO: reuse overflow buckets instead of using new ones, if there
		// is no iterator using the old buckets.  (If !oldIterator.)

		// xy contains the x and y (low and high) evacuation destinations.
		// 设置高低位搬迁的数组,低位搬迁就是对应说明的同编号搬迁,高位就是偏移 旧B 个间隔搬迁,先对低位 x 初始化
		var xy [2]evacDst 
		x := &xy[0]
		// 新的hmap 的 buckets 中旧 oldbucket 号 bmap
		x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))
		// 计算新的 bmap中 key 和 elem
		x.k = add(unsafe.Pointer(x.b), dataOffset) 
		x.e = add(x.k, bucketCnt*uintptr(t.keysize))

		// 如果不是同尺寸扩容,那么就是可能需要高位搬迁,对高位 y 的 bmap 初始化
		if !h.sameSizeGrow() {
			// Only calculate y pointers if we're growing bigger.
			// Otherwise GC can see bad pointers.
			y := &xy[1]
			y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))
			y.k = add(unsafe.Pointer(y.b), dataOffset)
			y.e = add(y.k, bucketCnt*uintptr(t.keysize))
		}

		// 开始双循环对 b 完整搬迁,外层从bmap到 bmap后的 overflow bmap 逐个扫,内层每个 bmap 内的 cell 逐个扫
		for ; b != nil; b = b.overflow(t) {
			// 分配待搬迁的 key,value的elem
			k := add(unsafe.Pointer(b), dataOffset)
			e := add(k, bucketCnt*uintptr(t.keysize))
			for i := 0; i < bucketCnt; i, k, e = i+1, add(k, uintptr(t.keysize)), add(e, uintptr(t.elemsize)) {
				top := b.tophash[i] // bmap当前 i 中的 top值
				if isEmpty(top) { // 如果是空值
					b.tophash[i] = evacuatedEmpty // 设置该i号cell 已搬迁 继续下一个cell
					continue
				}
				if top < minTopHash { // 如果top比最小hash值小,说明map出现了未知错误,直接退出程序
					throw("bad map state")
				}
				// 对 key 进行是否是指针引用的判定和解释
				k2 := k 
				if t.indirectkey() {
					k2 = *((*unsafe.Pointer)(k2))
				}
				var useY uint8
				if !h.sameSizeGrow() { // 如果不是同尺寸扩容,要判定是否高位搬迁
					// Compute hash to make our evacuation decision (whether we need
					// to send this key/elem to bucket x or bucket y).
					hash := t.hasher(k2, uintptr(h.hash0)) // 计算i号cell中 key 的 hash 值
					if h.flags&iterator != 0 && !t.reflexivekey() && !t.key.equal(k2, k2) {
						// If key != key (NaNs), then the hash could be (and probably
						// will be) entirely different from the old hash. Moreover,
						// it isn't reproducible. Reproducibility is required in the
						// presence of iterators, as our evacuation decision must
						// match whatever decision the iterator made.
						// Fortunately, we have the freedom to send these keys either
						// way. Also, tophash is meaningless for these kinds of keys.
						// We let the low bit of tophash drive the evacuation decision.
						// We recompute a new random tophash for the next level so
						// these keys will get evenly distributed across all buckets
						// after multiple grows.
						// 这段选择将数据搬迁使用高位还是低位的逻辑比较特殊,因为对于 NaN,每次计算 key 的hash值不一致,所以,这部分就根据 top的最低位判定,可以忽略
						useY = top & 1
						top = tophash(hash)
					} else {
						// 正常判定是否进行高位搬迁,应该用 新B 的高位
						if hash&newbit != 0 {
							useY = 1
						}
					}
				}

				if evacuatedX+1 != evacuatedY || evacuatedX^1 != evacuatedY {
					throw("bad evacuatedN")
				}

				// 标记当前 旧b 的i号 cell 是搬迁去新的 evacuatedX or evacuatedY
				b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY
				// 设置使用 x 搬迁还是 y 作为目标搬迁地址
				dst := &xy[useY]                 // evacuation destination
				// 如果目标地已经满了,对目标bmap拓展 overflow bmap
				if dst.i == bucketCnt {
					dst.b = h.newoverflow(t, dst.b)
					dst.i = 0
					dst.k = add(unsafe.Pointer(dst.b), dataOffset)
					dst.e = add(dst.k, bucketCnt*uintptr(t.keysize))
				}
				// 开始对目标新的 bmap cell 填充 top值,key,elem
				dst.b.tophash[dst.i&(bucketCnt-1)] = top // mask dst.i as an optimization, to avoid a bounds check
				if t.indirectkey() {
					*(*unsafe.Pointer)(dst.k) = k2 // copy pointer
				} else {
					typedmemmove(t.key, dst.k, k) // copy elem
				}
				if t.indirectelem() {
					*(*unsafe.Pointer)(dst.e) = *(*unsafe.Pointer)(e)
				} else {
					typedmemmove(t.elem, dst.e, e)
				}
				// 目标bmap的i,key,elem 后移
				dst.i++
				// These updates might push these pointers past the end of the
				// key or elem arrays.  That's ok, as we have the overflow pointer
				// at the end of the bucket to protect against pointing past the
				// end of the bucket.
				dst.k = add(dst.k, uintptr(t.keysize))
				dst.e = add(dst.e, uintptr(t.elemsize))
			}
		}
		// 旧 buckek 变迁完了,且bucket 是指针类型数据,则清理旧 bucket,帮助 GC 回收内存
		// Unlink the overflow buckets & clear key/elem to help GC.
		if h.flags&oldIterator == 0 && t.bucket.ptrdata != 0 {
			b := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))
			// Preserve b.tophash because the evacuation
			// state is maintained there.
			ptr := add(b, dataOffset)
			n := uintptr(t.bucketsize) - dataOffset
			memclrHasPointers(ptr, n)
		}
	}
	// 如果 oldbucket 和 h中需要evacuate的bucket编号一致,那再计算出下一个待搬迁的 bucket 编号,每个 growWork 会进行 2次 搬迁,搬完两个原始buckets中的2个完整的 bucket
	if oldbucket == h.nevacuate {
		advanceEvacuationMark(h, t, newbit)
	}
}

整个 map 的搬迁操作,基本理顺了,其中比较让人困惑的是 h.flags&iterator != 0 && !t.reflexivekey() && !t.key.equal(k2, k2) 主要用来处理特殊的 ‘NaN’ 做 key,此时 map是没法取出 ‘NaN’ 的 value,因为其每次 hash值都不同。但是用 for 进行遍历时候,能够遍历出。

map 还有 delete操作和 遍历,delete 不做说明。对于遍历,golang 的map遍历是无序的,因为扩容后数据会重新迁移 bucket,顺序就是会改变(即便不是这样,golang 的map遍历也是随机从buckets中取一个开始,也是无需的),并且,在遍历时如果一个 map 尚处在扩容未搬迁完的 growing中,遍历如何操作呢?如果能够将上文的源码理解,再去理解map的遍历,基本手到擒来。

整个阅读过程,一定要先理解map结构,hmap,bmap,曹大的图看懂了,理解代码就容易。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值