这是我参与更文挑战的第2天,活动详情查看:更文挑战
一段基本代码:
1 package main
2
3 func main() {
4 m := make(map[int]stringm, 16)
5 m[1] = "hello"
6 m[2] = "world"
7
8 v, ok := m[1]
9 _, _ = fn(v, ok)
10 }
11
12 func fn(v string, ok bool) (string, bool) {
13 return v, ok
14 }
特意给了行数,后期为了追踪函数
具体map中途进过什么函数,需要 go tool compile -N -S testmap.go | grep "CALL";来具体看看函数调用:
0x0071 00113 (testmap.go:4) CALL runtime.makemap(SB)
0x0099 00153 (testmap.go:5) CALL runtime.mapassign_fast64(SB)
0x00ea 00234 (testmap.go:6) CALL runtime.mapassign_fast64(SB)
0x0146 00326 (testmap.go:8) CALL runtime.mapaccess2_fast64(SB)
0x0207 00519 (testmap.go:6) CALL runtime.gcWriteBarrier(SB)
0x0218 00536 (testmap.go:5) CALL runtime.gcWriteBarrier(SB)
0x0222 00546 (testmap.go:3) CALL runtime.morestack_noctxt(SB)
这个就很清楚了:
make->makemapset->mapassign,至于fast这个就看你的key类型,这个会在编译器确定get->mapaccess2/mapaccess1,这个时候就有comma之分了,语法糖的事了,后面说- 还有一个 写屏障,这个得后面分析(我也不知道为什么有这个)
Get
先从get来看整个结构。大致说一下流程:
- 先看map底层存储是否为
nil或是底层都没有数据,直接返回零值 - 判断读写冲突 ->
concurrent map read and map write,经典错误了 - 计算hash值 <- (存储时key使用的hash算法对key进行hash计算,并加入
hash0做扰动,增加随机性) - 计算m值 <- (1 << B - 1,比如 B=5,m=31,二进制表达上全为1)
- 计算
bucket地址 <- (需要上面的hash&m来确定桶序号:从 h.bucket 基地址出发) - 检测
oldbuckets是否为nil,有则说明在扩容ing;求一下key在oldbuckets中的桶序号 - 如果上述求的的
oldb还没有迁移,则在oldb中开始循环查找
目前:确定了桶序号,那就在桶里面开始查询;如果出现hash冲突,则在 overflow bucket 中继续找
- 取出
hash的高8位,作为tophash比较值(可以快速试错,高8位值会和bmap中tophash[]循环比较,用于快速试错(循环是廉价操作,而且比较的也只是数字)。如果都没有匹配,则进入当前桶的overflowbucket) - 匹配上,则定位
key/value的位置(也是指针偏移操作,和前面找桶序号一致) - 定位到
key相等则继续定位value;不相等说明hash冲突
上述就大致介绍了 get 的过程,具体代码:runtime.mapaccess1
总结一下:
- 寻找桶slot ->
hash(key)的低B位(B是按照当时元素数量计算,对数计算) - 寻找slot中的
key_index->hash(key)的高8位,会击中头部的tophash[]某一个,击中哪一个index,就会偏移几个然后找到这个key/value
扩容
扩容发生在 Set 过程中,所以从 Set 中出发:
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
...
// If we hit the max load factor or we have too many overflow buckets,
// and we're not already in the middle of growing, start growing.
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again
}
...
}
可见扩容时间有两种:
- 装载因子 > 6.5(具体算法:13/2)
overflowbucket过多;这里会出现桶过多,元素很少,浪费空间
针对这两种,解决方案如下:
- 元素多桶少,简单就是扩桶:
B = B+1,newbucket = oldbucket_num*2;但是元素还在oldbucket,需要迁移 - 元素少桶多,解决方式:开辟一个newbucket,将
oldbucket渐进式移动到newbucket,桶数量没有发生变化
然后正式来看看扩容具体是怎么渐进式扩容(其实这里和 redis 差不多)。扩容入口在 runtime.hashGrow:
- 先确定扩容标准:看是元素多,还是桶多 ->
bigger = 1 ? 0 - 原先的
bucket挂载成oldbuckets - 根据上面扩容标准申请新的
bucket空间:h.B+bigger - 最后将其他属性赋值上:
h.oldbuckets = oldbucketsh.buckets = newbucketsh.B += bigger-> grow 2倍h.nevacuate = 0-> 迁移进度(这个会作为后期的一个判断扩容的标志)h.noverflow = 0-> overflow bucket numh.flags |= sameSizeGrow-> 这个会在overflow中被设置(这个在判断扩容标准的时候就被定义出来了,以便后面扩容时是否分出 XY 区做判断)
其实可以发现,整个过程只是准备空间,也就是前期准备工作完成。这个时候整个 hmap 的 oldbucket && newbucket 都是有指向的。
然后开始真正的扩容,数据迁移在 runtime.evacuate 中完成的,它会对传入桶中的元素进行再分配:
同时从上面的分析来看,
overflow扩容是samesizeGrow;另外一个就真的doublesizeGrow
- 启动扩容是判断
h.oldbuckets != nil,为true->growWork(),正式开启扩容;每次扩容移动2个bucket;如果oldbuckets != nil,那就还得继续这个扩容 - 为什么需要搬迁元素?是因为我们访问到这个元素所在
slot,发现他可以搬迁/扩容 - 首先定位到
h.oldbuckets中哪一个slot要被扩容/迁移(定位oldbucket slot) - 这个时候就区分开
samesize || doublesize,会按照之前给定的h.flags来判断时哪个:doublesize,会讲double后的h.buckets,分成XY区samesize,默认只使用X区
- 先说
doublesize:-
- 因为
h.B此时已经是oldB+1,同时我们现在是寻找新的bucket_slot,按照之前的 低B位,我们需要比之前多前进1位
- 因为
-
- 多前进1位的数字,就会决定该
key被移动到X|Y
- 多前进1位的数字,就会决定该
-
- 如果是 0,说明没有动
slot_index,还是之前的位置(X区);反之(Y区)
- 如果是 0,说明没有动
-
samesize:直接就是把之前在overflow中的移动到现有的slot下的bmap
未完待续。。。(还差 Set, Delete)