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
首部和哈希因子,而没有为桶数组分配空间。否则进行如下动作:
map
首部和哈希因子进行初始化;hint
计算出桶的数量。具体地,找到可以容纳hint
个元素,且每个桶的平均负载量<=6.5个元素的数组大小;makeBucketArray
为桶数组分配空间。map
元素的访问mapaccess1
,mapaccess2
,mapaccessK
,mapaccess1_fat
,mapaccess2_fat
均为map
元素的访问方法。
在Go语言编译的类型检查期间,会根据接受参数的个数决定使用的运行时方法:
runtime.mapaccess1
,该函数只返回一个指向目标值的指针;runtime.mapaccess2
,除了目标值,还会返回一个用于表示当前目标值是否存在的布尔值;mapaccessK
只用于map
迭代器;_fat
方法则用于当值占用空间大于zeroVal
数组,需要返回一个额外的零值。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
和mapaccess1
的不同点在于,mapaccess2
在找到目标值时会多返回一个true
,在未找到时会返回false
。
mapaccessK
和mapaccess1
的不同在于,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)
dataOffset
是bmap
的大小。
由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时预分配溢出桶。
预分配的溢出桶所在的地址紧随桶数组之后。
tophash
,用哈希值的高八位和桶保存的键哈希值高八位对比,看是否相等;// 和 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()完成。
}
由上述代码可以看出,哈希表的扩容可分为两种:
sameSizeGrow
,在出现较多溢出桶时会整理哈希的内存减少空间的占用。(这是因为删除操作会在桶中留下空洞,徒占了内存空间,迁移后去除了空洞,达到整理的目的)map
在hashGrow
中仅分配了新的桶数组,并没有将元素迁移到新的桶数组中。实际上,元素迁移是在元素插入(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标志位去掉
}
}
由上述代码看出:
nevacuate
,可以得知迁移的进度,如果nevacuate
的指向已经超出旧桶数组的最高下标,说明迁移完成,可以释放对旧桶数组的引用。