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值
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的内存模型为:
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
}
总结
函数: 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
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)
}
}
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
}
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
基本流程都是一样的
map无序的原因在于
bmap中overflow的作用
啥是tophash,作用是啥
扩容的时机
为什么bmap采取的是key/key/key/value/value的形式,而非key/value的形式
hmap中flags的作用,B的作用