Go map 源码详解【2】—— map 插入

本文将深入探索 map 插入元素的逻辑,将会涉及到map 的扩容,以及overflow bucket 的应用等。

写入 key-val

向 map 中写入 key-val 的步骤如下所示,

  1. 根据 key 计算出 hash 值,找到对应的 bucket
  2. 再根据 hash 值计算出 top
  3. 遍历 bucket 中的 tophash,分为以下三种情况
  • tophash 中的值与top 相同,进行更新操作
  • tophash 中还有未使用的 slot,直接写入这个 slot
  • tophash 中没有可用的 slot,
  • -> 根据当前负载因子以及 overflow bucket 数量决定是否扩容。扩容完成后重新插入
  • -> 获取 overflowbucket(ovbucket),如果没有可用 overflowbucket 则创建新的。写入到ovbucket中。

源码

主流程函数

mapassign_faststr

这个函数的主要作用是向 map 中写入 key,并且返回 val 对应的内存地址。

go 复制代码
func mapassign_faststr(t *maptype, h *hmap, s string) unsafe.Pointer {
    if h == nil {
       panic(plainError("assignment to entry in nil map"))
    }
    if raceenabled { //竞态检测器,运行时用于检测数据的竞太竞争
       callerpc := getcallerpc()
       racewritepc(unsafe.Pointer(h), callerpc, abi.FuncPCABIInternal(mapassign_faststr))
    }
    if h.flags&hashWriting != 0 { 
       fatal("concurrent map writes")
    }
    key := stringStructOf(&s)
    hash := t.Hasher(noescape(unsafe.Pointer(&s)), uintptr(h.hash0)) //计算 key 的 hash 值

    // Set hashWriting after calling t.hasher for consistency with mapassign.
    h.flags ^= hashWriting //这里是存在竞态条件,读与写经典情况

    if h.buckets == nil {
       h.buckets = newobject(t.Bucket) // newarray(t.bucket, 1)
    }

again:
    bucket := hash & bucketMask(h.B)
    if h.growing() {
       growWork_faststr(t, h, bucket)
    }
    b := (*bmap)(add(h.buckets, bucket*uintptr(t.BucketSize)))
    top := tophash(hash) //取哈希值的高 8 位,如果小于 minTopHash 则加上 minTopHash以避免与规定的状态值冲突。

    var insertb *bmap
    var inserti uintptr
    var insertk unsafe.Pointer

bucketloop:
    for {
       for i := uintptr(0); i < abi.MapBucketCount; i++ {
          //没有 hash 冲突,判断是否为空
          if b.tophash[i] != top {
             if isEmpty(b.tophash[i]) && insertb == nil {
                insertb = b
                inserti = i
             }
             if b.tophash[i] == emptyRest {
                break bucketloop
             }
             continue
          }
          k := (*stringStruct)(add(unsafe.Pointer(b), dataOffset+i*2*goarch.PtrSize))
          if k.len != key.len {
             continue
          }
          if k.str != key.str && !memequal(k.str, key.str, uintptr(key.len)) {
             continue
          }
          // already have a mapping for key. Update it.
          inserti = i
          insertb = b
          // Overwrite existing key, so it can be garbage collected.
          // The size is already guaranteed to be set correctly.
          k.str = key.str
          goto done
       }
       //获取当前 bucket 中的没有使用的overflow bucket,如果没有空闲 overflow bucket 则新创建。
       ovf := b.overflow(t)
       if ovf == nil {
          break
       }
       b = ovf
    }

    // Did not find mapping for key. Allocate new cell & add entry.

    // If we hit the max load factor or we have too many overflow buckets,
    // and we're not already in the middle of growing, start growing.
    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
    }

    if insertb == nil {
       // The current bucket and all the overflow buckets connected to it are full, allocate a new one.
       //获取 overflow,如果当前所有的 overflow buckets 已经使用,则创建新的。
       insertb = h.newoverflow(t, b)
       inserti = 0 // not necessary, but avoids needlessly spilling inserti
    }
    //向 tophash 中写入 top 值
    insertb.tophash[inserti&(abi.MapBucketCount-1)] = top // mask inserti to avoid bounds checks

    insertk = add(unsafe.Pointer(insertb), dataOffset+inserti*2*goarch.PtrSize)
    // store new key at insert position
    *((*stringStruct)(insertk)) = *key
    h.count++

done:
    //根据插入的 bucket(insertb) 以及 tophash 中的 i(inserti) 计算出对应的 val 所在的内存地址
    elem := add(unsafe.Pointer(insertb), dataOffset+abi.MapBucketCount*2*goarch.PtrSize+inserti*uintptr(t.ValueSize))
    if h.flags&hashWriting == 0 {
       fatal("concurrent map writes")
    }
    h.flags &^= hashWriting
    return elem
}

map 扩容

hashGrow

go 复制代码
func hashGrow(t *maptype, h *hmap) {
  // If we've hit the load factor, get bigger.
  // Otherwise, there are too many overflow buckets,
  // so keep the same number of buckets and "grow" laterally.
  bigger := uint8(1)
  //通过当前容量+1 与现有的 bucket 数量计算负载因子
  if !overLoadFactor(h.count+1, h.B) {
   bigger = 0
   h.flags |= sameSizeGrow
  }
  oldbuckets := h.buckets
  //创建新的bucket 列表
  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
  h.nevacuate = 0
  h.noverflow = 0
  
  //overflow 的创建
  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
  }

  // the actual copying of the hash table data is done incrementally
  // by growWork() and evacuate().
}

扩容后的重新插入与数据迁移

growWork

扩容后再次插入 key

go 复制代码
func growWork(t *maptype, h *hmap, bucket uintptr) {
    // make sure we evacuate the oldbucket corresponding
    // to the bucket we're about to use
    evacuate(t, h, bucket&h.oldbucketmask())

    // evacuate one more oldbucket to make progress on growing
    if h.growing() {
       evacuate(t, h, h.nevacuate)
    }
}

evacuate

迁移数据,当 key 还没有迁移时,会将 key 所在的 bucket 以及其 overflow bucket 都进行迁移。

扩容的情况

扩容后,key 的哈希值本身不会改变,但由于新的 mask 多了一位(即位数增加),导致哈希值与新 mask 计算结果发生变化。原 bucket 中的所有 key 通过 mask 计算后,除了新增的高位之外,其余位与原来相同 。也就是说,区别仅在新增的那一位是 0 还是 1。因此,每个 key 在迁移时,只可能落在两个目标 bucket 之一。在代码中,这体现为预先初始化的两个桶:xy

sameSizeGrow

同等数量的 bucket 迁移,迁移前后所在的桶编号没有发生变化。

go 复制代码
func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
    //获取数据所在的旧的 bucket 桶
    b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.BucketSize)))
    //计算 oldbucket 的数量
    newbit := h.noldbuckets()
    //整个 bucket 是否完成迁移
    if !evacuated(b) {
       // TODO: reuse overflow buckets instead of using new ones, if there
       // is no iterator using the old buckets.  (If !oldIterator.)

       // xy contains the x and y (low and high) evacuation destinations.
       //旧桶中的key hash 值是不变的,迁移到新桶中,需要多取一位。这一位0与 1,就决定是到 x桶(高位为 0) 还是 y桶。
       //因为要迁移桶内的所有元素,所以先初始化好两个位置。
       var xy [2]evacDst
       x := &xy[0]
       x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.BucketSize)))
       //第一个 key 存放的地址
       x.k = add(unsafe.Pointer(x.b), dataOffset)
       //第一个 val 存放的地址
       x.e = add(x.k, abi.MapBucketCount*uintptr(t.KeySize))

       if !h.sameSizeGrow() {
          // Only calculate y pointers if we're growing bigger.
          // Otherwise GC can see bad pointers.
          y := &xy[1]
          //这里直接加难以理解,还是得从位运算考虑。其实就是在新桶可能所在的 bucket 位置 (不确定最高位是否为 1)
          //比如旧桶有 4 个,B为 3。mask 为 11
          //新桶有 8 个,B为 4.
          //如果新桶的 hash 计算出来为 6,位表示为 110. 旧桶 mask 计算后为 oldbucket=10
          //oldbucket+newbit=10+100 新桶的位置
          y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.BucketSize)))
          y.k = add(unsafe.Pointer(y.b), dataOffset)
          y.e = add(y.k, abi.MapBucketCount*uintptr(t.KeySize))
       }
       //在旧桶中查询旧的 key,如果没找到还得去 overflow 中找
       for ; b != nil; b = b.overflow(t) {
          k := add(unsafe.Pointer(b), dataOffset)
          e := add(k, abi.MapBucketCount*uintptr(t.KeySize))
          //遍历bucket 中所有的 key 与 val
          for i := 0; i < abi.MapBucketCount; i, k, e = i+1, add(k, uintptr(t.KeySize)), add(e, uintptr(t.ValueSize)) {
             top := b.tophash[i]
             if isEmpty(top) {
                b.tophash[i] = evacuatedEmpty
                continue
             }
             if top < minTopHash {
                throw("bad map state")
             }
             k2 := k
             //间接存储,获取实际的指针。可能 key 太大了,使用的是指针
             if t.IndirectKey() {
                k2 = *((*unsafe.Pointer)(k2))
             }
             var useY uint8
             //可以看到这里直接对 k2 进行的 hash,也就是说整体 bucket的迁移
             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))
                //特殊情况暂且不考虑
                if h.flags&iterator != 0 && !t.ReflexiveKey() && !t.Key.Equal(k2, k2) {
                   // If key != key (NaNs), then the hash could be (and probably
                   // will be) entirely different from the old hash. Moreover,
                   // it isn't reproducible. Reproducibility is required in the
                   // presence of iterators, as our evacuation decision must
                   // match whatever decision the iterator made.
                   // Fortunately, we have the freedom to send these keys either
                   // way. Also, tophash is meaningless for these kinds of keys.
                   // We let the low bit of tophash drive the evacuation decision.
                   // We recompute a new random tophash for the next level so
                   // these keys will get evenly distributed across all buckets
                   // after multiple grows.
                   useY = top & 1
                   top = tophash(hash)
                } else {
                   //判断 hash 新 bucket 数量掩码后的最高位
                   if hash&newbit != 0 {
                      useY = 1
                   }
                }
             }

             if evacuatedX+1 != evacuatedY || evacuatedX^1 != evacuatedY {
                throw("bad evacuatedN")
             }
             //设置状态位
             b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY
             //获取 dst,用 useY即可直接选择。
             dst := &xy[useY] // evacuation destination
             //dst 的 bukcet 已满,使用 overflow bucket
             if dst.i == abi.MapBucketCount {
                dst.b = h.newoverflow(t, dst.b)
                dst.i = 0
                dst.k = add(unsafe.Pointer(dst.b), dataOffset)
                dst.e = add(dst.k, abi.MapBucketCount*uintptr(t.KeySize))
             }
             dst.b.tophash[dst.i&(abi.MapBucketCount-1)] = top // mask dst.i as an optimization, to avoid a bounds check
             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++
             // These updates might push these pointers past the end of the
             // key or elem arrays.  That's ok, as we have the overflow pointer
             // at the end of the bucket to protect against pointing past the
             // end of the bucket.
             //移动 key,val
             dst.k = add(dst.k, uintptr(t.KeySize))
             dst.e = add(dst.e, uintptr(t.ValueSize))
          }
       }
       // Unlink the overflow buckets & clear key/elem to help GC.
       if h.flags&oldIterator == 0 && t.Bucket.Pointers() {
          // 定位到旧桶
          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
          //清理 tophash 之后的内存空间。
          memclrHasPointers(ptr, n)
       }
    }
    //更新迁移状态
    if oldbucket == h.nevacuate {
       advanceEvacuationMark(h, t, newbit)
    }
}

疑问

当overflow bucket 过多的时候,也会创建新的同等大小的bucketArray。这样做为什么能够减少overflow backet 的数量?(以下是 AI 的回答,得结合删除与插入学习 map 的空洞)

  1. 空间利用率提高:删除了空洞,数据更紧凑
  2. 重新分配内存:新的 bucket 数组是干净的,没有历史碎片
  3. 溢出链重新计算:原来可能需要溢出的数据,现在可能不需要了
相关推荐
小王子102411 分钟前
Django+DRF 实战:自定义异常处理流程
后端·django·web开发
考虑考虑15 分钟前
go中的切片
后端·go
天南星1 小时前
java-WebSocket在Java生态中的发展历程
java·后端·websocket
工藤学编程1 小时前
分库分表之实战-sharding-JDBC绑定表配置实战
数据库·分布式·后端·sql·mysql
fmvrj342022 小时前
云原生:数字化转型的核心引擎
后端
码出极致2 小时前
Redisson分布式缓存与数据一致性保障
后端
用户790349033712 小时前
springboot集成redisson实现redis分布式锁
后端
陈随易2 小时前
程序员的新玩具,MoonBit(月兔)编程语言科普
前端·后端·程序员
码出极致2 小时前
Redisson秒杀系统中的分布式锁应用
后端
xiaok2 小时前
@Param注解的作用
java·后端