Golang Map
Map
参考链接:
- 万字解析Golang基于桶思想的map实现原理 - MelonTe - 博客园
- (2 封私信 / 36 条消息) Go Map底层实现原理 - 知乎
- (2 封私信 / 36 条消息) 深入理解Go语言的Map - 知乎
数据结构
1 | type hmap struct { |
桶结构
- hash值计算:通过 hash 函数计算得出哈希值。
- 确定桶的位置:通过 hash 值的后面 B 位得出桶的系数,每个桶都有 8 个 slot,每一个slot 最多放置 8 个数据。总共有
2^B
个桶。- 如果桶的八个 slot 未满,那么直接存入空闲 slot。
- 如果当前桶的 8 个 slot 都满了,那么就会开辟一个新的溢出桶,放置在溢出桶里面。溢出桶是链表结构,通过指针连接。
- 扩容:触发条件是:
- 装载因子过高(元素数 / 桶数 > 6.5)
- 溢出桶过多(表示分布均匀)
查询流程
以键 k4 为例子:
- 首先进行 hash 计算,计算得出 k4 的 hash 值(在 64 位操作系统中一般 hash 值为 64 位)
- 通过最后的 B 计算得出在几号桶。
- 根据 k4 对应的 hash 值得出在桶的哪个位置需要注意的是,在实现中并没有发现 key,value 的显示定义,这是因为为了实现泛型,key,value 是直接保存在 tophash 之后的内存地址中的,需要在运行时通过 动态内存布局和运行时计算得出的
- 如果前八位一致,那么就直接读出数据
- 否则需要对比 key 完整的 hash 值,如果匹配,则读出数据
- 如果都没有找到,那么直接去溢出桶中去寻找。
存放数据
- 通过 key 的 hash 值的后面 B 位确定在哪个桶中
- 遍历当前桶,通过 key 的 tophash 和 hash,防止 key 重复,然后找到第一个可以插入的位置
- 如果当前桶元素已满,那么就会通过 overflow 链接创建一个新的桶来保存数据。
扩容机制
go 的扩容机制分为两种:
- 等量扩容:如果 map 的溢出桶的数量过多,导致性能下降,说明 kv 分布不均匀,此时触发等量扩容,hash 桶的数量不变,但是会重新分配 kv 的位置,减少桶的数量,增加 kv 的密度。
- 增量扩容:如果负载因子超标(count / 2 ** B),那么说明 kv 数量过多,桶的数量不够,那么就让 bucket 数量翻倍,让所有的 kv 对重新分配到新的桶数组中,目的是为了减少 kv 的密度,降低每个桶的 kv 数量,优化查询时间。
扩容前置
- 在我们的hmap结构中有一个oldbuckets吗,扩容刚发生时,会先将老数据存到这个里面。
- 每次对map进行删改操作时,会触发从oldbucket中迁移到bucket的操作【非一次性,分多次】
- 在扩容没有完全迁移完成之前,每次get或者put遍历数据时,都会先遍历oldbuckets,然后再遍历buckets。
等量扩容
由于map中不断的put和delete key,桶中可能会出现很多断断续续的空位,这些空位会导致连接的bmap溢出桶很长,导致扫描时间边长。这种扩容实际上是一种整理,把后置位的数据整理到前面。这种情况下,元素会发生重排,但不会换桶。
增量扩容
避免 hash 冲突的方法
- 拉链法:将多个命中到同一个桶的键值对,按照插入顺序依次存放在桶中。
- 开放寻址法:对于命中到同一个桶的多个键值对,采取向后寻找,知道找到空的桶再将值放入其中
方法 | 优点 |
---|---|
拉链法 | 简单常用,容易实现,无需事先分配空间 |
开放寻址法 | 无需额外的内存来存放指针,存放的元素在内存上基本连续 |
显然,Go采取的是拉链法,桶数组中的每一个桶,严格来说应该是一个链表结构的桶数组,它通过overflow
指针链接上了下一个溢出桶,使得多个键值对能存放在同一个桶中。若当前桶八个槽位都满了,就开辟一个新的溢出桶,放置在溢出桶里面。
杂谈
为什么说 map 不是并发安全的
- 数据竞争:如一个 goroutine 扩容时,另一个 goroutine 正在读写,可能破坏内部结构。
- 内存损坏:并发修改桶的
tophash
或键值对区域可能导致内存越界或数据错乱。
下面有一个扩容竞争过程:
- g1 查询一个 k1
- 计算出hash,确定桶的系数,但是还未查询到 buckets
- g2 添加一个 k2,触发扩容条件
- 数据迁移到 oldbuckets,buckets 为新
- g1 进行查询,会出现查询失败。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 !