数据结构
go的map采用数组+链表形式存储,数据存放于hmap
中:
type hmap struct {
count int // 哈希表的元素个数,即len()
flags uint8 // map状态
B uint8 // 2^B为桶的数量
noverflow uint16 // 溢出桶的数量(预估)
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // 扩容时用于保存之前的buckets字段
nevacuate uintptr // 下一个迁移的桶编号
extra *mapextra // 溢出桶
}
type mapextra struct {
// overflow和oldoverflow保证所有溢出桶的存活,不被gc回收
overflow *[]*bmap // 存放的所有溢出桶的地址
oldoverflow *[]*bmap // 存放的所有老的溢出桶的地址
nextOverflow *bmap // 指向的下一个溢出桶的地址
}
在buckets中存放的是bmap
:
// A bucket for a Go map.
type bmap struct {
tophash [bucketCnt]uint8 // bucketCnt=8
}
//编译期间数据结构
type bmap struct{
topbits [8]uint8 //用于表示标志位或hash值高八位来快速定位K/V位置
keys [8]keytype
value [8]valuetype
overflow uintptr //连接下个bmap溢出桶
}
bmap
中仅包含了tophash
字段,通过比较不同键的哈希的高8位可以减少访问键值对次数以提高性能。bmap
在编译时确定K/V的类型,会存储有key、value、溢出桶信息。每个bmap
中最多只会有8个元素,超出部分回连接溢出桶进行存储。
源码分析
初始化
// makemap implements Go map creation for make(map[k]v, hint).
func makemap(t *maptype, hint int, h *hmap) *hmap {
// 判断申请内存是否超过限制
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
// initialize Hmap
if h == nil {
h = new(hmap)
}
h.hash0 = fastrand()
// 如果hint大于8并且hint大于(1<<b)*6.5 就每次增长1
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
// allocate initial hash table
// if B == 0, the buckets field is allocated lazily later (in mapassign)
if h.B != 0 {
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil) // 初始化buckets和溢出桶
if nextOverflow != nil {
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
// overLoadFactor reports whether count items placed in 1<<B buckets is over loadFactor.
func overLoadFactor(count int, B uint8) bool {
return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}
扩容
hmap的扩容会在两种情况下触发:
- 元素过多:hmap中的元素大于8个并且元素数量大于
6.5 * (2 ^B)
(见overLoadFactor函数),此时会进行翻倍扩容,即hmap.B+=1
- 桶过多:
B <= 15
时,溢出桶的数量大于等于(2 ^ B)
;B > 15
时,溢出桶的数量大于等于(2 ^ 15)
(见tooManyOverflowBuckets函数),此时会进行等量扩容,将桶中的元素重新排列,以缩减溢出桶的数量
hmap采用渐进式扩容方式,可以避免一次性扩容带来的性能瞬时抖动。当老桶中的数据还没被迁移时,get就会从老桶中获取。
sync.Map
适合读多更新多但新增少的场景,结构体如下:
// Map is like a Go map[interface{}]interface{} but is safe for concurrent use
type Map struct {
mu Mutex // 互斥锁
// 只读数据,无需加锁
read atomic.Value // readOnly
// 访问需要加锁,新添加的key都会先放到dirty中
dirty map[interface{}]*entry
// 统计访问read没有未命中然后穿透访问dirty的次数
misses int
}
// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
m map[interface{}]*entry
amended bool // true if the dirty map contains some key not in m.
}
// An entry is a slot in the map corresponding to a particular key.
type entry struct {
// If p == nil, the entry has been deleted, and either m.dirty == nil or m.dirty[key] is e.
//
// If p == expunged, the entry has been deleted, m.dirty != nil, and the entry is missing from m.dirty.
//
// Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty != nil, in m.dirty[key].
p unsafe.Pointer // *interface{}
}
sync.Map
会把读和写的数据分离存储,读取数据时先尝试无锁从read中读取,dirty会记录新增/删除的数据,以减少对read的修改,对于更新操作也有多处使用CAS操作以减少lock操作(见Store
方法)。
参考:
go语言之map源码分析
Go 语言设计与实现——哈希表