偷偷的告诉你go语言map的底层实现是这样的

2021/12/01 go

偷偷的告诉你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,而是会返回相应类型的零值。

Search

    Table of Contents