Golang之Map实现

615 阅读12分钟

1. Map的底层结构

1.1 什么是Map

Map是由Key-Value构成的,并且同一个Key只能出现一次,这就要求Key可以进行相等比较。它的任务是设计一种数据结构用来维护一个集合的数据,并且可以同时对集合进行增删查改的操作。最主要的数据结构有两种:哈希表(hash table)和搜索树(search tree)。

哈希表实现用一个哈希函数将Key分配到不同桶(Bucket,也就是一个数组的某个位置),这样,查询的性能开销主要在哈希函数计算的计算和数组的访问。在大部分场景下,哈希表的查询性能很高。

但哈希表一般会存在哈希冲突的问题,就是不同的Key计算出了相同的哈希值,分配到同一个Bucket上。常见的解决方案有两种:拉链法和地址开放法。拉链法将一个Bucket实现成一个链表,落在一个Bucket的Key都会插入这个链表。地址开放法则是在发生哈希碰撞后,在数据的后面挑选空位,用来放置新的Key。哈希表的平均查询时间复杂度是O(1)O(1),最差时间复杂度是O(N)O(N)

搜索树一般都是采用自平衡搜索树:红黑树、AVL树。自平衡搜索树的最差查询时间复杂度是O(logN)O(logN)

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指针连接起来。

map结构drawio.png 这里的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。 image.png key的遍历

总的来说,无非就是两层遍历,目的就是遍历所有的cell,直到找到key。 image.png 关于哈希值的高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,整体流程如下图所示: map写key.png

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,这样就退化成链表了,查询的时间复杂度变成了O(n)O(n)。那理想情况下是不是一个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). image.png 条件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如下:

image.png 起始bucket和offset分别是3和2。遍历的bucket顺序就是:3 -> 0 -> 1 -> 2 image.png

  1. 遍历3号bucket时,先检查老1号bucket是否迁移完成,发现迁移完成,直接遍历3号bucket即可。从3号bucket的2号cell开始遍历。
  2. 接着继续0号bucket。发现老0号bucket没有迁移完成,于是当前遍历的bucket改成了老0号bucket,那是不是老0号的key-val都要遍历呢?不是这样的,这里重新计算老0号bucket内key的hash值,如果第B位是0,那么执行遍历,否则跳过。
  3. 然后到了1号bucet,发现老1号bucket迁移完成,所以继续遍历新1号bucket即可。
  4. 到了2号bucket,因为2号bucket可能来源是老0号bucket,所以这里再次回到老0号,遍历key的hash第B位是1的key,遍历完老0号bucket。
  5. 又回到了3号bucket,发现3号bucket遍历完成。于是整个遍历结束了。

7. 问题与思考

  1. 能不能对key或val取地址? 不能,因为在扩容过程中,key或val的地址可能发生变化。 就算是通过unsafe.Point()方法获取到地址,也不能长期持有。
  2. map是安全的吗? 不是,读和写操作分别设置读写标记,如果发生读、写于写、写冲突,会Panic。
  3. 可以同时对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