go map源码

概要

分别讲

  1. bmap
  2. hmap
  3. get方法的逻辑
  4.  

 

这是一个bmap

 
 
bmap里面包含
  1. 一个长度为8的tophash数组,保持了每个key的高8位hash值和状态信息
    1. 状态具体,包括,
    2. 某一个位置是不是空的,
    3. 是不是正在扩容,等等
  2. 一个长度为8的key数组,保存了key
  3. 一个长度为8的value数组,保存了value
  4. 一个指向溢出桶的指针,!实际使用了在内存中移动地址的方式获取到下一个桶,
 

这里使用了内存对齐形式的排列方式
什么是内存对齐?
1
数据在内存上不是紧密排布的,不是一个数据挨着一个数据排列的,而是数据一定会放在自己宽度的整倍数上,这是因为cpu由于硬件限制只能从0,4,8这样的起始位置一次取4个字节(假设要取一个 4字节的int)
2
举例子
如果是map[int64]int8
int64就是8字节
int8就是1字节
背景的一个小格就是一个字节
然后
假设这个map有2个元素
按内存对齐的排列方式,需要 每个key占8字节,2个value一共占8字节。总共:8*3=24字节
按一般的排列方式,需要,第一个key占8字节,第一个value占8字节,第2个key再占8字节,第2个value再占8字节,总共:8*4=32字节。
 
 
如果不对齐,有空闲的,有点浪费内存(上面画的是数据在内存上的排布吗,为什么有空?是的,是内存排布,这么空就是规则。数据在内存的是在宽度的整数倍上排列的。假设此时按8字节为最小内存单元大小。)
3
所以做成内存对齐的结构更节省空间

再讲hmap

 
这是一个hmap
 
1
hmap使用数组+链表法存储数据
hmap里面的buckets,就是他的数组,就是多个bmap,是内存中申请的一片连续的空间
2
mapextra里面存overflow是一个数组
数组里面存了bmap上溢出桶的指针
所以说溢出桶有2种访问方式
hmap.mapextra.overflow的数组可以找到
从bmap.overflow也可以找到
3
hmap的oldbuckets用于扩容

 

get的逻辑

  1. 计算hash
  2. 找到桶
    1. 取hash值的低B位bit找到桶
    2. ps:源码中大量使用了位运算来加速
    3. 如果当前桶没有,尝试去找溢出桶
  3. 找到桶中的8个位置的哪一个位置
    1. 先比较tophash,如果相同,那么继续比较key
    1. 如果key相同就找到了
    2. PS:get方法有2种,一种带是否存在key的bool返回,一种不带。本质上2个函数,逻辑一样
 
 
PS:用于选择桶序号的是哈希的最低B位,而用于加速访问的是哈希的高 8 位,这种设计能够减少同一个桶中有大量相等 tophash 的概率影响性能
 
 

找桶的例子:
假设是4个桶,B=2,桶掩码就是 0b11
不过hash值是多少,低B位只有4种组合,分别是00,11,01,10
然后 0b11 & hash值= 只有4中结果,分别是0,1,2,3
就找到了桶

 

set的逻辑

  1. 计算hash
  2. 找到桶
  3. 遍历8个位置,找一个空位置,set进去
  4. 当前桶如果都满了,尝试去溢出桶找空位置
  5. 如果都没找到,新建溢出桶

 

grow的逻辑

1
渐进式扩容。不是一次搞定,而是每次set和delete的时候移动一个桶
2
触发扩容的时机是,当负载因子大于6.5
或者溢出桶太多
  1. 负载因子等于,k-v对数量  除以  桶数量
  2. ps:6.5在源码中是 乘13 除以2 实现的,估计也是为了加速
3
使用hmap的oldbuckets对象来扩容
每次扩容,桶数量乘2
4
每次移动一个桶的逻辑:
把一个旧桶的数据(包括溢出桶)移动到2个新桶上
移动的时候还有规律,一半去新桶数组的上半段,一半去新桶数组的下半段
假设,之前一共4个桶,现在一共8个桶,正在处理第1个桶,那么在扩容后,数据去第1个桶和第5个桶
 

 
 

 

 
posted @ 2022-12-26 17:05  董客园  阅读(40)  评论(0编辑  收藏  举报