map数据结构
Golang的map使用哈希表作为底层实现,一个哈希表里可以有多个哈希表节点,也即bucket,而每个bucket就保存了map中的一个或一组键值对。
runtime/map.go:hmap
type hmap struct{
count int //当前保存的数据元素
...
B uint8 // buckets 的对数
...
buckets unsafe.pointer //bucket 数组指针,数组的大小为2^B
...
}
下图展示一个4个bucket的map案例
bucket哈希桶
bucket 数据结构
type bmap struct {
tophash [8]uint8 //存储哈希值的高8位
data byte[1] //key value数据:key/key/key/.../value/value/value...
overflow *bmap //溢出bucket的地址
}
- tophash是个长度为8的数组,哈希值相同的键(准确的说是哈希值低位相同的键)存入当前bucket时会将哈希值的高位存储在该数组中,以方便后续匹配。
- data区存放的是key-value数据,存放顺序是key/key/key/…value/value/value,如此存放是为了节省字节对齐带来的空间浪费。
- overflow 指针指向的是下一个bucket,据此将所有冲突的键连接起来。
键值对存储(key, value)
哈希表通常会有一堆桶来存储键值对,一个个键值对来了必然要选择一个桶,先通过hash函数将“键”处理一下,得到一个hash值,利用hash值从m的个中选择一个,桶编号区间为[0,m-1]
如何从m个桶中选择:
第一种:
- 取模法(hash%m)
第二种:
- 与运算法(hash & (m-1))
若想确保运算结果落在区间[0,m-1]而不会出现空桶,就要限制桶的个数m必须是2的整数幂,这样m的二进制数肯定只含一个1(比如:m = 4 二进制表示:0000 0100,m-1 二进制表示:0000 0011),如果桶的数目不是2的整数次幂,就会出现有些桶不会被选中,如果后来又有新的键值对选择这个桶,就会发生hash冲突,后续将介绍到.
与运算法拉链法
基本过程如下(与运算法)
假设当前 B=4 即桶数量为2^B=16个,要从map中获取k4对应的value
- 根据key值算出哈希值
- 取哈希值低位B 位与m-1(m表示桶的数量)与运算确定bucket位置
- 查找该key是否已经存在,如果存在则直接更新值
- 如果没找到将key,将key插入
- 如果当前桶元素已满,会通过overflow链接创建一个新的桶,来存储数据。
查找过程
- 根据key值算出哈希值
- 取哈希值低位 B 位与m-1(m表示桶的数量)与运算确定bucket位置
- 取哈希值高位(10101010)在tophash数组中查询
- 如果tophash[i]中存储值也与哈希值相等,则去找到该bucket中的key值进行比较
- 当前bucket没有找到,则继续从下个overflow的bucket中查找。
- 如果当前处于搬迁过程,则优先从oldbuckets查找
注:如果查找不到,也不会返回空值,而是返回相应类型的0值。
hash冲突
当有两个或以上数量的键被哈希到了同一个bucket时,我们称这些键发生了冲突。
解决hash冲突的方法有两种,第一种开放地址法 , 第二种: 链地址法 , Go语言使用链地址法也叫拉链法来解决冲突键
拉链法:
hsah
下图为冲突后的map
bucket数据结构指示下一个bucket的指针称为overflow bucket,意为当前bucket盛不下而溢出的部分。哈希冲突并不是好事情,它降低了存取效率
负载因子
- 负载因子用于衡量一个哈希表冲突情况,公式为:
负载因子 = 键数量 / bucket数量
- 哈希表需要将负载因子控制在合适的大小,超过其阀值需要进行rehash,也即键值对重新组织:
- 哈希因子过小,说明空间利用率低
- 哈希因子过大,说明冲突严重,存取效率低
扩容
扩容的条件
为了保证访问效率,当新元素将要添加进map时,都会检查是否需要扩容,扩容实际上是以空间换时间的手段。
触发扩容的条件有二个:
- 负载因子 > 6.5时,也即平均每个bucket存储的键值对达到6.5个。
- overflow数量 > 2^15时,也即overflow数量超过32768时。
增量扩容
当负载因子过大时,就新建一个bucket,新的bucket长度是原来的2倍,然后旧bucket数据搬迁到新的bucket。
考虑到如果map存储了数以亿计的key-value,一次性搬迁将会造成比较大的延时,Go采用逐步搬迁策略,即每次访问map时都会触发一次搬迁,每次搬迁2个键值对。
下图展示了包含一个bucket满载的map
当map种存储了7个键值对,只有一个bucket.此负载因子为7.再次插入数据时发生扩容操作,扩容之后将新插入键写入bucket.
当第8个键值对插入时,触发扩容,扩容示意图如下:
hmap数据结构中oldbuckets成员指身原bucket,而buckets指向了新申请的bucket。新的键值对被插入新的bucket中。
后续对map的访问操作会触发迁移,将oldbuckets中的键值对逐步的搬迁过来。当oldbuckets中的键值对全部搬迁完毕后,删除oldbuckets。
还有等量扩容,本文没写