go基础之map-增和改(二)

go基础之map-增和改(二)

写在之前

在上篇文章《go基础之map-写在前面(一)》介绍了map的数据结构,本篇会详细介绍map的增和改的代码实现,由于增和改的实现基本上差不多,所以就纳到一起分析了。如果想详细查看源码的注释,可以查看我的GitHub,欢迎批评指正。我的打算是把一些常用的数据结构都分析一遍,如果有志同道合的人,可以联系我。

环境说明

我的具体调试环境在《go基础之map-写在前面(一)》已经说明的非常仔细了,现在只讲我分析增和改的调试代码。

package main

import (
	"fmt"
	"strconv"
)

func main() {
	m1 := make(map[string]string, 9)
	fmt.Println(m1)
	for i := 0; i < 20; i++ {
		str := strconv.Itoa(i)
		m1[str] = str
	}
}

老规矩编译一波,看看第9行的申明到底干了啥?

go tool compile -N -l -S main.go > main.txt

编译结果有点多,我只列举重点代码:

"".main STEXT size=394 args=0x0 locals=0x98
	0x0000 00000 (main.go:8)	TEXT	"".main(SB), ABIInternal, $152-0
    ....
	0x0036 00054 (main.go:9)	LEAQ	type.map[string]string(SB), AX
	0x003d 00061 (main.go:9)	PCDATA	$0, $0
	0x003d 00061 (main.go:9)	MOVQ	AX, (SP)
	0x0041 00065 (main.go:9)	MOVQ	$9, 8(SP)
	0x004a 00074 (main.go:9)	MOVQ	$0, 16(SP)
	0x0053 00083 (main.go:9)	CALL	runtime.makemap(SB)
	....
	0x0107 00263 (main.go:13)	PCDATA	$0, $5
	0x0107 00263 (main.go:13)	LEAQ	type.map[string]string(SB), DX
	0x010e 00270 (main.go:13)	PCDATA	$0, $4
	0x010e 00270 (main.go:13)	MOVQ	DX, (SP)
	0x0112 00274 (main.go:13)	PCDATA	$0, $6
	0x0112 00274 (main.go:13)	MOVQ	"".m1+56(SP), BX
	0x0117 00279 (main.go:13)	PCDATA	$0, $4
	0x0117 00279 (main.go:13)	MOVQ	BX, 8(SP)
	0x011c 00284 (main.go:13)	PCDATA	$0, $0
	0x011c 00284 (main.go:13)	MOVQ	CX, 16(SP)
	0x0121 00289 (main.go:13)	MOVQ	AX, 24(SP)
	0x0126 00294 (main.go:13)	CALL	runtime.mapassign_faststr(SB)
	....
  • 第9行调用了runtime.makemap方法做一些初始化操作,我把map的初始容量设为大于8底层才会走该方法,否则会调用runtime.makemap_small方法。
  • 第22行调用了runtime.mapassign_faststr方法,该方法对应main.go第13行的赋值方法m1[str] = str

我们找到了方法,在后面就可以在$go_sdk_path/src/runtime/map.go$go_sdk_path/src/runtime/map_faststr.go
找到方法,然后断点调试即可。

makemap_small和makemap的区别

makemap_small的代码如下:

// makemap_small implements Go map creation for make(map[k]v) and
// make(map[k]v, hint) when hint is known to be at most bucketCnt
// at compile time and the map needs to be allocated on the heap.
func makemap_small() *hmap {
	h := new(hmap)
	h.hash0 = fastrand()
	return h
}

该代码的实现十分简单,就设置了一个hash种子,其他的例如申通桶内存的操作只有在真正赋值数据的时候才会创建桶。该方法在什么情况会被调用呢?如注释说说"hint is known to be at most bucketCnt at compile time and the map needs to be allocated on the heap",bucketCnt就是8个,所以上面我的示例代码为何要设初始容量为9的原因就在这里。
我就直接略过这种情况,因为在实际应用场景下还是要指定容量,避免后面因为频繁扩容造成性能损失,makemap的代码如下:

// 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 {
	mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
	// 是否超出了最大的可分配虚拟内存或者超出了uintptr表示的值
	if overflow || mem > maxAlloc {
		hint = 0
	}

	// initialize Hmap
	if h == nil {
		h = new(hmap)
	}
	// 随机hash因子
	h.hash0 = fastrand()

	// Find the size parameter B which will hold the requested # of elements.
	// For hint < 0 overLoadFactor returns false since hint < bucketCnt.
	// 计算B的值,桶的个数为 1 << B
	B := uint8(0)
	// 不断的循环得到最大的B值
	for overLoadFactor(hint, B) {
		B++
	}
	h.B = B

	// allocate initial hash table
	// if B == 0, the buckets field is allocated lazily later (in mapassign)
	// If hint is large zeroing this memory could take a while.
	if h.B != 0 {
		var nextOverflow *bmap
		// 根据B的值去申请桶,包括逸出桶
		h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
		if nextOverflow != nil {
			h.extra = new(mapextra)
			// nextOverflow指向逸出桶的内存地址
			h.extra.nextOverflow = nextOverflow
		}
	}

	return h
}

看程序注释大概明白该代码的作用就是得到B值和申请桶,overLoadFactor方法是用了6.5的扩容因子去计算出最大的B值,保证你申请的容量count要大于 (1>> B) * 6.5, 这个扩容因子想必大家都不陌生,在java中是0.75,为什么在go中是0.65呢?在runtime/map.go开头处有测试数据,综合考虑来说选择了6.5。大家可能注意到maptype用来申请桶的内存块了,下面看看maptype的代码,也有助于理解map的结构:

type maptype struct {
	typ    _type
	// map的key的类型
	key    *_type
	// map的value的类型
	elem   *_type
	// 桶的类型
	bucket *_type // internal type representing a hash bucket
	// function for hashing keys (ptr to key, seed) -> hash
	hasher     func(unsafe.Pointer, uintptr) uintptr
	// key的类型的大小
	keysize    uint8  // size of key slot
	// value类型元素的大小
	elemsize   uint8  // size of elem slot
	// 桶里面所有元素的大小
	bucketsize uint16 // size of bucket
	flags      uint32
}

makemap方法里面math.MulUintptr(uintptr(hint), t.bucket.size)用到了bucket的size,这里这个size和maptype的bucketsize一模一样都是272(《go基础之map-写在前面(一)》有介绍为什么是272),所以就能计算出需要分配的内存。仔细分析makemap的字段,可以发现定义了map的基本数据结构,后面代码用来申请桶的内存块的时候都使用了这个数据结构。
makemap第36行代码调用了方法makeBucketArray方法来申请内存,我们简单看看它里面的细节:

// makeBucketArray initializes a backing array for map buckets.
// 1<<b is the minimum number of buckets to allocate.
// dirtyalloc should either be nil or a bucket array previously
// allocated by makeBucketArray with the same t and b parameters.
// If dirtyalloc is nil a new backing array will be alloced and
// otherwise dirtyalloc will be cleared and reused as backing array.
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
	base := bucketShift(b)
	nbuckets := base
	// For small b, overflow buckets are unlikely.
	// Avoid the overhead of the calculation.
	if 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)
		sz := t.bucket.size * nbuckets
		up := roundupsize(sz)
		if up != sz {
			nbuckets = up / t.bucket.size
		}
	}

	if dirtyalloc == nil {
		// 申请nbuckets个桶,包括了逸出桶,所以所有桶在内存中其实连续的,虽然逻辑上有差异
		buckets = newarray(t.bucket, int(nbuckets))
		b0 := (*dmap)(add(buckets, uintptr(0)*uintptr(t.bucketsize)))
		println(b0.debugOverflows)
	} 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)
		}
	}

	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
}

注意12行处有个优化点,当B小于4的时候,也就是初始申请map的容量的时候的count < (1 >> B) * 6.5的时候,很大的概率其实不会使用逸出桶。当B大于4的时候,程序就预估出一个逸出桶的个数,在26行处就一并申请总的桶的内存块。第27行的代码是在源码中没有的,我只是用来调试代码所用,这个在《go基础之map-写在前面(一)》有介绍这个小技巧。在第49行就通过bucketsize计算出逸出桶的位置,并且在51到53行有个技巧,给最后一个桶的溢出桶指针设置了桶的起始地址,这个在后面系列的博客会介绍到为何这么使用。
ok,现在map的数据如下:
go基础之map-增和改(二)
只有2个桶,而且每个桶的tophash值都是默认值0,由于此时key和value都为空,故没有展示出来。extra没有值是因为B小4没有溢出桶导致的。

添加元素(没触发扩容的情况)

m1[str] = str我上面分析了是对应的map_faststr.go里面的mapassign_faststr方法。第一次添加的key和value都是string类型。

func mapassign_faststr(t *maptype, h *hmap, s string) unsafe.Pointer {
	//d := (*dmap)(unsafe.Pointer(uintptr(h.buckets)))
	//bucketD := uintptr(0)
	//for bucketD < bucketShift(h.B)+3 {
	//	flag := false
	//	for _, debugKey := range d.debugKeys {
	//		if debugKey == "" {
	//			continue
	//		}
	//		if flag == false {
	//			print("bucket:")
	//			println(bucketD)
	//		}
	//		print("key:")
	//		println(debugKey)
	//		flag = true
	//	}
	//	bucketD++
	//	d = (*dmap)(unsafe.Pointer(uintptr(h.buckets) + bucketD*uintptr(t.bucketsize)))
	//}
	//println()
	//取出第三位是否是1,如果是1则表示正有另外一个协程在往map里面写数据
	if h.flags&hashWriting != 0 {
		throw("concurrent map writes")
	}
	key := stringStructOf(&s)
	//获取key的hash值
	hash := t.hasher(noescape(unsafe.Pointer(&s)), uintptr(h.hash0))

	// Set hashWriting after calling t.hasher for consistency with mapassign.
	// 将标志位设置为正在写
	h.flags ^= hashWriting

	if h.buckets == nil {
		h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
	}

again:
	// 获取该key落到第几个bucket,每个bucket指的是类似链并的bmap结构
	mask := bucketMask(h.B)
	bucket := hash & mask
	// 如果存在扩容情况
	if h.growing() {
		// 从oldbuckets里面复制到新申请的buckets里面
		growWork_faststr(t, h, bucket)
	}
	// 寻址到第几个bmap
	b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
	// 得到bmap的tophash值
	top := tophash(hash)

	var insertb *bmap          // 插入到哪个bmap里面
	var inserti uintptr        // 插入到bmap哪个位置
	var insertk unsafe.Pointer // 插入key到bmap哪个位置

	//找到一个空的地方插入该key
bucketloop:
	for {
		for i := uintptr(0); i < bucketCnt; i++ {
			if b.tophash[i] != top {
				if isEmpty(b.tophash[i]) && insertb == nil {
					insertb = b
					inserti = i
				}
				// 一开始都是0,也就是emptyRest
				if b.tophash[i] == emptyRest {
					break bucketloop
				}
				continue
			}
			// 到这里已经找到tophash了,2个不同的key也有可能相等,继续判断是否key相等
			// 在bucket中的key位置
			k := (*stringStruct)(add(unsafe.Pointer(b), dataOffset+i*2*sys.PtrSize))
			// 字符串key的长度都不等的话肯定不是一个key
			if k.len != key.len {
				continue
			}
			// 要么2个字符串直接相等,要么直接内存地址相等
			if k.str != key.str && !memequal(k.str, key.str, uintptr(key.len)) {
				continue
			}
			// already have a mapping for key. Update it.
			// 找到了相同的key,则要去更新value
			inserti = i
			insertb = b
			goto done
		}
		// 插入第9个的时候会走向这里,但是溢出的hmap是没有的
		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.
	// 如果次数个数超出了增长因子,或者没有超出增长因子,但是有太多的逸出桶了,这个和java的hashmap一样,当太多红黑树了,还是会影响查找效率,因为理想情况下,map的
	// 查找效率应该是o(1)
	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 insertb == nil {
		// all current buckets are full, allocate a new one.
		insertb = h.newoverflow(t, b)
		inserti = 0 // not necessary, but avoids needlessly spilling inserti
	}
	// 把tophash值放到topsh槽里面去
	insertb.tophash[inserti&(bucketCnt-1)] = top // mask inserti to avoid bounds checks

	// 把key放到bmap里面
	// dataOffset是为了得到内存对齐后的key的位置
	// 为什么插入的是2*sys.PtrSize呢,因为string其实占了16字节
	insertk = add(unsafe.Pointer(insertb), dataOffset+inserti*2*sys.PtrSize)
	// store new key at insert position
	// 这块内存就放key的值
	*((*stringStruct)(insertk)) = *key
	// key个数加1
	h.count++

done:
	// done不关心是否是更新还是新增,拿到相应的位置即可
	// 找到value存的内存位置
	elem := add(unsafe.Pointer(insertb), dataOffset+bucketCnt*2*sys.PtrSize+inserti*uintptr(t.elemsize))
	if h.flags&hashWriting == 0 {
		throw("concurrent map writes")
	}
	// 将标志位恢复
	h.flags &^= hashWriting
	return elem
}
  1. 2到21行是我的调试代码,打印下桶里面有哪些键值对。
  2. 23行和32行都是对写标志位的操作,可见,map不支持多个goroutine写操作。
  3. 26行把key转成stringStructOf类型,后面方便用stringStructOf里面的len和具体的string的字符数组值str,这也是个优化点,少了后面通过len(str)的计算,提高效率。
  4. 28行noescape防止key被逃逸分析,计算出key的hash。
  5. 38行到54行的again的代码块主要计算式key应该落在哪个buket,为什么作为一个代码块操作呢?是因为在触发扩容的时候,会重新计算落到哪个bucket。40行计算出bucket掩码,这里二进制值是10,41行和hash做与运算,得到的值就是要把key应该存的桶号。这里也是个优化操作,通过二进制运算提高效率。第50行计算得到的值正是放到bucket里面的前8个hash槽里面。先忽略掉251行的扩容情况。
  6. 57行到123行的bucketloop代码块主要作用是找到key和value存取的位置,并把key放到bucket所在的内存位置。里面有2个for循环,外循环轮询bucket以及bucket的逸出桶,里循环轮询桶的8个tophash槽,如果找到空的tophash槽(66行和67行)就执行到done语句块。71行之后就是key的高8位hash码相等了,那么就有可能bucket已经存在了这个key,所以就先比key的长度,再比较内存。102~105行先忽略。107-110会申请一个逸出桶然后把key存到逸出桶的第一个位置。113行把tophash值放到hash槽里面。
  7. 至于第66行为什么要比较hash槽等于emptyRest才算找到了呢?这个在后面的系列会介绍到。

done代码块的代码比较清晰,就是得到value放的内存位置,并且把状态设置为写完成。

done:
	// done不关心是否是更新还是新增,拿到相应的位置即可
	// 找到value存的内存位置
	elem := add(unsafe.Pointer(insertb), dataOffset+bucketCnt*2*sys.PtrSize+inserti*uintptr(t.elemsize))
	if h.flags&hashWriting == 0 {
		throw("concurrent map writes")
	}
	// 将标志位恢复
	h.flags &^= hashWriting
	return elem
}

现在的map的内存结构是什么样的呢?
go基础之map-增和改(二)

一直到在发生扩容前的map内存结构是怎样的呢

go基础之map-增和改(二)

为啥明明2个桶都没填充完就要马上扩容了呢?这是因为扩容因子作用了:

// overLoadFactor reports whether count items placed in 1<<B buckets is over loadFactor.
func overLoadFactor(count int, B uint8) bool {
	return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}

count此时值是13,13 * (2 / 2) = 13,但是在下次的13的key放进来的时候就会发生扩容了。

发生扩容

上面说到key为13的时候发生扩容,下面具体分析如何扩容的:

// 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.
	// 如果次数个数超出了增长因子,或者没有超出增长因子,但是有太多的逸出桶了,这个和java的hashmap一样,当太多红黑树了,还是会影响查找效率,因为理想情况下,map的
	// 查找效率应该是o(1)
	if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
		d := (*dmap)(unsafe.Pointer(uintptr(h.buckets)))
		bucketD := uintptr(0)
		for bucketD < bucketShift(h.B)+3 {
			flag := false
			for i, debugKey := range d.debugKeys {
				if debugKey == "" {
					continue
				}
				println(d.tophash[i])
				if flag == false {
					print("bucket:")
					println(bucketD)
				}
				print("key:")
				println(debugKey)
				flag = true
			}
			bucketD++
			d = (*dmap)(unsafe.Pointer(uintptr(h.buckets) + bucketD*uintptr(t.bucketsize)))
		}
		println()
		hashGrow(t, h)
		goto again // Growing the table invalidates everything, so try again
	}

在上面的2层for循环里面虽然找到了bucket还有剩余位置,但是第7行的overLoadFactor(h.count+1, h.B)计算出要发生扩容。8~28行是我的调试代码,用来打印出此时map的内存结构。hashGrow会做具体的扩容操作,然后执行again从新计算落入哪个bucket。
看看hashGrow干了嘛:

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)
	//如果你不是因为超过了负载因子而是因为太多元素导致的,则不扩容二倍
	if !overLoadFactor(h.count+1, h.B) {
		bigger = 0
		h.flags |= sameSizeGrow
	}
	oldbuckets := h.buckets
	// 扩容那么就扩1倍
	newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)

	flags := h.flags &^ (iterator | oldIterator)
	if h.flags&iterator != 0 {
		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

	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
	}
	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().
}
  1. 当不是因为逸出桶太多导致的扩容,那么就扩容2倍,桶是原来的桶的2倍。
  2. 第13行会申请内存,这里没有nextOverflow,下面会分析makeBucketArray方法。
  3. 由于hmap的extra是nil,所以后面的代码基本忽略。
  4. 将hmap的oldbuckets的指针指向原来的buckets,将hmap的buckets指针指向新申请的桶。oldbuckets是用于将老的桶里面的数据逐步拷贝到新申请的桶里面。
    我画一下此时的map的内存结构,方便对比上面没扩容时候的内存结构:
    go基础之map-增和改(二)

然后会走到agian代码块里面的这段代码:

	// 如果存在扩容情况
	if h.growing() {
		// 从oldbuckets里面复制到新申请的buckets里面
		growWork_faststr(t, h, bucket)
	}

growing方法就是判断是否有oldbuckets,此时会走向growWork_faststr方法去复制数据到新桶里面。此时bucket的值是 0-3其中的一个值,此时我调试的值是0,该值就是该key即将落入新申请桶的编号。

func growWork_faststr(t *maptype, h *hmap, bucket uintptr) {
	// make sure we evacuate the oldbucket corresponding
	// to the bucket we're about to use
	// bucket&h.oldbucketmask() 会得到一个oldbuckets中的一个bucket,然后把该bucket里面的数据移到新的bucket里面去,即新申请的。
	mask := h.oldbucketmask()
	oldbucket := bucket & mask
	// 迁移逻辑
	evacuate_faststr(t, h, oldbucket)

	// evacuate one more oldbucket to make progress on growing
	// 如果还有oldbuckets需要迁移
	if h.growing() {
		// 继续移除oldbuckets第nevacuate+1个,也是继续迁移上次迁移后面的那一个桶
		evacuate_faststr(t, h, h.nevacuate)
	}
}

第6行的与操作肯定得到的桶号是在老桶的序号内,即将要迁移的就是该桶。evacuate_faststr就是迁移逻辑。12~15行意思就是如果还有待迁移的桶,那么就继续迁移。
接下来就到了扩容里面真正重点的代码了:

func evacuate_faststr(t *maptype, h *hmap, oldbucket uintptr) {
	// 得到即将移动到新的bucket的老bucket的地址
	b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
	// 多少个老的bucket
	newbit := h.noldbuckets()
	// 判断是否已经被移动到新的bucket
	if !evacuated(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.
		// xy是把老的bucket的数据分配到2个新的bucket里面去,hash&newbit != 0 决定放到x还是y的bucket里面去
		var xy [2]evacDst
		// x和老的bucket同一个位置
		x := &xy[0]
		// bucket的起始地址
		x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))
		// bucket内的key的起始地址
		x.k = add(unsafe.Pointer(x.b), dataOffset)
		// bucket内的value的起始地址
		x.e = add(x.k, bucketCnt*2*sys.PtrSize)

		if !h.sameSizeGrow() {
			// Only calculate y pointers if we're growing bigger.
			// Otherwise GC can see bad pointers.
			// 偏移newbit位,得到y的bucket位置,也就是说x和y隔了老buckets个数的地址
			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*2*sys.PtrSize)
		}

		// 开始复制到新的bucket,注意也会遍历溢出桶
		for ; b != nil; b = b.overflow(t) {
			// bucket的key的起始地址
			k := add(unsafe.Pointer(b), dataOffset)
			// bucket的value的起始地址
			e := add(k, bucketCnt*2*sys.PtrSize)
			for i := 0; i < bucketCnt; i, k, e = i+1, add(k, 2*sys.PtrSize), add(e, uintptr(t.elemsize)) {
				top := b.tophash[i]
				if isEmpty(top) {
					b.tophash[i] = evacuatedEmpty
					continue
				}
				if top < minTopHash {
					throw("bad map state")
				}
				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).
					// 求得key的hash值决定放到x还是y
					hash := t.hasher(k, uintptr(h.hash0))
					if hash&newbit != 0 {
						useY = 1
					}
				}

				// 挡在移动key的过程当中,会给tophsh设置一些标志位,evacuatedX表示会移动到x,evacuatedY表示会移动y,
				// 当useY是1的时候就移动到y
				b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY, enforced in makemap
				// 得到移动的目标
				dst := &xy[useY] // evacuation destination

				// 一个bmap只存8个key/value,超过了就要用dest的溢出桶
				if dst.i == bucketCnt {
					// 申请一个溢出桶
					dst.b = h.newoverflow(t, dst.b)
					// 新桶要重置位0
					dst.i = 0
					// 得到新桶的key的位置
					dst.k = add(unsafe.Pointer(dst.b), dataOffset)
					// 得到新桶的value的位置
					dst.e = add(dst.k, bucketCnt*2*sys.PtrSize)
				}
				// 复制tophash
				dst.b.tophash[dst.i&(bucketCnt-1)] = top // mask dst.i as an optimization, to avoid a bounds check

				// Copy key.
				// 把老桶的key复制到dst
				*(*string)(dst.k) = *(*string)(k)
				// 把老桶的value复制到dst
				typedmemmove(t.elem, dst.e, e)
				// 桶个数+1
				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的key向前偏移一个key的大小
				dst.k = add(dst.k, 2*sys.PtrSize)
				// dst的value向前偏移一个key的大小
				dst.e = add(dst.e, uintptr(t.elemsize))
			}
		}
		// 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.
			// 保留了8个tophash槽不释放,只释放key和value的内存
			// 得到key的内存地址
			ptr := add(b, dataOffset)
			// 得到一个bucket除了8个tophash槽之后的偏移
			n := uintptr(t.bucketsize) - dataOffset
			// 释放地址
			memclrHasPointers(ptr, n)
		}
	}

	// 这段代码比较魔幻
	// 据我分析,第一次进入应该当时oldbucket为0的时候。后面也有可能进入,取决于nevacuate的值
	// 【0,nevacuate+1】之前的oldbuckets已经被迁移了.
	// 上面只是把bucket里面的数据清除掉了,但是tophash值还在。
	if oldbucket == h.nevacuate {
		advanceEvacuationMark(h, t, newbit)
	}
}
  1. 第3行得到待迁移的老桶地址。
  2. 第5行得到偏移的桶数,该值等于老桶数。这样的用意是把老桶的数据按照hash分散在新桶的里面。画图说明下:go基础之map-增和改(二)8~95行的意思和上图差不多一致,有兴趣对照我的注释看一看。
  3. 96~109行会去释放掉刚刚迁移完成的老桶,需要注意下,当有迭代老桶的时候就先不释放数据,当key和value都不是指针的时候也先不释放,到后面会解释到何时释放。
  4. 580~586行意思就是去标记释放了多少个桶被释放了,此时oldbucketh.nevacuate都是0,所以会进去标记。下面进去看看里面是怎么标记的。
func advanceEvacuationMark(h *hmap, t *maptype, newbit uintptr) {
	// 已经释放完一个oldbucket则加1
	h.nevacuate++
	// Experiments suggest that 1024 is overkill by at least an order of magnitude.
	// Put it in there as a safeguard anyway, to ensure O(1) behavior.
	// 最多往后面找1024个oldbucket是否已经迁移,查找太多会影响效率?
	// 每次插入值的时候就会尝试迁移一部分
	stop := h.nevacuate + 1024
	if stop > newbit {
		stop = newbit
	}
	// 最多找1024个oldbuckets,并且判断nevacuate的下一个是否已经搬移,如果判断则+1
	for h.nevacuate != stop && bucketEvacuated(t, h, h.nevacuate) {
		h.nevacuate++
	}
	// 如果nevacuate等于oldbuckets的个数的值那么整个oldbuckets已经迁移完成了
	if h.nevacuate == newbit { // newbit == # of oldbuckets
		// Growing is all done. Free old main bucket array.
		// 释放oldbuckets
		h.oldbuckets = nil
		// Can discard old overflow buckets as well.
		// If they are still referenced by an iterator,
		// then the iterator holds a pointers to the slice.
		// 释放逸出桶的内存
		if h.extra != nil {
			h.extra.oldoverflow = nil
		}
		// 更改标志位,把第4位变为0
		h.flags &^= sameSizeGrow
	}
}
  1. 3行就标记已经释放完一个桶了。

  2. 8行算是一个权衡,最多往后面查看1024个桶,为什么不去查完呢?我理解的是到了1024个桶其实已经算一个比较大的map了,如果桶的真的很多的话,那就会影响效率,注意我们上面分析的扩容其实一直在添加13这个key的过程中,如果耗费太多时间的话那就不太好了。而且go map的扩容设计就不是想一次性把所有老桶的数据一下搬过来,一次搬不过来,那在下次添加key的时候继续搬。

  3. 12~15行就是去往后面查看桶是否已经被迁移完,迁移完就+1。

  4. 17行判断是否已经迁移完成,把oldbuckets的指针设为nil

  5. 21~27行就是判断是有hmap的extral是否为空,这里着重说明下:
    ①、extral为空,那能说明该hamp是没有逸出桶的,这个推断在这里不重要;但是它也能说明extraoverflowoldoverflow也为空,这个推断就比较重要了。当桶里面的key和value都不是指针的时候,那么这里overflowoldoverflow就不会为空了(如果后面有时间的话我再续写一篇博客说明下,因为我该系列的博客都是规划的map[string][string],key和value都是指针,所以不存在该情况),所以此时会把oldoverflow设为空指针。
    ②、还记得上面释放桶的时候这段代码吗?t.bucket.ptrdata != 0。当桶的key和value都不是指针的时候,这里就是0了,所以上面就不会释放桶里的数据,但是在这里把oldoverflow设置为nil指针,就能释放桶了。

  6. 再回到上面growWork_faststr方法,因为还有另外一个桶需要迁移,故会继续执行evacuate_faststr迁移。这次就会判断if h.nevacuate == newbit成功,并且释放oldbuckets,ok,这里一次添加的key的过程当中就迁移完成了。

最后就回到again代码块了,下面就是重新寻桶,寻桶的位置,继续不断的放key,直到下次发生扩容。很遗憾,我在这里没有打印出所有桶的数据,不能画出此时map的内存结构。大致内存结构是老桶的数据会按照hash分散到新的4个桶里面。

总结

上面分析了map的增加key的代码,改的代码和增的情况99.9%像,详细看我的分析或者我的源码注释,应该都明白了。如果有什么疑惑的地方在下面给我留言我们一起探讨吧。
go基础之map-增和改(二)

上一篇:桶排序


下一篇:AWS白皮书 中文版(中英对照版) - 002 什么是云计算?