go语言中map的底层结构
map是一个指向hmap的指针,占用8个字节
hmap的结构体如下
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/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
}
解释其中一些字段的含义:
count:len(map)
flags:取值如下
// flags
iterator = 1 // there may be an iterator using buckets
oldIterator = 2 // there may be an iterator using oldbuckets
hashWriting = 4 // a goroutine is writing to the map
sameSizeGrow = 8 // the current map growth is to a new map of the same size
B:2的B次方,代表桶的数量每个桶至少可以容纳8个键值对
noverflow:溢出桶的数量估计
hash0:哈希种子
buckets:指针指向bmap,bmap是桶的结构,一个bmap至少可以放8个键值对,如果不够用会有指针指向溢出桶(overflow字段)
oldbuckets:指向旧桶(map扩容过程),非nil代表处于迁移状态
nevacuate:代表小于这个值的所有桶都已经被迁移了,但是我理解大于这个桶的可能也被迁移了,所以这个值代表第一个还没被迁移的桶的编号,当这个东西等于旧桶数组的长度时,说明迁移完成
map的访问原理
简单概括:
1、首先map不能处于写的状态,因为golang中的map不支持并发读写,如果map在写的过程中,还有读的情况发生,map会报fatal error
2、计算hash值,根据hash值找到对应的索引位置
3、如果map处在扩容状态,那么判断当前桶有没有被迁移,被迁移则去新桶中找,没被迁移去旧桶中找
4、找到桶之后,先逐一比较tophash的值,如果hash值相等,再比较key是否相同
5、如果桶里面没有找到,还要继续遍历溢出桶,直到把key找到,或者把溢出桶也找完
map的赋值原理
1、同理,map不能处于写的状态,否则fatal error
2、计算出hash值,然后把map置于写状态
3、根据hash值找到桶的位置
4、判断map是不是在扩容,如果在扩容,那么就把这个桶迁移掉,同时还帮忙多迁移一个桶
5、然后计算tophash,根据tophash去找桶里面有没有对应的key,如果有对应的key,把对应的value重新写入,如果没有对应的key,那么找第一个空位置(tophash为空的位置),写入key和value,还有对应的tophash
6、假如没有空位置,也没有找到key,那么要申请溢出桶,先用map预申请的溢出桶,如果map预申请的溢出桶不够用,再申请新的溢出桶
7、map置回正常状态
map的扩容
map扩容是因为hash冲突太多,导致找一个key的效率太低
第一种情况:负载因子大于6.5,1个桶本来有8个位置,现在已经用掉6.5个了,说明找一个key,可能要遍历6.5次,性能已经下降,这时候会进行双倍扩容。
第二种情况:溢出桶太多,这种情况是因为,假设比较巧,很多hash值就在一个桶上冲突了,导致这个桶对应的溢出桶链表很长,这时候,因为只有一个桶,它的溢出桶很长,所以整体负载因子不高,然后又删除了很多数据,map并不会把溢出桶的内存回收,map只会把对应位置的key、value还有tophash清空,这时候,这个桶有好长的溢出桶,可能一个桶加上5、6个溢出桶,但里面正儿八经放的数据没几个,导致遍历这个桶性能下降。如果只有一个桶可能还能接受,假设其他桶也一样,一会儿写,一会儿删除,导致就是没到负载因子超标的情况,但是因为写,写了又删,结果溢出桶很多位置空出来了,就是要遍历很长的溢出桶。
这时候,会进行等量扩容,就是重新申请桶,然后把旧桶搬移过去,搬移过去的好处是,现在map里面数据已经很分散了,这样把数据紧凑排列起来,就能省很多溢出桶,如果说紧凑排列了,溢出桶还是很多怎么办,那就会导致负载因子增加,很多会出发双倍扩容。
map的扩容不是一次性的,是慢慢扩容的,每次写的时候,都会进行扩容,读的时候,不会进行扩容,因为读的时候如果扩容,扩容是不能并发操作的,但是读是可以并发操作的,结果两个线程同时读,因为在扩容,这样就冲突了,所以读的时候,是不会触发map的扩容的,只有写的时候,会触发。
那么有一些key假设一直没有被重新写入数据,那么是不是就永远迁移不到了?不会,因为迁移的时候,会帮忙多迁移一个桶。
map的删除
1、同理,首先不能map在写入状态,否则fatal error
2、算hash,把桶置于写状态,根据hash找对应的位置
3、判断map是不是在扩容,如果map在扩容,那么就把这个桶迁移掉,然后再多迁移一个桶
4、找到key就把tophash、key、value都删掉,把map置回正常状态
map的遍历
map的遍历是随机的,每一次遍历的顺序都不一样
同理,遍历的时候,不能再写入状态,否则fatal error
还有一点,就是如果map处于扩容状态,遍历的顺序是按照扩容之后的顺序来的