Go 语言 map源码分析及图解(一)(查找、写入、删除K/V值)


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结构图如下所示,bucketsoldbuckets 字段指向的都是一块连续的内存区域([ ]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值过程如下:

  1. key值会通过哈希函数得到hash值
  2. hash值与桶数-1(也就是2^B-1)进行与 操作得到所在桶的编号,找到相应的bmp
  3. 计算hash值高八位,与bmp中的tophash数组一一对比,找到偏移值
  4. 根据偏移值找到对应的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   //清除协程写标记位
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值