map源码浅析

设计概述

runtime/map.go文件开头描述了map的设计要点。

  • 一个map是一个哈希表(hashtable);
  • 数据被组织成bucket数组,每个bucket最多存8个键值对;
  • 哈希值的低位用于选择bucket数组的下标。每个bucket包含哈希值的若干高位,用于定位一个bucket内的键值对;
  • 如果多于八个键被哈希到同一个bucket中,将一个额外的bucket连接到其后面(拉链法,额外的bucket被称为overflow bucket);
  • 当哈希表需要扩容时,分配一个两倍大小的bucket数组;
  • 在扩容后,bucket被增量地(incrementally)从旧的bucket数组移到新的bucket数组;
  • map的迭代器遍历bucket数组并根据遍历顺序返回键;
  • 为了在遍历中保持语义,map不会在一个bucket中移动键;
  • 当在正在发生迁移的map中遍历时,迭代器在旧的bucket数组上迭代,并检查键是否已经被迁移(evacuated)。

map的首部

先粗略看一下map的各个首部。

// A header for a Go map.
type hmap struct {
	count     int // map的大小,也就是len()的值
	flags     uint8	// 状态标识,用于控制goroutine写入和扩容的状态,详见下文
	B         uint8  // 桶的数量,2^B个
	noverflow uint16 // 溢出桶(overflow)个数
	hash0     uint32 // 哈希因子

	buckets    unsafe.Pointer // 2^B个bucket的数组
	oldbuckets unsafe.Pointer // 扩容后的旧bucket数组
	nevacuate  uintptr        // 迁移计数器,此指针之前的所有桶已被迁移,即nevacuate指向桶数组已迁移桶的最高下标

	extra *mapextra // 可选的域
}


type mapextra struct {
	// 如果键值都不包含指针,并且允许内联,会将bucket类型标志为不包含指针。这样做避免了GC扫描整个map。
	// 为了保证overflow bucket存活,将各个指向溢出桶的指针保存到overflow和oldoverflow中。
	// overflow和oldoverflow只在键值均不包含指针时使用。
	// overflow包含了hmap.buckets的溢出桶,oldoverflow包含了hmap.oldbuckets的溢出桶。
	// 这种间接寻址使得可以在hiter(用于对map进行迭代)中保存slice的指针。
	overflow    *[]*bmap	// 指向一个元素为*bmap的slice的指针
	oldoverflow *[]*bmap

	// nextOverflow为空闲溢出桶的指针
	nextOverflow *bmap
}
// bmap表示一个桶
type bmap struct {
	// tophash包含该bucket中键的哈希值的高八位
	// 如果tophash[0] < minTopHash,tophash[0]表示桶迁移状态。
	tophash [bucketCnt]uint8	// bucketCnt == 8,一个桶有八个位置
	// 将所有键、值保存在一起,即以|k|k|k|k|v|v|v|v|的方式保存,可以避免内存对齐造成空间浪费。
	// 如map[int64]int8,为了对齐,占一字节的值需要用7个字节填充对齐。
}

上述bmap结构体定义只有一个tophash保存桶中各键的哈希值的高八位,实际上bmap还保存了各个键值对。根据编译期间的 cmd/compile/internal/gc.bmap 可以重建其结构:

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

map初始化

func makemap64(t *maptype, hint int64, h *hmap) *hmap {
	// 当hint类型为int64时校验将其转换为int再转换为int64是否值不变,
	// 如果hint的值发生变化,则将hint设置为0.
	if int64(int(hint)) != hint {
		hint = 0
	}
	return makemap(t, int(hint), h)
}

// 当hint<=8且map需要在堆上分配时调用该方法创建map.
// 该方法仅分配一个hmap首部,初始化哈希因子后返回。
func makemap_small() *hmap {
	h := new(hmap)
	h.hash0 = fastrand()
	return h
}

// makemap实现了标准的map初始化动作。
// 如果编译器确定map或者第一个bucket可以在栈上创建,h和bucket可能不是nil。
// 如果h != nil,map可以在h中直接创建。
// 如果h.buckets != nil,指向的桶可以用作第一个桶。
func makemap(t *maptype, hint int, h *hmap) *hmap {
	// 将hint和t.bucket.size相乘,并检查乘积是否溢出。
	// mem即hint个t.bucket.size大小的桶所需的内存大小。
	mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
	// 如果乘积溢出,或所需内存超过最大可分配内存,将hint设为0.
	if overflow || mem > maxAlloc {
		hint = 0
	}

	// 分配hmap首部
	if h == nil {
		h = new(hmap)
	}
	// 初始化哈希因子
	h.hash0 = fastrand()

	// 找到可以容纳所需元素数的参数B。
	B := uint8(0)
	// 将hint个元素放到2^B个桶中,检查每个桶的元素数是否大于loadFactor(即6.5)
	// 将B自增直至平均每个桶的元素数<=6.5.
	for overLoadFactor(hint, B) {
		B++
	}
	h.B = B
	
	// 分配哈希表
	// 如果B == 0,桶被延迟分配
	// 如果hint很大,将内存清零需要花费一些时间。
	if h.B != 0 {
		var nextOverflow *bmap
		// makeBucketArray初始化并返回一个桶数组。
		// 有可能会预分配一些溢出桶,即 nextOverflow。
		// 如果预分配了溢出桶,则将它放到h.extra.nextOverflow中。
		h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
		if nextOverflow != nil {
			h.extra = new(mapextra)
			h.extra.nextOverflow = nextOverflow
		}
	}

	return h
}

由上述初始化方法可以看到,当创建的map容量很小时(hint <= 8),仅初始化map首部和哈希因子,而没有为桶数组分配空间。否则进行如下动作:

  1. 检查创建数组大小是否合法;
  2. map首部和哈希因子进行初始化;
  3. 根据传入的 hint 计算出桶的数量。具体地,找到可以容纳hint个元素,且每个桶的平均负载量<=6.5个元素的数组大小;
  4. 调用makeBucketArray为桶数组分配空间。

map元素的访问

mapaccess1mapaccess2mapaccessKmapaccess1_fatmapaccess2_fat均为map元素的访问方法。

在Go语言编译的类型检查期间,会根据接受参数的个数决定使用的运行时方法:

  • 当接受参数仅为一个时,会使用runtime.mapaccess1,该函数只返回一个指向目标值的指针;
  • 当接受两个参数的时候,会使用runtime.mapaccess2,除了目标值,还会返回一个用于表示当前目标值是否存在的布尔值;
  • mapaccessK只用于map迭代器;
  • _fat方法则用于当值占用空间大于zeroVal数组,需要返回一个额外的零值。

mapaccess1

mapaccess1是访问map元素的一个方法,其源码及解析如下:

// mapaccess1返回指向h[key]的一个指针。mapaccess1永远不会返回nil,
// 而会返回一个零对象的引用,用于当键不在map中时代表元素的类型。
// 因为返回的指针会使整个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)
	}
	// 如果map首部为nil或map中没有元素
	if h == nil || h.count == 0 {
		// 如果该maptype的哈希函数会panic,运行一下该maptype的哈希函数,然后将一个空字节返回。
		if t.hashMightPanic() {
			t.hasher(key, 0) // see issue 23734
		}
		return unsafe.Pointer(&zeroVal[0])
	}
	// 并发读写,直接panic.
	if h.flags&hashWriting != 0 {
		throw("concurrent map read and map write")
	}
	// 根据键和哈希因子求哈希值
	hash := t.hasher(key, uintptr(h.hash0))
	// hash&m对哈希值进行取余操作
	m := bucketMask(h.B)
	// hash&m相当于hash%tableSize,即哈希值对数组大小取余,定位到键应该位于第几个桶。
	// (hash&m)*uintptr(t.bucketsize)则偏移了(hash&m)个桶的内存位置。
	// 桶数组起始地址+偏移值定位到桶的内存位置。
	b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
	// 如果有oldbuckets,则说明当前map仍在迁移中
	if c := h.oldbuckets; c != nil {
		// 如果不是容量不变的迁移,说明现在的数组大小比之前增长了一倍,则将掩码右移一位得oldbuckets的掩码。
		if !h.sameSizeGrow() {
			m >>= 1
		}
		// 数组起始地址+偏移值在旧数组中定位到桶的内存位置。
		oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
		// 如果还没迁移,则将它迁移到新数组中。
		// 由上文map首部知,evacuated()实际是检查了桶oldb的tophash域的第一个byte的值。
		if !evacuated(oldb) {
			b = oldb
		}
	}
	// 根据哈希值计算tophash值,tophash即哈希值的高8位。
	// 由于小于minTopHash的tophash用于指示桶是否已经迁移,
	// 如果tophash
	top := tophash(hash)
	// 从定位到的bucket及其溢出链查找key
bucketloop:
	for ; b != nil; b = b.overflow(t) {	// b.overflow(t)跳到下一个溢出桶
		// 遍历一个桶的八个位置
		for i := uintptr(0); i < bucketCnt; i++ {
			// 先比较tophash
			if b.tophash[i] != top {
				// 如果tophash值为emptyRest,则往后的位置已经没有元素了,
				// 也没有更多溢出桶了。
				if b.tophash[i] == emptyRest {
					break bucketloop
				}
				continue
			}
			// 到达此处,说明key的tophash与桶内槽位的tophash相等。
			// 计算键的位置,由dataOffset+i*uintptr(t.keysize)可以看出,
			// 桶内元素的内存布局为|k|k|k|k|k|k|k|k|v|v|v|v|v|v|v|v|.
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
			// 检查保存的是指向键的指针还是直接保存了键。
			if t.indirectkey() {
				k = *((*unsafe.Pointer)(k))	// 保存了指针,则对其进行取值
			}
			// equal方法真正比较了完整的哈希值。
			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])
}

mapaccess2

mapaccess2mapaccess1的不同点在于,mapaccess2在找到目标值时会多返回一个true,在未找到时会返回false

mapaccessK

mapaccessKmapaccess1的不同在于,mapaccessK会将键、值同时返回。

键值在桶中的布局

map的首部部分提到,一个桶中的键值的排列顺序为:

|k|k|k|k|k|k|k|k|v|v|v|v|v|v|v|v|

上文提及,这样做的好处是在内存对齐中不会浪费空间。下面分析一下这样布局如何寻址某一个键/值。

mapaccess1中出现了dataOffset域,其定义如下:

// unsafe.Offsetof返回一个字段在结构体中的偏移值。
// dataOffset是bmap的大小。
dataOffset = unsafe.Offsetof(struct {
		b bmap
		v int64
	}{}.v)

dataOffsetbmap的大小。
bmap的定义:

type bmap struct {
	tophash [bucketCnt]uint8
	// 在此结构体后依次紧随bucketCnt个keys和BucketCnt个elems.
}

即一个bmap后紧跟8个键、8个值。在mapaccess1中是这样定位键的:

// b是桶的地址,b+dataOffset即第一个键的地址,
// b+dataOffset+i*keySize即第i个键的地址。
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))

定位值:

// b是桶的地址
// b+dataOffset是第一个键的地址
// b+dataOffset+bucketCnt*keySize是第一个值的地址
// b+dataOffset+bucketCnt*keySize+i*elemSize是第i个值的地址
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))

桶数组的创建

上文提到,一个桶后紧随8个键和8个值,这里分析桶数组是怎么创建的。

初始化部分,makemap调用了makeBucketArray创建桶数组,其实现如下:

// makeBucketArray初始化一个支撑map的桶数组。
// 1<
// dirtyalloc要么为nil,要么是一个之前分配的t和b都相同的桶数组。
// 如果dirtyalloc为nil,将分配一个新的桶数组,否则将会把dirtyalloc清空并重用。
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
	base := bucketShift(b)	// 1<
	nbuckets := base
	// b较小时,不太可能有需要溢出桶的情况,仅在b>=4时,分配溢出桶
	if b >= 4 {
		// 额外分配了2^(b-4)个桶
		// 这是根据map将会插入元素的数量的中位数
		// 为将来需要的溢出桶分配空间
		nbuckets += bucketShift(b - 4)
		sz := t.bucket.size * nbuckets
		up := roundupsize(sz)
		if up != sz {
			nbuckets = up / t.bucket.size
		}
	}

	if dirtyalloc == nil {
		// newarray创建了新的桶数组
		buckets = newarray(t.bucket, int(nbuckets))
	} else {
		// 重用旧的桶数组
		buckets = dirtyalloc
		// t和b相同的情况下,size亦相同
		size := t.bucket.size * nbuckets
		// 清空内存
		if t.bucket.ptrdata != 0 {
			memclrHasPointers(buckets, size)
		} else {
			memclrNoHeapPointers(buckets, size)
		}
	}

	if base != nbuckets {
		// base != nbuckets,说明预分配了一些溢出桶。
		// 为了保证跟踪这些溢出桶的花销最小,在这里约定,
		// 如果一个预分配的溢出桶的溢出指针为nil,通过碰撞指针能得到更多的溢出桶。
		// 那么最后一个溢出桶就需要一个非nil的指针,这里设置为buckets。
		
		// buckets即桶数组开头,加上base个bucketSize,得到第一个溢出桶的地址
		nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
		// buckets加上(nbuckets-1)个bucketSize,得到最后一个溢出桶的地址
		last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
		// 将最后一个溢出桶的溢出链设置为桶数组的地址,指示后面没有更多的溢出桶了。
		last.setoverflow(t, (*bmap)(buckets))
	}
	return buckets, nextOverflow
}

由以上实现知,仅在b>=4时预分配溢出桶,即map的大小不小于16时预分配溢出桶。
预分配的溢出桶所在的地址紧随桶数组之后。

key的定位流程

  1. 计算哈希值,根据桶数组的大小对哈希值取余,得到键应该在桶数组的下标;
  2. 遍历桶中每个键的tophash,用哈希值的高八位和桶保存的键哈希值高八位对比,看是否相等;
  3. 如果相等,进一步比较两键是否相等,如果相等,即返回结果;
  4. 遍历完一个桶中的所有键后,跳到下一个溢出桶检查其中的键。

赋值

// 和 mapaccess 类似,但在key不存在时分配一个槽位给它。
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

	// 如果桶数组还没分配,分配一个长度为1的桶数组
	if h.buckets == nil {
		h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
	}

again:
	// 对哈希进行取余,得到桶的位置
	bucket := hash & bucketMask(h.B)
	// 如果map正在发生迁移,将桶从旧数组迁移到新数组
	if h.growing() {
		growWork(t, h, bucket)
	}
	// 数组头地址+bucketSize个桶的大小,得到应该插入的桶的位置
	b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
	top := tophash(hash)

	// inserti即应该插入的tophash数组的下标
	var inserti *uint8
	// 指向应该插入键的内存地址
	var insertk unsafe.Pointer
	// 指向应该插入值的内存地址
	var elem unsafe.Pointer
	
bucketloop:
	for {
		// 检查桶中的每个键
		for i := uintptr(0); i < bucketCnt; i++ {
			// tophash不相等
			if b.tophash[i] != top {
				// 如果第i个值的tophash为空,且要插入的下标inserti还未设置
				if isEmpty(b.tophash[i]) && inserti == nil {
					inserti = &b.tophash[i]
					// b为应插入的桶的位置,b+dataOffset为桶中第一个键的位置
					// b+dataOffset+i*keySize为桶中第i个键的位置
					insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
					// b+dataOffset+bucketCnt*keySize+i*elemSize为第i个值的位置
					elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
				}
				// 如果第i个键的tophash == emptyRest,说明后面没有更多k/v了,也没有更多溢出桶了
				if b.tophash[i] == emptyRest {
					break bucketloop
				}
				continue
			}
			// b+dataOffset+i*keySize得到第i个键的位置
			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)
			}
			// 第i个值的位置
			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
	}

	// 在map中没有找到键,分配新的槽位
	// 如果达到了最大加载因子,或有太多的溢出桶,且目前没有正在进行扩容,则开始扩容。
	if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
		hashGrow(t, h)	// hashGrow将会让桶数组增大一倍
		goto again // 重新开始全部工作
	}

	// 没有设置插入索引inserti,说明桶和溢出桶都满了,需要分配一个新的溢出桶。
	// 在新溢出桶的第一个位置插入k, v
	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))
	}
	
	// 间接键,bucket槽位->指向新内存块的指针kmem->新内存块
	if t.indirectkey() {
		// kmem指向新分配的内存块
		kmem := newobject(t.key)
		// insertk指向要插入的槽位,将要插入的槽位的值设置为刚分配内存块的kmem指针
		// bucket槽位->kmem->新分配的内存块
		*(*unsafe.Pointer)(insertk) = kmem
		// 将insertk设置为kmem,即指向新分配的内存块
		insertk = kmem
	}
	// 间接值,bucket槽位->指向新内存块的指针vmem->新内存块
	if t.indirectelem() {
		vmem := newobject(t.elem)
		// elem即bucket槽位的地址,让bucket槽位指向vmem
		*(*unsafe.Pointer)(elem) = vmem
	}
	// 将要key指向的键赋值到insertk指向的内存
	typedmemmove(t.key, insertk, key)
	// 在此前inserti已被赋值为tophash数组元素的地址
	// 设置其指向的tophash槽位为当前键的tophash
	*inserti = top
	h.count++

done:
	// h.flags与hashWriting向与为零,说明写标志被清除了
	if h.flags&hashWriting == 0 {
		throw("concurrent map writes")
	}
	// 即h.flags = h.flags&(^hashWriting),去除写标志
	h.flags &^= hashWriting
	// 如果是间接值,去除间接层
	// 即a->b->c变为a->c
	if t.indirectelem() {
		elem = *((*unsafe.Pointer)(elem))
	}
	return elem
}

哈希表的扩容

map的扩容是在插入的时候触发的。当map达到了最大加载因子,或者map中存在过多overflow bucket,则调用hashGrow开始扩容。

	// 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
	}

hashGrow的源码如下:

func hashGrow(t *maptype, h *hmap) {
	bigger := uint8(1)
	if !overLoadFactor(h.count+1, h.B) {
		// 没有达到加载因子,不进行扩容。
		bigger = 0
		h.flags |= sameSizeGrow
	}
	oldbuckets := h.buckets
	newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)

	flags := h.flags &^ (iterator | oldIterator)	// 声明新flag,并将iterator和oldIterator两个位置为0
	if h.flags&iterator != 0 {	// 若h.flags置位iterator,则将新flag置位oldIterator
		flags |= oldIterator
	}
	// commit the grow (atomic wrt gc)
	// 将map的一些meta更新
	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
	}

	// 实际的迁移由growWork()和evacuate()完成。
}

由上述代码可以看出,哈希表的扩容可分为两种:

  1. sameSizeGrow,在出现较多溢出桶时会整理哈希的内存减少空间的占用。(这是因为删除操作会在桶中留下空洞,徒占了内存空间,迁移后去除了空洞,达到整理的目的)
  2. 达到加载因子而触发的扩容,会将整个桶数组的大小扩大一倍。

maphashGrow中仅分配了新的桶数组,并没有将元素迁移到新的桶数组中。实际上,元素迁移是在元素插入(mapassign)、删除(mapdelete)操作中增量完成的。这两个函数通过调用growWork完成一次迁移。

增量式迁移

每次迁移通过调用growWork完成。

// 传入的bucket即需要迁移到的新桶的指针。
func growWork(t *maptype, h *hmap, bucket uintptr) {
	// 如果是等量扩容,bucket&h.oldbucketmask()得到的旧桶相对位置与新桶相对位置相同;
	// 如果是容量翻倍扩容,bucket&h.oldbucketmask()实际上通过掩码将bucket的最高位置零,得到旧桶相对位置。
	evacuate(t, h, bucket&h.oldbucketmask())

	// 再迁移一个桶。
	if h.growing() {
		evacuate(t, h, h.nevacuate)
	}
}

在看evacuate代码前,先理解evacDst,它表示一个迁移目的地:

// evacDst 抽象为一个迁移目的地。
type evacDst struct {
	b *bmap          // 要迁移到的桶
	i int            // 键值对应该放到该桶数组的下标
	k unsafe.Pointer // 指向键的目的地
	e unsafe.Pointer // 指向值的目的地
}

evacuate源码如下:

func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
	b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))	// oldbucket加上目标旧桶偏移值,得到旧桶指针。
	newbit := h.noldbuckets()	// h.noldbuckets计算了此次扩容之前桶数组的大小。
	if !evacuated(b) {
		// x和y分别指代需要迁移的桶的两个目的地:一个的相对位置与旧桶的相对位置相同;如果是扩容一倍:另一个桶y的相对位置则是旧桶的相对位置加上旧桶数组的大小。
		// 扩容一倍时,旧桶的键值对将分散到两个桶中;等量扩容时,旧桶中所有值都将迁移到对应其位置的一个桶中。(如果有溢出,则在该桶后面添加溢出桶)
		var xy [2]evacDst
		x := &xy[0]
		x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))
		x.k = add(unsafe.Pointer(x.b), dataOffset)
		x.e = add(x.k, bucketCnt*uintptr(t.keysize))

		if !h.sameSizeGrow() {
			// 仅在扩容一倍时计算y指针,否则GC将发现坏指针。
			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))
		}

		// 遍历该桶及其溢出桶。
		for ; b != nil; b = b.overflow(t) {
			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]
				if isEmpty(top) {
					// 将tophash表示为已迁移的空值
					b.tophash[i] = evacuatedEmpty
					continue
				}
				if top < minTopHash {
					throw("bad map state")
				}
				k2 := k
				if t.indirectkey() {
					k2 = *((*unsafe.Pointer)(k2))
				}
				var useY uint8
				if !h.sameSizeGrow() {
					// 计算哈希值来判断应该迁移到x还是y。
					hash := t.hasher(k2, uintptr(h.hash0))
					if h.flags&iterator != 0 && !t.reflexivekey() && !t.key.equal(k2, k2) {
						useY = top & 1
						top = tophash(hash)
					} else {
						if hash&newbit != 0 {
							useY = 1
						}
					}
				}

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

				b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY
				dst := &xy[useY]                 // evacuation destination

				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))
				}
				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)
				}
				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))
			}
		}
		
		// 至此,此位置桶及其溢出桶都已迁移完成。
		if h.flags&oldIterator == 0 && t.bucket.ptrdata != 0 {
			b := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))
			// 仍保留tophash,因为迁移状态保存与此
			ptr := add(b, dataOffset)
			n := uintptr(t.bucketsize) - dataOffset
			memclrHasPointers(ptr, n)	// 清理指针帮助GC回收垃圾
		}
	}

	// h.nevacuate是迁移计数器,此指针之前的所有桶已被迁移。
	// 如果当前迁移的桶是h.nevacuate指示的桶,则需要对h.nevacuate指针向前移动。
	if oldbucket == h.nevacuate {
		advanceEvacuationMark(h, t, newbit)
	}
}

func advanceEvacuationMark(h *hmap, t *maptype, newbit uintptr) {
	h.nevacuate++
	// 实验表面1024太大,但仍放在这作为保护,保证O(1)复杂度。
	stop := h.nevacuate + 1024
	// 若stop大于旧桶数组大小,则置为旧桶数组大小。
	if stop > newbit {
		stop = newbit
	}
	// 检查桶数组中下一个桶是否已经被迁移,若已经迁移,则自增h.nevacuate。
	for h.nevacuate != stop && bucketEvacuated(t, h, h.nevacuate) {
		h.nevacuate++
	}
	if h.nevacuate == newbit { // newbit即旧桶数组大小,nevacuate指示的位置已超出桶数组边界,说明迁移完成。
		// 扩容完成,释放旧桶数组
		h.oldbuckets = nil
		// 能够将 overflow bucket 也释放掉。
		if h.extra != nil {
			h.extra.oldoverflow = nil
		}
		h.flags &^= sameSizeGrow	// 将sameSizeGrow标志位去掉
	}
}

由上述代码看出:

  1. 等量扩容下,新桶的offset与旧桶的offset相同,只需在迁移中去除delete留下的空洞位置;
  2. 扩容一倍时,旧桶的键值对将会迁移到新桶数组的两个桶;
  3. 通过维护nevacuate,可以得知迁移的进度,如果nevacuate的指向已经超出旧桶数组的最高下标,说明迁移完成,可以释放对旧桶数组的引用。

你可能感兴趣的:(Go,哈希算法,散列表,数据结构)