c++中map插入数据_Go Map源码解读之Map插入数据

点击上方蓝色“后端开发杂谈”关注我们, 专注于后端日常开发技术分享

map 插入

本文主要是针对map插入的源码分析, 可能篇幅有些过长,且全部是代码, 请耐心阅读. 源码位置 src/runtime/map.go

插入操作

  • 插入操作, 实际上就是找到一个写入 value 的内存地址, 后续通过内存地址操作进行赋值.

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    // 如果h是空指针,赋值会引起panic
    // 例如以下语句
    // var m map[string]int
    // m["k"] = 1
    if h == nil {
        panic(plainError("assignment to entry in nil map"))
    }

    // 如果开启了竞态检测 -race
    if raceenabled {
        callerpc := getcallerpc()
        pc := funcPC(mapassign)
        racewritepc(unsafe.Pointer(h), callerpc, pc)
        raceReadObjectPC(t.key, key, callerpc, pc)
    }
    // 如果开启了memory sanitizer -msan
    if msanenabled {
        msanread(key, t.key.size)
    }
    // 有其他goroutine正在往map中写key, 会抛出以下错误
    if h.flags&hashWriting != 0 {
        throw("concurrent map writes")
    }
    // 通过key和哈希种子, 算出对应哈希值
    hash := t.hasher(key, uintptr(h.hash0))

    // 将flags的值与hashWriting做按位 "异或" 运算
    // 因为在当前goroutine可能还未完成key的写入, 再次调用t.hasher会发生panic.
    h.flags ^= hashWriting

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

again:
    // bucketMask返回值是2的B次方减1
    // 因此,通过hash值与bucketMask返回值做按位与操作,返回的在buckets数组中的第几号桶
    bucket := hash & bucketMask(h.B) // 获取bucket的位置
    // 如果map正在搬迁(即h.oldbuckets != nil)中, 则先进行搬迁工作(当前的bucket). 
    if h.growing() {
        growWork(t, h, bucket)
    }

    // 计算出上面求出的第几号bucket的内存位置
    // post = start + bucketNumber * bucketsize
    b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
    top := tophash(hash) // 获取 bucket 内的原始的位置(即hash的高8位)

    var inserti *uint8         // 记录 tophash 的值的指针
    var insertk unsafe.Pointer // 记录 key 的底层内存位置(要剥离指针)
    var elem unsafe.Pointer    // 记录 value 的底层内存位置

bucketloop:
    for {
        // 遍历桶中的8个cell
        for i := uintptr(0); i             // 这里分两种情况:
            // 第一种情况是cell位的tophash值和当前tophash值不相等.
            // 在 b.tophash[i] != top 的情况下, 理论上有可能会是一个空槽位.
            // 一般情况下 map 的槽位分布是这样的, e 表示 empty:
            // [h0][h1][h2][h3][h4][e][e][e]
            // 但在执行过 delete 操作时,可能会变成这样:
            // [h0][h1][e][e][h5][e][e][e]
            // 所以如果再插入的话,会尽量往前面的位置插
            // [h0][h1][e][e][h5][e][e][e]
            //          ^
            //          ^
            //       这个位置
            // 所以在循环的时候还要顺便把前面的空位置先记下来
            // 因为有可能在后面会找到相等的key,也可能找不到相等的key
            if b.tophash[i] != top {
                // 如果cell位为空(b.tophash[i] <= emptyOne), 那么就可以在对应位置进行插入
                if isEmpty(b.tophash[i]) && inserti == nil {
                    inserti = &b.tophash[i]
                    // 这里需要注意实际的 bmap 结构. dataOffset 是前面的8个 tophash 的偏移量
                    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 和 overflow 都是空的. 但是前面已经记录了当前的位置, 无需再次记录
                if b.tophash[i] == emptyRest {
                    break bucketloop // goto done
                }
                continue
            }

            // 第二种情况是cell位的tophash值和当前的tophash值相等
            // indirectkey()  // store ptr to key instead of key itself
            // indirectelem() // store ptr to elem instead of elem itself
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            if t.indirectkey() {
                k = *((*unsafe.Pointer)(k))
            }
            // 注意: 即使当前cell位的tophash值相等,不一定它对应的key也是相等的,所以还要做一个key值判断
            if !t.key.equal(key, k) {
                continue
            }

            // 到这里,说明key是相等的. 如果已经有该key了, 就更新它
            // needkeyupdate() // true if we need to update key on an overwrite
            if t.needkeyupdate() {
                typedmemmove(t.key, k, key)
            }
            // 这里获取到了要插入key对应的value的内存地址
            // pos = start(bucket) + dataOffset + 8*keysize + i*elemsize
            elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
            goto done
        }

        // 如果桶中的8个cell遍历完,还未找到对应的空cell或覆盖cell,那么就进入它的溢出桶中去遍历
        // *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-sys.PtrSize)), 返回 *bmap
        // 说明: t.bucketsize 是 bucket 的大小, 而最后一个指针就是 *bmap
        ovf := b.overflow(t)
        // 如果连溢出桶中都没有找到合适的cell,跳出循环. 
        if ovf == nil {
            break // 终止外层循环
        }
        b = ovf
    }

    // 在已有的桶和溢出桶中都未找到合适的cell供key写入, 那么有可能会触发以下两种情况
    // 情况一:
    // 判断当前map的装载因子是否达到设定的6.5阈值,或者当前map的溢出桶数量是否过多. 如果存在这两种情况之一,则进行扩容操作. 
    // hashGrow()实际并未完成扩容,对哈希表数据的搬迁(复制)操作是通过growWork()来完成的. 
    // 重新跳入again逻辑,在进行完growWork()操作后,再次遍历新的桶. 
    // 分别分析情况1(装载因子) 和 情况2(buckets与overflow buckets)
    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
    }

    // 情况二:
    // 在不满足情况一的条件下,会为当前桶再新建溢出桶,并将tophash,key插入到新建溢出桶的对应内存的0号位置
    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))
    }

    // 在插入位置存入新的key和value
    if t.indirectkey() {
        kmem := newobject(t.key)
        *(*unsafe.Pointer)(insertk) = kmem
        insertk = kmem
    }
    if t.indirectelem() {
        vmem := newobject(t.elem)
        *(*unsafe.Pointer)(elem) = vmem
    }
    typedmemmove(t.key, insertk, key) // 写入 key
    *inserti = top                    // 写入 tophash
    h.count++                         // map中的key数量+1

done:
    // 插入操作
    if h.flags&hashWriting == 0 {
        throw("concurrent map writes")
    }
    h.flags &^= hashWriting
    if t.indirectelem() {
        elem = *((*unsafe.Pointer)(elem))
    }
    return elem // 返回 value 的底层内存位置
}

注: mapassign 的代码当中, 没有直接将 value 写入内存, 而是将 value 在内存当中的对应地址返回, 后续对内存地址写入进行操作.

  • 创建新的 overflow

func (h *hmap) newoverflow(t *maptype, b *bmap) *bmap {
    var ovf *bmap
    // 先检查是否有预分配的 overflow bucket, 如果有, 则从其中获取一个, 否则, 需要重新创建一个 bucket
    if h.extra != nil && h.extra.nextOverflow != nil {
        // 我们已经预分配了 overflow buckets [连续的内存地址]. 详细状况参考 makeBucketArray() 函数
        ovf = h.extra.nextOverflow
        if ovf.overflow(t) == nil {
            // 不是最后一个预分配的溢出存储桶. 这时候只需要修改nextOverflow地址指向下一个溢出桶(因为内存是连续的)
            h.extra.nextOverflow = (*bmap)(add(unsafe.Pointer(ovf), uintptr(t.bucketsize)))
        } else {
            // 最后一个预分配的溢出存储桶, 其地址有效, 指向了当前 buckets
            // 重置此存储桶上的 overflow 指针(该指针已设置为非nil标记值).
            ovf.setoverflow(t, nil)
            h.extra.nextOverflow = nil
        }
    } else {
        ovf = (*bmap)(newobject(t.bucket))
    }

    // 修改 noverflow
    h.incrnoverflow()
    // key和value 非指针
    if t.bucket.ptrdata == 0 { 
        h.createOverflow() // 创建 extra 和 overflow
        *h.extra.overflow = append(*h.extra.overflow, ovf) // 将  overflow 存储到 extra 当中
    }
    b.setoverflow(t, ovf) 
    return ovf
}
  • 修改 noverflow 的值

// incrnoverflow 递增 h.noverflow.
// noverflow 计算溢出桶的数量.
// 这用于触发相同大小的 map 增长. 
// 为了使hmap保持较小, noverflow是一个uint16.
// 当存储桶很少时, noverflow是一个精确的计数.
// 当有很多存储桶时, noverflow是一个近似计数.
func (h *hmap) incrnoverflow() {
    // 如果overflow buckets的数量与buckets的数量相同, 将触发相同大小的 map 增长.
    // 我们需要能够计数到 1<if h.B 16 {
        h.noverflow++
        return
    }

    // 以 1 / (1 <// 当我们达到1<<15 - 1时, 我们将有大约与桶一样多的溢出桶.
    mask := uint32(1)<-15) - 1
    // Example: if h.B == 18, then mask == 7,
    // and fastrand & 7 == 0 with probability 1/8.
    if fastrand()&mask == 0 {
        h.noverflow++
    }
}

相关辅助函数

// 地址偏移(内存必须连续)
func add(p unsafe.Pointer, x uintptr) unsafe.Pointer {
   return unsafe.Pointer(uintptr(p) + x)
}

// 2^b, b的有效范围[0-63]
func bucketShift(b uint8) uintptr {
   return uintptr(1) <8 - 1))
}

// 扩容的条件:
// 1. 判断已经达到装载因子的临界点(6.5), 即元素数量 >= 桶(bucket)个数 * 6.5, 
// 这个时候说明大部分桶是可能是快满了(平均每个桶插入6.5个键值对). 如果插入新元素, 
// 有大概率需要溢出桶(overflow bucket)上.
//
// 2. 判断溢出桶是否太多, 当桶总数 = 桶总数, 则认为溢出
// 桶太多. 当桶总数 >= 2^15, 当溢出桶总数 >= 2^15 时, 则认为溢出桶太多了.
//
// 只要满足上述任意一个条件, 就需要进行扩容.

// 扩容的条件1
func overLoadFactor(count int, B uint8) bool {
   return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}

// 扩容的条件2
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
   if B > 15 {
      B = 15
   }
   return noverflow >= uint16(1)<15)
}


// store ptr to key instead of key itself
func (mt *maptype) indirectkey() bool { 
   return mt.flags&1 != 0
}
// store ptr to elem instead of elem itself
func (mt *maptype) indirectelem() bool { 
   return mt.flags&2 != 0
}
// true if k==k for all keys, 它的反例是k为NANs
func (mt *maptype) reflexivekey() bool { 
   return mt.flags&4 != 0
}
// true if we need to update key on an overwrite
func (mt *maptype) needkeyupdate() bool { 
   return mt.flags&8 != 0
}
// true if hash function might panic
func (mt *maptype) hashMightPanic() bool { 
   return mt.flags&16 != 0
}
推荐阅读
  • Go Map源码解读之数据结构

  • Go Map源码解读之Map创建

beb947044a28f32457611b62bb82b4ad.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值