点击上方蓝色“后端开发杂谈”关注我们, 专注于后端日常开发技术分享
map 插入
本文主要是针对map插入的源码分析, 可能篇幅有些过长,且全部是代码, 请耐心阅读. 源码位置 src/runtime/map.go
插入操作
插入操作, 实际上就是找到一个写入 value 的内存地址, 后续通过内存地址操作进行赋值.
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
// 如果h是空指针,赋值会引起panic
// 例如以下语句
// var m map[string]int
// m["k"] = 1
if h == nil {
panic(plainError("assignment to entry in nil map"))
}
// 如果开启了竞态检测 -race
if raceenabled {
callerpc := getcallerpc()
pc := funcPC(mapassign)
racewritepc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
// 如果开启了memory sanitizer -msan
if msanenabled {
msanread(key, t.key.size)
}
// 有其他goroutine正在往map中写key, 会抛出以下错误
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
// 通过key和哈希种子, 算出对应哈希值
hash := t.hasher(key, uintptr(h.hash0))
// 将flags的值与hashWriting做按位 "异或" 运算
// 因为在当前goroutine可能还未完成key的写入, 再次调用t.hasher会发生panic.
h.flags ^= hashWriting
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
// bucketMask返回值是2的B次方减1
// 因此,通过hash值与bucketMask返回值做按位与操作,返回的在buckets数组中的第几号桶
bucket := hash & bucketMask(h.B) // 获取bucket的位置
// 如果map正在搬迁(即h.oldbuckets != nil)中, 则先进行搬迁工作(当前的bucket).
if h.growing() {
growWork(t, h, bucket)
}
// 计算出上面求出的第几号bucket的内存位置
// post = start + bucketNumber * bucketsize
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
top := tophash(hash) // 获取 bucket 内的原始的位置(即hash的高8位)
var inserti *uint8 // 记录 tophash 的值的指针
var insertk unsafe.Pointer // 记录 key 的底层内存位置(要剥离指针)
var elem unsafe.Pointer // 记录 value 的底层内存位置
bucketloop:
for {
// 遍历桶中的8个cell
for i := uintptr(0); i // 这里分两种情况:
// 第一种情况是cell位的tophash值和当前tophash值不相等.
// 在 b.tophash[i] != top 的情况下, 理论上有可能会是一个空槽位.
// 一般情况下 map 的槽位分布是这样的, e 表示 empty:
// [h0][h1][h2][h3][h4][e][e][e]
// 但在执行过 delete 操作时,可能会变成这样:
// [h0][h1][e][e][h5][e][e][e]
// 所以如果再插入的话,会尽量往前面的位置插
// [h0][h1][e][e][h5][e][e][e]
// ^
// ^
// 这个位置
// 所以在循环的时候还要顺便把前面的空位置先记下来
// 因为有可能在后面会找到相等的key,也可能找不到相等的key
if b.tophash[i] != top {
// 如果cell位为空(b.tophash[i] <= emptyOne), 那么就可以在对应位置进行插入
if isEmpty(b.tophash[i]) && inserti == nil {
inserti = &b.tophash[i]
// 这里需要注意实际的 bmap 结构. dataOffset 是前面的8个 tophash 的偏移量
insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
}
// 后面所有的 cell 和 overflow 都是空的. 但是前面已经记录了当前的位置, 无需再次记录
if b.tophash[i] == emptyRest {
break bucketloop // goto done
}
continue
}
// 第二种情况是cell位的tophash值和当前的tophash值相等
// indirectkey() // store ptr to key instead of key itself
// indirectelem() // store ptr to elem instead of elem itself
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
// 注意: 即使当前cell位的tophash值相等,不一定它对应的key也是相等的,所以还要做一个key值判断
if !t.key.equal(key, k) {
continue
}
// 到这里,说明key是相等的. 如果已经有该key了, 就更新它
// needkeyupdate() // true if we need to update key on an overwrite
if t.needkeyupdate() {
typedmemmove(t.key, k, key)
}
// 这里获取到了要插入key对应的value的内存地址
// pos = start(bucket) + dataOffset + 8*keysize + i*elemsize
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
goto done
}
// 如果桶中的8个cell遍历完,还未找到对应的空cell或覆盖cell,那么就进入它的溢出桶中去遍历
// *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-sys.PtrSize)), 返回 *bmap
// 说明: t.bucketsize 是 bucket 的大小, 而最后一个指针就是 *bmap
ovf := b.overflow(t)
// 如果连溢出桶中都没有找到合适的cell,跳出循环.
if ovf == nil {
break // 终止外层循环
}
b = ovf
}
// 在已有的桶和溢出桶中都未找到合适的cell供key写入, 那么有可能会触发以下两种情况
// 情况一:
// 判断当前map的装载因子是否达到设定的6.5阈值,或者当前map的溢出桶数量是否过多. 如果存在这两种情况之一,则进行扩容操作.
// hashGrow()实际并未完成扩容,对哈希表数据的搬迁(复制)操作是通过growWork()来完成的.
// 重新跳入again逻辑,在进行完growWork()操作后,再次遍历新的桶.
// 分别分析情况1(装载因子) 和 情况2(buckets与overflow buckets)
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
}
// 情况二:
// 在不满足情况一的条件下,会为当前桶再新建溢出桶,并将tophash,key插入到新建溢出桶的对应内存的0号位置
if inserti == nil {
// all current buckets 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))
}
// 在插入位置存入新的key和value
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) // 写入 key
*inserti = top // 写入 tophash
h.count++ // map中的key数量+1
done:
// 插入操作
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
h.flags &^= hashWriting
if t.indirectelem() {
elem = *((*unsafe.Pointer)(elem))
}
return elem // 返回 value 的底层内存位置
}
注:
mapassign
的代码当中, 没有直接将 value 写入内存, 而是将 value 在内存当中的对应地址返回, 后续对内存地址写入进行操作.
创建新的
overflow
func (h *hmap) newoverflow(t *maptype, b *bmap) *bmap {
var ovf *bmap
// 先检查是否有预分配的 overflow bucket, 如果有, 则从其中获取一个, 否则, 需要重新创建一个 bucket
if h.extra != nil && h.extra.nextOverflow != nil {
// 我们已经预分配了 overflow buckets [连续的内存地址]. 详细状况参考 makeBucketArray() 函数
ovf = h.extra.nextOverflow
if ovf.overflow(t) == nil {
// 不是最后一个预分配的溢出存储桶. 这时候只需要修改nextOverflow地址指向下一个溢出桶(因为内存是连续的)
h.extra.nextOverflow = (*bmap)(add(unsafe.Pointer(ovf), uintptr(t.bucketsize)))
} else {
// 最后一个预分配的溢出存储桶, 其地址有效, 指向了当前 buckets
// 重置此存储桶上的 overflow 指针(该指针已设置为非nil标记值).
ovf.setoverflow(t, nil)
h.extra.nextOverflow = nil
}
} else {
ovf = (*bmap)(newobject(t.bucket))
}
// 修改 noverflow
h.incrnoverflow()
// key和value 非指针
if t.bucket.ptrdata == 0 {
h.createOverflow() // 创建 extra 和 overflow
*h.extra.overflow = append(*h.extra.overflow, ovf) // 将 overflow 存储到 extra 当中
}
b.setoverflow(t, ovf)
return ovf
}
修改
noverflow
的值
// incrnoverflow 递增 h.noverflow.
// noverflow 计算溢出桶的数量.
// 这用于触发相同大小的 map 增长.
// 为了使hmap保持较小, noverflow是一个uint16.
// 当存储桶很少时, noverflow是一个精确的计数.
// 当有很多存储桶时, noverflow是一个近似计数.
func (h *hmap) incrnoverflow() {
// 如果overflow buckets的数量与buckets的数量相同, 将触发相同大小的 map 增长.
// 我们需要能够计数到 1<if h.B 16 {
h.noverflow++
return
}
// 以 1 / (1 <// 当我们达到1<<15 - 1时, 我们将有大约与桶一样多的溢出桶.
mask := uint32(1)<-15) - 1
// Example: if h.B == 18, then mask == 7,
// and fastrand & 7 == 0 with probability 1/8.
if fastrand()&mask == 0 {
h.noverflow++
}
}
相关辅助函数
// 地址偏移(内存必须连续)
func add(p unsafe.Pointer, x uintptr) unsafe.Pointer {
return unsafe.Pointer(uintptr(p) + x)
}
// 2^b, b的有效范围[0-63]
func bucketShift(b uint8) uintptr {
return uintptr(1) <8 - 1))
}
// 扩容的条件:
// 1. 判断已经达到装载因子的临界点(6.5), 即元素数量 >= 桶(bucket)个数 * 6.5,
// 这个时候说明大部分桶是可能是快满了(平均每个桶插入6.5个键值对). 如果插入新元素,
// 有大概率需要溢出桶(overflow bucket)上.
//
// 2. 判断溢出桶是否太多, 当桶总数 = 桶总数, 则认为溢出
// 桶太多. 当桶总数 >= 2^15, 当溢出桶总数 >= 2^15 时, 则认为溢出桶太多了.
//
// 只要满足上述任意一个条件, 就需要进行扩容.
// 扩容的条件1
func overLoadFactor(count int, B uint8) bool {
return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}
// 扩容的条件2
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
if B > 15 {
B = 15
}
return noverflow >= uint16(1)<15)
}
// store ptr to key instead of key itself
func (mt *maptype) indirectkey() bool {
return mt.flags&1 != 0
}
// store ptr to elem instead of elem itself
func (mt *maptype) indirectelem() bool {
return mt.flags&2 != 0
}
// true if k==k for all keys, 它的反例是k为NANs
func (mt *maptype) reflexivekey() bool {
return mt.flags&4 != 0
}
// true if we need to update key on an overwrite
func (mt *maptype) needkeyupdate() bool {
return mt.flags&8 != 0
}
// true if hash function might panic
func (mt *maptype) hashMightPanic() bool {
return mt.flags&16 != 0
}
推荐阅读
Go Map源码解读之数据结构
Go Map源码解读之Map创建