【导读】这篇文章是作者的学习笔记,主要关注 golang map的 底层结构、get、set、del和扩容方面的细节。
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}
buckets 数组内部存的就是 bmap 的数组。
// mapaccess1 returns a pointer to h[key]. Never returns nil, instead
// it will return a reference to the zero object for the elem type if
// the key is not in the map.
// NOTE: The returned pointer may keep the whole map live, so don't
// hold onto it for very long.
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
// 如果 map 为 nil 或者没有元素,直接返回 value 零值
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.hasher(key, 0) // see issue 23734
}
return unsafe.Pointer(&zeroVal[0])
}
// 读写冲突检查
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}
// 计算 hash 值
hash := t.hasher(key, uintptr(h.hash0))
// 计算 2^B-1
m := bucketMask(h.B)
// hash&m 计算出后 B 为,定位 bucket 位置
// 这里的运算应该是对 hash%buckets 的长度 来定位,这里使用&运算来代替%运算加速。
// hash % len(buckets)
// => hash % 2^B
// => hash & (2^B-1)
// => hash & m
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
// old 不为空,说明在扩容,因此需要判断是否去 oldbucket 中查找
if c := h.oldbuckets; c != nil {
// 如果不是等量扩容则需要将 m/2,因为扩容时新 buckets 的容量是 old 的两倍
if !h.sameSizeGrow() {
// There used to be half as many buckets; mask down one more power of two.
m >>= 1
}
oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
// oldb 未被扩容搬运,表示需要去 oldbucket 中查找,则将 b 指向 old
if !evacuated(oldb) {
b = oldb
}
}
// 取 hash 值的高 8 位
top := tophash(hash)
bucketloop:
// 遍历 bmap 及其溢出链
for ; b != nil; b = b.overflow(t) {
// bucketCntBits = 3
// bucketCnt = 1 << bucketCntBits
// 一个 bmap 最多存 8 个 k-v
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
// emptyRest 表示当前 tophash 对应的 k-v 为空,并且其后都为空,因此不用继续找下去了
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
// 找到 tophash 对应值的 key,定位到 key 的位置
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
// 比较 key,如果相等则表示找到了对应的 k-v
if t.key.equal(key, k) {
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
if t.indirectelem() {
e = *((*unsafe.Pointer)(e))
}
return e
}
}
}
// 所有的 bmap 都找过了,都没有,则返回 value 的零值
return unsafe.Pointer(&zeroVal[0])
}
用到的其他函数代码
// 取后 h.B 位
// m := uintptr(1) << (h.B & (sys.PtrSize*8 - 1)) - 1
m := bucketMask(h.B)
func bucketMask(b uint8) uintptr {
return bucketShift(b) - 1
}
func bucketShift(b uint8) uintptr {
// Masking the shift amount allows overflow checks to be elided.
return uintptr(1) << (b & (sys.PtrSize*8 - 1))
}
// 计算 hash 的高 8 位
func tophash(hash uintptr) uint8 {
top := uint8(hash >> (sys.PtrSize*8 - 8))
// minTopHash 及之前的一些值,是 map 预定义用来表示状态的一些值
// 如果计算的高 8 位小于需要加上 minTopHash 则需要加上 minTopHash,以区别正常 hash 值和状态值
if top < minTopHash {
top += minTopHash
}
return top
}
get 操作还有返回 bool 值表示 k-v 是否存在的操作。实际由两个函数来实现。
实现细节相同,mapaccess2 在返回值上多添加一个 bool
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer
func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool)
与 get 操作类似,底层使用mapassign
函数实现
// Like mapaccess, but allocates a slot for the key if it is not present in the map.
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
hash := t.hasher(key, uintptr(h.hash0))
h.flags ^= hashWriting
// bucket 为空,分配第一个新的 bucket
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
bucket := hash & bucketMask(h.B)
// 如果是扩容状态,则优先扩容当前 bucket
if h.growing() {
growWork(t, h, bucket)
}
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
top := tophash(hash)
// inserti -> 表示新 k-v 在 tophash 数组中的位置
// insertk -> 表示新 k-v 的 key 在 keys 中的位置
// elem -> 表示新 k-v 的 value 的地址
var inserti *uint8
var insertk unsafe.Pointer
var elem unsafe.Pointer
bucketloop:
// 与 mapaccess 类似。遍历 bmap 及其溢出链
for {
for i := uintptr(0); i < bucketCnt; i++ {
// 判断新 k-v 是否在 bucket 中已经存在
if b.tophash[i] != top {
// 如果当前 tophash 位置不是新 k-v 的位置,则判断他是否为空位
// 在插入新空位时,应该尽可能的在前面的空位插入,因此需要做 inserti == nil 判断,
// inserti != nil 表示已经找到了更前的空位,不使用新位置。
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))
}
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
// tophash 值与新 k-v 的高 8 位相同,则判断 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) {
continue
}
// key 相同,则更新即可
if t.needkeyupdate() {
typedmemmove(t.key, k, key)
}
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
goto done
}
// 遍历溢出链
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
// 没有在 map 找到 key,需要分配新的位置
// 如果 map 中键值对数量触发了扩容,或者 bmap 溢出过多,则需要扩容
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
// 触发了扩容,调整了 map 结构,因此需要重新查找合适的位置
goto again // Growing the table invalidates everything, so try again
}
// 表示在 bmap 及其溢出链都没有合适的位置,即都满了,则需要创建新的溢出 bmap,将新 k-v 分配到新的 bmap 中
if inserti == nil {
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)
*inserti = top
h.count++
done:
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
h.flags &^= hashWriting
if t.indirectelem() {
elem = *((*unsafe.Pointer)(elem))
}
return elem
}
mapassign
函数最后的返回值为 value 的地址,需要通过汇编将实际的 value 值赋值到 value 的地址上。
https://github.com/cch123/golang-notes/blob/master/map.md
这里有件比较奇怪的事情,mapassign 并没有把用户
m[k] = v
时的 v 写入到 map 的 value 区域,而是直接返回了这个值所应该在的内存地址。那么把 v 拷贝到该内存区域的操作是在哪里做的呢?var a = make(map[int]int, 7)
for i := 0; i < 1000; i++ {
a[i] = 99999
}看看生成的汇编部分:
0x003f 00063 (m.go:9) MOVQ DX, (SP) // 第一个参数
0x0043 00067 (m.go:9) MOVQ AX, 8(SP) // 第二个参数
0x0048 00072 (m.go:9) MOVQ CX, 16(SP) // 第三个参数
0x004d 00077 (m.go:9) PCDATA $0, $1 // GC 相关
0x004d 00077 (m.go:9) CALL runtime.mapassign_fast64(SB) // 调用函数
0x0052 00082 (m.go:9) MOVQ 24(SP), AX // 返回值,即 value 应该存放的内存地址
0x0057 00087 (m.go:9) MOVQ $99999, (AX) // 把 99999 放入该地址中赋值的最后一步实际上是编译器额外生成的汇编指令来完成的,可见靠 runtime 有些工作是没有做完的。这里和 go 在函数调用时插入 prologue 和 epilogue 是类似的。编译器和 runtime 配合,才能完成一些复杂的工作。
底层使用mapdelete
函数实现,首先查找到 cell 对应的位置,再将 key 和 value 内存清空,查找过程与 get 操作类似。
在清空内存后,还需要调整 bmap 及其溢出链的 tophash 的状态值,将一些 emptyOne 调整为 emptyReset
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)
}
// 定位到对应的 bucket 上
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
// 记录对应 bucket 的第一个 bmap,后续的 reset 需要用到
bOrig := b
top := tophash(hash)
search:
// 与赋值类似,遍历 bmap 及其溢出链
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
if b.tophash[i] == emptyRest {
break search
}
continue
}
// 定位到对应的 key 的位置
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 的内存
if t.indirectkey() {
*(*unsafe.Pointer)(k) = nil
} else if t.key.ptrdata != 0 {
memclrHasPointers(k, t.key.size)
}
// 定位 value 的位置,清空 value 的内存
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)
}
// 将当前位置对应的 tophash 设置为 emptyOne 状态,表示当前 cell 为空
b.tophash[i] = emptyOne
// 调整当前 bucket 中 cell 的状态,将一些 emptyOne 设置为 emptyRest
// emptyOne 表示当前 cell 为空,emptyRest 表示当前 cell 及其后都为空,因此 emptyRest 可以避免多余的遍历操作
// 如果当前删除的 k-v 为 bmap 中的最后一个,即第 7 位
if i == bucketCnt-1 {
// 当前 bmap 后面还有溢出链,并且溢出链中有内容,则跳到收尾处理
if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
goto notLast
}
} else {
// 当前删除的 k-v 不是最后一个,则需要判断他的下一位是否为 emptyRest,
// 如果下一位状态不 emptyRest,则表示其后还有内存,则跳到收尾处理
if b.tophash[i+1] != emptyRest {
goto notLast
}
}
// 如果能到这里,表示当前删除的 k-v 的后面所有 cell 状态都为 emptyRest,
// 因此需要调整当前 cell 为 emptyRest,并且继续向前调整
for {
b.tophash[i] = emptyRest
// 如果当前 cell 为 bmap 中的首位,则需要找溢出链的前一个 bmap
if i == 0 {
// 表示当前已经是首个 bmap,因此调整完了。
if b == bOrig {
break
}
// 类似单向链表查找前一项,找到前一个 bmap
c := b
for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
}
// 此时 b 指向了前一个的 bmap。将 i 调整为 bmap 的最后一位,即 7
i = bucketCnt - 1
} else { // 如果当前 cell 不是首位,则直接 i--往前找 cell
i--
}
// 如果往前找到的 cell 位置状态都为 emptyOne,则需要将他们置为 emptyRest,
// 直到找到一个 cell 位置不为空
if b.tophash[i] != emptyOne {
break
}
}
notLast:
// 最后的收尾处理,将 map 元素数量减 1
h.count--
break search
}
}
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
h.flags &^= hashWriting
}
扩容触发时机,在mapassign
函数中,当需要分配新 bucket 时会检查是否需要扩容
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
}
扩容的两个判断条件:
// 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)
}
// tooManyOverflowBuckets reports whether noverflow buckets is too many for a map with 1<<B buckets.
// Note that most of these overflow buckets must be in sparse use;
// if use was dense, then we'd have already triggered regular map growth.
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
// If the threshold is too low, we do extraneous work.
// If the threshold is too high, maps that grow and shrink can hold on to lots of unused memory.
// "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)
}
扩容函数,扩容前的设置在hashGrow
函数中
func hashGrow(t *maptype, h *hmap) {
// 判断是否为超过扩容因子触发的扩容
bigger := uint8(1)
if !overLoadFactor(h.count+1, h.B) {
bigger = 0
h.flags |= sameSizeGrow
}
// 将 buckets 挂载到 old 上
oldbuckets := h.buckets
// 创建大小为 2^(h.B+bigger) 的新 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
h.nevacuate = 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
}
// the actual copying of the hash table data is done incrementally
// by growWork() and evacuate().
}
map 中 buckets 的重新分配调整在growWork
函数中,因此mapassign
函数中,是先通过判断overLoadFactor
函数和tooManyOverflowBuckets
函数来检测是否需要扩容,如果需要扩容,则调用hashGrow
函数来设置扩容状态,最后再跳回如何代码段中,调用 growWork() 来扩容
if h.growing() {
growWork(t, h, bucket)
}
扩容函数
func growWork(t *maptype, h *hmap, bucket uintptr) {
// 确保搬运的 bucket 是正在使用的
evacuate(t, h, bucket&h.oldbucketmask())
// 如果还是扩容状态,则再搬运一个
if h.growing() {
evacuate(t, h, h.nevacuate)
}
}
搬运函数evacuate
,该函数一次只能搬运一个 bucket
func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
// 定位当前 bucket 的位置
b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
// 记录扩容前 map 的容量大小,即扩容前的 2^B
newbit := h.noldbuckets()
// 判断当前 bucket 是否已经完成扩容
if !evacuated(b) {
// x 和 y 指向扩容后在新 buckets 中的位置
var xy [2]evacDst
x := &xy[0]
// 定位目标 bmap 地址
x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))
// 定位目标 bmap 的 keys 地址
x.k = add(unsafe.Pointer(x.b), dataOffset)
// 定位目标 bmap 的 values 地址
x.e = add(x.k, bucketCnt*uintptr(t.keysize))
// 如果是增量扩容,则 y 表示扩容后的高位地址,在增量扩容时,由于是两倍放大,因此目标 bmap 有两个,一个低位一个高位
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))
}
// 遍历 old 中的该 bmap 及其溢出链
for ; b != nil; b = b.overflow(t) {
k := add(unsafe.Pointer(b), dataOffset)
e := add(k, bucketCnt*uintptr(t.keysize))
// 遍历搬运所有 cell
for i := 0; i < bucketCnt; i, k, e = i+1, add(k, uintptr(t.keysize)), add(e, uintptr(t.elemsize)) {
top := b.tophash[i]
// 如果该位为空,则将其置为已经搬运过的状态
if isEmpty(top) {
b.tophash[i] = evacuatedEmpty
continue
}
// 异常情况,未被搬运的只能是空或者 tophash 正常值的 cell
if top < minTopHash {
throw("bad map state")
}
k2 := k
if t.indirectkey() {
k2 = *((*unsafe.Pointer)(k2))
}
var useY uint8
if !h.sameSizeGrow() {
// 对需要搬运的 cell 的 key 做一次 hash 运算,决定需要搬运到 x 位置还是 y 位置
hash := t.hasher(k2, uintptr(h.hash0))
if h.flags&iterator != 0 && !t.reflexivekey() && !t.key.equal(k2, k2) {
// 针对 key 为 NaN 的特殊处理
useY = top & 1
top = tophash(hash)
} else {
// 对 hash 判断新的高位是否为 1 来决定搬运到 x 位置还是 y 位置
// 例如对于 oldhash 我们关注后 3 位,因此增量扩容后,则需要关注后 4 位
// 因此如果第 4 位为 0,则表示还是在地位,如果为 1,则表示搬运到高位
if hash&newbit != 0 {
useY = 1
}
}
}
if evacuatedX+1 != evacuatedY || evacuatedX^1 != evacuatedY {
throw("bad evacuatedN")
}
// evacuatedX + 1 == evacuatedY
// evacuatedX 表示搬运到地位,evacuatedY 表示搬运到高位,与上面算的 useY 做加表示新状态
b.tophash[i] = evacuatedX + useY
// 目的 bmap 的地址
dst := &xy[useY]
// 如果目标 bmap 已经满了,则创建溢出 bmap
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))
}
// 设置目标 bmap 的 tophash
dst.b.tophash[dst.i&(bucketCnt-1)] = top // mask dst.i as an optimization, to avoid a bounds check
// 搬运 key
if t.indirectkey() {
*(*unsafe.Pointer)(dst.k) = k2 // copy pointer
} else {
typedmemmove(t.key, dst.k, k) // copy elem
}
// 搬运 value
if t.indirectelem() {
*(*unsafe.Pointer)(dst.e) = *(*unsafe.Pointer)(e)
} else {
typedmemmove(t.elem, dst.e, e)
}
// 目标 bmap 中的 cell 数量增加
dst.i++
// 将 dst 的 k 指针和 e 指针后移一个单位,指向新的空位
dst.k = add(dst.k, uintptr(t.keysize))
dst.e = add(dst.e, uintptr(t.elemsize))
}
}
// 清除 old bucket
if h.flags&oldIterator == 0 && t.bucket.ptrdata != 0 {
b := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))
// 只清除 keys 和 values,保留 tophash 来表示状态
ptr := add(b, dataOffset)
n := uintptr(t.bucketsize) - dataOffset
memclrHasPointers(ptr, n)
}
}
if oldbucket == h.nevacuate {
// 更新搬运进度
advanceEvacuationMark(h, t, newbit)
}
}
搬运的后续处理函数
func advanceEvacuationMark(h *hmap, t *maptype, newbit uintptr) {
// 搬运进度+1,nevacuate 表示在其之前的 bucket 都已经搬运完成了
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
for h.nevacuate != stop && bucketEvacuated(t, h, h.nevacuate) {
h.nevacuate++
}
// 搬运进度等于 old buckets 的长度,则说明整个 old buckets 已经搬运完成了
if h.nevacuate == newbit { // newbit == # of oldbuckets
// 清除 old buckets,函数根据 h.oldbuckets 是否为 nil 来表示是否正在扩容
h.oldbuckets = nil
// 清除 old overflow bucket
if h.extra != nil {
h.extra.oldoverflow = nil
}
h.flags &^= sameSizeGrow
}
}
调用一次evacuate
函数只能搬运一个 bucket,而且调用evacuate
函数的地方只在growWork
函数中,而growWork
函数的调用只存在mapassign
函数和mapdelete
函数中,因此好像只有主动触发了这两个方法才会使得扩容继续进行。对于扩容边界的地方,再增添一个新的 cell,触发扩容,此时查看 hmap 的 oldbuckets 为非 nil,表示处于扩容状态,推测需要后续手动触发增加扩容进度。这样的设计占用 oldbuckets 和 newbuckets 两份内存不会浪费吗?
转自:Qraffa
qraffa.cn/2020/12/03/golang-map/
- EOF -
2、探索 mmap
Go 开发大全
参与维护一个非常全面的Go开源技术资源库。日常分享 Go, 云原生、k8s、Docker和微服务方面的技术文章和行业动态。
关注后获取
回复 Go 获取6万star的Go资源库
分享、点赞和在看
支持我们分享更多好文章,谢谢!