文章目录
map扩容的源码分析见 下一节
map基本结构
hmap是map的核心数据结构:
type hmap struct {
count int // 当前的元素个数
flags uint8
B uint8 // 桶的数量为2的B次方,方便进行哈希的与运算
noverflow uint16 // 溢出桶的数量
hash0 uint32 // 哈希种子,计算哈希值使用
buckets unsafe.Pointer // 指向当前桶地址
oldbuckets unsafe.Pointer // 扩容时指向旧桶
nevacuate uintptr // 扩容进度计数器
extra *mapextra // 该结构体包含溢出桶位置信息
}
type mapextra struct {
overflow *[]*bmap //指向当前溢出桶首地址
oldoverflow *[]*bmap //指向旧的溢出桶首地址
nextOverflow *bmap //指向下一块可用的溢出桶
}
hmap结构图如下所示,buckets
和oldbuckets
字段指向的都是一块连续的内存区域([ ]bmp)
桶bmap的结构体字段如下
type bmap struct {
tophash [bucketCnt]uint8
}
//编译期间展开如下
type bmap struct{
topbits [8]uint8 //用于表示标志位或hash值高八位来快速定位K/V位置
keys [8]keytype
value [8]valuetype
pad uintptr //此字段go1.16.2版本已删除
overflow uintptr //连接下个bmap溢出桶
}
bmp结构体负责存储key/value值,结构图如下:
hash值定位K/V值
在哈希表中,寻找key/value值过程如下:
- key值会通过哈希函数得到hash值
- hash值与桶数-1(也就是2^B-1)进行与 操作得到所在桶的编号,找到相应的bmp
- 计算hash值高八位,与bmp中的tophash数组一一对比,找到偏移值
- 根据偏移值找到对应的key/value
整个流程如下图所示:
map创建
如mp := make(map[int]int, 20)
代码最终会调用 makemap
函数,此函数负责对hmap结构体进行一系列初始化工作。
func makemap(t *maptype, hint int, h *hmap) *hmap {
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
if h == nil {
h = new(hmap)
}
h.hash0 = fastrand()
B := uint8(0) //2^B为map实际的桶数
for overLoadFactor(hint, B) { //根据make传入的预设值hint来计算B数值
B++
}
h.B = B //最终2^B数值不会小于hint
if h.B != 0 {
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil) //桶的分配空间初始化操作
if nextOverflow != nil { //处理有溢出桶的情况,B<4没有溢出桶
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow //指针指向第一块溢出桶
}
}
return h
}
计算桶的数量
overLoadFactor
函数根据make传入的预设值来计算B的值
loadFactorNum = 13
loadFactorDen = 2
bucketCntBits = 3
bucketCnt = 1 << bucketCntBits //等价于8
func bucketShift(b uint8) uintptr {
return uintptr(1) << (b & (sys.PtrSize*8 - 1)) //就是返回2的b次方数值
}
func overLoadFactor(count int, B uint8) bool {
return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
//等价于:count > 8 && count > 13*(2^B / 2)
//把位置换下,count/2^B > 6.5 6.5也就是哈希表的负载因子
}
申请buckets内存空间
makeBucketArray
函数负责给桶申请一段连续的内存空间
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
base := bucketShift(b) //bucketShift计算2^b的值,base就是桶的总数
nbuckets := base
if b >= 4 { //大于4才使用溢出桶
nbuckets += bucketShift(b - 4) //溢出桶数量为2^(b-4)
sz := t.bucket.size * nbuckets
up := roundupsize(sz)
if up != sz {
nbuckets = up / t.bucket.size
}
}
if dirtyalloc == nil { //dirtyalloc 默认为nil
buckets = newarray(t.bucket, int(nbuckets)) //申请一块连续内存提供给[]bmp
} else {
...
}
if base != nbuckets { //不相等则说明有溢出桶
nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize))) //nextOverflow指向第一个溢出桶
last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize))) //last指向最后一个溢出桶
last.setoverflow(t, (*bmap)(buckets)) //最后一个溢出桶的溢出位又设为buckets,成个环了
}
return buckets, nextOverflow
}
tophash标记位介绍
tophash是一个长度为8的数组,当tophash对应的K/V被使用时,存的是key的哈希值的高8位;当tophash对应的K/V未被使用时,存的是K/V对应位置的状态。
emptyRest = 0 //当前对应K/V位置可用,且后面位置也是空闲状态
emptyOne = 1 //仅表示对应K/V位置可用
evacuatedX = 2 //记录翻倍扩容迁移时桶编号不变
evacuatedY = 3 //记录翻倍扩容迁移时移动到另一个编号桶
evacuatedEmpty = 4 //迁移完成标志
minTopHash = 5
为了区分是存的hash值还是标志位状态,当计算的哈希值小于minTopHash时,会直接加上minTopHash
func tophash(hash uintptr) uint8 {
top := uint8(hash >> (sys.PtrSize*8 - 8)) //取高八位
if top < minTopHash {
top += minTopHash
}
return top
}
查找K/V值 (mapaccess1)
v := map[key]
最终会调用mapaccess1
函数
v, ok := map[key]
最终会调用mapaccess2
函数
两个函数逻辑上基本一样,下面主要分析mapaccess1
函数,该函数如果key查找成功会返回一个指向value地址的指针,失败会返回byte数组的零值,源代码如下:
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
//...一些元素检查
hash := t.hasher(key, uintptr(h.hash0)) //hash值
m := bucketMask(h.B) //返回掩码,值为2^B-1
//hash&m找到桶编号作为偏移值找到对应的bmap结构体
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
if c := h.oldbuckets; c != nil { //存在旧桶,说明正在扩容状态中
if !h.sameSizeGrow() { //判断是否翻倍扩容
m >>= 1 //翻倍扩容时,新的桶数是旧的2倍,m需要减半才能找到旧桶编号
}
oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize))) //找到对应的旧桶位置
if !evacuated(oldb) {
b = oldb //如果旧桶没有完成数据迁移,那么更新b指向旧桶bmap
}
}
top := tophash(hash) //取hash值高八位,因为bmp.tophash中0-4是标志位,所以hash值小于5的自动加5
bucketloop:
//遍历当前bmp和溢出桶
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
if b.tophash[i] == emptyRest { //emptyRest代表后续都是空闲的K/V空间,没必要再寻找了
break bucketloop
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) //根据偏移值找到key
if t.indirectkey() { //如果key很大就会存指针
k = *((*unsafe.Pointer)(k))
}
if t.key.equal(key, k) { //继续判断key是否真的相等,保险
//e指向value内存地址
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
if t.indirectelem() { //e很大会存指针,需要解引用
e = *((*unsafe.Pointer)(e))
}
return e
}
}
}
//var zeroVal [maxZero]byte
return unsafe.Pointer(&zeroVal[0]) //返回byte形式的零值
}
查找、读写、删除都会涉及到扩容,详细的扩容过程后面再做介绍。
写入K/V值(mapassign)
map[key]=100
代码调用mapassign
函数实现,该函数返回一个指向value的指针。
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
//...元素检查
if h.flags&hashWriting != 0 {
throw("concurrent map writes") //map禁止多协程写操作
}
hash := t.hasher(key, uintptr(h.hash0))
h.flags ^= hashWriting //表示当前的goroutine正在写入
again:
bucket := hash & bucketMask(h.B) //找到对应桶编号
if h.growing() { //如果哈希表处于扩容状态,要进行数据迁移工作,后面详细介绍
growWork(t, h, bucket)
}
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize))) //b指向对应bmp结构体
top := tophash(hash) //hash值的高八位,自动加minTopHash(5)对应K/V值
var inserti *uint8 //指向对应tophash地址
var insertk unsafe.Pointer //指向对应key地址
var elem unsafe.Pointer //指向对应value地址
bucketloop:
for {
//遍历8位tophash
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
if isEmpty(b.tophash[i]) && inserti == nil { //说明有一个空闲的K/V位置,标记各个地址
//预先记录空闲位置的各个地址,不着急写入,因为K/V值可能已经存在
inserti = &b.tophash[i]
insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
}
if b.tophash[i] == emptyRest {
break bucketloop //emptyRest代表后面都是空闲区域,不可能找到已存在的Key值了,放心写入
}
continue
}
//此时存在同样的高八位hash,说明可能已经存在对应的K/V值了
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() { //如果key很大就会存指针,需要解引用
k = *((*unsafe.Pointer)(k))
}
if !t.key.equal(key, k) { //高八位key值对应的key值不一定一样,再检查下
continue
}
if t.needkeyupdate() { //更新key值,默认不更新
typedmemmove(t.key, k, key)
}
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
goto done //已经得到指向value地址的elem,直接进行收尾工作
}
ovf := b.overflow(t) //溢出桶检测
if ovf == nil {
break
}
b = ovf
}
//写入数据后检查,如果负载因子大于6.5或溢出桶过多会触发扩容
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h) //进行扩容操作,创建新桶并让h.buckets指向新桶内存地址
goto again //扩容后重新写入操作
}
if inserti == nil { //当前桶没空闲位置了,需要一个新的溢出桶中
newb := h.newoverflow(t, b) //从hamp结构体中拿一个可用溢出桶
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
elem = add(insertk, bucketCnt*uintptr(t.keysize))
}
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++
done:
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
h.flags &^= hashWriting //&^ 指定位清零,当前goroutine结束对哈希表的写入
if t.indirectelem() {
elem = *((*unsafe.Pointer)(elem))
}
return elem //返回指向value的指针,对其赋值(map[1]=100)就可以改变value值
}
删除K/V值(mapdelete)
delete(map[key])
代码 最终调用mapdelete
函数
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
//...元素检查
if h.flags&hashWriting != 0 {
throw("concurrent map writes") //map禁止多协程写操作
}
hash := t.hasher(key, uintptr(h.hash0)) //hash值
h.flags ^= hashWriting //表示当前的goroutine正在写入
bucket := hash & bucketMask(h.B) //找到对应桶编号
if h.growing() { //处理扩容情况
growWork(t, h, bucket)
}
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize))) //b指向对应bmp结构体
bOrig := b //预先记录最开始指向的bmp结构体
top := tophash(hash) //hash值的高八位,自动加minTopHash(5)对应K/V值
search:
//遍历key/value值
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
if b.tophash[i] == emptyRest {
break search //emptyRest代表后面都是空闲区域,不可能找到对应的Key值了
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) //指向key
k2 := k
if t.indirectkey() { //key很大会存指针
k2 = *((*unsafe.Pointer)(k2))
}
if !t.key.equal(key, k2) {
continue
}
//key如果是指针形式就进行如下清除操作,
if t.indirectkey() {
*(*unsafe.Pointer)(k) = nil
} else if t.key.ptrdata != 0 {
memclrHasPointers(k, t.key.size)
}
//e指向value
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
//value如果是指针形式就进行如下清除操作
if t.indirectelem() {
*(*unsafe.Pointer)(e) = nil
} else if t.elem.ptrdata != 0 {
memclrHasPointers(e, t.elem.size)
} else {
memclrNoHeapPointers(e, t.elem.size)
}
//至此完成key和value的清除,则将tophash置为emptyOne代表对应K/V值是空闲可用状态
b.tophash[i] = emptyOne
//如果tophash[i]后面位置也为emptyRest的话,说明后面都是空闲的K/V,当前tophash也应更新为emptyRest
if i == bucketCnt-1 {
if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
goto notLast
}
} else {
if b.tophash[i+1] != emptyRest {
goto notLast
}
}
//此时tophash[i]后面都为emptyRest,
for {
b.tophash[i] = emptyRest //for循环中把前面所有连续的emptyOne置为emptyRest
if i == 0 {
if b == bOrig { //此时b可能是溢出桶,需要往前遍历
break //找到初始bmp的头了,结束循环
}
c := b //此时的b为溢出桶,下面for循环找到b前面的一个溢出桶
for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
}
i = bucketCnt - 1 //更新i为尾部位置
} else {
i--
}
if b.tophash[i] != emptyOne {
break //可能有K/V值或者是emptyRest,不必再处理
}
}
notLast:
h.count-- //总元素-1
if h.count == 0 {
h.hash0 = fastrand() //数据为01重新设定hash种子,增加随机性?
}
break search
}
}
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
h.flags &^= hashWriting //清除协程写标记位
}