golang map原理剖析
本文最后更新于8 天前,其中的信息可能已经过时,如有错误请留言

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处于扩容状态,遍历的顺序是按照扩容之后的顺序来的

感谢阅读!如有疑问请留言
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇