go语言map设计
哈希表
哈希表是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
存储过程
桶的选择方法
哈希表通常会用一堆桶来存储键值对。一个键值对来了,需要选择一个桶。先通过哈希函数把键处理一下,得到一个哈希值。现在利用这个哈希值从m个桶中选择一个桶编号从0到m-1,有两种选择桶的方式比较常见,第一种取模法,第二种与运算法。
-
取模法:
哈希值与桶的个数m取模,得到一个桶编号。
-
与运算法:
哈希值与m-1进行与运算,若想确保运算结果落在区间[0,m-1]不会出现空桶,就要限制桶的个数m必须是2的整数次幂。这样m的二进制表示一定只有一位为1。m-1的二进制表示一定是低于这一位的所有位均为1.如果桶的个数不是2的整数次幂,就有可能出现有些桶绝对不会被选中的情况。
哈希冲突
此时桶已经被选好了。假设2号桶被占用,后来又有新的键值对选择这个桶,就是发生了哈希冲突。常用的两种解决哈希哈希冲突的方法,第一种开放地址法,第二种,拉链法。
-
开放地址法
编号2这个桶被占用了,就找它后面没有占用的桶来用。这里选择3号桶。当查找k2这个键值对时,会首先查找编号为2的桶,会先定位到编号为2的桶,但是经过比较key不相等,会遍历它后面的桶,直到它后面的key相等。或者遇到空桶,证明这个key不存在。
-
拉链法
编号为2的桶被占用了,在它后面链一个新桶存储这个键值对,查找key2时,会先找到2号桶,所以会顺着链表往后找,这就是拉链法。
哈希表扩容
但是哈希冲突的发生会影响哈希表的读写效率,选择散列均匀的哈希函数可以减少哈希冲突的发生,适时对哈希表进行扩容也是保障读写效率的有效手段。通常会把存储键值对的数目与桶的数目的比值作为是否需要扩容的判断依据。这个比值被称为"负载因子"。
LoadFactor=count/m
需要扩容时需要分配更多的桶,它们就是新桶,需要把旧桶里面的键值对都迁移到新桶里。如果哈希表存储的键值比较多,一次性迁移所有桶花费的时间比较显著。所以通常会在哈希表扩容时,先分配足够多的新桶,然后用一个字段(oldbuckets)记录旧桶的位置。再记录一个字段(nevacuate)记录酒桶迁移的进度。例如记录下一个要迁移的旧桶的编号。
在哈希表每次读写操作时,如果检测到当前处于扩容阶段,就完成一部分键值对迁移任务,直到所有旧桶迁移完成。旧桶不再使用就算了真正完成一次哈希表的扩容。像这样把键值对迁移的时间分摊到多次哈希表操作的方式,就是"渐进式扩容"。可以避免一次性扩容带来的性能瞬时抖动。
go语言map设计
go语言Map类型底层实现是哈希表,map类型的变量本质上是一个指针。指向hmap结构体。源码位置:/usr/local/go/src/runtime/map.go
数据结构
hmap
type hmap struct {
count int //键值对数目
flags uint8
B uint8 //进入桶的数目2^B
noverflow uint16 //noverflow 用来记录溢出桶的数量
hash0 uint32
buckets unsafe.Pointer //记录桶的位置
oldbuckets unsafe.Pointer //保存旧桶位置
nevacuate uintptr //即将迁移的旧桶编号
extra *mapextra //溢出桶信息
}
桶和溢出桶
桶bmap
map使用的桶的结构即bmap的结构
cont(
bucketCntBits = 3
bucketCnt = 1 << bucketCntBits
...
)
...
type bmap struct {
tophash [bucketCnt]uint8
}
一个桶里可以放8个键值对,但是为了让内存排列更为紧凑,8个key放在一起,8个value放在一起。前8个key前面是8个tophash,每个tophash都是对应哈希值的高8位。最后位置overflow是个bmap型的指针,指向的是一个溢出桶,溢出桶的内存布局与常规桶相同,是为了减少扩容次数引入的,当一个桶存满了,还有可用的溢出桶时 ,就会在桶后面列出一个溢出桶,会往这里面存。
溢出桶mapextra
实际上如果哈希表要分配的桶的数目大于24,就认为使用到溢出桶的概率比较大,就会预分配2(B-4)个溢出桶备用,这些溢出桶在内存中是连续的,只是前2^B个用做常规桶,后面的用作溢出桶。如图:
type mapextra struct {
overflow *[]*bmap //记录目前已经被使用的地址
oldoverflow *[]*bmap // 用于在扩容阶段存储旧桶用到的那些溢出桶的地址
nextOverflow *bmap //下一个空闲溢出桶
}
hmap结构体最后有一个extra字段,指向一个mapextra结构体,里面指向的都是溢出桶相关的信息。nextoverflow指向下一个空闲溢出桶。overflow记录目前已经被使用的地址。假如编号为2的桶满了,会在后面链一个溢出桶,nextoverflow指向下一个空闲桶,noverflow用来记录溢出桶的数量,此时只用了一个oldoverflow,用于在扩容阶段存储旧桶用到的那些溢出桶的地址。如图:
扩容规则
count/(2^B) > 6.5 //翻倍扩容 hmap.B++
LoadFactor没有超标 //等量扩容
noverflow较多 // 等量扩容
noverflow何时算多?
- 第一种:当B<=15时,overflow >= 2^B
- 第二种:当B>15时,overflow >= 2^15
什么是等量扩容?
所谓等量扩容就是创建和旧桶数目一样多的新桶。然后把原来的键值对迁移到新桶中。
但是既然和旧桶数目一样多那等量扩容有何用?
考虑什么啥情况下桶的负载因子没有超过上限值呢?
当很多键值对被删除的情况下,桶的负载因子没有超过上限,同样数目的键值对迁移到新桶中,能够更加紧凑的排列,从而减少溢出桶的使用,这就是等量扩容的意义所在。
如图:
当很多键值对被删除的情况下,桶的负载因子没有超过上限。而是占用了溢出桶的空间。如果使用等量扩容,旧桶的数据迁移到新桶中,数据会更加紧凑,减少了溢出桶的使用。