目标
需要弄懂下面的问题:
- map的基本数据结构定义
- map的增、删、改、查的实现
- 扩容机制
- range map为什么是无序的
- map为什么不安全,以及sync.Map为什么安全
工具
go tool compile -S main.go
通过上面命令可以输出汇编指令,方便获取实际调用的方法。
数据结构
map对应的数据结构为 hmap:
|
|
由上面的数据结构可以知道:
B
作为2的幂次,意义是方便通过bucket := hash & bucketMask(h.B)
定位到key和哪个bucket关联- 如果分配桶的数目大于2^4次方,会分配2^(B-4)次方个溢出桶
增刪改查
创建
|
|
上面m1初始化调用的 runtime.makemap_small,编译期能确定map长度小于bucketCnt,没有初始化bucket。m2初始化调用 runtime.makemap,通过传入的size,获取对应的对数B进行初始化。
查询
查询某个key的流程如下:
- 将k1通过Hash函数生成64位二进制数d1
- 将d1和hmap的B-1值进行与操作获取具体bucket号
- 遍历前面获取到的bucket,将d1的首8位和bucket里的tophash值进行比较查找,查找到了即拿当前索引去计算值的索引返回值。没有查找到就看啊可能overflow是否为空,不为空则查找关联的下一个bucket
map的查询访问涉及下面两个函数, mapaccess2多了一个key是否存在的返回值:
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer
func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool)
增加和修改
不考虑扩容的情况下,map的增加与map的查询访问基本逻辑是一致的。首先遵循map访问的方式通过后B位定位bmap,通过前8位快速比较tophash。当map中不存在这个key,会记录bmap中的第一个空闲的tophash,并插入该key。当map中存在这个key,会更新该key的value,涉及到的函数主要是 mapassign
删除
map的删除与map的查询访问基本逻辑也是一致的。遍历bmap与overflow寻找目标key,如果找到则清空tophash并删除key/value释放内存,涉及的函数时 mapdelete
扩容机制
map扩容的目的时解决Hash冲突,防止复杂度增加,尽量保持算法O(1)的时间复杂度。Golang的map扩容时渐进式扩容,写入操作中发生,触发扩容的时机有以下两个:
- 情况一,
overLoadFactor函数计算count/2^B(长度/桶)是否大于6.5,大于就进行2倍扩容,即
hmap.B++
- 情况二,map存在局部bmap包含过多overflow的情况(B <= 15 && noverflow >= 2^B || B > 15 && noverflow >= 2^15),此时map会认为局部的bmap可以进行tophash密集排列,进行等量扩容,创建一样多的新通,将旧桶迁移到新桶,进行密集排列
不知道6.5这个阈值怎么定下来的,每个桶装8个,定个6.5 😅。
range map为什么是无序的
遍历map对应的函数分别是 runtime.mapiterinit和 runtime.mapiternext:
|
|
如上面代码所示,在遍历开始的时候,随机取的开始bucket,所以每次遍历的顺序都不一样,但是理论上是开始bucket不一样,但是整体bucket是按照写入顺序的。
map为什么不安全,以及sync.Map为什么安全
官方设计就是不支持多个协程进行map的并发读写操作,相关源码如下:
|
|
至于sync.Map为什么安全,见 golang-sync-map。