- map声明示例(注意点声明不会分配内存需要执行make才可以,分配内存后才能赋值使用)
- slice,map,function不可以作为key,因为不可以进行等值判断)
- 假设在map中已经存在三个节点,现在插入第四个
- 先通过hash函数计算,然后根据map 大小长度对其取模,如果计算出来的位置已经有其他信息占据,则会往后移动直到找到空位置
- 假设hashmap中已经存在一个节点,现在继续往里面加入一个
- 通过hashi计算,如果计算出来的位置上已经存在其他信息,则是通过链表的形式往下加入
- count: 指的是当前map的大小,即当前存放的元素个数,使用len()函数获取长度时,实际就是过unsafe.Poiner读取的该值
- flags: 用来表示当前map的状态,例如当前map是否是写入状态等,在多个地方需要使用此值,如mapaccess1()
- B: 代表 bucket 数组的容量(bucket capacity),也就是 bucket 数组中 bucket 的最大个数,但需要考虑一个重要因素装载因子,所以真正可以使用的桶只有loadFactor * 2^B 个,如果超出此值将触发扩容,默认装载因子是6.5
- noverflow 溢出buckets数量
- hash0: 生产hash码的hash种子,随机数种子,在操作键值的时候,需要引入此值加入随机性
- buckets 底层数组指针,指向当前map的底层数组指针
- oldbuckets 同是底层数组指针,一般在进行map迁移的时候,用来指向原来旧数组。只有迁移过程中此字段才有意义,此字段数组大小只有buckets 的一半
- nevacuate 进度计算器,表示扩容进度,小于此地址的 buckets 迁移完成
- extra 可选字段,溢出桶专用,只要有桶装满就会使用
// A header for a Go map.
type hmap struct {
count int // 元素个数,调用 len(map) 时,直接返回此值
flags uint8 //代表当前 map 的状态(是否处于正在写入的状态等)
B uint8 // 指示bucket数组的大小,也就是 bucket 数组中 bucket 的最大个数(buckets 的对数 log_2)
//map中溢出桶的数量。当溢出的桶太多时,map 会进行 same-size map growth,其实质是避免桶过大导致内存泄露
noverflow uint16
hash0 uint32 // 代表生成 hash 的随机数种子
buckets unsafe.Pointer // 指向 buckets 数组,大小为 2^B,如果元素个数为0,就为 nil
oldbuckets unsafe.Pointer // 是在 map 扩容时存储旧桶的,当所有旧桶中的数据都已经转移到了新桶中时,则清空
nevacuate uintptr // 在扩容时使用,用于标记当前旧桶中小于 nevacuate 的数据都已经转移到了新桶中
extra *mapextra // 存储 map 中的溢出桶
}
- tophash: 长度为8的数组,在计算key的hash值时存储数据时,分高位低位,如果低位相同,会将高位存储在该数组中,以方便后续匹配
- data区存放的是key-value数据,存放顺序是key/key/key/…value/value/value,注意,没有将每个键值单独存储在一个字段中,主要是为了解决内存对齐带来的浪费问题,例如map[int64]int8 ,如果按key/value/key/value/… 这种方式存储的话,则在每一个key/value之后需要padding7个字节才行。按key/key/key/… 和 value/value/value/…方式存储时这种方式只需要在最后一个值的后面进行一次padding就可以了
- overflow 指针指向的是下一个bucket,当一个bucket存满类,则会创建新的bucket,并使用 overflow 字段进行bucket之间的链接,形成单向链表功能
- 注意: data和overflow并不是在结构体中显示定义的,而是直接通过指针运算进行访问的
//runtime/map.go/bmap
type bmap struct {
tophash [8]uint8 //存储哈希值的高8位
data byte[1] //key value数据:key/key/key/.../value/value/value...
overflow *bmap //溢出bucket的地址
}
- 在桶内会根据 key 计算hash 值的高 8 位来决定 key 到底落入桶内的哪个位置(一个桶内最多有 8 个位置)
- 桶在存储的 tophash 字段后,会存储 key 数组和 value 数组
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}
- 早期版本中桶默认大小为 8 个元素, Go 1.17 版本及其以后的版本中,默认情况下每个桶的大小为 6 个键值对
- 在扩容过程中每个桶的大小也可能会随之改变,所以桶的大小不是一个固定的值
minTopHash 不同版本默认值不同,1.17 版本之前默认 4, 在1.17版本为了减小哈希表扩容时的内存占用则被改为了 2(???)
func evacuated(b *bmap) bool {
h := b.tophash[0]
//emptyOne =0, minTopHash=5
return h > emptyOne && h < minTopHash
}
type mapextra struct {
//用于存储溢出 bucket 中的键值对
overflow *[]*bmap
oldoverflow *[]*bmap
//指向下一个空的溢出桶的指针
nextOverflow *bmap
}
const (
// 一个桶中最多能装载的键值对(key-value)的个数为8
bucketCntBits = 3
bucketCnt = 1 << bucketCntBits
// 触发扩容的装载因子为13/2=6.5
loadFactorNum = 13
loadFactorDen = 2
// 键和值超过128个字节,就会被转换为指针
maxKeySize = 128
maxElemSize = 128
// 数据偏移量应该是bmap结构体的大小,它需要正确地对齐。
// 对于amd64p32而言,这意味着:即使指针是32位的,也是64位对齐。
dataOffset = unsafe.Offsetof(struct {
b bmap
v int64
}{}.v)
// 每个桶(如果有溢出,则包含它的overflow的链接桶)在搬迁完成状态(evacuated* states)下,
//要么会包含它所有的键值对,要么一个都不包含(但不包括调用evacuate()方法阶段,
//该方法调用只会在对map发起write时发生,在该阶段其他goroutine是无法查看该map的)。
//简单的说,桶里的数据要么一起搬走,要么一个都还未搬。
// tophash除了放置正常的高8位hash值,还会存储一些特殊状态值(标志该cell的搬迁状态)。
//正常的tophash值,最小应该是5,以下列出的就是一些特殊状态值。
emptyRest = 0 // 表示cell为空,并且比它高索引位的cell或者overflows中的cell都是空的。(初始化bucket时,就是该状态)
emptyOne = 1 // 空的cell,cell已经被搬迁到新的bucket
evacuatedX = 2 // 键值对已经搬迁完毕,key在新buckets数组的前半部分
evacuatedY = 3 // 键值对已经搬迁完毕,key在新buckets数组的后半部分
evacuatedEmpty = 4 // cell为空,整个bucket已经搬迁完毕
minTopHash = 5 // tophash的最小正常值
// flags
iterator = 1 // 可能有迭代器在使用buckets
oldIterator = 2 // 可能有迭代器在使用oldbuckets
hashWriting = 4 // 有协程正在向map写人key
sameSizeGrow = 8 // 等量扩容
// 用于迭代器检查的bucket ID
noCheck = 1<<(8*sys.PtrSize) - 1
)
- 首先调用MulUintptr()根据 hint 计算 map 需要的空间大小
- 通过new()函数初始化hmap,并调用fastrand()获取一个随机数,设置为随机种子
- 通过overLoadFactor(hint, B)函数检查当前 map 的负载因子是否超过阈值默认6.5,如果超过阈值,则需要扩容,累加B,
- 如果B==0进行延时初始化操作(赋值的时候才进行初始化),否则分配 bucket 数组的空间,并将其初始化为零值中的在makeBucketArray()函数中
// src/runtime/map.go
// 用于创建一个新的 map
func makemap(t *maptype, hint int, h *hmap) *hmap {
// 计算 map 需要的空间大小
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
// 初始化 Hmap
if h == nil {
h = new(hmap)
}
h.hash0 = fastrand() // 生成用于计算哈希值的随机数
// 计算 bucket 数量
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
// 分配 bucket 数组的空间,并将其初始化为零值
if h.B != 0 {
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil { // 如果存在溢出 bucket 数组,则分配额外的空间来存储
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
// 判断 bucket 数量是否超过负载因子的上限
func overLoadFactor(n, B uint8) bool {
return float64(n) > loadFactor * float64(uint(1)<<(B+1))
}
//不同版本overLoadFactor()内部实现不同?
func overLoadFactor(count int, B uint8) bool {
return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}
- 调用bucketShift()计算 bucket 数组的大小
- 对于b<4的话(桶的数量< 2^4),基本不需要溢出桶,对于b>4(桶的数量> 24)的话,则需要创建2(b-4)个溢出桶,并将其连接到主 bucket 数组的末尾
- 数组分配通过函数 newarray() 实现,第一个参数是元素类型,每二个参数是数组长度,返回的是数组内存首地址
// src/runtime/map.go
// 用于创建 bucket 数组
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
// 计算 bucket 数组的大小
base := bucketShift(b)
nbuckets := base
if b >= 4 {
// 如果 bucket 数量比较大,则估算所需的溢出 bucket 数量
nbuckets += bucketShift(b - 4)
sz := t.bucket.size * nbuckets
up := roundupsize(sz)
// 确保 bucket 数组的大小是 t.bucket.size 的倍数
if up != sz {
nbuckets = up / t.bucket.size
}
}
// 分配 bucket 数组的空间,并将其初始化为零值
if dirtyalloc == nil {
buckets = newarray(t.bucket, int(nbuckets))
} else {
buckets = dirtyalloc
size := t.bucket.size * nbuckets
if t.bucket.ptrdata != 0 {
memclrHasPointers(buckets, size)
} else {
memclrNoHeapPointers(buckets, size)
}
}
// 如果存在溢出 bucket 数组,则进行初始化
if base != nbuckets {
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
}
- 在桶内会根据 key 计算hash 值的高 8 位来决定 key 到底落入桶内的哪个位置(一个桶内最多有 8 个位置)。
- 桶在存储的 tophash 字段后,会存储 key 数组和 value 数组
- 执行hasher()计算插入数据key的hash值
- 执行bucket := hash & bucketMask(h.B),根据计算出的hash值确认当前key存储的桶
- 遍历所属桶和此桶串联的溢出桶,寻找key(通过桶的tohash字段和key值)
- 判断是否需要扩容,需要则执行growWork(t, h, bucket)
- 如果当前链上所有桶都满了,创建一个新的溢出桶,串联在末尾,然后更新相关字段
- 根据key是否存在,在桶中更新或者新增key/value值
// 往map中添加元素/修改元素值
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
//校验 map 是否为 nil
if h == nil {
panic(plainError("assignment to entry in nil map"))
}
//如果开启了竞争检测,则记录写操作
if raceenabled {
callerpc := getcallerpc()
pc := funcPC(mapassign)
racewritepc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
//如果开启了内存检测,则标记 key 内存区域为已读
if msanenabled {
msanread(key, t.key.size)
}
//校验map是否未初始化,或正在并发写操作,如果存在,则抛出异常
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
// 第一部分: 确认哈希值
hash := t.hasher(key, uintptr(h.hash0))
//标记当前正在写入 map
h.flags ^= hashWriting
//如果 map 为空,先进行初始化
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
// 第二部分: 根据hash值确认key所属的桶
bucket := hash & bucketMask(h.B)
//判断是否需要扩容
if h.growing() {
//执行扩容逻辑
growWork(t, h, bucket)
}
//找到 key 所在的桶
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
top := tophash(hash)
//遍历桶和溢出桶,查找 key
var inserti *uint8
var insertk unsafe.Pointer
var elem unsafe.Pointer
bucketloop:
// 第三部分: 遍历所属桶和此桶串联的溢出桶,寻找key(通过桶的tohash字段和key值)
for {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
//如果桶中当前位置为空,记录该位置
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
}
//如果找到了 key,更新 elem 的值
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
if !t.key.equal(key, k) {
continue
}
if t.needkeyupdate() {
typedmemmove(t.key, k, key)
}
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
goto done
}
//如果当前桶没有找到 key,查找溢出桶
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
//如果 map 已经满,触发扩容
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
//注意hashGrow()只是为扩容做的一些准备工作,例如申请内存,而真正进行扩容的是 growWork() 和 evacuate()函数
hashGrow(t, h)
goto again
}
// 第四部分: 当前链上所有桶都满了,创建一个新的溢出桶,串联在末尾,然后更新相关字段
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是否存在,在桶中更新或者新增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
}
//复制 key 的值到 insertk 指向的内存区域中
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
}
- 假设hash值为: 10010111 | 000011110110110010001111001010100010010110010101010 │ 00110
- h.B = 5,则取后面的5位 00110,值为6,则需要将key:value存放在第6号的bucket中
// 一个桶中最多能装载的键值对(key-value)的个数为8
bucketCntBits = 3
bucketCnt = 1 << bucketCntBits
- 执行hasher()计算hash值并根据hash值找到桶
- 遍历桶和桶串联的溢出桶,寻找key
- 如果根据hash值定位到桶正在进行搬迁,并且这个bucket还没有搬迁到新哈希表中,那么就从老的哈希表中找。
- 在bucket中进行顺序查找,使用高八位进行快速过滤,高八位相等,再比较key是否相等,找到就返回value。如果当前bucket找不到,就往下找溢出桶,都没有就返回零值
// map中查找元素
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if raceenabled && h != nil {
callerpc := getcallerpc()
pc := funcPC(mapaccess1)
racereadpc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
if msanenabled && h != nil {
msanread(key, t.key.size)
}
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.hasher(key, 0) // see issue 23734
}
return unsafe.Pointer(&zeroVal[0])
}
//校验map是否未初始化,或正在并发写操作,如果存在,则抛出异常
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}
// 第一部分:计算hash值并根据hash值找到桶
hash := t.hasher(key, uintptr(h.hash0))
m := bucketMask(h.B)
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
if c := h.oldbuckets; c != nil {
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)))
if !evacuated(oldb) {
b = oldb
}
}
top := tophash(hash)
// 第二部分:遍历桶和桶串联的溢出桶,寻找key
bucketloop:
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 e
}
}
}
return unsafe.Pointer(&zeroVal[0])
}
- 首先会判断当前操作的map中写保护表示如果不是0,抛出异常
- 执行hashe()计算当前key的hash值
- 执行hash & bucketMask(h.B),根据hash值获取到对应的桶,并判断是否需要扩容,迁移,需要则执行growWork()
- 遍历桶和桶串联的溢出桶
- 找到key,然后将桶的该key的tohash值置空,相当于删除值
- 执行"h.flags&hashWriting == 0"解除写保护
- 删除key仅仅只是将其对应的tohash值置空,如果kv存储的是指针,那么会清理指针指向的内存,否则不会真正回收内存,内存占用并不会减少
- 如果正在扩容,并且操作的bucket没有搬迁完,那么会搬迁bucket
// map中删除元素
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
if raceenabled && h != nil {
callerpc := getcallerpc()
pc := funcPC(mapdelete)
racewritepc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
if msanenabled && h != nil {
msanread(key, t.key.size)
}
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.hasher(key, 0) // see issue 23734
}
return
}
// 第一部分: 写保护,校验map是否未初始化,或正在并发写操作,如果存在,则抛出异常
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
// 第二部分: 获取hash值
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
// 第三部分: 根据hash值确定桶,
bucket := hash & bucketMask(h.B)
//并判断是否需要扩容
if h.growing() {
//如果需要执行扩容,数据迁移逻辑
growWork(t, h, bucket)
}
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
bOrig := b
top := tophash(hash)
// 第四部分:遍历桶和桶串联的溢出桶
search:
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,然后将桶的该key的tohash值置空,相当于删除值了
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
}
// Only clear key if there are pointers in it.
if t.indirectkey() {
*(*unsafe.Pointer)(k) = nil
} else if t.key.ptrdata != 0 {
memclrHasPointers(k, t.key.size)
}
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)
}
b.tophash[i] = emptyOne
// If the bucket now ends in a bunch of emptyOne states,
// change those to emptyRest states.
// It would be nice to make this a separate function, but
// for loops are not currently inlineable.
if i == bucketCnt-1 {
if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
goto notLast
}
} else {
if b.tophash[i+1] != emptyRest {
goto notLast
}
}
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--
break search
}
}
// 第六部分: 解除写保护
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
h.flags &^= hashWriting
}
- map 底层的hmap结构中维护了 count 和 B(桶数组的长度)两个值
- count 表示当前 map 中的 key-value 对儿数量
- B 表示桶数组的长度,也就是 map 所拥有的最大桶数量,由于方便哈希计算桶数量始终是一个 2 的整数幂,因此 B 也表示实际的桶数量:2^B 个桶
- 通过把 count 除以 2^B ,就可以得到 map 的负载因子,即 key-value 对儿与桶的平均比值,特就是"负载因子 = 键数量/bucket数量", 例如一个bucket数量为4,包含4个键值对的哈希表来说,这个哈希表的负载因子为1
- 增量扩容: 例如当装载因子"键数量/bucket数量"超过6.5的时触发的扩容,就是说元素太多,执行扩容添加一倍的桶,保证元素个数最多只能达到桶总容量的65%
- 等量扩容: 例如先是大量的添加元素,后来又删除了,导致出现了太多的空桶和overflow溢出桶,查找key时,就需要遍历多桶,效率下降,这时就需要重新对元素进行位置调整,类似碎片整理一样,开辟一个新 bucket 空间,将溢出桶中的元素向前面的bucket迁移,将老 bucket 中的元素移动到新 bucket,使得同一个 bucket 中的 key 排列地更紧密, 并且: 如果插入 map 的 key 发生hash冲突落到同一个 bucket ,超过桶的容量时个就会产生 overflow bucket溢出桶,造成溢出桶过多,移动元素也解决不了这个问题,最终造成哈希表退化成了一个链表,操作效率变成了 O(n),等量扩容时创建和旧桶数目一样多的新桶,把原来的键值对迁移到新桶中
- 当B小于15时,也就是常规桶 2^ B 小于 2^ 15 时,并且溢出桶大于2^B触发等量扩容;
- 当 B 大于等于 15,也就是常规桶总数 2^ B 大于等于 2^ 15,并且溢出桶的数量超过 2^15,触发等量扩容。
- overLoadFactor(): 判断负载因子是否大于6.5
- tooManyOverflowBuckets(): 判断buckets 数量是否过多
- growing()是否已经发生扩容,还未迁移完
- 如果上方条件满足执行hashGrow(),申请内存分配新的buckets,并将老的buckets挂到oldbuckets字段上, 真正搬迁的动作在growWork()中
if !h.growing() && (overLoadFactor(int64(h.count), h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
}
// overLoadFactor reports whether count items placed in 1<
// 装载因子>6.5
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<
// 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)
}
只是分配新的buckets,并将老的buckets挂到oldbuckets字段上, 真正搬迁的动作在growWork()中
func hashGrow(t *maptype, h *hmap) {
// B+1 相当于是原来 2 倍的空间
bigger := uint8(1)
// 对应条件 2
if !overLoadFactor(int64(h.count), h.B) {
// 进行等量的内存扩容,所以 B 不变
bigger = 0
h.flags |= sameSizeGrow
}
// 将老 buckets 挂到 buckets 上
oldbuckets := h.buckets
// 申请新的 buckets 空间
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger)
// x = 01010011
// y = 01010100
// z = x &^ y = 00000011
// 如果 y bit 位为 1,那么结果 z 对应 bit 位就为 0,否则 z 对应 bit 位就和 x 对应 bit 位的值相同。
//
// 先把 h.flags 中 iterator 和 oldIterator 对应位清 0,然后如果发现 iterator 位为 1,
// 那就把它转接到 oldIterator 位,使得 oldIterator 标志位变成 1。
// 潜台词就是:buckets 现在挂到了 oldBuckets 名下了,对应的标志位也转接过去吧。
flags := h.flags &^ (iterator | oldIterator)
if h.flags&iterator != 0 {
flags |= oldIterator
}
// 提交 grow 的动作
h.B += bigger
h.flags = flags
h.oldbuckets = oldbuckets
h.buckets = newbuckets
// 搬迁进度为 0
h.nevacuate = 0
// overflow buckets 数为 0
h.noverflow = 0
// ……
}
扩容完成后需要做数据的迁移,执行growWork()函数,数据的迁移不是一次完成的,是使用时才会做对应bucket的迁移。也就是逐步做到的数据迁移
func growWork(t *maptype, h *hmap, bucket uintptr) {
// 确认搬迁老的 bucket 对应正在使用的 bucket
// 为了确认搬迁的 bucket 是我们正在使用的 bucket。
// oldbucketmask() 函数返回扩容前的 map 的 bucketmask
evacuate(t, h, bucket&h.oldbucketmask())
// 再搬迁一个 bucket,以加快搬迁进程
if h.growing() {
evacuate(t, h, h.nevacuate)
}
}
//如果 oldbuckets 不为空,说明还需要继续迁移
func (h *hmap) growing() bool {
return h.oldbuckets != nil
}
- 首先定位到老的 bucket 地址
- 执行evacuated() 判断当前bucket桶是否已迁移
- 在迁移时,根据增量扩容,等量扩容不同策略,迁移逻辑不同, 在等量扩容时,新的 buckets 数量和之前相等,因此可以按序号来搬,比如原来在 0 号 bucktes,到新的地方后,仍然放在 0 号 buckets
- 针对增量扩容,新的 buckets 数量是之前的一倍,要重新计算 key 的哈希,才能决定它到底落在哪个 bucket, 扩容时内部声明了X, Y part变量,桶的数量是原来的 2 倍,前一半桶被称为 X part,后一半桶被称为 Y part。一个 bucket 中的 key 可能会分裂落到 2 个桶,一个位于 X part,一个位于 Y part(取决于扩容后,倒数第B位是 0 还是 1)。所以在搬迁一个 cell 之前,需要知道这个 cell 中的 key 是落到哪个 Part
func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
// 定位老的 bucket 地址
b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
// 结果是 2^B,如 B = 5,结果为32
newbit := h.noldbuckets()
// key 的哈希函数
alg := t.key.alg
// 如果 b 没有被搬迁过,先判断当前bucket是不是已经转移(oldbucket 标识需要搬迁的bucket 对应的位置)
if !evacuated(b) {
var (
// 表示bucket 移动的目标地址
x, y *bmap
// 指向 x,y 中的 key/val
xi, yi int
// 指向 x,y 中的 key
xk, yk unsafe.Pointer
// 指向 x,y 中的 value
xv, yv unsafe.Pointer
)
// 默认是等 size 扩容,前后 bucket 序号不变
// 使用 x 来进行搬迁
x = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))
xi = 0
xk = add(unsafe.Pointer(x), dataOffset)
xv = add(xk, bucketCnt*uintptr(t.keysize))、
// 如果不是等 size 扩容,前后 bucket 序号有变
// 使用 y 来进行搬迁
if !h.sameSizeGrow() {
// y 代表的 bucket 序号增加了 2^B
y = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))
yi = 0
yk = add(unsafe.Pointer(y), dataOffset)
yv = add(yk, bucketCnt*uintptr(t.keysize))
}
// 遍历所有的 bucket,包括 overflow buckets
// b 是老的 bucket 地址
for ; b != nil; b = b.overflow(t) {
k := add(unsafe.Pointer(b), dataOffset)
v := add(k, bucketCnt*uintptr(t.keysize))
// 遍历 bucket 中的所有 cell
for i := 0; i < bucketCnt; i, k, v = i+1, add(k, uintptr(t.keysize)), add(v, uintptr(t.valuesize)) {
// 当前 cell 的 top hash 值
top := b.tophash[i]
// 如果 cell 为空,即没有 key
if top == empty {
// 那就标志它被"搬迁"过
b.tophash[i] = evacuatedEmpty
// 继续下个 cell
continue
}
// 正常不会出现这种情况
// 未被搬迁的 cell 只可能是 empty 或是
// 正常的 top hash(大于 minTopHash)
if top < minTopHash {
throw("bad map state")
}
k2 := k
// 如果 key 是指针,则解引用
if t.indirectkey {
k2 = *((*unsafe.Pointer)(k2))
}
// 默认使用 X,等量扩容
useX := true
// 如果不是等量扩容
if !h.sameSizeGrow() {
// 计算 hash 值,和 key 第一次写入时一样
hash := alg.hash(k2, uintptr(h.hash0))
// 如果有协程正在遍历 map
if h.flags&iterator != 0 {
// 如果出现 相同的 key 值,算出来的 hash 值不同
if !t.reflexivekey && !alg.equal(k2, k2) {
// 只有在 float 变量的 NaN() 情况下会出现
if top&1 != 0 {
// 第 B 位置 1
hash |= newbit
} else {
// 第 B 位置 0
hash &^= newbit
}
// 取高 8 位作为 top hash 值
top = uint8(hash >> (sys.PtrSize*8 - 8))
if top < minTopHash {
top += minTopHash
}
}
}
// 取决于新哈希值的 oldB+1 位是 0 还是 1
// 详细看后面的文章
useX = hash&newbit == 0
}
// 如果 key 搬到 X 部分
if useX {
// 标志老的 cell 的 top hash 值,表示搬移到 X 部分
b.tophash[i] = evacuatedX
// 如果 xi 等于 8,说明要溢出了
if xi == bucketCnt {
// 新建一个 bucket
newx := h.newoverflow(t, x)
x = newx
// xi 从 0 开始计数
xi = 0
// xk 表示 key 要移动到的位置
xk = add(unsafe.Pointer(x), dataOffset)
// xv 表示 value 要移动到的位置
xv = add(xk, bucketCnt*uintptr(t.keysize))
}
// 设置 top hash 值
x.tophash[xi] = top
// key 是指针
if t.indirectkey {
// 将原 key(是指针)复制到新位置
*(*unsafe.Pointer)(xk) = k2 // copy pointer
} else {
// 将原 key(是值)复制到新位置
typedmemmove(t.key, xk, k) // copy value
}
// value 是指针,操作同 key
if t.indirectvalue {
*(*unsafe.Pointer)(xv) = *(*unsafe.Pointer)(v)
} else {
typedmemmove(t.elem, xv, v)
}
// 定位到下一个 cell
xi++
xk = add(xk, uintptr(t.keysize))
xv = add(xv, uintptr(t.valuesize))
} else { // key 搬到 Y 部分,操作同 X 部分
// ……
// 省略了这部分,操作和 X 部分相同
}
}
}
// 如果没有协程在使用老的 buckets,就把老 buckets 清除掉,帮助gc
if h.flags&oldIterator == 0 {
b = (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
// 只清除bucket 的 key,value 部分,保留 top hash 部分,指示搬迁状态
if t.bucket.kind&kindNoPointers == 0 {
memclrHasPointers(add(unsafe.Pointer(b), dataOffset), uintptr(t.bucketsize)-dataOffset)
} else {
memclrNoHeapPointers(add(unsafe.Pointer(b), dataOffset), uintptr(t.bucketsize)-dataOffset)
}
}
}
// 更新搬迁进度
// 如果此次搬迁的 bucket 等于当前进度
if oldbucket == h.nevacuate {
// 进度加 1
h.nevacuate = oldbucket + 1
// 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 个 bucket
stop := h.nevacuate + 1024
if stop > newbit {
stop = newbit
}
// 寻找没有搬迁的 bucket
for h.nevacuate != stop && bucketEvacuated(t, h, h.nevacuate) {
h.nevacuate++
}
// 现在 h.nevacuate 之前的 bucket 都被搬迁完毕
// 所有的 buckets 搬迁完毕
if h.nevacuate == newbit {
// 清除老的 buckets
h.oldbuckets = nil
// 清除老的 overflow bucket
// 回忆一下:[0] 表示当前 overflow bucket
// [1] 表示 old overflow bucket
if h.extra != nil {
h.extra.overflow[1] = nil
}
// 清除正在扩容的标志位
h.flags &^= sameSizeGrow
}
}
}
- count: 指的是当前map的大小,即当前存放的元素个数,使用len()函数获取长度时,实际就是过unsafe.Poiner读取的该值
- flags: 用来表示当前map的状态,例如当前map是否是写入状态等,在多个地方需要使用此值,如mapaccess1()
- B: bucket 数组的容量(bucket capacity),也就是 bucket 数组中 bucket 的最大个数,但需要考虑一个重要因素装载因子,所以真正可以使用的桶只有loadFactor * 2^B 个,如果超出此值将触发扩容,默认装载因子是6.5
- noverflow 溢出buckets数量
- hash0: 生产hash码的hash种子,随机数种子,在操作键值的时候,需要引入此值加入随机性
- buckets 底层数组指针,指向当前map的底层数组指针
- oldbuckets 同是底层数组指针,一般在进行map迁移的时候,用来指向原来旧数组。只有迁移过程中此字段才有意义,此字段数组大小只有buckets 的一半
- nevacuate 进度计算器,表示扩容进度,小于此地址的 buckets 迁移完成
- extra 可选字段,溢出桶专用,只要有桶装满就会使用
- tophash: 长度为8的数组,在计算key的hash值时存储数据时,分高位低位,如果低位相同,会将高位存储在该数组中,以方便后续匹配
- data区: 存放的是key-value数据,为了解决内存对齐带来的浪费问题存放顺序是key/key/key/…value/value/value,例如map[int64]int8 ,如果按key/value/key/value/… 这种方式存储的话,则在每一个key/value之后需要padding7个字节才行,按key/key/key/… 和 value/value/value/…方式存储时这种方式只需要在最后一个值的后面进行一次padding就可以了)实际在编译后会把data区分为keys,values两个数组属性
- overflow 指向的是下一个bucket的指针,一个bucket每8个kv键值对存放在一个桶中存满类,则会创建新的bucket,并使用 overflow 字段进行bucket之间的链接,形成单向链表功能
- 首先调用MulUintptr()根据 hint 计算 map 需要的空间大小
- 通过new()函数初始化hmap,并调用fastrand()获取一个随机数,设置为随机种子
- 通过overLoadFactor(hint, B)函数检查当前 map 的负载因子是否超过阈值默认6.5,如果超过阈值,则需要扩容,累加B,
- 如果B==0进行延时初始化操作(赋值的时候才进行初始化),否则分配 bucket 数组的空间,并将其初始化为零值中的在makeBucketArray()函数中
- 调用bucketShift()计算 bucket 数组的大小
- 对于b<4的话(桶的数量< 2^4),基本不需要溢出桶,对于b>4(桶的数量> 24)的话,则需要创建2(b-4)个溢出桶,并将其连接到主 bucket 数组的末尾
- 数组分配通过函数 newarray() 实现,第一个参数是元素类型,每二个参数是数组长度,返回的是数组内存首地址
- 执行hasher()计算插入数据key的hash值
- 执行bucket := hash & bucketMask(h.B),根据计算出的hash值确认当前key存储的桶
- 遍历所属桶和此桶串联的溢出桶,寻找key(通过桶的tohash字段和key值)
- 判断是否需要扩容,需要则执行growWork(t, h, bucket)
- 如果当前链上所有桶都满了,创建一个新的溢出桶,串联在末尾,然后更新相关字段
- 根据key是否存在,在桶中更新或者新增key/value值
- 在存储数据时首先会调用hasher()计算key的hash值
- 通过了解map底层hmap结果,内部有一个B属性表示bucket 数组的容量(bucket capacity),也就是 bucket 数组中 bucket 的最大个数
- 拿到hash值后取模2B次方,如果B为5则hash取模25=32,拿到的编号就是对应桶的编号
- 如果计算出的桶号中已经存在其他 key,通过链地址法将相同桶号的 key 串联成一个链表
- 接下来定位cell,Cell位置计算公式为执行tophash()获取hash的高8位,然后遍历桶中所有cell,找到bmap.tophash值为当前计算出的cell进行存储
- 如果当前bucket中没有找到,就去当前bucket 中的溢出桶overflow中查找,如果overflow为nil,则直接返回空值即可
- 执行hasher()计算hash值并根据hash值找到桶
- 遍历桶和桶串联的溢出桶,寻找key
- 如果根据hash值定位到桶正在进行搬迁,并且这个bucket还没有搬迁到新哈希表中,那么就从老的哈希表中找。
- 在bucket中进行顺序查找,使用高八位进行快速过滤,高八位相等,再比较key是否相等,找到就返回value。如果当前bucket找不到,就往下找溢出桶,都没有就返回零值
- 该函数内会对B的数量进行+1,也就是扩容为原理的2倍,分配新的buckets,并将老的buckets挂到oldbuckets字段上,
- 查看插入与删除元素时内部执行的growWork()函数,这才是真正搬迁的底层函数,数据的迁移不是一次完成的,是使用时才会做对应bucket的迁移,该函数内部会调用一个evacuate()
- 首先定位到老的 bucket 地址
- 执行evacuated() 判断当前bucket桶是否已迁移
- 在迁移时,根据增量扩容,等量扩容不同策略,迁移逻辑不同, 在等量扩容时,新的 buckets 数量和之前相等,因此可以按序号来搬,比如原来在 0 号 bucktes,到新的地方后,仍然放在 0 号 buckets
- 针对增量扩容,新的 buckets 数量是之前的一倍,要重新计算 key 的哈希,才能决定它到底落在哪个 bucket, 扩容时内部声明了X, Y part变量,桶的数量是原来的 2 倍,前一半桶被称为 X part,后一半桶被称为 Y part。一个 bucket 中的 key 可能会分裂落到 2 个桶,一个位于 X part,一个位于 Y part(取决于扩容后,倒数第B位是 0 还是 1)。所以在搬迁一个 cell 之前,需要知道这个 cell 中的 key 是落到哪个 Part
- map 在扩容后会发生 key 的迁移,原来落在同一个 bucket 中的 key,迁移后位置可能就会变(bucket 序号加上了 2^B)。而遍历的过程,就是按顺序遍历 bucket,同时按顺序遍历 bucket 中的 key。迁移后有的key 的位置发生了变化,有些有些key没有,遍历 map 的结果就是原来的顺序了
- 当我们在遍历 go 中的 map 时,并不是固定地从 0 号 bucket 开始遍历,每次都是从一个随机值序号的 bucket 开始遍历,并且是从这个 bucket 的一个随机序号的 cell 开始遍历。这样,即使你是一个写死的 map,仅仅只是遍历它,也不太可能会返回一个固定序列的 key/value 对了。
- float 型可以作为 key,但是由于精度的问题,会导致一些诡异的问题,慎用
- 从语法上看,除了 slice,map,functions 这几种类型以外,只要是可比较的类型都可以作为 key,具体包括:布尔值、数字、字符串、指针、通道、接口类型、结构体、只包含上述类型的数组,这些类型的共同特征是支持 = = 和 != 操作符,k1 == k2 时,可认为 k1 和 k2 是同一个 key。
- 如果是结构体, hash 后的值相等并且字面值相等,认为是相同的 key。注意有些字面值相等,hash出来的值不一定相等,比如引用。
- 读之前调用 RLock() 函数,读完之后调用 RUnlock() 函数解锁;写之前调用 Lock() 函数,写完之后调用 Unlock() 解锁。
- 另外,sync.Map 是线程安全的 map,也可以使用。
如果通过其他 hack 的方式,例如 unsafe.Pointer 等获取到了 key 或 value 的地址,也不能长期持有,因为一旦发生扩容,key 和 value 的位置就会改变,之前保存的地址也就失效了。
- 都为 nil
- 非空、长度相等,指向同一个 map 实体对象
- 相应的 key 指向的 value “深度”相等: 直接将使用 map1 == map2 是错误的。这种写法只能比较 map 是否为 nil。因此只能是遍历map 的每个元素,比较元素是否都是深度相等。
可以在查找、赋值、遍历、删除的过程中都会检测写标志,一旦发现写标志置位(等于1),则直接 panic。赋值和删除函数在检测完写标志是复位之后,先将写标志位置位,才会进行之后的操作。检测写标志:
h.flags |= hashWriting
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
// src/runtime/map.go
const (
// Maximum number of key/elem pairs a bucket can hold.
bucketCntBits = 3
bucketCnt = 1 << bucketCntBits
......
}
- 根据h.B 来的值,来取出hash结果的最后 b.B 位数字(低位),定位到bucket
- 再根据hash结果的高8位,实现在bucket定位到指定的位置
- 最后根据位置分别从 bmap.key和bmap.values中读取存储的值内容
package main
import "fmt"
func main() {
ageMap := make(map[string]int)
ageMap["qcrao"] = 18
// 不带 comma 用法
age1 := ageMap["stefno"]
fmt.Println(age1)
// 带 comma 用法
age2, ok := ageMap["stefno"]
fmt.Println(age2, ok)
}
- map 的键不能是空值nil,会引发 panic。因为 map 内部使用了散列函数来计算键的哈希值,而 nil 值没有地址
- map 中的 value 可以存储空数据,但是需要注意零值和 nil 值之间的区别,如果 value 类型的零值是一个空值,可以存储,如果value 类型的零值是一个非空值,那么存储空数据时就需要使用指针类型或引用类型来代表空数据,例如使用 nil 指针或 nil 切片等
- new 函数用于为 map 分配内存空间,并返回一个指向该 map 的指针。使用 new 函数初始化 map 时,需要使用 make 函数为 map 的值赋初值
- make 函数用于创建一个非零长度的 map,并返回该 map 的值。使用 make 函数初始化 map 时,不需要再进行后续的赋初值操作
- 可以尝试调整 map 的大小,重新分配内存空间
- Go 语言中,map 内部使用的哈希函数是 Go 的运行时库提供的,可以通过设置环境变量 GOMAXPROCS 来控制哈希函数的数量。如果需要更好的哈希函数,可以使用第三方库提供的哈希函数,例如 hash/fnv 包中提供的 FNV1 和 FNV1a
- 可以尝试使用有序的 map,例如使用 Go 语言中的 sort 包中提供的排序算法
- 将 map 中的键存储到一个数组或切片中,并排序,通过数组切片获取key,然后在map中读取
- 使用有序的 map:可以使用第三方库提供的有序的 map,例如使用 github.com/elliotchance/redismap
- 使用 sync.Map
- sync.Map 内部使用了一种基于分段锁的方法来保证并发安全。具体来说,它将整个 map 分成多个小的分段,每个分段都有一个锁来保护其中的数据。当一个 goroutine 需要访问 map 中的某个键值对时,它会根据键的哈希值找到相应的分段,并尝试获取该分段的锁。只有获取到锁之后,才能访问该分段中的数据。这种方法可以有效地降低锁的竞争程度,提高并发性能
- sync.Map 提供了一些并发安全的 API,例如 Store、Load、LoadOrStore、Delete 和 Range 等方法,这些方法都是并发安全的,可以在多个 goroutine 中同时访问 map,并且不会出现数据不一致的问题