阅读 558

Go map的数据结构和源码扩容分析

数据结构

go map的数据结构的话大概是这个图的样子

hashmap bmap

主要就两个结构,hmapbmap

hmap

hmap是来表示map的结构体。

type hmap struct {
	count     int
	flags     uint8
	B         uint8
	noverflow uint16
	hash0     uint32
	buckets    unsafe.Pointer
	oldbuckets unsafe.Pointer
	nevacuate  uintptr
	extra *mapextra 
}
复制代码

分别分析下这些变量的作用

  • count 元素数量
  • flags 状态标识,比如被写,被遍历等
  • B 桶(bmap)数量的对数,也就是说桶的数量是2^B个
  • hash0 哈希种子,增加哈希函数的随机性
  • buckets 指向bmap数组的指针
  • oldbuckets 指向旧bmap数组的指针,与扩容有关,下面再介绍
  • nevacute 表示扩容进度

bmap

bmap就是,hmap的buckets指向的就是bmap数组。

type bmap struct {
	tophash [bucketCnt]uint8
}
复制代码

看似很少,但实际在编译期间会动态创建一个新的结构

type bmap struct {
    topbits  [8]uint8
    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr
    overflow uintptr
}
复制代码

解释下这些变量

  • topbits 即上面的tophash,取了hash值的高八位

img

  • keys,values 键值对,都是长度为8的数组,意味着一个桶最多放8个kv
  • pad 对齐填充
  • overflow 溢出桶,可以理解为指针指向一个新的桶,即链表法

bmap内存模型

可以看到key是放在一起的,value也是放到一起的,这样的目的是为了节省空间

定位

先看张图

mapacess

假设hmap的B为5,那么后5位决定放在哪个bmap桶里。

如果是插入的情况,会对桶进行内遍历,找到第一个空位然后插入进去,把tophash值设为hash值前八位。

如果是查找的情况,对桶进行内遍历,hash值的前八位与每个cell的tophash进行比较。如果在 bucket 中没找到,并且 overflow 不为空,还要继续去 overflow bucket 中寻找,直到找到或是所有的cell都找遍了,函数返回 h[key] 的指针,如果 h 中没有此 key,那就会返回一个 key 相应类型的零值,不会返回 nil。

对于查找来说,获得hash值后只会通过后B位来定位到哪个bmap桶,然后要进行内循环寻找对应的kv,如果没有的话,还要去溢出桶去找。

相对于java Hashmap来说,这个八个长度的数组就相当于链表了,当发生哈希碰撞后,存储到一个bmap里。

扩容

当元素数量变多时,会导致碰撞变多,那么bmap里的值就更多,查找效率就会变低,所以到一定程度时就需要扩容。

需要一个指标衡量,就是负载因子。

go的负载因子规则是

loadFactor := count / (2^B)
复制代码

即元素数量除以桶的数量,默认为6.5。

Java的Hashmap的负载因子默认是0.75,所以Go相对于java来说更倾向于用空间换时间。

触发扩容有两种情况:

  1. 负载因子超过了默认值
  2. overflow的bucket数量过多

第一种很好理解,就是正常元素过多造成的扩容

第二种情况就是因为元素不断的进行增删造成溢出桶很多,元素很少,没有满足负载因子的默认值,但是效率很低

tooManyOverflowBuckets情况

面对这两种情况,扩容的策略并不同,我分别解释一下

负载因子超过默认值

这种情况只需要最简单的扩容,即把B加1,使桶扩容两倍的大小。但Go并没有直接把桶的元素转移,而是采取了类似于redis的渐进式扩容,这也就解释了hmap里oldbucket的作用。

扩容后会先将olbucket指向数组,每次插入修改删除时都会调用growwork()方法,尝试搬迁,搬迁后序号不变。

overflow的bucket数量过多

这种情况与java的hashmap扩容很相似,都属于链表上桶进行位置变换,来减少链表长度,提高效率。

B+1,使扩容两倍,通过判断新增的那一位是否为1,来决定桶放的位置。

并发问题

map 并不是一个线程安全的数据结构。同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic。

文章分类
后端
文章标签