尝试引用非结构体数组的字段_Go Map源码解读之数据结构

点击上方蓝色“后端开发杂谈”关注我们, 专注于后端日常开发技术分享

map 数据结构与实际的数据结构

map 中的数据被存放在一个数组中的, 数组的元素是桶(bucket), 每个桶至多包含8个键值对数据. 哈希值低位(low-order bits)用于选择桶, 哈希值高位(high-order bits)用于在一个独立的桶中区别出键. 哈希值高低位示意图如下:

1f5281544b347173f90fe113d0d401bb.png
image
源码位于 src/runtime/map.go.

结构体

// A header for a Go map.
type hmap struct {
    count     int    // 代表哈希表中的元素个数, 调用len(map)时, 返回的就是该字段值.
    flags     uint8  // 状态标志, 下文常量中会解释四种状态位含义.
    B         uint8  // buckets(桶)的对数log_2(哈希表元素数量最大可达到装载因子*2^B)
    noverflow uint16 // 溢出桶的大概数量.
    hash0     uint32 // 哈希种子.

    buckets    unsafe.Pointer // 指向buckets数组的指针, 数组大小为2^B, 如果元素个数为0, 它为nil.
    oldbuckets unsafe.Pointer // 如果发生扩容, oldbuckets是指向老的buckets数组的指针, 老的buckets数组大小是新
                              // 的buckets的1/2.非扩容状态下, 它为nil. 它是判断是否处于扩容状态的标识

    nevacuate  uintptr        // 表示扩容进度, 小于此地址的buckets代表已搬迁完成.

    extra *mapextra // 这个字段是为了优化GC扫描而设计的. 当key和value均不包含指针, 并且都可以inline时使用.
                    // extra是指向mapextra类型的指针.
}

// mapextra holds fields that are not present on all maps.
type mapextra struct {
    // 就使用 hmap 的 extra 字段来存储 overflow buckets, 

    // 如果 key 和 value 都不包含指针, 并且可以被 inline(<=128 字节), 则将 bucket type 标记为不包含指针 (使用
    // ptrdata 字段, 为0表示不包含指针). 这样可以避免 GC 扫描整个 map. 但是 bmap.overflow 是一个指针. 这时候我
    // 们只能把这些 overflow 的指针都放在 hmap.extra.overflow 和 hmap.extra.oldoverflow 中了.
    //  
    // 当 key 和 elem 不包含指针时, 才使用 overflow 和 oldoverflow. 
    // overflow 包含的是 hmap.buckets 的 overflow bucket, 
    // oldoverflow 包含扩容时的 hmap.oldbuckets 的 overflow bucket.
    overflow    *[]*bmap
    oldoverflow *[]*bmap

    // 指向空闲的 overflow bucket 的指针(第一个空闲的bucket地址)
    nextOverflow *bmap
}

// A bucket for a Go map.
type bmap struct {
    // tophash包含此桶中每个键的哈希值最高字节(高8位)信息(也就是前面所述的high-order bits).
    // 如果tophash[0] 
    tophash [bucketCnt]uint8
}

常量值

const (
    // 一个桶中最多能装载的键值对(key-value)的个数为8
    bucketCntBits = 3
    bucketCnt     = 1 <// 8

    // 触发扩容的装载因子为13/2=6.5
    loadFactorNum = 13
    loadFactorDen = 2

    // 键和值超过128个字节, 就会被转换为指针
    maxKeySize  = 128
    maxElemSize = 128

    // 数据偏移量应该是bmap结构体的大小, 它需要正确地对齐. 
    // 对于amd64p32而言, 这意味着: 即使指针是32位的, 也是64位对齐. 
    dataOffset = unsafe.Offsetof(struct {
        b bmap
        v int64
    }{}.v)

    // 每个桶(如果有溢出, 则包含它的overflow的链桶) 在搬迁完成状态(evacuated states)下, 要么会包含它所有的键值对,
    // 要么一个都不包含(但不包括调用evacuate()方法阶段,该方法调用只会在对map发起write时发生,在该阶段其他goroutine
    // 是无法查看该map的). 简单的说,桶里的数据要么一起搬走,要么一个都还未搬.
    //
    // tophash除了放置正常的高8位hash值, 还会存储一些特殊状态值(标志该cell的搬迁状态). 正常的tophash值, 
    // 最小应该是5,以下列出的就是一些特殊状态值. 
    emptyRest      = 0 // 空的cell, 并且比它高索引位的cell或者overflows中的cell都是空的. (初始化bucket时,就是该状态)
    emptyOne       = 1 // 空的cell, cell已经被搬迁到新的bucket
    evacuatedX     = 2 // 键值对已经搬迁完毕,key在新buckets数组的前半部分
    evacuatedY     = 3 // 键值对已经搬迁完毕,key在新buckets数组的后半部分
    evacuatedEmpty = 4 // cell为空,整个bucket已经搬迁完毕
    minTopHash     = 5 // tophash的最小正常值

    // flags
    iterator     = 1 // 可能有迭代器在使用buckets
    oldIterator  = 2 // 可能有迭代器在使用oldbuckets
    hashWriting  = 4 // 有协程正在向map写人key
    sameSizeGrow = 8 // 等量扩容

    // 用于迭代器检查的bucket ID
    noCheck = 1<8*sys.PtrSize) - 1 // 系统的最大值
)

bmap(即map当中的bucket)内存结构

// src/cmd/compile/internal/gc/reflect.go:bmap
// bucket 结构 
func bmap(t *types.Type) *types.Type {
    if t.MapType().Bucket != nil {
        return t.MapType().Bucket
    }

    bucket := types.New(TSTRUCT)
    keytype := t.Key()
    elemtype := t.Elem()
    dowidth(keytype)
    dowidth(elemtype)
    if keytype.Width > MAXKEYSIZE {
        keytype = types.NewPtr(keytype)
    }
    if elemtype.Width > MAXELEMSIZE {
        elemtype = types.NewPtr(elemtype)
    }

    field := make([]*types.Field, 0, 5)

    // The first field is: uint8 topbits[BUCKETSIZE].
    arr := types.NewArray(types.Types[TUINT8], BUCKETSIZE)
    field = append(field, makefield("topbits", arr))

    arr = types.NewArray(keytype, BUCKETSIZE)
    arr.SetNoalg(true)
    keys := makefield("keys", arr)
    field = append(field, keys)

    arr = types.NewArray(elemtype, BUCKETSIZE)
    arr.SetNoalg(true)
    elems := makefield("elems", arr)
    field = append(field, elems)

    // 确保 overflow 指针是结构中的最后一个内存, 因为运行时假定它可以使用size-ptrSize作为 overflow 指针的偏移量. 
    // 一旦计算了偏移量和大小, 我们就要仔细检查下面的属性(在已经忽略检查代码).
    //
    // BUCKETSIZE为8, 因此该结构在此处已对齐为64位.
    // 在32位系统上, 最大对齐方式为32位, 并且溢出指针将添加另一个32位字段, 并且该结构将以无填充结尾.
    // 在64位系统上, 最大对齐方式为64位, 并且溢出指针将添加另一个64位字段, 并且该结构将以无填充结尾.
    // 但是, 在nacl/amd64p32上, 最大对齐方式是64位, 但是溢出指针只会添加一个32位字段, 因此, 如果该结构需要64位填充
    // (由于key或elem的原因), 则它将最后带有一个额外的32位填充字段.
    // 通过在此处发出填充.
    if int(elemtype.Align) > Widthptr || int(keytype.Align) > Widthptr {
        field = append(field, makefield("pad", types.Types[TUINTPTR]))
    }

    // 如果keys和elems没有指针, 则map实现可以在侧面保留一个 overflow 指针列表, 以便可以将 buckets 标记为没有指针.
    // 在这种情况下, 通过将 overflow 字段的类型更改为 uintptr, 使存储桶不包含任何指针.
    otyp := types.NewPtr(bucket)
    if !types.Haspointers(elemtype) && !types.Haspointers(keytype) {
        otyp = types.Types[TUINTPTR]
    }
    overflow := makefield("overflow", otyp)
    field = append(field, overflow)

    // link up fields
    bucket.SetNoalg(true)
    bucket.SetFields(field[:])
    dowidth(bucket)

    t.MapType().Bucket = bucket

    bucket.StructType().Map = t
    return bucket
}

hmap (即map) 内存结构

// src/cmd/compile/internal/gc/reflect.go:hmap
func hmap(t *types.Type) *types.Type {
    if t.MapType().Hmap != nil {
        return t.MapType().Hmap
    }

    bmap := bmap(t)

    // type hmap struct {
    //    count      int
    //    flags      uint8
    //    B          uint8
    //    noverflow  uint16
    //    hash0      uint32
    //    buckets    *bmap
    //    oldbuckets *bmap
    //    nevacuate  uintptr
    //    extra      unsafe.Pointer // *mapextra
    // }
    // must match runtime/map.go:hmap.
    fields := []*types.Field{
        makefield("count", types.Types[TINT]),
        makefield("flags", types.Types[TUINT8]),
        makefield("B", types.Types[TUINT8]),
        makefield("noverflow", types.Types[TUINT16]),
        makefield("hash0", types.Types[TUINT32]), // Used in walk.go for OMAKEMAP.
        makefield("buckets", types.NewPtr(bmap)), // Used in walk.go for OMAKEMAP.
        makefield("oldbuckets", types.NewPtr(bmap)),
        makefield("nevacuate", types.Types[TUINTPTR]),
        makefield("extra", types.Types[TUNSAFEPTR]),
    }

    hmap := types.New(TSTRUCT)
    hmap.SetNoalg(true)
    hmap.SetFields(fields)
    dowidth(hmap)

    // The size of hmap should be 48 bytes on 64 bit and 28 bytes on 32 bit platforms.
    // 5("count", "buckets", "oldbuckets", "nevacuate", "extra")
    if size := int64(8 + 5*Widthptr); hmap.Width != size {
        Fatalf("hmap size not correct: got %d, want %d", hmap.Width, size)
    }

    t.MapType().Hmap = hmap
    hmap.StructType().Map = t
    return hmap
}

bmap 也就是 bucket(桶)的内存模型图解如下(代码逻辑就是上述的 bmap 函数).

fe1f8374463afe753bb304806b5e9b49.png
image

该桶的第7,8位cell还未有对应的键值对. 注意: key和value是各自存储起来的, 并非想象中的 key/value/key/value…的形式. 这样的做法好处在于消key/value之间的padding, 例如map[int64]int. 还有,在8个键值对数据后面还有一个overflow指针, 因为桶中最多只能装8个键值对, 如果有多余的键值对落到当前桶, 那么就需要再构建一个桶(溢出桶), 通过 overflow 指针链接起来.

最后, 这里展示一个 B=4 的完整 map 结构:

1355721151946b80408fea4d29ab0422.png
image
  • 参考链接
    1.8 万字详解 Go 是如何设计 Map 的

e32b9dadc27b2864f4996dfc9f72e011.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值