偷偷的告诉你go语言map的底层实现是这样的
map的底层实现?
map底层是用hash表实现的,一个hash表有多个bucket,而每个bucket保存map中的一个或多组键值对
2、map底层结构是hmap
type hmap struct {
count int //当前保存元素的个数
B uint //bucket 数组大小
buckets unsafe.Pointer //bucket数组,数组长度为2^B
oldbuckets unsafe.Pointer //老旧bucket数组,用于扩容
}
每个元素经过hash运算后会落到每个桶中
3、bucket数据结构
type bmap struct {
tophash [8]uint8 //存储hash值的高8位
data []byte //key value 数据:key/key/key/key/key..../value/value/value/value
overflow *bmap //溢出bucket的地址
}
1)hash值相同的键存入当前桶的时候,会将hash值高8位存储在该数组中,以便后续匹配
2)data存放的是key-value数据,存放顺序是key/key/key/key/key..../value/value/value/value ,
如此存放是为了节省字节对齐带来的空间浪费
4、hash冲突
当有两个或者以上数量的键被hash到了同一个桶时,我们称这些键发生了冲突,go使用链地址法来解决键冲突。
由于每个bucket可以存放8个键值对,所以同一个bucket存放超过8个键值对时就会再创建一个键值对,用类似
链表的方式将bucket连接起来。
5、负载因子
负载因子用于衡量一个hash表冲突的情况,公示为
负载因子 = 键数量/bucket 数量
负载因子过小或过大都不理想
负载因子过大,说明冲突严重,存储效率低
负载因子过小,可能预分配的空间空间太大,也可能是大部分元素被删除造成的。随着元素不断的添加到map中
负载因子会逐渐升高
负载因子过大,需要申请更多的bucket,并对所有的键值对重新组织,使其均匀的分布到这些bucket中,这个过程叫rehash.
go语言中map的负载因子达到6.5会触发rehash。
6、扩容条件
负载因子大于6.5
overflow的数量大于2^15
7、增量扩容
当负载因子过大时,就会新建一个bucket数组,新的bucket数组的长度是原来的2倍,然后旧的bucket数组中的数据搬迁到新的
bucket数组中。
考虑到map存储了数亿亿计的键值对,那么一次搬迁会造成较大的延迟,go采用逐步搬迁策略,每次访问map的时会触发一次搬迁,每次
搬迁两个键值对
8、等量扩容
就是重新搬迁,把松散的键值对重新排列一次,以使bucket的使用率更高,进而保证更快的存储速度。
9、增删改查
查找过程
1)根据key值计算hash值
2)取hash值低位与hmap.B取模来确定bucket的位置
3)取hash值高位,在tophash数组中查询定位到key高八位位置
4)在data数组中通过key在tophash高8位对应的位置找到对应的key
5) 比较要找的key是否在data数组中,如果存在,就取key对应的value
6) 如果没有找到,就继续从溢出的bucket中查找
7)如果当前map处于搬迁的过程,那么优先从oldbuckets 数组中查找,不再从新的buckets数组中查找
8) 如果查询不到,那么也不会返回nil,而是会返回相应类型的零值。