相关常量解析
bucketCntBits = 3 // 代表的是bit
bucketCnt = 1 << bucketCntBits // 代表的是一个bucket(bmap)最大存储8个key
loadFactorNum = 13
loadFactorDen = 2 // 通过这2者计算得出 负载因子(负载因子关乎到什么时候触发扩容)
maxKeySize = 128
maxElemSize = 128 //
emptyRest = 0 : 代表该topHash对应的K/V可用 ,或者表示该位置及其后面的bucket也是可用的
emptyOne = 1 : 仅代表该topHash对应的K/V可用
evacuatedX = 2 : 与rehash有关,代表的是原先的元素可能被迁移到了X位置(原地),当然也有可能迁移到了Y位置
evacuatedY = 3
evacuatedEmpty = 4 : 当这个bucket中的元素都迁移完之后,设置evacuatedEmpty
minTopHash = 5
当topHash<=5的时候,存储的是状态,否则存储的是hash值
- 每一个tophash对应的下标都是一个kv
map结构体
-
src/runtime/map.go
-
内部对象是hmap
-
type hmap struct { // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.. count int // map中的元素个数 flags uint8 // 标识状态 B uint8 // 用于设定buckets的最大个数,为 2^B 个,既 len(buckets)=2^B noverflow uint16 // 溢出的buckets 个数 hash0 uint32 // hash seed,涉及到hash函数 buckets unsafe.Pointer // buckets的指针对象 oldbuckets unsafe.Pointer // 当触发扩容时的buckets nevacuate uintptr // 渐进是rehash的进度,有点类似于redis extra *mapextra // }
-
-
同时,与Java的hashMap类似,也是有桶的概念,在golang中则是 bmap
-
type bmap struct { tophash [bucketCnt]uint8 // 可以发现,一个bucket 只会存储8个key } 编译后生成的实际对象为: type bmap struct { topbits [8]uint8 keys [8]keytype values [8]valuetype pad uintptr overflow uintptr // 当 k,v 都是非指针对象时,为了避免被gc扫描,会将overflow 移动到hmap,使得bmap依旧不涵指针 }
-
-
bmap的内存模型为:
-
key/key/key => value/value/value的形式,而不是 key/value/key/value的形式
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G2p99OKJ-1631841576712)(/Users/joker/Nutstore Files/我的坚果云/复习/imgs/golang_map_bmap.png)]
-
MAP的初始化
-
m1 := make(map[int]int) // 对应的内部函数是: makemap_small m2 := make(map[int]int, 10) // 对应的函数是: makemap 创建map支持传递一个参数,表示初始大小
-
func makemap_small() *hmap { h := new(hmap) h.hash0 = fastrand() return h } 只是简单的new 一个,不会初始化bucket数组
-
核心函数:
-
func makemap(t *maptype, hint int, h *hmap) *hmap { mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size) if overflow || mem > maxAlloc { hint = 0 } if h == nil { h = new(hmap) } // 获取一个随机因子 h.hash0 = fastrand() // hint 表示的是创建map的时候,预期的大小值,这个与Java的hashMap类似,最终都会使得初始容量为2的n次方 B := uint8(0) for overLoadFactor(hint, B) { B++ } h.B = B // 当B==0的时候,代表的是,只有当被put写入的时候,才会触发buckets的初始化 if h.B != 0 { var nextOverflow *bmap // 申请创建bucket数组 h.buckets, nextOverflow = makeBucketArray(t, h.B, nil) if nextOverflow != nil { h.extra = new(mapextra) h.extra.nextOverflow = nextOverflow } } return h }
-
-
总结
- map初始化的时候,总的来说只有2种情况,一种是
- makemap_small: 这种方式,只会创建hmap,而不会初始化buckets
- makemap: 将容量自动修改为2的n次方,然后初始化buckets数组
- map初始化的时候,总的来说只有2种情况,一种是
MAP # put
-
函数: src/runtime/map.go#mapassign
-
第一阶段: 初始化阶段
-
// ... 省略一些常规的debug 和校验 // 判断是否并发读写 if h.flags&hashWriting != 0 { throw("concurrent map writes") } // 编译时就会得到对应的hash函数 hash := t.hasher(key, uintptr(h.hash0)) // 标识处于写状态(用于并发读写判断) h.flags ^= hashWriting // 如果是makemap_small,则此时是没有初始化buckets的 if h.buckets == nil { h.buckets = newobject(t.bucket) // newarray(t.bucket, 1) }
-
-
第二阶段: 定位bucket阶段
-
// 获取bucket的内存地址 b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize))) // 获取hash的高8位作为key top := tophash(hash) var inserti *uint8 var insertk unsafe.Pointer var elem unsafe.Pointer bucketloop: for { // 遍历每个cell for i := uintptr(0); i < bucketCnt; i++ { // 如果当前hash与高8位hash不匹配 if b.tophash[i] != top { // 如果bucket为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)) } // 如果当前的bucket处于 overflow状态,代表着容量不足,则直接跳出整个写入 if b.tophash[i] == emptyRest { break bucketloop } continue } // 开始更新值 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,value , if t.needkeyupdate() { typedmemmove(t.key, k, key) } // 最后通过指针句柄移动,指向新的value elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize)) goto done } // 如果上述没有退出,意味着当前bucket中的元素都满了,我们需要获取下一个 ovf := b.overflow(t) if ovf == nil { // 代表这所有的bucket都满了 break } // 遍历获取下一个bucket,继续for循环 b = ovf } // 表明没有找到 相同的key,没有插入,所以可能是bucket都满了 // 如果当前需要触发扩容(既当前的每个bucket中的平均元素个数>=loadfactor的时候就会触发扩容) if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) { // 开始扩容 hashGrow(t, h) // 因为扩容涉及到rehash,所以需要重新走一遍 goto again }
-
-
第三阶段: 申请新的bucket阶段
-
当到这里时,代表着,bucket满了,需要申请新的bucket,然后一切开始重新赋值 if inserti == nil { // The current bucket and all the overflow buckets connected to it 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)) } // 存储k,v 如果不是指针,则还需要解引用 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 typedmemmove(t.key, insertk, key) *inserti = top h.count++ // 最后,消除flag位 done: if h.flags&hashWriting == 0 { throw("concurrent map writes") } h.flags &^= hashWriting if t.indirectelem() { elem = *((*unsafe.Pointer)(elem)) } return elem
-
-
Map 扩容 rehash
-
golang的rehash是一种渐进式hash的过程,先通过hashGrow申请新的bucket数组(或者是压根不申请:既第二种触发情况),然后每次写数据或者是读数据的时候,会判断当前map是否处于rehash过程,是的话,则会辅助rehash
-
最关键函数是evacuate
-
扩容的原因分两种
-
一种是因为达到了loadFactor而导致的扩容
-
另外一种则是因为 太多overflow而导致
-
// 装载因子超过 6.5 func overLoadFactor(count int, B uint8) bool { return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen) } // overflow buckets 太多 func tooManyOverflowBuckets(noverflow uint16, B uint8) bool { if B > 15 { B = 15 } return noverflow >= uint16(1)<<(B&15) }
-
-
如果是前者,则直接扩容bucket一倍( 既 二进制下左移一位)
-
-
func evacuate(t *maptype, h *hmap, oldbucket uintptr) { b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))) newbit := h.noldbuckets() if !evacuated(b) { // 先判断当前bucket是否已经rehash过了 (通过内部的flag来判断,因为bmap会被装换成上面的bmap) var xy [2]evacDst // 因为扩容可能会是2倍扩容,所以定义一个长度为2 的数组,0用来定位之前的元素 x := &xy[0] 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)) if !h.sameSizeGrow() { // 两倍扩容,所以可能会影响到另外一个元素,因此记录被影响的元素 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)) } // 从当前bucket开始,遍历每个bucket,因为bucket都是连在一起的 for ; b != nil; b = b.overflow(t) { k := add(unsafe.Pointer(b), dataOffset) e := add(k, bucketCnt*uintptr(t.keysize)) // 遍历bucket内部的每个元素 for i := 0; i < bucketCnt; i, k, e = i+1, add(k, uintptr(t.keysize)), add(e, uintptr(t.elemsize)) { top := b.tophash[i] // 如果是一个空的值(既未赋值),则直接标记为被rehash过了 if isEmpty(top) { b.tophash[i] = evacuatedEmpty continue } // 不为空,但是又不是初始值,panic if top < minTopHash { throw("bad map state") } // 如果是指针,则触发解引用 k2 := k if t.indirectkey() { k2 = *((*unsafe.Pointer)(k2)) } var useY uint8 if !h.sameSizeGrow() { // 如果是 2倍扩容,则重新计算一次hash值 hash := t.hasher(k2, uintptr(h.hash0)) if h.flags&iterator != 0 && !t.reflexivekey() && !t.key.equal(k2, k2) { // 表明当前有routine在遍历这个map,同时,重新计算hash后key不匹配,代表着该value需要 挪到一个新的bucket,因此,为了有一个新的hash, 这里会做一个 &1的操作,该操作的妙处在于 使得rehash后的bucket下标,要么在原来的位置,要么是在 bucketIndex+2^B 个位置处 useY = top & 1,这点其实与Java很像 ,但是Java怎么实现的我忘了 :-( top = tophash(hash) } else { 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 := &xy[useY] // evacuation destination // 如果当前元素的bucket刚好是最后一个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)) } dst.b.tophash[dst.i&(bucketCnt-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++ dst.k = add(dst.k, uintptr(t.keysize)) dst.e = add(dst.e, uintptr(t.elemsize)) } } // 最后,将hmap与oldBuckets解引用,使得可以被gc if h.flags&oldIterator == 0 && t.bucket.ptrdata != 0 { b := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)) ptr := add(b, dataOffset) n := uintptr(t.bucketsize) - dataOffset memclrHasPointers(ptr, n) } } if oldbucket == h.nevacuate { // 最后判断是否 rehash全部完毕,是则消除一些flag位 advanceEvacuationMark(h, t, newbit) } }
Map的删除
-
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) { // ....省略debug信息 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") } // 获取这个 key所对应的hash hash := t.hasher(key, uintptr(h.hash0)) // 添加安全标识 h.flags ^= hashWriting // 通过hash的高8位获取得到对应的bucket下标 bucket := hash & bucketMask(h.B) // 如果此时正在扩容,则辅助扩容 if h.growing() { growWork(t, h, bucket) } // 通过偏移量:获取bmap(cell)的内存地址,这时候是链表的首位 b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize))) bOrig := b // 获取hash的高8位 top := tophash(hash) search: for ; b != nil; b = b.overflow(t) { for i := uintptr(0); i < bucketCnt; i++ { if b.tophash[i] != top { // 如果该cell已经被标识为全部都为空,代表着后面的不需要去查询判断,快速结束 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 } if t.indirectkey() { *(*unsafe.Pointer)(k) = nil } else if t.key.ptrdata != 0 { memclrHasPointers(k, t.key.size) } // 获取对应的value e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize)) // 清除value if t.indirectelem() { *(*unsafe.Pointer)(e) = nil } else if t.elem.ptrdata != 0 { memclrHasPointers(e, t.elem.size) } else { memclrNoHeapPointers(e, t.elem.size) } // 标识该cell可用 b.tophash[i] = emptyOne if i == bucketCnt-1 { if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest { goto notLast } // 说明上一个bucket的topHash[0]已经被设置为了emptyRest,既整个bucket都是可用的 } else { if b.tophash[i+1] != emptyRest { goto notLast } // 说明下一个topHash已经被设置为了emptyRest,则之前的都是可用的 } // 则设置为emptyRest 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-- } if b.tophash[i] != emptyOne { break } } notLast: h.count-- // Reset the hash seed to make it more difficult for attackers to // repeatedly trigger hash collisions. See issue 25237. if h.count == 0 { h.hash0 = fastrand() } break search } } // 消除保护位 if h.flags&hashWriting == 0 { throw("concurrent map writes") } h.flags &^= hashWriting }
Map的获取
-
func mapaccessK(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, unsafe.Pointer) { if h == nil || h.count == 0 { return nil, nil } hash := t.hasher(key, uintptr(h.hash0)) m := bucketMask(h.B) // 通过hash的低8位,获取得到对应的bucket地址 b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + (hash&m)*uintptr(t.bucketsize))) // 说明正在扩容 if c := h.oldbuckets; c != nil { // 如果不是等size扩容 if !h.sameSizeGrow() { // 则获取得到之前的bucket的地址 // There used to be half as many buckets; mask down one more power of two. m >>= 1 } oldb := (*bmap)(unsafe.Pointer(uintptr(c) + (hash&m)*uintptr(t.bucketsize))) if !evacuated(oldb) { // 如果之前的还没有被rehash,说明数据还在原来的地方,则利用之前的bucket b = oldb } } top := tophash(hash) bucketloop: // 遍历cell进行匹配,然后找到则返回结果 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 bucketloop } continue } k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) if t.indirectkey() { k = *((*unsafe.Pointer)(k)) } 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 k, e } } } return nil, nil }
总结
-
golang map中设置了一大堆状态变量,如emptyOne,emptyRest 等等,用处在于可以快速的失败
-
map在底层内部是 hmap+bmap实现,hash冲突的解决方法与Java类似,也是通过拉链法解决,默认情况下,一个bmap只能存储8个k,v ,并且k,v在bmap中的内存模型为key紧密排列之后再value紧密排列,原因在于减少padding
-
bmap的内部的overflow指向的是hmap的extra ,作用在于避免被gc扫描
-
与Java类似,也有一个关键的负载因子,golang 默认为6.5 ,该值的计算方式为 count / bucket的数量 ,既 计算的结果为 每个bucket推荐存储的cell数量
-
golang 的map的扩容采取的是与redis类似的,渐进式rehash,既一次只扩容2个bucket,同时golang的扩容时机分两种,一种是达到负载因子,还有一种则是过多的overflowBucket (这个值的最大值为 2^15个),达到负载因子的扩容,会导致整个bucket的数量扩容一倍,后者则是等size扩容
-
golang rehash的时候与Java类似,也是 要么是在原地,要么是当前bucket的位置的两倍,具体实现是通过 原先的hash & 1
-
基本流程都是一样的
- golang map bucket定位是通过 hash的低6位来定位得到bucket,得到bucket之后,通过hash的高8位,得到topHash的下标
- 如果该bucket找不到对应的值,则到该bucket的overflow bucket中定位
- 然后是遍历内部的cell ,topHash匹配,则开始对应的处理
- 查找
- 如果当前正在扩容,既oldBuckets 不为空,则会先判断是samesize扩容还是已被扩容, 如果是双倍扩容,则先获取之前的bucket地址,判断是否已经全部rehash,未结束的话,则用原来的bucket
- 如果对应的topHash中有匹配上的,则直接返回
- 添加
- 判断是否正在扩容,正处于扩容的话,会先辅助扩容
- 然后遍历bucket中的cell,如果有重复的,则更新,否则的话插入新的数据
- 最后判断是否满足扩容的两个条件,是的话,则开始准备扩容,但是不是直接扩容,而是标记为可以扩容了
- 删除
- 同样,也会判断是否正在扩容,是的话,辅助扩容
- 然后遍历bucket中的cell,匹配则置空,最后会优化判断是否之前的bucket也是empty了(辅助以后的操作)
问题
-
map无序的原因在于
- rehash的时候会重新计算一次hash,加入一个随机因子
-
bmap中overflow的作用
- 作用在于当bucket溢出时(因为bucket中的元素个数是固定的8个),既当有第9个key也在这个bucket的时候,则会创建一个bucket.然后通过overflow指针连接形成链表
- 为什么bucket中的个数是固定的
- 因为bmap的tophash是高8位hash ,所以也就8位了(但是好像没啥依据)
- overflow 个数什么时候增加
- 当put的时候,这个bucket元素满了,并且overflow的bucket都满了,则会申请创建一个新的bucket,指向overflow
-
啥是tophash,作用是啥
- tophash是hash的高8位
- 作用在于:
- tophash是hash的高8位,作用在于快速定位, 因为每个bucket都有一个hash,这个topHash 可以与其快速匹配,不满足则快速下一个
-
扩容的时机
-
- 当 每个bucket中的cell个数>= loadFactor的时候
- overflow的个数> 2^15 方的时候(最大值为2^15)
-
-
为什么bmap采取的是key/key/key/value/value的形式,而非key/value的形式
- 还是与操作系统有关,操作系统cache 是以cache line的形式存储在cache block中,每一行的大小是固定的,如果同一个数据缓存在2个cache line中,既命中率低,则效率低,所以会有padding ,map的前者这种形式,使得padding只需要放到value的最后就行,而不需要 key/value/padding 这种形式
-
hmap中flags的作用,B的作用
- flags的作用在于判断是否处于并发读写状态,写的时候会标识为写状态,读同理