map
一、简单结构
map类型的变量本质上是一个hmap类型的指针,键值对通过哈希表来储存。如下图:
二、哈希表的设计
1、结构
hash表首先有一段储存储存数据的连续空间,我们将数据存到这段连续的空间(可以把它当组成一个数组)里面,如下图所示,把key、value以及key的hash值存到连续的空间里面
2、寻址
然后就是当前的key-value具体要存到哪一个位置呢?现在主流的通常有两种方法:
1、取模运算:hash % m
2、位运算:hash&(m-1)
我们都知道位运算肯定是更加的高效,但是必须得限制m为2的整数幂。golang里面采用了幂运算的方法来寻址
3、hash冲突
当我们两个key发生hash冲突了怎么处理呢?我们也介绍两种的方法
1、拉链法:这种方法会把发生hash冲突的key-value数据用链表的方式把数据组织起来。如下图所示,当发生hash冲突的时候,后面插入的数据会链接到发生冲突的数据后面
2、开放地址法:比如下图,我们编号0的位置已经被占了,那么我们就把数据放在编号1的位置,如果编号1的位置也被占了,由此类推,我们继续寻址,直到找到空闲的地方。
当我们去查找数据的时候,我们先会查找0位置,如果不是我们查找的key,会继续向下查找,知道查找到最后的位置,很显然,这种方法不适合负载因子(后面会介绍)过大的情况
上面两种方法对比:拉链法相对于查找数据比较友好,查找的次数不会太多。开放地址法则不会产生不连续的内存空间,每次申请的都是一块连续的内存空间。在golang中,采用了类似于拉链法,为什么说类似呢?后面会详细分析一下map的数据结构。
4、扩容
当我们的数据已经远远超过8个,采用拉链法,那么hash表就会退化成链表。开发地址法就更不行了。这时候我们就会涉及到扩容的问题。扩容我们主要搞清楚两个问题就行了:何时扩容 、怎么扩容
何时扩容
还记得我们上文提到的负载因子吗?负载因子就是来标识何时扩容的一个参数。它是哈希表中存储的键值对数量和桶数量的比值,通常需要设定一个触发扩容的最大负载因子
loadFactor = keyCount / bucketCount
我们假设我们的负载因子是50%,当我们来了一个新的key12的时候,就会触发扩容
我们直接的想法是,当达到负载因子的时候,直接把buckets的容量翻倍。然后把数据都copy到新的地址空间去。如下所示
这么迁移会有个问题。当旧桶的数据非常多的时候呢?这时候迁移数据就会占用大量时间,影响我们主程序。所以几乎在所有的语言中,都会采用渐进式扩容。
这种方式下,旧桶里的键值对是分多次挪到新桶里的,可以在触发扩容后,先分配新桶,然后标记当前哈希表正在扩容,并在哈希表的读写操作中判断若在扩容中,则迁移一部分键值对到新桶里,这样可以把扩容消耗的时间分散到多次操作中。
好了,上面我们大概介绍了hash表的一些特性和问题,接下来我们再介绍一下golang中的map长什么样子。
三、map的数据结构
图解结构
map底层的哈希表通过与运算的方式选择桶,所以hmap中并不直接记录桶的个数,而是记录这个数目是2的多少次幂。下面我们先看看map使用的桶长什么样子。我们先看看之前的map基本结构图,知道golang中一个桶其实就是一个bmap。
下图是一个bmap的数据结构。我们可以看到bmap其实是一段连续内存空间。
为了内存使用更加紧凑,8个键放一起,8个值放一起。对应每个key只保存其哈希值的高8位(tophash)。而每个键值对的tophash、key和value的索引顺序一一对应。
可以看到这个其实和我们上面说的拉链法比较相似,如果发生hash冲突的时候,会在bmap中向后写入,这样既保证了查找次数较少,又能保证内存比较连续。
8个存满了怎么办?为了减少扩容次数,这里引入了溢出桶(overflow)。溢出桶的内存布局与常规桶相同。如果哈希表要分配的桶的数目大于2^4,就会预分配2^(B-4)个溢出桶备用。这些常规桶和溢出桶在内存中是连续的,只是前2^B个用作常规桶,后面的用作溢出桶。
源码分析
下面我们具体来看看源码:
type hmap struct {
count int //map中值的数量
flags uint8 //动作标识(比如正在写数据)
B uint8 // 常规桶个数等于2^B
noverflow uint16 // 溢出桶数量
hash0 uint32 // hash 种子
buckets unsafe.Pointer //桶的指针,指向的是 2^B 个bmap
oldbuckets unsafe.Pointer //旧桶的指针(扩容时使用)
nevacuate uintptr //扩容时迁移的位置
extra *mapextra // 所有溢出桶指针
}
type mapextra struct {
overflow *[]*bmap //已经用到的溢出桶
oldoverflow *[]*bmap //渐进式扩容时,保存旧桶用到的溢出桶
nextOverflow *bmap //下一个尚未使用的溢出桶
}
type bmap struct {
tophash [bucketCnt]uint8 //存储tophash的值
}
可以看到如果当前桶存满了以后,检查hmap.extra.nextoverflow还有可用的溢出桶,就在这个桶后面链上这个溢出桶,然后继续往这个溢出桶里
四、创建map
我们知道创建map一般用make(map)
的方式,下面我们看看创建map具体是怎么操作的,下图是创建map时的主流程:
- 根据需要初始化的大小来确定
B
的值 - 如果
B
的值≥4 ,确定溢出桶的数量 - 分配桶和溢出桶的内存
// make map返回的是指针
func makemap(t *maptype, hint int, h *hmap) *hmap {
if h == nil {
h = new(hmap)
}
h.hash0 = fastrand()
// 根据hint初始化桶的大小
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
// 如果桶的数量大于0,初始化桶的内存
if h.B != 0 {
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil {
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
//给map bucket分配内存
// @param t map的类型
// @param b 桶的个数2^b
// @param dirtyalloc 是否要把返回的buckets指向dirtyalloc地址
// @return buckets buckets地址
// @return nextOverflow 溢出桶地址
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
base := bucketShift(b)
nbuckets := base //bucket的数量
//当b大于4是,添加溢出桶,溢出桶的大小为 2^(b-4)个
if b >= 4 {
nbuckets += bucketShift(b - 4)
}
//申请桶的空间
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)
}
}
//有溢出桶,nextOverflow指向溢出桶的地址
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获取val
当我们使用val,ok := map[key]
会发生什么呢?同样,我们先通过一个流程图了解一下主流程:
- 判断map是否为空
- 判断是否有协程在并发的写、
- 定位到key对应的桶位置,如果map正在扩(等量)容且该桶还未被迁移,定位到旧桶的位置
- 遍历该桶的每个tophash、key,直到找到和指定key相等的数据
// @param t map的类型
// @param h map数据
//通过key获取val
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
//map为nil或者没有数据,返回零值
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.hasher(key, 0)
}
return unsafe.Pointer(&zeroVal[0])
}
//@Step1 有并发的协程正在写,抛出异常(panic)
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}
//@Step2 计算key的hash,定位到当前key的hash桶位置
hash := t.hasher(key, uintptr(h.hash0))
m := bucketMask(h.B)
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
//@Step3 判断有没有旧桶(正在迁移)
// 如果旧桶数据没有被迁移,定位到当前key在旧桶的位置
// 如果当前旧桶没有被迁移,则迁移桶
if c := h.oldbuckets; c != nil {
if !h.sameSizeGrow() { //不是进行同长度的扩容(当map被删除过多时,需要重新整合数据,避免浪费过多空间)
m >>= 1
}
oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
if !evacuated(oldb) {
b = oldb
}
}
//取top hash
top := tophash(hash)
//@Step4 从bucket中找出key对应的val,循环遍历top hash
bucketloop:
for ; b != nil; b = b.overflow(t) { //是否有溢出桶
for i := uintptr(0); i < bucketCnt; i++ {
//top hash是否相等
if b.tophash[i] != top {
if b.tophash[i] == emptyRest { //如果top hash为emptyRest,则表示后面没数据了
break bucketloop
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
// key是否相等,相等则返回对应的val
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])//返回零值
}
六、添加key value
添加key-val的时候发生了什么呢?同样,我们先来看一下流程图:
- 判断map是否为空、判断有没有协程并发写
- 如果buckets为空,申请一个长度为1的buckets
- 找出改key对应的桶位置
- 如果map正在迁移切该桶没有被迁移,迁移该桶
- 遍历该桶,如果找到相同的key,返回val的位置。如果没有找到,找出下一个空位置,赋值tophash、key,返回val的位置
- 判断map是否需要扩容,如果扩容,返回5的操作
- 如果当前buckets和溢出buckets都没有位置了,添加一个溢出buckets,赋值tophash、key到第一个空位,返回val的位置
// 新增或者替换key val m[key]=val
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
//@Step1 如果map为nil,panic
if h == nil {
panic(plainError("assignment to entry in nil map"))
}
// 判断有没有协程正在写map
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
hash := t.hasher(key, uintptr(h.hash0))
// 设置map写的标志
h.flags ^= hashWriting
//@Step2 buckets为nil,new一个
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
//@Step3 找出key hash对应的桶
bucket := hash & bucketMask(h.B)
if h.growing() {
// @Step4 如果桶需要迁移,则把旧桶迁移
growWork(t, h, bucket)
}
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
top := tophash(hash)
var inserti *uint8
var insertk unsafe.Pointer
var elem unsafe.Pointer
//@Step5 寻找map中有没有存在的key
// 5-1 如果存在,更新key的值,则返回val的位置
// 5-2 如果不存在,则记录bucket最近一个空位置的tophash 、key、value的位置
// 5-2-1 判断bucket有没有溢出,如果没有溢出,则下一步。
// 5-2-2 溢出了则找出下一个溢出桶,继续bucketloop上述操作
bucketloop:
for {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
//判断当前元素是否为空,如果为空,记录第一个为空的位置(方便找不到key时插入)
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
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if !t.key.equal(key, k) {
continue
}
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
//找到了key
goto done
}
//在常规桶中没有找到数据,在溢出桶继续找
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
//@Step6 如果添加一个元素会造成bucket超过负载(6.5),或者溢出bucket太多
// 扩充桶,返回上面逻辑bucketloop继续寻找val位置
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again
}
//@Step7 当前bucket和溢出桶都满了,重新添加一个溢出桶
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、tophash的位置。h.count +1
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++
// 返回val的位置
done:
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
h.flags &^= hashWriting
if t.indirectelem() {
elem = *((*unsafe.Pointer)(elem))
}
return elem
}
七、删除key value
删除key的时候发生了什么呢?同样,我们先来看一下流程图:
// 删除key、val
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
//判断map是否处于写状态
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
hash := t.hasher(key, uintptr(h.hash0))
//把map置为写状态
h.flags ^= hashWriting
//1 找出对应的桶位置
// 如果需要迁移,继续迁移
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)
//2
// 2-1 找出tophash key val位置
// 2-2 把tophash置为 emptyOne(当前位置为空,但后面还有元素)
// 2-3 当前bucket后面没有元素,则置为emptyRest(当前位置为空,且后面没有元素)
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 { //如果发现对应的tophash已经是emptyRest状态,则标识后面没有数据了
break search
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
k2 := k
if !t.key.equal(key, k2) {
continue
}
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
//清除val
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(当前位置为空,但后面还有元素)
b.tophash[i] = emptyOne
//3 判断下一个位置(可能是溢出桶第一个)tophash是不是emptyRest
// 3-1 如果不是,直接到notLast结束流程
// 3-2 如果是,往前搜索,把所有emptyOne置为emptyRest
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
}
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:
//数量减1
h.count--
break search
}
}
}
六、迭代key value
遍历key-val就比较简单了。给定一个随机桶位置startBucket,和随机桶内数据位置offset,开始遍历map的每个值:
// 初始化迭代器
func mapiterinit(t *maptype, h *hmap, it *hiter) {
//1 初始化迭代器
it.t = t
if h == nil || h.count == 0 {
return
}
it.h = h
it.B = h.B
it.buckets = h.buckets
//2 给一个随机数,决定迭代桶的开始位置和桶内开始迭代的顺序
r := uintptr(fastrand())
if h.B > 31-bucketCntBits {
r += uintptr(fastrand()) << 31
}
it.startBucket = r & bucketMask(h.B) //桶的偏移量
it.offset = uint8(r >> h.B & (bucketCnt - 1)) //桶内偏移量
it.bucket = it.startBucket
}
//迭代器迭代
// 1 从随机偏移桶循环迭代每个桶数据
// 2 桶内从随机偏移量开始遍历
// 3 列出该方法隐掉了当正在进行扩容时怎么迭代,需要了解参考源码
func mapiternext(it *hiter) {
if h.flags&hashWriting != 0 { //有协程正在写
throw("concurrent map iteration and map write")
}
t := it.t
bucket := it.bucket
b := it.bptr
i := it.i
checkBucket := it.checkBucket
next:
if b == nil { //当前桶指针为nil,标识桶内数据已经遍历完成,需要遍历下一个桶
if bucket == it.startBucket && it.wrapped { //已经遍历到开始的桶
it.key = nil
it.elem = nil
return
}
bucket++
if bucket == bucketShift(it.B) {
bucket = 0
it.wrapped = true
}
i = 0
}
for ; i < bucketCnt; i++ {
offi := (i + it.offset) & (bucketCnt - 1) //从桶内哪个位置开始遍历
if isEmpty(b.tophash[offi]) || b.tophash[offi] == evacuatedEmpty {//没数据
continue
}
k := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.keysize))
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(offi)*uintptr(t.elemsize))
b = b.overflow(t)
i = 0
goto next
}
参考: