核心源码
// src/runtime/hashmap.go/mapassign
// 触发扩容时机
if !h.growing() && (overLoadFactor(int64(h.count), h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
}
// 装载因子超过 6.5
func overLoadFactor(count int64, B uint8) bool {
return count >= bucketCnt && float32(count) >= loadFactor*float32((uint64(1)<<B))
}
// overflow buckets 太多
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
if B < 16 {
return noverflow >= uint16(1)<<B
}
return noverflow >= 1<<15
}
func hashGrow(t *maptype, h *hmap) {
// B+1 相当于是原来 2 倍的空间
bigger := uint8(1)
// 对应条件 2
if !overLoadFactor(int64(h.count), h.B) {
// 进行等量的内存扩容,所以 B 不变
bigger = 0
h.flags |= sameSizeGrow
}
// 将老 buckets 挂到 buckets 上
oldbuckets := h.buckets
// 申请新的 buckets 空间
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger)
flags := h.flags &^ (iterator | oldIterator)
if h.flags&iterator != 0 {
flags |= oldIterator
}
// 提交 grow 的动作
h.B += bigger
h.flags = flags
h.oldbuckets = oldbuckets
h.buckets = newbuckets
// 搬迁进度为 0
h.nevacuate = 0
// overflow buckets 数为 0
h.noverflow = 0
// ……
}
扩容条件
装载因子
//count 就是 map 的元素个数,2^B 表示 bucket 数量
loadFactor := count / (2^B)
当满足下面任一条件时会触发自动扩容
- 装载因子超过阈值,源码中的阈值为6.5
当所有的bucket都装满时,装载因子=8
对于这种情况,元素太多而bucket太少,扩容时将B加1,直接将bucket扩容两倍,老的buckets挂在oldbucket上 - overflow的bucket数量过多。当B小于等于15时,overflow的bucket的数量大于2B,当B大于15时,overflow的bucket的数量大于215。
这意味着虽然每个bucket中的数据并不多,但是存在着大量的overflow bucket,这很有可能是先插入删除了大量的数据,插入过程并没有触发条件1,注意overflow bucket中的所有元素删除了之后也不会释放overflow bucket ,这导致的问题是虽然数据量不大,但是查找时需要遍历的bucket(包括overflow bucket)多,也会降低效率
这种情况其实元素没那么多,但是overflow bucket很多,说明很多的bucket都没有装满,因此扩容时会开辟一个新的bucket空间,将老的bucket中的元素移动到新bucket是的同一个bucket中的key排列的更紧密。这就把原来在overflow bucket中的元素移动到了bucket中
扩容过程
map扩容需要将原有的key value重新搬迁到新的内存地址,如果有大量的数据需要搬迁会严重影响性能。
Go 采取了渐进式搬迁方式,原有的key并不会一次性搬迁完毕,每次最多只会搬迁2个bucket
核心代码中的hashGrow()并没有真正进行搬迁,它只是分配好了新的buckets并把老的buckets挂到了oldbuckets字段上。只有在插入,修改,删除key的时候才会进行真正的搬迁。
扩容结果
对于条件 1,从老的 buckets 搬迁到新的 buckets,由于 bucktes 数量不变,因此可以按序号来搬,比如原来在 0 号 bucktes,到新的地方后,仍然放在 0 号 buckets。
对于条件 2,就没这么简单了。要重新计算 key 的哈希,才能决定它到底落在哪个 bucket。例如,原来 B = 5,计算出 key 的哈希后,只用看它的低 5 位,就能决定它落在哪个 bucket。扩容后,B 变成了 6,因此需要多看一位,它的低 6 位决定 key 落在哪个 bucket。这称为 rehash。
因此,某个 key 在搬迁前后 bucket 序号可能和原来相等,也可能是相比原来加上 2^B(原来的 B 值),取决于 hash 值 第 6 bit 位是 0 还是 1。
例如,原始 B = 2,1号 bucket 中有 2 个 key 的哈希值低 3 位分别为:010,110。由于原来 B = 2,所以低 2 位 10 决定它们落在 2 号桶,现在 B 变成 3,所以 010、110 分别落入 2、6 号桶。
有种特殊的情况是有一种 key,每次对它计算 hash,得到的结果都不一样。比如一个float类型的 数字,在搬迁的时候会遇到再次计算它的哈希值的时候和之前计算的哈希值不一样。这种数据是无法get到的,只能在遍历的时候出现。搬迁这种数据的时候只通过tophash的最低位来决定分配到扩容后的哪一个部分。