go 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 //下一个尚未使用的桶
}
type bmap struct {
tophash [bucketCnt]uint8 //8个键值对,键的哈希值高8位。64位处理次一次性刚好取出所有tophash
keys [8]keytype //8个key 不支持泛型所以在编辑阶段才可以确定类型
values [8]valuetype //8个values 不支持泛型所以在编辑阶段才可以确定类型
overflow uintptr 指向溢出桶的位置
}
初始化
运行时(make)
- 计算哈希占用的内存是否溢出或超出能分配最大值
- 获取哈希种子
- 根据需要桶的数量计算容量 (确定hmap中的b)
- 创建bmap数组(数组在内存中是连续的地址空间)
注:当b大于4时会额外创建2的b-4次方的桶用作溢出桶 - 如果使用了溢出桶则将信息写入hmap的extra中
func makemap(t *maptype, hint int, h *hmap) *hmap {
//计算哈希占用的内存是否溢出或超出能分配最大值
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
//初始化hmap
if h == nil {
h = new(hmap)
}
//获取哈希种子
h.hash0 = fastrand()
//通过循环位运算计算出需要使用的b的大小
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
if h.B != 0 {
var nextOverflow *bmap
//创建底层桶数组
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
//如果使用了溢出桶则将信息写入hmap
if nextOverflow != nil {
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
//根据b计算桶数量(2的b次方)
base := bucketShift(b)
nbuckets := base
//如果b大于4则预分配 2的b减4次方个桶
if b >= 4 {
nbuckets += bucketShift(b - 4)
...//一段大小效险代码
}
if dirtyalloc == nil {
//根据类型与数量分配内存
buckets = newarray(t.bucket, int(nbuckets))
} else {...}
if base != nbuckets {//如果使用了溢出桶则拿到溢出桶的位置
nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
...
}
return buckets, nextOverflow
}
字面量
当元素个数小于25时
直接k v赋值
否则
创建两个切片 一个k 一个v循环赋值
访问切片
根据哈希种子和key获取哈希值,桶编号,桶,tophash
如果当前oldbuckets 不为空,则将当前桶替换为oldbuckets中的桶
遍历桶和溢出桶中的元素
先比较tophash再比较key 获取value
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
...//一顿检查
hash := t.hasher(key, uintptr(h.hash0))//获取哈希值
m := bucketMask(h.B)//获取按位与掩码
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))//获取桶地址
if c := h.oldbuckets; c != nil {...}//如果当前在扩容阶段则将桶换为oldbuckets中的桶
top := tophash(hash)//根据hash获取top hash
bucketloop:
for ; b != nil; b = b.overflow(t) {//先遍历桶再遍历溢出桶
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {//遍历tophash
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
//获取元素的key
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
//比较访问key与元素key
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])
}
写入切片
根据哈希种子和key获取哈希值,桶编号,桶,tophash
如果在扩容中则先执行扩容
遍历桶和溢出桶中的元素
先比较tophash再比较key 获取要使用的元素tophash 地址,k地址 v地址
如果获取不到判断是否触发扩容 再则执行newoverflow 获取预留溢出桶,或者新建溢出桶 将元素设置在该桶的第0个上
设置key与tophash返回value地址
由外面函数设置value值
扩容
扩容条件
- 当前不在拓容
- 当前元素个数>13*桶的个数/2(负载因子6.5从这里来的)
- 溢出桶过超过常规桶,或常规桶大于2的15次方后溢出桶超过2的15次方
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
}
func overLoadFactor(count int, B uint8) bool {//loadFactorNum=13 loadFactorDen=2
return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
if B > 15 {
B = 15
}
return noverflow >= uint16(1)<<(B&15)
}
触发扩容
func hashGrow(t *maptype, h *hmap) {
bigger := uint8(1)//b的增量默认加1也就是扩容一倍
if !overLoadFactor(h.count+1, h.B) {//如果当前不是因为负载因子触发则为等量扩容
bigger = 0
h.flags |= sameSizeGrow
}
oldbuckets := h.buckets //保存旧桶地址
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)//开辟数组
...
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
}
}
渐进式扩容
当添加修改或修除元素时会判断当前是否在扩容状态如果是则会执行如下代码
func growWork(t *maptype, h *hmap, bucket uintptr) {
evacuate(t, h, bucket&h.oldbucketmask())//执行当前桶的渐进式扩容操作
if h.growing() {
evacuate(t, h, h.nevacuate)//抛执行hmap中记录的桶扩容操作
}
}
func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
newbit := h.noldbuckets()//获取旧桶个数
if !evacuated(b) {
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) //获取key地址
x.e = add(x.k, bucketCnt*uintptr(t.keysize)) //获取value地址
if !h.sameSizeGrow() {//如果是翻倍扩容则
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)//获取key地址
e := add(k, bucketCnt*uintptr(t.keysize))//获取value地址
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) {//为空则再见 迁移中则除间隙
b.tophash[i] = evacuatedEmpty
continue
}
...
var useY uint8 //默认等量扩容迁移到同编号的桶中
if !h.sameSizeGrow() {//如果是增量扩容计算是否放至另一个编号的桶中
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
}
}
}
...
dst := &xy[useY] //确定新桶
if dst.i == bucketCnt { //如果新桶元素已满则再获取溢出桶
dst.b = h.newoverflow(t, dst.b)
dst.i = 0 //初始化标记
dst.k = add(unsafe.Pointer(dst.b), dataOffset) //确定k地址
dst.e = add(dst.k, bucketCnt*uintptr(t.keysize))//确定value地址
}
dst.b.tophash[dst.i&(bucketCnt-1)] = top //设置新桶tophash
//为新桶key拷贝值
if t.indirectkey() {
*(*unsafe.Pointer)(dst.k) = k2 // copy pointer
} else {
typedmemmove(t.key, dst.k, k) // copy elem
}
//为新桶value拷贝值
if t.indirectelem() {
*(*unsafe.Pointer)(dst.e) = *(*unsafe.Pointer)(e)
} else {
typedmemmove(t.elem, dst.e, e)
}
//指向下一元素key value (改标记)
dst.i++
dst.k = add(dst.k, uintptr(t.keysize))
dst.e = add(dst.e, uintptr(t.elemsize))
}
}
//读不动了不管了
...
}
//如果本次迁移的是hmap中预定的迁移则删除旧桶中的值并将hmap的指向下一个
if oldbucket == h.nevacuate {
advanceEvacuationMark(h, t, newbit)
}
}
func advanceEvacuationMark(h *hmap, t *maptype, newbit uintptr) {
h.nevacuate++
...//读不动
if h.nevacuate == newbit {//置空旧桶
h.oldbuckets = nil
if h.extra != nil {
h.extra.oldoverflow = nil
}
h.flags &^= sameSizeGrow
}
}
删除哈希
//只是将tophash打上空标记
b.tophash[i] = emptyOne
不可寻址
正是因为扩容过程中会发生键值对迁移,键值对的地址也会发生改变,所以才说map的元素是不可寻址的,如果要取一个value的地址则不能通过编译。
//k := &a["k1"]会在编译阶段出错
cannot take the address of a["k1"]
作者:柏为杰

浙公网安备 33010602011771号