设计概述
map
源码位于runtime/map.go
。
map的设计概述如下:
map
是一个哈希表;- 数据被组织成
bucket
数组,每个bucket
最多存8个键值对; - 哈希值的低位用于选择
bucket
数组的下标(取余)。每个bucket
包含哈希值的若干高位,用于定位一个bucket
内的键值对; - 如果多于八个键被哈希到同一个
bucket
中,将一个额外的bucket
连接到其后面(拉链法,额外的bucket
被称为overflow bucket
); - 当哈希表需要扩容时,分配一个两倍大小的
bucket
数组; - 在扩容后,
bucket
被增量地(incrementally)从旧的bucket
数组移到新的bucket
数组; map
的迭代器遍历bucket
数组并根据遍历顺序返回键;- 为了保证迭代器不会访问同一个键两次,
map
不会在一个bucket
内移动一个键; - 当在正在发生迁移的
map
中遍历时,迭代器在旧的bucket
数组上迭代,并检查键是否已经被迁移(evacuated)。
map
的首部
先粗略看一下map
中的各个字段。
// A header for a Go map.
type hmap struct {
count int // map的大小,也就是对map使用len()的值
flags uint8 // 状态标识,用于控制goroutine写入和扩容的状态,详见下文
B uint8 // 桶的数量,2^B个
noverflow uint16 // 溢出桶(overflow)个数
hash0 uint32 // 哈希因子
buckets unsafe.Pointer // 2^B个bucket的数组
oldbuckets unsafe.Pointer // 扩容后的旧bucket数组
nevacuate uintptr // 迁移计数器,此指针之前的所有桶已被迁移,即,nevacuate指向桶数组已迁移桶的最高下标
extra *mapextra // 可选的域
}
// bmap表示一个桶
type bmap struct {
// tophash包含该bucket中键的哈希值的高八位
// 如果tophash[0] < minTopHash,tophash[0]表示桶迁移状态。
tophash [bucketCnt]uint8 // bucketCnt == 8,一个桶有八个位置
// 跟在tophash后面的事八个键和八个值。
// 将所有键、值分别放在一起,即以|k|k|k|k|v|v|v|v|的方式保存,可以避免内存对齐造成空间浪费。
// 如map[int64]int8,为了做内存对齐,占一字节的int8需要用7个字节填充对齐。
}
上述bmap
结构体定义只有一个tophash
保存桶中各键的哈希值的高八位,实际上bmap
还保存了各个键值对。根据编译期间的 cmd/compile/internal/gc.bmap
可以重建其结构:
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}
type mapextra struct {
// 如果键值都不包含指针,并且允许内联,会将bucket类型标志为不包含指针。这样做避免了GC扫描整个map。
// 为了保证空闲的overflow bucket在GC过程中存活,将各个指向溢出桶的指针保存到overflow和oldoverflow中。
// overflow和oldoverflow只在键值均不包含指针时使用。
// overflow包含了hmap.buckets的溢出桶,oldoverflow包含了hmap.oldbuckets的溢出桶。
// 这种间接寻址使得可以在hiter(用于对map进行迭代)中保存slice的指针。
overflow *[]*bmap // 指向一个元素为*bmap的slice的指针
oldoverflow *[]*bmap
// nextOverflow为空闲溢出桶的指针
nextOverflow *bmap
}
map
的初始化
map
的初始化有两种:
m := make(map[string]int)
// 指定map长度
m := make(map[string]int, 8)
其实际调用的是makemap
函数。
func makemap64(t *maptype, hint int64, h *hmap) *hmap {
// 当hint类型为int64时校验将其转换为int再转换为int64是否值不变,
// 如果hint的值发生变化,则将hint设置为0.
if int64(int(hint)) != hint {
hint = 0
}
return makemap(t, int(hint), h)
}
// 当hint<=8且map需要在堆上分配时调用该方法创建map.
// 该方法仅分配一个hmap首部,初始化哈希因子后返回。
func makemap_small() *hmap {
h := new(hmap)
h.hash0 = fastrand()
return h
}
// makemap实现了标准的map初始化动作。
// 如果编译器确定map或者第一个bucket可以在栈上创建,h和bucket可能不是nil。
// 如果h != nil,map可以在h中直接创建。
// 如果h.buckets != nil,指向的桶可以用作第一个桶。
func makemap(t *maptype, hint int, h *hmap) *hmap {
// 将hint和t.bucket.size相乘,并检查乘积是否溢出。
// mem即hint个t.bucket.size大小的桶所需的内存大小。
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
// 如果乘积溢出,或所需内存超过最大可分配内存,将hint设为0.
if overflow || mem > maxAlloc {
hint = 0
}
// 分配hmap首部
if h == nil {
h = new(hmap)
}
// 初始化哈希因子
h.hash0 = fastrand()
// 找到可以容纳所需元素数的参数B。
B := uint8(0)
// 计算B值。计算方法:
// 假设将hint个元素放到2^B个桶中,检查每个桶的平均元素数是否大于最大加载因子6.5
// 将B自增直至满足平均每个桶的元素数<=6.5.
for overLoadFactor(hint, B) {
B++
}
h.B = B
// 分配哈希表
// 如果B == 0,桶被延迟分配。
// 如果hint很大,分配内存需要花费一定时间。
if h.B != 0 {
var nextOverflow *bmap
// makeBucketArray初始化并返回一个桶数组。
// 它有可能会预分配一些溢出桶,即 nextOverflow。
// 如果预分配了溢出桶,把它挂到h.extra.nextOverflow。
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil {
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
由上述初始化方法可以看到,当创建的map
容量很小时(hint <= 8),仅初始化map
首部和哈希因子,而没有为桶数组分配空间。否则进行如下动作:
- 参数判断,检查创建数组大小是否合法;
- 对
map
首部和哈希因子进行初始化; - 根据传入的
hint
计算出桶的数量。具体地,找到 容纳hint
个元素,且每个桶的平均负载量<=6.5个元素 的数组大小; - 调用
makeBucketArray
为桶数组分配空间。
最后返回h
,它的类型是*hmap
,说明平时使用的 map
其实是一个指针。相比之下 slice
创建返回的是slice
结构体,如下:
func makeslice(et *_type, len, cap int) slice
slice
结构体如下:
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}
两者的区别是,当map
作为函数参数时,在函数内部对map
的操作会影响调用者持有的map
;而slice
作为参数时,在被调用函数内对slice
进行append
或者移除末尾元素对调用函数内的slice
不会产生影响。
map
元素的访问
要查找一个map
的值,首先要给出key
,对这个key
进行哈希,得到哈希值。根据得到的hash对桶数组取余,确定key
应该落在哪个桶中。然后顺序比较桶中的八个key(先比较hash高八位,相等时再比较键),找到key就能够定位要查找的value。
如果没有找到,且还有溢出桶,则跳到溢出桶继续上述比较。
示意图如下:
插入过程与上述类似,只不过是在桶中找到第一个空位,将键值对插入。
源码中,mapaccess1
,mapaccess2
,mapaccessK
,mapaccess1_fat
,mapaccess2_fat
均为map
元素的访问方法。
在Go语言编译的类型检查期间,会根据接受参数的个数决定使用的运行时方法:
- 当接受参数仅为一个时,会使用
runtime.mapaccess1
,该函数只返回一个指向目标值的指针; - 当接受两个参数的时候,会使用
runtime.mapaccess2
,除了目标值,还会返回一个用于表示当前目标值是否存在的布尔值; mapaccessK
只用于map
迭代器;_fat
方法则用于当值占用空间大于zeroVal
数组,需要返回一个额外的零值。
mapaccess1
mapaccess1
是访问map
元素主要方法,其源码及解析如下:
// mapaccess1返回指向h[key]的一个指针。mapaccess1永远不会返回nil,
// 而会返回一个零对象的引用,用于当键不在map中时代表元素的类型。
// 因为返回的指针会使整个map存活,尽量不要长时间持有它。
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if raceenabled && h != nil {
callerpc := getcallerpc()
pc := funcPC(mapaccess1)
racereadpc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
if msanenabled && h != nil {
msanread(key, t.key.size)
}
// 如果map首部为nil或map中没有元素,返回零值。
if h == nil || h.count == 0 {
// 如果该maptype的哈希函数会panic,运行一下该maptype的哈希函数,然后将一个空字节返回。
if t.hashMightPanic() {
t.hasher(key, 0) // see issue 23734
}
return unsafe.Pointer(&zeroVal[0])
}
// 并发读写,直接panic.
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}
// 根据键和哈希因子求哈希值
hash := t.hasher(key, uintptr(h.hash0))
// m是B个二进制1,通过hash&m能对哈希值进行取余操作。
m := bucketMask(h.B)
// hash&m相当于hash%tableSize,即哈希值对数组大小取余,定位到键应该位于第几个桶。
// (hash&m)*uintptr(t.bucketsize)则偏移了(hash&m)个桶的内存位置。
// 桶数组起始地址+偏移值定位到桶的内存位置。
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
// 如果有oldbuckets,则说明当前map仍在迁移中
if c := h.oldbuckets; c != nil {
// 如果不是容量不变的迁移,说明现在的数组大小比之前增长了一倍,则将掩码右移一位得oldbuckets的掩码。
if !h.sameSizeGrow() {
m >>= 1
}
// 数组起始地址+偏移值在旧数组中定位到桶的内存位置。
oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
// 如果还没迁移,就在旧数组中找键值对。
// 由上文map首部知,evacuated()实际是检查了桶oldb的tophash域的第一个byte的值。
if !evacuated(oldb) {
b = oldb
}
}
// 根据哈希值计算tophash值,tophash即哈希值的高8位。
// 由于小于minTopHash的tophash用于指示桶是否已经迁移,
// 如果tophash<minTopHash,则tophash+=minTopHash.
top := tophash(hash)
// 从定位到的bucket及其溢出链查找key
bucketloop:
for ; b != nil; b = b.overflow(t) {
// b.overflow(t)跳到下一个溢出桶
// 遍历一个桶的八个位置
for i := uintptr(0); i < bucketCnt; i++ {
// 先比较tophash
if b.tophash[i] != top {
// 如果tophash值为emptyRest,则往后的位置已经没有元素了,
// 也没有更多溢出桶了。
if b.tophash[i] == emptyRest {
break bucketloop
}