1.map内存模型与查询
1.1map内存模型
map的底层结构是一个hash表,对于碰撞使用拉链法实现,其中的数据结构如下所示
type hmap struct {
count int //元素个数
flags uint8
B uint8 // bucket的对数--log_2
noverflow uint16 // 计算key的hash时会传入hash函数
buckets unsafe.Pointer // 指向大小为 2^B 的Buckets数组.
oldbuckets unsafe.Pointer // 扩容的时候,大小为oldbuckets的两倍
··
}
type bmap struct {
//代表了go里面一个hash桶,里面存的每一个key的tophash(前八位hash值)
tophash [bucketCnt]uint8
}
1.每个桶是一个bmap struct,其中每个桶可以装载8个k-v值,overflow指向溢出桶,如果桶装满了就会指向下一个bmap
2.如果按照 将k-v放在一起存储,那在每一个 key/value 对之后都要额外 padding 7 个字节;而将所有的 key,value 分别绑定到一起,成为两个数组存储,则只需要在最后添加 padding。
1.2 map查询机制
1.查找某个键值:
- 首先,根据key的类型,调用对应的哈希函数计算出哈希值。
- 然后,根据哈希值的高位(高8位),找到对应的bucket。
- 接着,在bucket中查找对应的key。
- 最后,找到对应的key后,返回其value。
需要注意的是,由于哈希值有可能会发生碰撞,因此同一个bucket中可能会有多个key-value对,这时需要遍历bucket(包括溢出桶)中的所有元素,找到对应的key-value对。
2.插入修改:
map的插入和修改操作都是通过key来进行的。如果key已经存在于map中,那么它对应的value就会被替换成新的值。如果key不存在,那么它对应的key-value对就会被插入到map中。插入和修改操作的底层实现是一样的,都是通过哈希函数将key映射到对应的桶中,然后将key-value对插入到桶中。
3.Go语言中对map的遍历是无序的,遍历map的时候并不是固定从0号桶开始遍历的,而是每次都从一个随机的桶开始,并且从这个桶中随机的一个cell开始遍历。
2.map扩容机制
2.1扩容原因与扩容时刻
1.为什么要扩容:使用hash表的目的就是要快速的查到key值,但是随着map中添加的key越来越多,发生碰撞的概率越来越大,增删改查的效率也会降低,夸张来说可能从哈希表变成链表。
2.扩容时刻:判断是否有太多的溢出桶或者装载因子(一个公式,定义为6.5)—每个槽的key
造成大量溢出桶的原因:不停的增加元素,创建了很多bucket,但是没有到装载因子的临界值。后来有删除元素,减小元素的总数量,就会出现很多的溢出桶。
2.2扩容类型与具体方法
因为有以上两种需要扩容的原因,所以下面是两种扩容类型
- 1.等量扩容:数据不多但是溢出桶太多了,主要是对数据进行整理。
具体方法:开辟一个新的bucket空间,将老bucket中元素放到新的bucket中,使得key排列更紧密。
- 2.翻倍扩容:数据太多了
具体方法:将B+1,bucket直接变成两倍,就会存在新老bucket,但是此时元素都在老bucket中,并未迁移。对老桶中的一些设置进行更新(B,flag,oldbuckets,buckets等),更新extra结构体。
h.B += bigger
h.flags = flags
h.oldbuckets = oldbuckets
h.buckets = newbuckets
h.nevacuate = 0
h.noverflow = 0
2.3渐进式驱逐
渐进式驱逐(evacuate方法):每次操作一个旧桶的时候,将旧桶数据驱逐到新桶中,读取时不进行驱逐,只判断读取新桶还是旧桶。因此,在渐进式驱逐过程中,旧桶和新桶会共存一段时间,直到全部数据都被转移到新桶中为止,如果所有旧桶的数据迁移完成,回收旧桶。