go map 底层实现原理
go map 基本结构
go 中 map 的实现原理 其核心时由 hmap 和 bmap 两个结构体实现。
初始化
// 初始化一个可容纳10个元素的map
info = make(map[string]string,10)
- 一:创建一个hmap结构体对象
- 二:生成一个哈希因子 hash0 并赋值到 hmap 对象中
- 三:根据 hint=10, 并根据算法规则来创建B,当前B 为1
hint B
0~8 0
9~13 1
14~26 2
...
-
四:根据B去创建桶(bmap对象)并存放在buckets数组中,当前bmap的数量为2
- 当B<4时,根据B创建桶的个数规则为:2^B(标准桶)
- 当B>=4时,根据B创建桶的个数的规则为:2B+2(B-4) (标准桶+移除桶)
注意:每个bmap 中可以存储8个键值对,当不够存储时需要使用溢出桶,并将当前 bmap 中的 overrflow 字段指向溢出桶的
写入数据
info["name"] = "tom"
在map中写入如数据时,内部执行流程如下:
- 一:结合哈希因子和键
name
生产哈希值011011100011111110111011010
- 二:获取哈希值的
后B位
,并根据后B位的值来决定将此键值对放到哪个桶中(bmap)
将哈希值和桶掩码(B为1的二进制) 进行 & 运算,最终得到哈希值的后B位的值。假设当B为1时,其结果为0:
哈希值: 011011100011111110111011010
桶掩码: 0000000000000000000000001
结果: 0000000000000000000000000 = 0
- 三:在上一步确定桶之后, 接下来就在桶中写入数据
获取哈希值的tophash(哈希值的前8位),将tophash、key、value 分别写入如到桶中的三个数组中。如果桶已满,则通过overflow 找到溢出桶,并在溢出桶中继续写入
注意:以后在桶中查找数据时,会基于tophash来查找(tophash 相同则再去比较key)
- 四:hmap的个数count++(map中元素个数+1 也是len() 函数放回的值 )
读取数据
value := info["name"]
在map中读取数据时,内部的执行流程为:
- 一:结合哈希因子和键
name
生产哈希值 - 二:获取哈希值的
后B位
,并根据后B位的值来决定将此键值对放到哪个桶中(bmap) - 三:确定桶之后, 再根据key的哈希值计算出 tophash , 根据 tophash 和 key 却去桶中查找数据。
当前桶中没找到,则根据 overflow 再去溢出桶中找,均未找到则表示key不存在
map 扩容
在向 map 中添加数据,达到某个条件, 则会引发字典扩容。
扩容条件:
- map 中数据总个数/桶个数 > 6.5, 引起翻倍扩容
- 使用了太多的溢出桶时(溢出桶使用太多会导致map处理速度降低)
- B <= 15,已使用的溢出桶个数 >= 2 ^B 时, 引发等量扩容
- B > 15, 已使用的溢出桶格式胡>=2^15 时,引发等量扩容
//hashGrow函数在runtime/map.go里
func hashGrow(t *maptype, h *hmap) {
// B+1 相当于是原来 2 倍的空间
bigger := uint8(1)
// 对应条件 2
if !overLoadFactor(int64(h.count), h.B) {
// 进行等量的内存扩容,所以 B 不变
bigger = 0
h.flags |= sameSizeGrow
}
// 将老 buckets 挂到 buckets 上
oldbuckets := h.buckets
// 申请新的 buckets 空间
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger)
flags := h.flags &^ (iterator | oldIterator)
if h.flags&iterator != 0 {
flags |= oldIterator
}
// 提交 grow 的动作
h.B += bigger
h.flags = flags
h.oldbuckets = oldbuckets
h.buckets = newbuckets
// 搬迁进度为 0
h.nevacuate = 0
// overflow buckets 数为 0
h.noverflow = 0
// ……
}
当扩容之后:
- 一:B 会根据扩容后新桶的个数进行增加(翻倍扩容 B=旧B+1, 等量扩容 B = 旧B )
- 二:oldbuckets 指向原来的桶(旧桶)
- 三:buckets 指向新创建的桶(新桶 暂时没有数据)
- 四:nevacuate 设置为0,表示如果数据迁移的话,应该从原桶中对的第0个位置开始迁移
- 五:noverflow 设置为0 ,扩容后新桶中已使用的溢出桶为0
- 六:extra.oldoverflow 设置为旧桶 已使用的所有溢出桶。即: h.extra.oldoverflow = h.extra.overflow
- 七:extrra.overflow 设置为 null, 因为新桶中还未使用溢出桶
- 八:extra.nextOverflow 设置为新创建的桶中的第一个溢出桶的位置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DBW4eaOg-1666605607825)(go map 底层实现原理.assets/image-20221024172548744.png)]
map 迁移
扩容之后,必然要伴随着数据的迁移
翻倍扩容
如果是翻倍扩容,那么迁移就是将旧桶中的数据分流至新的两个桶中(比例不定),并且桶编号的位置为: 同编号位置 和 翻倍后对应的编号位置
迁移时会遍历某个旧桶中所有的key(包括溢出桶),并根据key重新生成哈希值,根据哈希值的 后B位
来决定将此键值对分流到那个新桶中。
扩容后,B的值在原来的基础上已加1,也就意味着通过多1位来计算此键值对要分流到新桶的位置,如上图:
- 当新增位(红色) 的值位 0, 则数据会迁移到与旧桶编号一致的位置
- 当新增位(红色) 的值位 1, 则数据会迁移到翻倍后对应编号的位置
例如:
旧桶个数位32,翻倍后新桶个数位64
在重新计算旧桶中的所有key哈希值时,红色位只能是0或1,所以桶中的所有数据的后B 位只能是以下两种情况:
- 000111 【】, 意味着要迁移到与旧桶编号一致的位置
- 100111 【39】,意味着要迁移到翻倍后对应编号的位置
注意: 同一个桶中key的哈希值的低B位一定时相同的,不然不会放到同一个桶中,所以同一个桶中黄色标记的位都是相同的。
等量扩容
等量扩容的数据迁移机制比较简单,就是将旧桶中的值迁移到新桶中
这种扩容和迁移的意义在于:当溢出桶比较多而每个桶种的数据又不多时,可以通过等量扩容和迁移让数据更紧凑