go基础之map-增和改(二)

  • 写在之前
  • 环境说明
  • makemap_small和makemap的区别
  • 添加元素(没触发扩容的情况)
  • 一直到在发生扩容前的map内存结构是怎样的呢
  • 发生扩容
  • 总结

写在之前

在上篇文章《go基础之map-写在前面(一)》介绍了map的数据结构,本篇会详细介绍map的增和改的代码实现,由于增和改的实现基本上差不多,所以就纳到一起分析了。如果想详细查看源码的注释,可以查看我的GitHub,欢迎批评指正。我的打算是把一些常用的数据结构都分析一遍,如果有志同道合的人,可以联系我。

环境说明

我的具体调试环境在《go基础之map-写在前面(一)》已经说明的非常仔细了,现在只讲我分析增和改的调试代码。

package mainimport ("fmt""strconv"
)func main() {m1 := make(map[string]string, 9)fmt.Println(m1)for i := 0; i < 20; i++ {str := strconv.Itoa(i)m1[str] = str}
}

老规矩编译一波,看看第9行的申明到底干了啥?

go tool compile -N -l -S main.go > main.txt

编译结果有点多,我只列举重点代码:

"".main STEXT size=394 args=0x0 locals=0x980x0000 00000 (main.go:8) TEXT    "".main(SB), ABIInternal, $152-0....0x0036 00054 (main.go:9)  LEAQ    type.map[string]string(SB), AX0x003d 00061 (main.go:9)  PCDATA  $0, $00x003d 00061 (main.go:9)  MOVQ    AX, (SP)0x0041 00065 (main.go:9)    MOVQ    $9, 8(SP)0x004a 00074 (main.go:9)   MOVQ    $0, 16(SP)0x0053 00083 (main.go:9)  CALL    runtime.makemap(SB)....0x0107 00263 (main.go:13)    PCDATA  $0, $50x0107 00263 (main.go:13) LEAQ    type.map[string]string(SB), DX0x010e 00270 (main.go:13) PCDATA  $0, $40x010e 00270 (main.go:13) MOVQ    DX, (SP)0x0112 00274 (main.go:13)   PCDATA  $0, $60x0112 00274 (main.go:13) MOVQ    "".m1+56(SP), BX0x0117 00279 (main.go:13)    PCDATA  $0, $40x0117 00279 (main.go:13) MOVQ    BX, 8(SP)0x011c 00284 (main.go:13)  PCDATA  $0, $00x011c 00284 (main.go:13) MOVQ    CX, 16(SP)0x0121 00289 (main.go:13) MOVQ    AX, 24(SP)0x0126 00294 (main.go:13) CALL    runtime.mapassign_faststr(SB)....
  • 第9行调用了runtime.makemap方法做一些初始化操作,我把map的初始容量设为大于8底层才会走该方法,否则会调用runtime.makemap_small方法。
  • 第22行调用了runtime.mapassign_faststr方法,该方法对应main.go第13行的赋值方法m1[str] = str

我们找到了方法,在后面就可以在$go_sdk_path/src/runtime/map.go$go_sdk_path/src/runtime/map_faststr.go
找到方法,然后断点调试即可。

makemap_small和makemap的区别

makemap_small的代码如下:

// makemap_small implements Go map creation for make(map[k]v) and
// make(map[k]v, hint) when hint is known to be at most bucketCnt
// at compile time and the map needs to be allocated on the heap.
func makemap_small() *hmap {h := new(hmap)h.hash0 = fastrand()return h
}

该代码的实现十分简单,就设置了一个hash种子,其他的例如申通桶内存的操作只有在真正赋值数据的时候才会创建桶。该方法在什么情况会被调用呢?如注释说说"hint is known to be at most bucketCnt at compile time and the map needs to be allocated on the heap",bucketCnt就是8个,所以上面我的示例代码为何要设初始容量为9的原因就在这里。
我就直接略过这种情况,因为在实际应用场景下还是要指定容量,避免后面因为频繁扩容造成性能损失,makemap的代码如下:

// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)// 是否超出了最大的可分配虚拟内存或者超出了uintptr表示的值if overflow || mem > maxAlloc {hint = 0}// initialize Hmapif h == nil {h = new(hmap)}// 随机hash因子h.hash0 = fastrand()// Find the size parameter B which will hold the requested # of elements.// For hint < 0 overLoadFactor returns false since hint < bucketCnt.// 计算B的值,桶的个数为 1 << BB := uint8(0)// 不断的循环得到最大的B值for overLoadFactor(hint, B) {B++}h.B = B// allocate initial hash table// if B == 0, the buckets field is allocated lazily later (in mapassign)// If hint is large zeroing this memory could take a while.if h.B != 0 {var nextOverflow *bmap// 根据B的值去申请桶,包括逸出桶h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)if nextOverflow != nil {h.extra = new(mapextra)// nextOverflow指向逸出桶的内存地址h.extra.nextOverflow = nextOverflow}}return h
}

看程序注释大概明白该代码的作用就是得到B值和申请桶,overLoadFactor方法是用了6.5的扩容因子去计算出最大的B值,保证你申请的容量count要大于 (1>> B) * 6.5, 这个扩容因子想必大家都不陌生,在java中是0.75,为什么在go中是0.65呢?在runtime/map.go开头处有测试数据,综合考虑来说选择了6.5。大家可能注意到maptype用来申请桶的内存块了,下面看看maptype的代码,也有助于理解map的结构:

type maptype struct {typ    _type// map的key的类型key    *_type// map的value的类型elem   *_type// 桶的类型bucket *_type // internal type representing a hash bucket// function for hashing keys (ptr to key, seed) -> hashhasher     func(unsafe.Pointer, uintptr) uintptr// key的类型的大小keysize    uint8  // size of key slot// value类型元素的大小elemsize   uint8  // size of elem slot// 桶里面所有元素的大小bucketsize uint16 // size of bucketflags      uint32
}

makemap方法里面math.MulUintptr(uintptr(hint), t.bucket.size)用到了bucket的size,这里这个size和maptype的bucketsize一模一样都是272(《go基础之map-写在前面(一)》有介绍为什么是272),所以就能计算出需要分配的内存。仔细分析makemap的字段,可以发现定义了map的基本数据结构,后面代码用来申请桶的内存块的时候都使用了这个数据结构。
makemap第36行代码调用了方法makeBucketArray方法来申请内存,我们简单看看它里面的细节:

// makeBucketArray initializes a backing array for map buckets.
// 1<<b is the minimum number of buckets to allocate.
// dirtyalloc should either be nil or a bucket array previously
// allocated by makeBucketArray with the same t and b parameters.
// If dirtyalloc is nil a new backing array will be alloced and
// otherwise dirtyalloc will be cleared and reused as backing array.
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.// Avoid the overhead of the calculation.if b >= 4 {// Add on the estimated number of overflow buckets// required to insert the median number of elements// used with this value of b.nbuckets += bucketShift(b - 4)sz := t.bucket.size * nbucketsup := roundupsize(sz)if up != sz {nbuckets = up / t.bucket.size}}if dirtyalloc == nil {// 申请nbuckets个桶,包括了逸出桶,所以所有桶在内存中其实连续的,虽然逻辑上有差异buckets = newarray(t.bucket, int(nbuckets))b0 := (*dmap)(add(buckets, uintptr(0)*uintptr(t.bucketsize)))println(b0.debugOverflows)} else {// dirtyalloc was previously generated by// the above newarray(t.bucket, int(nbuckets))// but may not be empty.buckets = dirtyallocsize := t.bucket.size * nbucketsif t.bucket.ptrdata != 0 {memclrHasPointers(buckets, size)} else {memclrNoHeapPointers(buckets, size)}}if base != nbuckets {// We preallocated some overflow buckets.// To keep the overhead of tracking these overflow buckets to a minimum,// we use the convention that if a preallocated overflow bucket's overflow// pointer is nil, then there are more available by bumping the pointer.// We need a safe non-nil pointer for the last overflow bucket; just use 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
}

注意12行处有个优化点,当B小于4的时候,也就是初始申请map的容量的时候的count < (1 >> B) * 6.5的时候,很大的概率其实不会使用逸出桶。当B大于4的时候,程序就预估出一个逸出桶的个数,在26行处就一并申请总的桶的内存块。第27行的代码是在源码中没有的,我只是用来调试代码所用,这个在《go基础之map-写在前面(一)》有介绍这个小技巧。在第49行就通过bucketsize计算出逸出桶的位置,并且在51到53行有个技巧,给最后一个桶的溢出桶指针设置了桶的起始地址,这个在后面系列的博客会介绍到为何这么使用。
ok,现在map的数据如下:

只有2个桶,而且每个桶的tophash值都是默认值0,由于此时key和value都为空,故没有展示出来。extra没有值是因为B小4没有溢出桶导致的。

添加元素(没触发扩容的情况)

m1[str] = str我上面分析了是对应的map_faststr.go里面的mapassign_faststr方法。第一次添加的key和value都是string类型。

func mapassign_faststr(t *maptype, h *hmap, s string) unsafe.Pointer {//d := (*dmap)(unsafe.Pointer(uintptr(h.buckets)))//bucketD := uintptr(0)//for bucketD < bucketShift(h.B)+3 {//  flag := false//    for _, debugKey := range d.debugKeys {//       if debugKey == "" {//           continue//      }//     if flag == false {//          print("bucket:")//            println(bucketD)//      }//     print("key:")//       println(debugKey)//     flag = true//  }// bucketD++//   d = (*dmap)(unsafe.Pointer(uintptr(h.buckets) + bucketD*uintptr(t.bucketsize)))//}//println()//取出第三位是否是1,如果是1则表示正有另外一个协程在往map里面写数据if h.flags&hashWriting != 0 {throw("concurrent map writes")}key := stringStructOf(&s)//获取key的hash值hash := t.hasher(noescape(unsafe.Pointer(&s)), uintptr(h.hash0))// Set hashWriting after calling t.hasher for consistency with mapassign.// 将标志位设置为正在写h.flags ^= hashWritingif h.buckets == nil {h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)}again:// 获取该key落到第几个bucket,每个bucket指的是类似链并的bmap结构mask := bucketMask(h.B)bucket := hash & mask// 如果存在扩容情况if h.growing() {// 从oldbuckets里面复制到新申请的buckets里面growWork_faststr(t, h, bucket)}// 寻址到第几个bmapb := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))// 得到bmap的tophash值top := tophash(hash)var insertb *bmap          // 插入到哪个bmap里面var inserti uintptr        // 插入到bmap哪个位置var insertk unsafe.Pointer // 插入key到bmap哪个位置//找到一个空的地方插入该key
bucketloop:for {for i := uintptr(0); i < bucketCnt; i++ {if b.tophash[i] != top {if isEmpty(b.tophash[i]) && insertb == nil {insertb = binserti = i}// 一开始都是0,也就是emptyRestif b.tophash[i] == emptyRest {break bucketloop}continue}// 到这里已经找到tophash了,2个不同的key也有可能相等,继续判断是否key相等// 在bucket中的key位置k := (*stringStruct)(add(unsafe.Pointer(b), dataOffset+i*2*sys.PtrSize))// 字符串key的长度都不等的话肯定不是一个keyif k.len != key.len {continue}// 要么2个字符串直接相等,要么直接内存地址相等if k.str != key.str && !memequal(k.str, key.str, uintptr(key.len)) {continue}// already have a mapping for key. Update it.// 找到了相同的key,则要去更新valueinserti = iinsertb = bgoto done}// 插入第9个的时候会走向这里,但是溢出的hmap是没有的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.// 如果次数个数超出了增长因子,或者没有超出增长因子,但是有太多的逸出桶了,这个和java的hashmap一样,当太多红黑树了,还是会影响查找效率,因为理想情况下,map的// 查找效率应该是o(1)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 {// all current buckets are full, allocate a new one.insertb = h.newoverflow(t, b)inserti = 0 // not necessary, but avoids needlessly spilling inserti}// 把tophash值放到topsh槽里面去insertb.tophash[inserti&(bucketCnt-1)] = top // mask inserti to avoid bounds checks// 把key放到bmap里面// dataOffset是为了得到内存对齐后的key的位置// 为什么插入的是2*sys.PtrSize呢,因为string其实占了16字节insertk = add(unsafe.Pointer(insertb), dataOffset+inserti*2*sys.PtrSize)// store new key at insert position// 这块内存就放key的值*((*stringStruct)(insertk)) = *key// key个数加1h.count++done:// done不关心是否是更新还是新增,拿到相应的位置即可// 找到value存的内存位置elem := add(unsafe.Pointer(insertb), dataOffset+bucketCnt*2*sys.PtrSize+inserti*uintptr(t.elemsize))if h.flags&hashWriting == 0 {throw("concurrent map writes")}// 将标志位恢复h.flags &^= hashWritingreturn elem
}
  1. 2到21行是我的调试代码,打印下桶里面有哪些键值对。
  2. 23行和32行都是对写标志位的操作,可见,map不支持多个goroutine写操作。
  3. 26行把key转成stringStructOf类型,后面方便用stringStructOf里面的len和具体的string的字符数组值str,这也是个优化点,少了后面通过len(str)的计算,提高效率。
  4. 28行noescape防止key被逃逸分析,计算出key的hash。
  5. 38行到54行的again的代码块主要计算式key应该落在哪个buket,为什么作为一个代码块操作呢?是因为在触发扩容的时候,会重新计算落到哪个bucket。40行计算出bucket掩码,这里二进制值是10,41行和hash做与运算,得到的值就是要把key应该存的桶号。这里也是个优化操作,通过二进制运算提高效率。第50行计算得到的值正是放到bucket里面的前8个hash槽里面。先忽略掉251行的扩容情况。
  6. 57行到123行的bucketloop代码块主要作用是找到key和value存取的位置,并把key放到bucket所在的内存位置。里面有2个for循环,外循环轮询bucket以及bucket的逸出桶,里循环轮询桶的8个tophash槽,如果找到空的tophash槽(66行和67行)就执行到done语句块。71行之后就是key的高8位hash码相等了,那么就有可能bucket已经存在了这个key,所以就先比key的长度,再比较内存。102~105行先忽略。107-110会申请一个逸出桶然后把key存到逸出桶的第一个位置。113行把tophash值放到hash槽里面。
  7. 至于第66行为什么要比较hash槽等于emptyRest才算找到了呢?这个在后面的系列会介绍到。

done代码块的代码比较清晰,就是得到value放的内存位置,并且把状态设置为写完成。

done:// done不关心是否是更新还是新增,拿到相应的位置即可// 找到value存的内存位置elem := add(unsafe.Pointer(insertb), dataOffset+bucketCnt*2*sys.PtrSize+inserti*uintptr(t.elemsize))if h.flags&hashWriting == 0 {throw("concurrent map writes")}// 将标志位恢复h.flags &^= hashWritingreturn elem
}

现在的map的内存结构是什么样的呢?

一直到在发生扩容前的map内存结构是怎样的呢

为啥明明2个桶都没填充完就要马上扩容了呢?这是因为扩容因子作用了:

// overLoadFactor reports whether count items placed in 1<<B buckets is over loadFactor.
func overLoadFactor(count int, B uint8) bool {return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}

count此时值是13,13 * (2 / 2) = 13,但是在下次的13的key放进来的时候就会发生扩容了。

发生扩容

上面说到key为13的时候发生扩容,下面具体分析如何扩容的:

// 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.// 如果次数个数超出了增长因子,或者没有超出增长因子,但是有太多的逸出桶了,这个和java的hashmap一样,当太多红黑树了,还是会影响查找效率,因为理想情况下,map的// 查找效率应该是o(1)if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {d := (*dmap)(unsafe.Pointer(uintptr(h.buckets)))bucketD := uintptr(0)for bucketD < bucketShift(h.B)+3 {flag := falsefor i, debugKey := range d.debugKeys {if debugKey == "" {continue}println(d.tophash[i])if flag == false {print("bucket:")println(bucketD)}print("key:")println(debugKey)flag = true}bucketD++d = (*dmap)(unsafe.Pointer(uintptr(h.buckets) + bucketD*uintptr(t.bucketsize)))}println()hashGrow(t, h)goto again // Growing the table invalidates everything, so try again}

在上面的2层for循环里面虽然找到了bucket还有剩余位置,但是第7行的overLoadFactor(h.count+1, h.B)计算出要发生扩容。8~28行是我的调试代码,用来打印出此时map的内存结构。hashGrow会做具体的扩容操作,然后执行again从新计算落入哪个bucket。
看看hashGrow干了嘛:

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)//如果你不是因为超过了负载因子而是因为太多元素导致的,则不扩容二倍if !overLoadFactor(h.count+1, h.B) {bigger = 0h.flags |= sameSizeGrow}oldbuckets := h.buckets// 扩容那么就扩1倍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 += biggerh.flags = flagsh.oldbuckets = oldbucketsh.buckets = newbucketsh.nevacuate = 0h.noverflow = 0if 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.overflowh.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().
}
  1. 当不是因为逸出桶太多导致的扩容,那么就扩容2倍,桶是原来的桶的2倍。
  2. 第13行会申请内存,这里没有nextOverflow,下面会分析makeBucketArray方法。
  3. 由于hmap的extra是nil,所以后面的代码基本忽略。
  4. 将hmap的oldbuckets的指针指向原来的buckets,将hmap的buckets指针指向新申请的桶。oldbuckets是用于将老的桶里面的数据逐步拷贝到新申请的桶里面。
    我画一下此时的map的内存结构,方便对比上面没扩容时候的内存结构:

然后会走到agian代码块里面的这段代码:

 // 如果存在扩容情况if h.growing() {// 从oldbuckets里面复制到新申请的buckets里面growWork_faststr(t, h, bucket)}

growing方法就是判断是否有oldbuckets,此时会走向growWork_faststr方法去复制数据到新桶里面。此时bucket的值是 0-3其中的一个值,此时我调试的值是0,该值就是该key即将落入新申请桶的编号。

func growWork_faststr(t *maptype, h *hmap, bucket uintptr) {// make sure we evacuate the oldbucket corresponding// to the bucket we're about to use// bucket&h.oldbucketmask() 会得到一个oldbuckets中的一个bucket,然后把该bucket里面的数据移到新的bucket里面去,即新申请的。mask := h.oldbucketmask()oldbucket := bucket & mask// 迁移逻辑evacuate_faststr(t, h, oldbucket)// evacuate one more oldbucket to make progress on growing// 如果还有oldbuckets需要迁移if h.growing() {// 继续移除oldbuckets第nevacuate+1个,也是继续迁移上次迁移后面的那一个桶evacuate_faststr(t, h, h.nevacuate)}
}

第6行的与操作肯定得到的桶号是在老桶的序号内,即将要迁移的就是该桶。evacuate_faststr就是迁移逻辑。12~15行意思就是如果还有待迁移的桶,那么就继续迁移。
接下来就到了扩容里面真正重点的代码了:

func evacuate_faststr(t *maptype, h *hmap, oldbucket uintptr) {// 得到即将移动到新的bucket的老bucket的地址b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))// 多少个老的bucketnewbit := h.noldbuckets()// 判断是否已经被移动到新的bucketif !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.// xy是把老的bucket的数据分配到2个新的bucket里面去,hash&newbit != 0 决定放到x还是y的bucket里面去var xy [2]evacDst// x和老的bucket同一个位置x := &xy[0]// bucket的起始地址x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))// bucket内的key的起始地址x.k = add(unsafe.Pointer(x.b), dataOffset)// bucket内的value的起始地址x.e = add(x.k, bucketCnt*2*sys.PtrSize)if !h.sameSizeGrow() {// Only calculate y pointers if we're growing bigger.// Otherwise GC can see bad pointers.// 偏移newbit位,得到y的bucket位置,也就是说x和y隔了老buckets个数的地址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*2*sys.PtrSize)}// 开始复制到新的bucket,注意也会遍历溢出桶for ; b != nil; b = b.overflow(t) {// bucket的key的起始地址k := add(unsafe.Pointer(b), dataOffset)// bucket的value的起始地址e := add(k, bucketCnt*2*sys.PtrSize)for i := 0; i < bucketCnt; i, k, e = i+1, add(k, 2*sys.PtrSize), add(e, uintptr(t.elemsize)) {top := b.tophash[i]if isEmpty(top) {b.tophash[i] = evacuatedEmptycontinue}if top < minTopHash {throw("bad map state")}var useY uint8if !h.sameSizeGrow() {// Compute hash to make our evacuation decision (whether we need// to send this key/elem to bucket x or bucket y).// 求得key的hash值决定放到x还是yhash := t.hasher(k, uintptr(h.hash0))if hash&newbit != 0 {useY = 1}}// 挡在移动key的过程当中,会给tophsh设置一些标志位,evacuatedX表示会移动到x,evacuatedY表示会移动y,// 当useY是1的时候就移动到yb.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY, enforced in makemap// 得到移动的目标dst := &xy[useY] // evacuation destination// 一个bmap只存8个key/value,超过了就要用dest的溢出桶if dst.i == bucketCnt {// 申请一个溢出桶dst.b = h.newoverflow(t, dst.b)// 新桶要重置位0dst.i = 0// 得到新桶的key的位置dst.k = add(unsafe.Pointer(dst.b), dataOffset)// 得到新桶的value的位置dst.e = add(dst.k, bucketCnt*2*sys.PtrSize)}// 复制tophashdst.b.tophash[dst.i&(bucketCnt-1)] = top // mask dst.i as an optimization, to avoid a bounds check// Copy key.// 把老桶的key复制到dst*(*string)(dst.k) = *(*string)(k)// 把老桶的value复制到dsttypedmemmove(t.elem, dst.e, e)// 桶个数+1dst.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.// dst的key向前偏移一个key的大小dst.k = add(dst.k, 2*sys.PtrSize)// dst的value向前偏移一个key的大小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.// 保留了8个tophash槽不释放,只释放key和value的内存// 得到key的内存地址ptr := add(b, dataOffset)// 得到一个bucket除了8个tophash槽之后的偏移n := uintptr(t.bucketsize) - dataOffset// 释放地址memclrHasPointers(ptr, n)}}// 这段代码比较魔幻// 据我分析,第一次进入应该当时oldbucket为0的时候。后面也有可能进入,取决于nevacuate的值// 【0,nevacuate+1】之前的oldbuckets已经被迁移了.// 上面只是把bucket里面的数据清除掉了,但是tophash值还在。if oldbucket == h.nevacuate {advanceEvacuationMark(h, t, newbit)}
}
  1. 第3行得到待迁移的老桶地址。
  2. 第5行得到偏移的桶数,该值等于老桶数。这样的用意是把老桶的数据按照hash分散在新桶的里面。画图说明下:8~95行的意思和上图差不多一致,有兴趣对照我的注释看一看。
  3. 96~109行会去释放掉刚刚迁移完成的老桶,需要注意下,当有迭代老桶的时候就先不释放数据,当key和value都不是指针的时候也先不释放,到后面会解释到何时释放。
  4. 580~586行意思就是去标记释放了多少个桶被释放了,此时oldbucketh.nevacuate都是0,所以会进去标记。下面进去看看里面是怎么标记的。
func advanceEvacuationMark(h *hmap, t *maptype, newbit uintptr) {// 已经释放完一个oldbucket则加1h.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.// 最多往后面找1024个oldbucket是否已经迁移,查找太多会影响效率?// 每次插入值的时候就会尝试迁移一部分stop := h.nevacuate + 1024if stop > newbit {stop = newbit}// 最多找1024个oldbuckets,并且判断nevacuate的下一个是否已经搬移,如果判断则+1for h.nevacuate != stop && bucketEvacuated(t, h, h.nevacuate) {h.nevacuate++}// 如果nevacuate等于oldbuckets的个数的值那么整个oldbuckets已经迁移完成了if h.nevacuate == newbit { // newbit == # of oldbuckets// Growing is all done. Free old main bucket array.// 释放oldbucketsh.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}// 更改标志位,把第4位变为0h.flags &^= sameSizeGrow}
}
  1. 3行就标记已经释放完一个桶了。

  2. 8行算是一个权衡,最多往后面查看1024个桶,为什么不去查完呢?我理解的是到了1024个桶其实已经算一个比较大的map了,如果桶的真的很多的话,那就会影响效率,注意我们上面分析的扩容其实一直在添加13这个key的过程中,如果耗费太多时间的话那就不太好了。而且go map的扩容设计就不是想一次性把所有老桶的数据一下搬过来,一次搬不过来,那在下次添加key的时候继续搬。

  3. 12~15行就是去往后面查看桶是否已经被迁移完,迁移完就+1。

  4. 17行判断是否已经迁移完成,把oldbuckets的指针设为nil

  5. 21~27行就是判断是有hmap的extral是否为空,这里着重说明下:
    ①、extral为空,那能说明该hamp是没有逸出桶的,这个推断在这里不重要;但是它也能说明extraoverflowoldoverflow也为空,这个推断就比较重要了。当桶里面的key和value都不是指针的时候,那么这里overflowoldoverflow就不会为空了(如果后面有时间的话我再续写一篇博客说明下,因为我该系列的博客都是规划的map[string][string],key和value都是指针,所以不存在该情况),所以此时会把oldoverflow设为空指针。
    ②、还记得上面释放桶的时候这段代码吗?t.bucket.ptrdata != 0。当桶的key和value都不是指针的时候,这里就是0了,所以上面就不会释放桶里的数据,但是在这里把oldoverflow设置为nil指针,就能释放桶了。

  6. 再回到上面growWork_faststr方法,因为还有另外一个桶需要迁移,故会继续执行evacuate_faststr迁移。这次就会判断if h.nevacuate == newbit成功,并且释放oldbuckets,ok,这里一次添加的key的过程当中就迁移完成了。

最后就回到again代码块了,下面就是重新寻桶,寻桶的位置,继续不断的放key,直到下次发生扩容。很遗憾,我在这里没有打印出所有桶的数据,不能画出此时map的内存结构。大致内存结构是老桶的数据会按照hash分散到新的4个桶里面。

总结

上面分析了map的增加key的代码,改的代码和增的情况99.9%像,详细看我的分析或者我的源码注释,应该都明白了。如果有什么疑惑的地方在下面给我留言我们一起探讨吧。

go基础之map-增和改(二)相关推荐

  1. properties(map)增.删.改.查.遍历

    import java.util.Map; import java.util.Properties; import java.util.Set;/*** properties(map)增.删.改.查. ...

  2. PySpark︱DataFrame操作指南:增/删/改/查/合并/统计与数据处理

    笔者最近需要使用pyspark进行数据整理,于是乎给自己整理一份使用指南.pyspark.dataframe跟pandas的差别还是挺大的. 文章目录 1.-------- 查 -------- -- ...

  3. Django基础五之Django模型层(二)多表操作

    Django基础五之Django模型层(二)多表操作 一 创建模型 表和表之间的关系 一对一.多对一.多对多 # 作者表 比较常用的信息放在这个表中 class Author(models.Model ...

  4. 关于跨DB增量(增、改)同步两张表的数据小技巧

    有些场景下,需要隔离不同的DB,彼此DB之间不能互相访问,但实际的业务场景又需要从A DB访问B DB的情形,这时怎么办?我认为有如下常规的三种方案: 1.双方提供RESET API,需要访问不同DB ...

  5. Flask-SQLAlchemy 对数据库的增查改删

    Flask-SQLAlchemy 对数据库的增查改删 使用 Flask-SQLAlchemy 连接数据库,可以通过 Python 对象来操作数据库. Flask-SQLAlchemy 连接数据库参考: ...

  6. Java基础之map总结

    map的基础理解 话不多说,先上图,可以这样简单的对容器中的map进行分类: 我们在Java开发中,除了最常用的基本数据类型和String对象之外,也会经常用到集合类.集合类中存放的都是对象的引用,而 ...

  7. MySQL基础知识之增删改查

    MySQL基础知识之增删改查 MySQL基本语法: 1.创建库:create database 库名: 创建带编码格式的库:create database 库名 character set 编码格式: ...

  8. 零基础入门学Python(十二)—— 魔法方法(上)

    零基础入门学Python系列内容的学习目录→\rightarrow→零基础入门学Python系列内容汇总. 魔法方法(上) 1. 构造和析构 1.1 _ _ init _ _(self[, ...]) ...

  9. linux增删查改语句,mysql基础知识之增删查改使用介绍

    mysql基础知识之增删查改使用介绍 本文主要介绍mysql常用的SELECT.INSERT.UPDATE.DELETE语句的使用,数据库的安装这里不做介绍,并且事先已经准备好相关数据. 本文中使用的 ...

  10. SpringBoot+Mybatis实现接口的增查改,以及json中拼接json数组

    增改查的评价表 DROP TABLE IF EXISTS `hpkh_police_review`; CREATE TABLE `hpkh_police_review` (`id` int(11) N ...

最新文章

  1. java等待欢迎界面_android welcome欢迎界面3秒后自动跳转
  2. 定义了浮动元素后margin-bottom失效的解决办法
  3. ThreadPoolExecutor线程池的理解与应用
  4. 5.7.20mysql修改密码,ubuntu系统mysql5.7.20忘记root密码修改
  5. 祝贺!两位 Apache Flink PMC 喜提 Apache Member
  6. c51为啥要宏定义时钟_51单片机时钟实训报告
  7. LinkedHashMap 简介
  8. 【动态规划笔记】状压dp:旅行商问题
  9. ISIS协议原理与配置
  10. 华硕鹰眼ZenFone Zoom推动潮流:比肩专业相机几率几何?
  11. ubuntu安装redis并使用
  12. 智能安防视频监控平台页面无法访问该如何排查?
  13. idea IntelliJ IDEA 2018.2.5 x64 破解
  14. 教你用身份证号查社保卡号 个人电脑号
  15. ubuntu 20.04 调节屏幕亮度
  16. 追风去,不如造风来——致敬蚂蚁区块链创新大赛中的参与者们
  17. python绘制相关系数热力图
  18. 省赛题目(4月23日)の答案
  19. ArcGIS相交闭合矩形线转面
  20. AirDisk存宝Q2有什么功能?

热门文章

  1. 系统定位的问题,优化的简述
  2. MotoSimEG-VRC软件:机器人工作路径轨迹跟踪功能使用方法
  3. 微信小程序制作科学计算器(控制台接受显示数据)
  4. vb6反编译详解_[原创]VB6反编译详解(一)
  5. python基础语法实验目的_Python基础语法
  6. 【步态识别】GLN 算法学习《Gait Lateral Network: Learning Discriminative and Compact Representations for Gait R》
  7. yum离线下载依赖包
  8. ffmpeg学习日记506-源码-av_image_copy()函数分析及功能
  9. HTML中使用vlc插件
  10. 高中信息技术:信息技术教学论