1. Map的底层结构
1.1 什么是Map
Map是由Key-Value构成的,并且同一个Key只能出现一次,这就要求Key可以进行相等比较。它的任务是设计一种数据结构用来维护一个集合的数据,并且可以同时对集合进行增删查改的操作。最主要的数据结构有两种:哈希表(hash table)和搜索树(search tree)。
哈希表实现用一个哈希函数将Key分配到不同桶(Bucket,也就是一个数组的某个位置),这样,查询的性能开销主要在哈希函数计算的计算和数组的访问。在大部分场景下,哈希表的查询性能很高。
但哈希表一般会存在哈希冲突的问题,就是不同的Key计算出了相同的哈希值,分配到同一个Bucket上。常见的解决方案有两种:拉链法和地址开放法。拉链法将一个Bucket实现成一个链表,落在一个Bucket的Key都会插入这个链表。地址开放法则是在发生哈希碰撞后,在数据的后面挑选空位,用来放置新的Key。哈希表的平均查询时间复杂度是,最差时间复杂度是。
搜索树一般都是采用自平衡搜索树:红黑树、AVL树。自平衡搜索树的最差查询时间复杂度是。
1.2 Map的内存模型
首先声明我使用的Golang版本:
go version go1.17 darwin/amd64
Golang的Map实现采用的是哈希表,使用拉链法解决哈希冲突问题。
在源码中,表示map的结构体是hmap,应该是hashmap的缩写。
// A header for a Go map.
type hmap struct {
count int // Key-Value对数量,调用len()方法直接返回此值
flags uint8
B uint8 // buckets数量以2为底的对数,即len(buckets)=2^B
noverflow uint16 // overflow的bucket的近似数量
hash0 uint32 // 计算Key的哈希种子
buckets unsafe.Pointer // 指向buckets的数组,大小为2^B.如果count==0,那么为nil
oldbuckets unsafe.Pointer // 指向之前的buckets数组,只有在扩容时才不为空
nevacuate uintptr // 扩容进度,小于此地址的bucket迁移完成
extra *mapextra // optional fields
}
buckets是一个指针,但最后指向的是一个结构体。
// A bucket for a Go map.
type bmap struct {
tophash [bucketCnt]uint8
}
但这只是表面(src/runtime/map.go)的结构,编译期间会给它加料,动态地创建一个新的结构(src/cmd/complie/internal/gc/reflect.go)。
type bmap struct {
topbits [8]uint8 // Key的哈希值数组,这里保存的是哈希值的高8位
keys [8]keytype
elems [8]elemtype
overflow uintptr
}
当Map的Key和Value都不包含指针时,会把Map标记为不包含指针,避免了GC扫描整个Map。但bmap.overflow字段是个指针类型,破坏了bmap不包含指针的本意,于是将buckets的所有overflow都迁移到mapextra字段来。
type mapextra struct {
// 只有在Map的Key和Value都不包含指针的情况下,才会使用overflow和oldoverflow;
// 如果Map的Key或Value包含了指针,直接使用bmap的overflow字段即可,不必多此一举。
overflow *[]*bmap // hmap.buckets的所有overflow bucket
oldoverflow *[]*bmap // hamp.oldbuckets的所有overflow bucket
nextOverflow *bmap // 空闲的overflow bucket
}
Map的内存结构如下图所示,bmap就是常说的桶,桶里面最多只能放置8对Key-Value,如果有第9对Key-Value进来,那就需要再构建一个bmap,通过overflow指针连接起来。
这里的Key和Value是各自放在一起的,这样的好处在于可以节省内存空间。
1.3 创建Map
从Golang语言使用层面来说,创建map很简单。
valToIndex := make(map[int32]int)
valToIndex := make(map[int32]int,10) // 指定map的大小
var valToIndex map[int32]int // valToIndex是nil, 直接添加Key-Value会panci
Map创建底层调用的是makemap函数,具体分析一下创建初始化hamp结构到底做了哪些事情。makemap函数返回的是一个指针,所以在函数内部对map的操作会影响实参。
// hint参数,就是调用make函数创建map时指定的map大小
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()
// 找到合适的B,足以容纳hint对key-value,
// 不会按照单个bucket容纳8对key-val来确定bucket数量,而是按合理的负载因子来计算bucket数量
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
// 初始化buckets和overflow
if h.B != 0 {
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil {
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
func overLoadFactor(count int, B uint8) bool {
// bucketCnt为8,单个bucket的容量大小
// loadFactorNum/loadFactorDen = 6.5是bucket的负载因子,这也说明bucket不应该放入过多key-val对,负载过高,需要扩容
return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
base := bucketShift(b)
nbuckets := base
// For small b, overflow buckets are unlikely. 如果buckets数量越大,那么需要扩容的可能性也更大,所以提前分配更多的bucket
// Avoid the overhead of the calculation.
if b >= 4 {
nbuckets += bucketShift(b - 4)
sz := t.bucket.size * nbuckets
up := roundupsize(sz)
if up != sz {
nbuckets = up / t.bucket.size
}
}
...
if base != nbuckets {
// [base, nbuckets - 1] 这段连续的空间作为nextoverflow的buckets
nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
last.setoverflow(t, (*bmap)(buckets))
}
return buckets, nextOverflow
}
2. 查找Key
Golang查找map中的key有两种方式:
// 如果key不存在,返回val对应类型的零值
val := valToIndex[100]
// comma ok语法,可以判断key是否存在
val, ok := valToIndex[100]
底层也分别对应了mapaccess1和mapacess2函数,没有什么太大的差别,只是mapaccess2函数多了一个返回参数罢了。
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.hasher(key, 0) // see issue 23734
}
return unsafe.Pointer(&zeroVal[0])
}
// 读写冲突检查,可见map在并发下不安全
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}
// 计算key的哈希值
hash := t.hasher(key, uintptr(h.hash0))
// m等于buckets的数量-1。m的二进制表示,就是低B位全是1.
m := bucketMask(h.B)
// hash&m就是对m取余,但位操作更高效。这里的b就是当前key对应的bucket位置
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
// 如果oldbuckets不为空,说明发生了扩容,key所在的bucket可能还没有发生迁移,所以还要从oldbuckets查找key
if c := h.oldbuckets; c != nil {
// 不是等量扩容,那说明当前buckets数量是oldbuckets数量的2倍
if !h.sameSizeGrow() {
// There used to be half as many buckets; mask down one more power of two.
m >>= 1
}
// 计算key在oldbuckets的地址
oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
if !evacuated(oldb) {
// key所属的bucket在oldbuckets还没有迁移,必须要从oldbuckets查找
b = oldb
}
}
// 取hash值的高8位,左移56位。
top := tophash(hash)
// 进入bucket的二层循环找到对应的key-val(第一层是bucket及overflow,第二层是bucket内部的8个cell)
bucketloop:
for ; b != nil; b = b.overflow(t) {
// 遍历bucket的8个cell
for i := uintptr(0); i < bucketCnt; i++ {
// 先通过tophash对比,如果不相等,key肯定也不相等。
if b.tophash[i] != top {
// emptyRest表示,bucket当前cell之后的cell都为空,如果有overflow,那overslow的cell也全为空。
// 这样肯定找不到对应的key了
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
// key位置定位
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
// 判断key是否想等
if t.key.equal(key, k) {
// val位置定位
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
if t.indirectelem() {
e = *((*unsafe.Pointer)(e))
}
// 返回对应的val
return e
}
}
}
// key不存在,返回val类型的零值
return unsafe.Pointer(&zeroVal[0])
key和value的定位方式
bucket 里 key 的起始地址就是 unsafe.Pointer(b)+dataOffset。第 i 个 key 的地址就要在此基础上跨过 i 个 key 的大小。value 的地址是在所有 key 之后,因此第 i 个 value 的地址还需要加上所有 key 的偏移。
// key的地址计算
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
// val的地址计算
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
// dataoffset的定义,因为cell的key前面是[8]uint8,所以这里加上了8个byte
dataOffset = unsafe.Offsetof(struct {
b bmap
v int64
}{}.v)
key所属的bucket和cell确定
取哈希值的低B位决定Key在哪个bucket。再用哈希值的高8位优先确定Key在bucket的那个cell,但高8位可能冲突,所以还是需要判断key是否想等,但高8位不等,可以判断该key就不在这个cell。
key的遍历
总的来说,无非就是两层遍历,目的就是遍历所有的cell,直到找到key。
关于哈希值的高8位,这里有几个特殊的含义。如果tophash介于[2,4]之间,说明该cell已经迁移完了。正常key计算出来的tophash如果小于mintopsh,会加上mintophash避免歧义。
emptyRest = 0 // 当前cell是空的,之后bucket和overflow的cell都是空的
emptyOne = 1 // this cell is empty
evacuatedX = 2 // key/elem is valid. Entry has been evacuated to first half of larger table.
evacuatedY = 3 // same as above, but evacuated to second half of larger table.
evacuatedEmpty = 4 // cell is empty, bucket is evacuated.
minTopHash = 5 // minimum tophash for a normal filled cell.
3. 新增或修改Key
Map修改key-val对是很常见的操作,有可能是新增一对key-val,但也可能是修改key对应的val。底层使用的是mapassign方法。改函数并没有传入val,但返回的就是key对应val的位置,所以取地址修改值即可。
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
// map已经处于写状态,并发不安全
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
hash := t.hasher(key, uintptr(h.hash0))
// 设置写标记
h.flags ^= hashWriting
// 初始化,需要分配新的buckets
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
bucket := hash & bucketMask(h.B)
// 如果map在扩容,这次修改,至多协助迁移2个bucket,渐进式扩容。
if h.growing() {
growWork(t, h, bucket)
}
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
top := tophash(hash)
// key的tophash数组位置
var inserti *uint8
// key在cell中的位置
var insertk unsafe.Pointer
// val的位置
var elem unsafe.Pointer
bucketloop:
for {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
// 这是个空cell,并且还没有确定新增key的位置,那这个cell可能就是key插入的位置
if isEmpty(b.tophash[i]) && inserti == nil {
inserti = &b.tophash[i]
insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
}
// 如果这个cell后面都是空cell,那key-val肯定是插入,不需要继续遍历寻找cell了
if b.tophash[i] == emptyRest {
break bucketloop
}
// 这里不返回而是继续遍历,有可能key已经存在,所以并不能简单的判断为插入,也有可能是更新
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
if !t.key.equal(key, k) {
continue
}
if t.needkeyupdate() {
typedmemmove(t.key, k, key)
}
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
goto done // 找到了key,返回val位置,那赋值就很好办了
}
// 继续遍历,寻找可能的更新或插入位置
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
// 如果map没有扩容,并且满足扩容条件,执行扩容,这里扩容,只是将buckets转移到oldbuckets,分配新的buckets空间,并没有将bucket的key-val进行迁移
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again // Growing the table invalidates everything, so try again
}
// 走到这里肯定是新增key-val场景了
// 所有的buckets都满了,新增一个overflow的bucket
if inserti == nil {
// all current buckets are full, allocate a new one.
newb := h.newoverflow(t, b)
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
elem = add(insertk, bucketCnt*uintptr(t.keysize))
}
if t.indirectkey() {
kmem := newobject(t.key)
*(*unsafe.Pointer)(insertk) = kmem
insertk = kmem
}
if t.indirectelem() {
vmem := newobject(t.elem)
*(*unsafe.Pointer)(elem) = vmem
}
// key可以放入新的cell
typedmemmove(t.key, insertk, key)
// 对应的tophash位置放入key的tophash数组
*inserti = top
// map中key-val数量增加1
h.count++
done:
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
// 清除写标记
h.flags &^= hashWriting
if t.indirectelem() {
elem = *((*unsafe.Pointer)(elem))
}
return elem
}
map新增key-val时,会判断是否达到扩容条件,如果满足扩容条件,则协助扩容,然后重新计算hash,分配cell,整体流程如下图所示:
4. 删除Key
删除map的key-val,底层使用的是mapdelete方法。通俗来说,就是找到对应的key,然后将key、val指针置为nil,待gc回收,将tophash标记为emptyone,如果发现key所在的cell后续的cell都为emptyrest,那么当前cell也标记为emptyrest,并且逆向遍历,将之前emptyone的cell都改成emptyrest状态,对后续cell能否分配key起到了帮助作用。
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
// 空map
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.hasher(key, 0) // see issue 23734
}
return
}
// 检查写标记
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
hash := t.hasher(key, uintptr(h.hash0))
// Set hashWriting after calling t.hasher, since t.hasher may panic,
// in which case we have not actually done a write (delete).
h.flags ^= hashWriting
bucket := hash & bucketMask(h.B)
// 如果在扩容,所有的修改操作,都会协助bucket迁移
if h.growing() {
growWork(t, h, bucket)
}
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
bOrig := b
top := tophash(hash)
search:
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
// 都是空cell,不存在key,跳出所有循环
if b.tophash[i] == emptyRest {
break search
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
k2 := k
if t.indirectkey() {
k2 = *((*unsafe.Pointer)(k2))
}
if !t.key.equal(key, k2) {
continue
}
// key指针变成nil,gc回收
if t.indirectkey() {
*(*unsafe.Pointer)(k) = nil
} else if t.key.ptrdata != 0 {
memclrHasPointers(k, t.key.size)
}
// val删除,gc回收
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
if t.indirectelem() {
*(*unsafe.Pointer)(e) = nil
} else if t.elem.ptrdata != 0 {
memclrHasPointers(e, t.elem.size)
} else {
memclrNoHeapPointers(e, t.elem.size)
}
// 找到了key所在的cell,将tophash标记为空,后面可以放入新的key
b.tophash[i] = emptyOne
// 清空当前的cell,如果后面的cell不是都为空,跳转到notlast
if i == bucketCnt-1 {
if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
goto notLast
}
} else {
if b.tophash[i+1] != emptyRest {
goto notLast
}
}
// 当前cell标记为emptyRest,并且逆向遍历cell,将之前的空cell标记为emptyRest,有助于cell的后续分配
// 这里需要结合新增或修改key-val来看,
for {
b.tophash[i] = emptyRest
if i == 0 {
if b == bOrig {
break // beginning of initial bucket, we're done.
}
// Find previous bucket, continue at its last entry.
c := b
for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
}
i = bucketCnt - 1
} else {
i--
}
// 不是空cell,跳出
if b.tophash[i] != emptyOne {
break
}
}
notLast:
// map的key-val数量减1
h.count--
break search
}
}
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
// 清除写标记
h.flags &^= hashWriting
}
5. Map扩容
随着Key-Val的增加,Key发生碰撞的概率也越来越大,bucket内的8个cell会被依次填满,甚至一个bucket后面还有好多个overflow,这样就退化成链表了,查询的时间复杂度变成了。那理想情况下是不是一个bucket只放一对key-val呢?这就有点浪费空间了。因此,需要有个指标来衡量一个bucket的负载情况,这个指标称为负载因子。在创建Map时提到过,负载因子的值是6.5,Map初始化的时候,创建的bucket数量就是根据负载因子和count计算出来的。
5.1 Map的扩容机制
向Map新增key-val时会检查是否达到扩容条件,分为两类
1. 负载因子超过了6.5。
这种情况很好理解,每个bucket有8个Cell,如果每个bucket都填充满,那负载因子就是8。因此当负载因子超过6.5了,说明很多bucket马上就填满或溢出了,查询和插入效率肯定会下降,这个时候选择扩容,是很有必要的,新的buckets数量是原来的2倍,属于翻倍扩容。
// 负载因子超过6.5
func overLoadFactor(count int, B uint8) bool {
return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}
2. overflow的bucket数量过多
如果buckcets的数量超过2^16,overflow的bucket数量超过2^15,那么触发扩容;如果buckets的数量小于2^16,且overflow的数量超过了buckets数量,也符合扩容的条件。
条件2可以看成是对条件1的补充,也就是说,在负载因子很小的时候,查询和插入的效率也不高。结合bucket的结构,不难想到,如果一个bucket总的key-val数量并不是很多,但分布在很多个bucket(overflow后面挂着多个bucket),那每次查询或插入,要遍历的bucket数量也会特别多,性能肯定也会下降。
什么情况回导致map出现这种情况呢?不停地插入、删除元素。先插入很多元素,导致创建了很多 bucket,但是负载因子达不到条件1的临界值,未触发扩容来缓解这种情况。之后,删除元素降低元素总数量(删除没有缩容操作),再插入很多元素,导致创建很多的 overflow bucket。
如果是条件2触发的扩容,buckets数量翻倍,解决不了问题。这个时候,会开辟一个新的bucket,bucket数量不变,将overflow的key-val迁移到新的bucket上,使得key-val排列的更加密集,集中在少数的几个bucket内。
// olverflow的bucket数量太多
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
// "too many" means (approximately) as many overflow buckets as regular buckets.
// See incrnoverflow for more details.
if B > 15 {
B = 15
}
// The compiler doesn't see here that B < 16; mask B to generate shorter shift code.
return noverflow >= uint16(1)<<(B&15)
}
5.2 扩容
满足扩容机制后,为新的bucket分配空间,同时将原来的bucket赋值给map的oldbuckets。这里并没有执行实际的扩容操作,将oldbucket的key-val放到新bucket内。
func hashGrow(t *maptype, h *hmap) {
// 判断是否为等量扩容,B+1表示新buckets数量是原来的2倍
bigger := uint8(1)
if !overLoadFactor(h.count+1, h.B) {
bigger = 0
h.flags |= sameSizeGrow
}
oldbuckets := h.buckets
// 新的buckets空间分配
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)
flags := h.flags &^ (iterator | oldIterator)
if h.flags&iterator != 0 {
flags |= oldIterator
}
// commit the grow (atomic wrt gc)
h.B += bigger
h.flags = flags
h.oldbuckets = oldbuckets
h.buckets = newbuckets
// 设置bucket的迁移进度
h.nevacuate = 0
// overflow的数量变成0
h.noverflow = 0
if h.extra != nil && h.extra.overflow != nil {
// Promote current overflow buckets to the old generation.
if h.extra.oldoverflow != nil {
throw("oldoverflow is not nil")
}
h.extra.oldoverflow = h.extra.overflow
h.extra.overflow = nil
}
if nextOverflow != nil {
if h.extra == nil {
h.extra = new(mapextra)
}
h.extra.nextOverflow = nextOverflow
}
}
5.3 bucket的迁移
一个Map可能存在大量的key-val,如果一次性将所有的key-val迁移到新的bucket内,耗时久,影响了map的其他操作,因此采用了渐进性迁移的方式,在单次写操作中,最多迁移2个bucket。
// oldbucket表示本次要迁移的oldbucket序号。
// 初始,根据某个key的hash值计算,后面可以根据nevacuate来决定搬迁哪个bucket(默认从0开始)
func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
// b表示的就是oldbuckets数组中本次迁移的bucket
b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
// newbit表示新创建buckets的hashmask
newbit := h.noldbuckets()
// 判断b是否已经迁移完了
if !evacuated(b) {
// xy表示b迁移的目标位置
var xy [2]evacDst
x := &xy[0]
// x表示迁移到新buckets的前部分
x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))
x.k = add(unsafe.Pointer(x.b), dataOffset)
x.e = add(x.k, bucketCnt*uintptr(t.keysize))
// 如果翻倍扩容,b将拆分成两个bucket,所以部分key-val迁移到buckets的后部分
if !h.sameSizeGrow() {
// Only calculate y pointers if we're growing bigger.
// Otherwise GC can see bad pointers.
y := &xy[1]
y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))
y.k = add(unsafe.Pointer(y.b), dataOffset)
y.e = add(y.k, bucketCnt*uintptr(t.keysize))
}
// 开始遍历b
for ; b != nil; b = b.overflow(t) {
k := add(unsafe.Pointer(b), dataOffset)
e := add(k, bucketCnt*uintptr(t.keysize))
for i := 0; i < bucketCnt; i, k, e = i+1, add(k, uintptr(t.keysize)), add(e, uintptr(t.elemsize)) {
top := b.tophash[i]
// 空cell,不用迁移
if isEmpty(top) {
b.tophash[i] = evacuatedEmpty
continue
}
if top < minTopHash {
throw("bad map state")
}
k2 := k
if t.indirectkey() {
k2 = *((*unsafe.Pointer)(k2))
}
var useY uint8
if !h.sameSizeGrow() {
// Compute hash to make our evacuation decision (whether we need
// to send this key/elem to bucket x or bucket y).
hash := t.hasher(k2, uintptr(h.hash0))
// 如果有其他goroutine正在遍历,并且key计算出来的hash值不等
if h.flags&iterator != 0 && !t.reflexivekey() && !t.key.equal(k2, k2) {
// 如果top的最低位是1,放到y,也就是buckets的后部分
useY = top & 1
top = tophash(hash)
} else {
// 如果hash值的第B位是1,也放到buckets的后部分
if hash&newbit != 0 {
useY = 1
}
}
}
if evacuatedX+1 != evacuatedY || evacuatedX^1 != evacuatedY {
throw("bad evacuatedN")
}
// tophash值表示该cell迁移到新bucket的前还是后部分了,不妨会看一下tophash小于mintophash的含义
b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY
dst := &xy[useY] // evacuation destination
// 因为新的bucket创建时没有overflow,这里需要新创建一个overflow的bucket
if dst.i == bucketCnt {
dst.b = h.newoverflow(t, dst.b)
dst.i = 0
dst.k = add(unsafe.Pointer(dst.b), dataOffset)
dst.e = add(dst.k, bucketCnt*uintptr(t.keysize))
}
// 新bucket的cell对应tophsh设定
dst.b.tophash[dst.i&(bucketCnt-1)] = top // mask dst.i as an optimization, to avoid a bounds check
// key,val的肤质
if t.indirectkey() {
*(*unsafe.Pointer)(dst.k) = k2 // copy pointer
} else {
typedmemmove(t.key, dst.k, k) // copy elem
}
if t.indirectelem() {
*(*unsafe.Pointer)(dst.e) = *(*unsafe.Pointer)(e)
} else {
typedmemmove(t.elem, dst.e, e)
}
dst.i++
// 目标bucket的cell也要前进
dst.k = add(dst.k, uintptr(t.keysize))
dst.e = add(dst.e, uintptr(t.elemsize))
}
}
// Unlink the overflow buckets & clear key/elem to help GC.
if h.flags&oldIterator == 0 && t.bucket.ptrdata != 0 {
b := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))
// Preserve b.tophash because the evacuation
// state is maintained there.
ptr := add(b, dataOffset)
n := uintptr(t.bucketsize) - dataOffset
memclrHasPointers(ptr, n)
}
}
// 在已知迁移进度的情况下
// 更新bucket的迁移进度
if oldbucket == h.nevacuate {
advanceEvacuationMark(h, t, newbit)
}
}
func advanceEvacuationMark(h *hmap, t *maptype, newbit uintptr) {
// 迁移进度 +1
h.nevacuate++
// Experiments suggest that 1024 is overkill by at least an order of magnitude.
// Put it in there as a safeguard anyway, to ensure O(1) behavior.
stop := h.nevacuate + 1024
if stop > newbit {
stop = newbit
}
// 尝试看看后面还有没有bucket已经迁移完成了。
// 因为迁移不是从0号bucket开始的,先随机从一个bucket开始
// 第二次迁移从nevacuate(默认是0)开始,然后nevacuate累加,那nevacuate后面的一些bucket可能已经迁移过了,所以这里多了这么一个判断
for h.nevacuate != stop && bucketEvacuated(t, h, h.nevacuate) {
h.nevacuate++
}
// 所有的oldbuckets都迁移完成了
if h.nevacuate == newbit { // newbit == # of oldbuckets
// Growing is all done. Free old main bucket array.
h.oldbuckets = nil
// Can discard old overflow buckets as well.
// If they are still referenced by an iterator,
// then the iterator holds a pointers to the slice.
if h.extra != nil {
h.extra.oldoverflow = nil
}
h.flags &^= sameSizeGrow
}
}
条件1触发的迁移,新的 buckets 数量是之前的2倍,要重新计算 key 的哈希,才能决定它到底落在哪个 bucket。如下图所示,B从5变成了6,所以如果hash值第6位是0,那么依旧放到原来序号的bucket,如果是1,那么新的bucket序号就会在原来的基础上添加2^(B-1).
条件2触发的迁移,新旧bukets数量没有变化,迁移到原来序号的bucket即可。
有一个特殊情况,就是一个Key每次计算的hash值都不一样,这个key就是math.NaN(),所以这个key可以在map中存在多个,当然也查询不出来,只能在遍历的时候才能看到。对于这个key的迁移,其实就看top hash的最低位了,上面代码也能的很明显。
6. Map遍历
首先,需要明确一下,Map的遍历是随机的,其次,在渐进式迁移过程中,map还是处于一个中间状态,也伴随着遍历操作。
map遍历调用的底层函数,可以通过go tool compile -s main.go查看汇编后端的函数调用,会涉及到map迭代器的初始化和next循环调用。
先看一下map迭代器的结构
type hiter struct {
key unsafe.Pointer
elem unsafe.Pointer
t *maptype
h *hmap
// 初始化指向的bucket
buckets unsafe.Pointer
bptr *bmap
overflow *[]*bmap
oldoverflow *[]*bmap
// 起始遍历的bucket序号
startBucket uintptr
// 起始遍历的cell序号
offset uint8
// 是否从头遍历了
wrapped bool
B uint8
i uint8
bucket uintptr
checkBucket uintptr
}
// 初始化map迭代器
func mapiterinit(t *maptype, h *hmap, it *hiter) {
......
// 通过一个随机数,确定遍历的起始bucket
// decide where to start
r := uintptr(fastrand())
if h.B > 31-bucketCntBits {
r += uintptr(fastrand()) << 31
}
it.startBucket = r & bucketMask(h.B)
// 开始遍历的cell也是随机的
it.offset = uint8(r >> h.B & (bucketCnt - 1))
// iterator state
it.bucket = it.startBucket
mapiternext(it)
}
// 真正的迭代过程
func mapiternext(it *hiter) {
h := it.h
t := it.t
bucket := it.bucket
b := it.bptr
i := it.i
checkBucket := it.checkBucket
next:
if b == nil {
// 当前遍历的bucket为空,并且回到起始遍历的bucket了,
if bucket == it.startBucket && it.wrapped {
// end of iteration
it.key = nil
it.elem = nil
return
}
if h.growing() && it.B == h.B {
// 如果处于扩容过程,需要判断oldbucket是否已经迁移完成
oldbucket := bucket & it.h.oldbucketmask()
b = (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
if !evacuated(b) {
checkBucket = bucket
} else {
b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
checkBucket = noCheck
}
} else {
b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
checkBucket = noCheck
}
bucket++
// 到了最后一个bucket,下次从0号bucket开始遍历
if bucket == bucketShift(it.B) {
bucket = 0
it.wrapped = true
}
i = 0
}
for ; i < bucketCnt; i++ {
offi := (i + it.offset) & (bucketCnt - 1)
if isEmpty(b.tophash[offi]) || b.tophash[offi] == evacuatedEmpty {
continue
}
k := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(offi)*uintptr(t.elemsize))
// 说明要遍历oldbucket,并且是翻倍扩容
if checkBucket != noCheck && !h.sameSizeGrow() {
if t.reflexivekey() || t.key.equal(k, k) {
// oldbucket一分为二,如果oldbucket迁移的key-val不会落到当前bucket,跳过
hash := t.hasher(k, uintptr(h.hash0))
if hash&bucketMask(it.B) != checkBucket {
continue
}
} else {
// math.NaN()这个特殊情况,还是取决于hash值的最低位。
if checkBucket>>(it.B-1) != uintptr(b.tophash[offi]&1) {
continue
}
}
}
if (b.tophash[offi] != evacuatedX && b.tophash[offi] != evacuatedY) ||
!(t.reflexivekey() || t.key.equal(k, k)) {
it.key = k
if t.indirectelem() {
e = *((*unsafe.Pointer)(e))
}
it.elem = e
} else {
rk, re := mapaccessK(t, h, k)
if rk == nil {
continue // key has been deleted
}
it.key = rk
it.elem = re
}
it.bucket = bucket
if it.bptr != b { // avoid unnecessary write barrier; see issue 14921
it.bptr = b
}
it.i = i + 1
it.checkBucket = checkBucket
return
}
b = b.overflow(t)
i = 0
goto next
}
假设当前map如下:
起始bucket和offset分别是3和2。遍历的bucket顺序就是:3 -> 0 -> 1 -> 2
- 遍历3号bucket时,先检查老1号bucket是否迁移完成,发现迁移完成,直接遍历3号bucket即可。从3号bucket的2号cell开始遍历。
- 接着继续0号bucket。发现老0号bucket没有迁移完成,于是当前遍历的bucket改成了老0号bucket,那是不是老0号的key-val都要遍历呢?不是这样的,这里重新计算老0号bucket内key的hash值,如果第B位是0,那么执行遍历,否则跳过。
- 然后到了1号bucet,发现老1号bucket迁移完成,所以继续遍历新1号bucket即可。
- 到了2号bucket,因为2号bucket可能来源是老0号bucket,所以这里再次回到老0号,遍历key的hash第B位是1的key,遍历完老0号bucket。
- 又回到了3号bucket,发现3号bucket遍历完成。于是整个遍历结束了。
7. 问题与思考
- 能不能对key或val取地址? 不能,因为在扩容过程中,key或val的地址可能发生变化。 就算是通过unsafe.Point()方法获取到地址,也不能长期持有。
- map是安全的吗? 不是,读和写操作分别设置读写标记,如果发生读、写于写、写冲突,会Panic。
- 可以同时对map进行遍历和删除操作吗? 如果是多个goroutine同时操作,肯定不行,参考第2条。 但如果是同一个goroutine操作,理论上是可以的。但遍历后的结果就是不确定的了,删除的Key可能再次出现在遍历结果中。这取决于遍历的时机,如果删除的key之前已经遍历过了,就会出现在结果集中。
func Test2(t *testing.T) {
vals := make(map[int]int, 10)
for i := 0; i < 20; i++ {
vals[i] = i
}
rand.Seed(time.Now().Unix())
for key, val := range vals {
t.Logf("travel key: %d, val: %d", key, val)
delete(vals, key+1)
}
}
// output:
travel key: 15, val: 15
travel key: 18, val: 18
travel key: 9, val: 9
travel key: 0, val: 0
travel key: 17, val: 17
travel key: 3, val: 3
travel key: 7, val: 7
travel key: 12, val: 12
travel key: 2, val: 2
travel key: 11, val: 11
travel key: 14, val: 14 // 删除{15,15},但{15,15}已经遍历完,出现了结果集中了
travel key: 5, val: 5