• Go之map详解


    map的结构

    map实现的两个关键数据结构

    • hmap 定义了map的结构
    • bmap 定义了hmap.buckets中每个bucket的结构
    // A header for a Go map.
    type hmap struct {
    	count     int // 元素的个数
    	flags     uint8 // 状态标记,标记map当前状态,是否正在写入
    	B         uint8   // 可以最多容纳 6.5 * 2 ^ B 个元素,6.5为装载因子
    	noverflow uint16 // 溢出的个数
    	hash0     uint32 // 哈希种子
    
    	buckets    unsafe.Pointer // 桶的地址
    	oldbuckets unsafe.Pointer // 旧桶的地址,用于扩容
    	nevacuate  uintptr        // 迁移进度,小于nevacuate的已经迁移完成
    
    	extra *mapextra // optional fields
    }
    // A bucket for a Go map.
    type bmap struct {
         //每个元素hash值的高8位,如果tophash[0] < minTopHash,表示这个桶的搬迁状态
    	tophash [bucketCnt]uint8
        // 接下来是8个key、8个value,但是我们不能直接看到;为了优化对齐,go采用了key放在一起,value放在一起的存储方式,
        // 再接下来是hash冲突发生时,下一个溢出桶的地址
    }
    //上面bmap结构是静态结构,在编译过程中runtime.bmap会拓展成以下结构体:
    type bmap struct{
    	tophash [8]uint8
    	keys [8]keytype
    	// keytype由编译器编译时候确定
    	values [8]elemtype
    	// elemtype由编译器编译时候确定
    	overflow uintptr
    	//overflow的下一个bmap,overflow是uintptr而不是*bmap类型,保证bmap完全不含指针,是为了减少gc,溢出桶存储到extra字段中
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    名词解释
    负载因子

    负载因子是衡量hash表中当前空间占用率的指标。在go中,就是平均每个bucket存储的元素个数。

    • 计算公式如下:

    LoadFactor(负载因子)= hash表中已存储的键值对的总数量/hash桶的个数(即hmap结构中buckets数组的个数)在各语言的实现中,都会确定一个负载因子的阈值,当负载因子超过这个阈值时,就要进行扩容,以平衡存储空间和查找元素时的性能。根据golang团队的测试数据,将负载因子定为了6.5,即平均每个bucket保存的元素≥6.5个时会触发扩容。

    B

    bucket个数为:2^B; 可以保存的元素数量是 负载因子 * 2^B。

    data := make(map[int]int,17)
    
    • 1

    根据计算公式

    初始元素个数 ≤ 2^B * 6.5
    172^2 * 6.5
    
    • 1
    • 2

    可以计算出B为2,初始的桶的个数为4
    其中:
    B<4时,根据B创建桶的个数的规则为:2^B(标准桶)
    B>=4时,根据B创建桶的个数的规则为:2^B + 2^(B-4) (标准桶+溢出桶)

    tophash

    tophash是一个长度为8的数组,它不仅仅用来存放key的哈希高8位,在不同场景下它还可以标记迁移状态,bucket是否为空等。弄懂tophash对我们深入了解map实现非常重要。

    当tophash对应的K/V被使用时,存的是key的哈希值的高8位;当tophash对应的K/V未被使用时,存的是K/V对应位置的状态。

    emptyRest      = 0 
    emptyOne       = 1 
    evacuatedX     = 2 
    evacuatedY     = 3
    evacuatedEmpty = 4
    minTopHash     = 5 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    当tophash[i] < 5时,表示存的是状态;
    当tophash[i] >= 5时,表示存的是哈希值;

    当计算的哈希值小于minTopHash时,会直接在原有哈希值基础上加上minTopHash,确保哈希值一定大于minTopHash。

    func tophash(hash uintptr) uint8 {
      	top := uint8(hash >> (sys.PtrSize*8 - 8))
    	if top < minTopHash {
        	top += minTopHash
      	}
    	return top
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    emptyRest

    这个值有两层意思:一是表示该tophash对应的K/V位置是可用的;二是表示该位置后面的K/V位置都是可用的。

    赋值:

    初始化的时,tophash会被置为emptyRest。

    删除map元素时,会判断是否需要把删除key对应的tophash置为emptyRest。

    作用:

    判断bucket是否为空

    当tophash[0]==emptyRest表示整个bucket都是空的,这就是源码里面判断bucket是否为空的方法。

    查找时快速判断后面位置是否还需遍历

    如在查找时,在一个bucket中,找到tophash[2]位置,发现值为emptyRest,就可以判断该bucket没有该元素,继续查找下一个bucket。

    emptyOne

    仅表示该tophash对应的K/V位置是可用的,其后面的是否可用不知道。

    赋值:

    删除map元素时,会把key对应的tophash先置为emptyOne,再继续判断是否需要置为emptyRest。

    evacuatedX && evacuatedY

    这两个状态与扩容有关,记录元素被迁移到了新桶的部位–X或Y。如果是等位迁移,旧桶的元素必然被迁移到X部;如果是扩容迁移,旧桶元素可能迁移到X部,也可能迁移到Y部。当迁移到X部时,旧桶tophash置为evacuatedX;当迁移到Y部时,旧桶tophash置为evacuatedY。

    举个例子说明:扩容迁移,要把旧桶1的元素迁到新桶,因为新桶长度增长了一倍,因此旧桶1元素可能被迁移到新桶的1或5。当元素迁移到了1时,把旧桶tophash置为evacuatedX;反之,迁移到了5时,tophash置为evacuatedY。要注意置的是旧桶的tophash。

    旧桶迁移完就被回收了,为啥还要记录每个元素迁移的位置?

    想了解原因,我们必须要考虑一个很复杂的场景:遍历map时,开始扩容。map遍历并不是原子操作,在遍历过程中会有数据插入、删除等,会导致map扩容。因为遍历发生在扩容前,因此一直是遍历老桶。这时有两种情况:老桶数据没有迁移,这时直接从老桶返回K/V就可以了;老桶数据已经迁移,这时就需要重新查找map。那怎么判断老桶数据是否迁移?这时就用到evacuatedX和evacuatedY,这两个就是用来标记老桶数据迁移状态的。

    evacuatedEmpty

    用于表示此单元已迁移

    创建map

    在这里插入图片描述

    func makemap_small() *hmap
    func makemap(t *maptype, hint int, h *hmap) *hmap
    func makemap64(t *maptype, hint int64, h *hmap) *hmap // hint类型为int64, 实质还是调用的 makemap
    
    • 1
    • 2
    • 3

    当创建map时不指定hint大小,那么调用makemap_small来进行创建 当指定了hint(代表初始化时可以保存的元素的个数)的大小的时候,若hint<=8, 使用makemap_small进行创建map,否则使用makemap创建map

    map查找

    Go map会在编译阶段转换成runtime包中的hmap。其中,bmap指向存储key-value的结构(数组)。数组元素为“桶”,每个桶中包含高8位的hash和相应的8个key-value,高8位hash用来快速找到目标key,其次是8个key,8个value(key和value分开存储,是为了防止key存储空间大于value时,value会自动占用key大小的空间,这样做可以减少空间的浪费),最后是指向溢出桶的指针(解决哈希冲突)。hash 表通过 hash 值的低几位(原理是对数组长度取余,但通常采用与运算来加速)去查找 hash 桶,然后在去查找到的 hash 桶中查找高8位的hash,快速锁定key,知道key,就可以获取其value了。如果遇到哈希冲突,即不同key产生的hash值一样,如此就需要额外进行key值的比较,这就要求存储的key值是可以比较相等的,
    在这里插入图片描述
    Go map扩容,数据迁移不是一次性迁移,而是等到访问到具体某个bucket时才将数据从旧bucket中迁移到新bucket中

    1. 一次性迁移会涉及到cpu资源和内存资源的占用,在数据量较大时,会有较大的延时,影响正常业务逻辑。因此Go采用渐进式的数据迁移,每次最多迁移两个bucket的数据到新的buckets中(一个是当前访问key所在的bucket,然后再多迁移一个bucket)
    2. cpu资源:扩容时需要迁移map中oldbuckets的元素,其中的rehash操作会消耗cpu的计算资源,有可能会影响到用户协程的调度
    map插入/删除

    在这里插入图片描述
    在这里插入图片描述

    扩容的条件:

    1. 超过负载 map元素个数>6.5*桶个数
    func overLoadFactor(count int, B uint8) bool{
    	return count > bucketCnt && uintptr(count)>loadFactor*bucketShift(B)
    }
    其中
    bucketCnt=8,一个桶可以装的最大元素个数
    loadFactor=6.5,负载因子,平均每个桶的元素个数
    bucketShift(8), 桶的个数
    2. 溢出桶太多
    当桶总数<2^15时,如果溢出桶总数>=桶总数,则认为溢出桶过多
    当桶总数>=2^15时,直接与2^15比较,当溢出桶总数>=2^15时,即认为溢出桶太多了
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    扩容机制:

    1.双倍扩容:针对条件1,新建一个buckets数组,新的buckets大小是原来的2倍,然后旧的buckets数据
    搬迁到新的buckets
    2.等量扩容:针对条件2,并不扩大容量,buckets数量维持不变,重新做一遍类似双倍扩容的搬迁操作,
    把松散的键值对重新排列一次,使得同一个bucket中的key排列地更紧密,节省空间,提高buckets利用
    率,进而保证更快的存取。该方法我们称之为等量扩容。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    扩容过程:
    在这里插入图片描述

    假设旧桶数组大小为2^B, 新桶数组大小为2*2^B,对于某个hash值X
    若 X & (2^B) == 0,说明 X < 2^B,那么它将落入与旧桶集合相同的索引xi中;
    否则,它将落入xi + 2^B中
    
    • 1
    • 2
    • 3
  • 相关阅读:
    优维EasyOps,打造新一代运维新方式
    数据库相关知识
    【python】python制作 连连看 游戏脚本(一)
    2023年中国磷酸二铵市场发展概况分析:市场集中度较高[图]
    【SpringBoot】
    C# Bitmap图像通过内存保存为raw图像
    窗口延时、侧输出流数据处理
    项目实战第二十三讲:标准中心-类目体系
    [附源码]java毕业设计新能源汽车租赁管理系统
    ICCV 2023 | 沉浸式体验3D室内设计装修,基于三维布局可控生成最新技术
  • 原文地址:https://blog.csdn.net/cocotww/article/details/137950968