点击上方蓝色“后端开发杂谈”关注我们, 专注于后端日常开发技术分享
map 数据结构与实际的数据结构
map 中的数据被存放在一个数组中的, 数组的元素是桶(bucket), 每个桶至多包含8个键值对数据. 哈希值低位(low-order bits)用于选择桶, 哈希值高位(high-order bits)用于在一个独立的桶中区别出键. 哈希值高低位示意图如下:
源码位于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
函数).
该桶的第7,8位cell还未有对应的键值对. 注意: key和value是各自存储起来的, 并非想象中的 key/value/key/value…的形式. 这样的做法好处在于消key/value之间的padding, 例如map[int64]int
. 还有,在8个键值对数据后面还有一个overflow指针, 因为桶中最多只能装8个键值对, 如果有多余的键值对落到当前桶, 那么就需要再构建一个桶(溢出桶), 通过 overflow 指针链接起来.
最后, 这里展示一个 B=4 的完整 map 结构:
参考链接
1.8 万字详解 Go 是如何设计 Map 的