map的底层是一个hmap结构体,这个结构体组成是这样的:
type hmap struct {
count int // 当调用len用来返回map的长度时,就会返回它。
flags uint8 // 标志着hmap处于什么状态,读还是写?
B uint8 // 这里不了解为什么要大写,B是hmap中buckets的对数, 2^B = len(buckets)
noverflow uint16 // 溢出桶的大概数量。
hash0 uint32 // 后文计算hash值时会传入它作为参数,生成的hash具有随机性(hash种子)
buckets unsafe.Pointer // 指针指向bmap结构体数组
oldbuckets unsafe.Pointer // 指针指向旧的bmap结构体数组
nevacuate uintptr // 英文有疏散的意思,在这里表示hamp的迁移进度
extra *mapextra // 据名思意,也就是额外的map。为了优化GC而设计的。
}
这里的B就能够表示整个map的buckets的数量。其实也就是bmap结构体数组的大小,那么我们来介绍一下bmap结构体
type bmap struct {
tophash [bucketCnt]uint8
}
这里表示的仅仅是表面上bmap结构体的样子。实际上,在编译器他会发生一些动态的扩充。
type bmap struct {
tophash [8]uint8 // tophash中放置的是key的 “类型”
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}
当 map 的 key 和 value 都不是指针,并且 size 都小于 128 字节的情况下,会把 bmap 标记为不含指针,这样可以避免 gc 时扫描整个 hmap。但是,我们看 bmap 其实有一个 overflow 的字段,是指针类型的,破坏了 bmap 不含指针的设想,这时会把 overflow 移动到 extra 字段来。
type mapextra struct {
// overflow contains overflow buckets for hmap.buckets.
// oldoverflow contains overflow buckets for hmap.oldbuckets.
overflow *[]*bmap
oldoverflow *[]*bmap
// nextOverflow 包含空闲的 overflow bucket,这是预分配的 bucket
nextOverflow *bmap
}
通过下图可以更好的理解bmap中存放key-value的情况。
bmap中最多放置8个key-value键值对。并且这些键值对是通过数组的形式分别存储的。好处是什么呢(个人理解)?
map[int64]uint8
先假设一个key,一个value的连续存储。而key和value的类型又不相同(如上图所示),所占据的内存大小也不相同。因此在内存分配的时候就会一段大,一段小。可能会浪费内存空间。
而采用连续的key和连续的value进行分别组合的话,内存就会比较均匀完整。
每个bmap只能存放8个键值对,因此如果有第9个键值对时,就会通过链表法将overflow指向下一个bmap。
在64bit机器上,随机生成的key就是64位的。而最后B位就决定了这个key要被存储在哪个桶中去。因为前边已经说过了,一共有2^B个桶,而最后B位恰好就能决定放在哪个桶中。举个例子:
10010111 | 000011110110110010001111001010100010010110010101010 │ 01010
假设上述是生成的一个key。而01010
表示二进制数。将二进制转化为十进制的数就是这个key要存储的桶子下标,也就是10,那么他就会被放在下标为10的桶子中去。
那么如何确定key的下标呢?这时候就要用到高8位了。在源码中会将key向右移动56个位置来找到key的地址。一样是通过进制转换将key地址确定为:151。因此这个tophash值就是151。
更新理解:这里的高八位用来计算出tophash值,根据这个tophash值与bucket中的8个tophash依次进行比较。如果能够找到相等的tophash,则该key大概率会出现在这个bucket中,如果找不到相等的key,则一定不会在该bucket中。
再找到tophash之后,并不是通过下标来找到key的位置。因为存储key-value的是数组,而数组又是一段连续的内存。因此可以根据tophash直接推算出key的地址,通过key和value的地址来取出具体的值。
当两个不同的key落入到同一个位置上时,就会发生hash冲突,这时候就会使用链表法,通过overflow指向下一个bmap中去。
下面给出一张非常详细的图:
map在扩容的时候每次只会扩容两个bmap。扩容进度可以通过tophash[0]中的值可以看到
// 这个值有两层意思:一是表示该tophash对应的K/V位置是可用的;二是表示该位置后面的K/V位置都是可用的。
emptyRest = 0 // 该cell为空,并且任何一个cell都为空。完全初始化状态。
// 仅表示该tophash对应的K/V位置是可用的,其后面的是否可用不知道。
emptyOne = 1 // 该cell为空。
evacuatedX = 2 // 旧的key val已经被搬迁,但新的bucket中key val并没有完全分离
evacuatedY = 3 // 旧的key val已经被搬迁,但新的bucket中key val并没有完全分离
evacuatedEmpty = 4 // 空的搬迁状态,说明已经完成搬迁
minTopHash = 5 // tophash 的最小正常值
当map进行遍历时,会先进行迭代器初始化,然后再循环迭代器。
map遍历是无序的,这是因为底层会生成一个随机数,使用这个随机数会随机确定从哪个bucket开始进行遍历。然后再确定从这个bucket的哪个cell开始遍历。
假设我们有下图**(图片来自参考资料链接,侵权删)**所示的一个 map,起始时 B = 1,有两个 bucket,后来触发了扩容(这里不要深究扩容条件,只是一个设定),B 变成 2。并且, 1 号 bucket 中的内容搬迁到了新的 bucket,1 号
裂变成 1 号
和 3 号
;0 号
bucket 暂未搬迁。老的 bucket 挂在在 *oldbuckets
指针上面,新的 bucket 则挂在 *buckets
指针上面。
这时,我们对此 map 进行遍历。假设经过初始化后,startBucket = 3,offset = 2。于是,遍历的起点将是 3 号 bucket 的 2 号 cell,下面这张图就是开始遍历时的状态:
标红的表示起始位置,bucket 遍历顺序为:3 -> 0 -> 1 -> 2。
如果遍历到一个已经迁移过的bucket,那就按照随机形式进行遍历,直到结束。
如果遍历到一个没有迁移的bucket。这里需要先知道一个bucket(1号)会扩容为两个bucket,(新1号和新2号)。因此在遍历未迁移的bucket时,会去遍历bucket的新1号。因为分裂之后的旧bucket中的kv都会放在新1号中。剩余的扩展内容放在新2号中。
只有在两种情况下才会触发map的扩容机制:
map的装载因子大于一个阀值,这个阀值在源码中表示为6.5。
当overflow中的bucket过多:
当B<15,也就是说bucket总数2^B < 2^15。overflow中的bucket > 2^B。
当B>15,也就是说bucket总数2^B >= 2^15。overflow中的bucket > 2^15。
这里的装载因子计算公式为:
loadFactor := count / (2^B)
那么为什么要设置第二个限制条件呢?原因在于hash冲突
map不是线程安全的,在对map进行插入、删除、循环遍历的时候只要触发写的操作,就会出现painc异常。但sync.Map是线程安全的。
makeslice时返回的是一个原类型的slice,而makemap时返回的是一个指针类型的map。因此slice作为参数时,拷贝的是slice的一个副本。map作为参数时,拷贝的时map的地址。当函数内部对map进行修改时也会影响到函数外map的内部值,即使触发了扩容机制也一样。
map是无序的。因为map在遍历的时候会先随机生成一个数值。根据这个数值来决定从哪个bucket开始遍历,从bucket的哪个cell开始遍历。因此map不会有序。
将map中的key取出来进行排序,然后按照key的顺序进行取值即可有序的输出map。
count的类型是uint8,范围只有0-255,如果map大于这个范围该如何进行表示呢?
答:因为看错类型了,count的类型为int,在64位机下,int的范围是2^64,根本用不完!
查找一个值时,前8位确定了tophash的位置。那么如何确定key的位置?key的位置是不是以下标进行存储的?
答案已经更新过,就在key定位处可以看到。
tophash[0]的位置存储迁移情况,那么它到底是存储值,还是记录更新状态?
这里我的理解是:因为map的迁移是两个两个进行的。因此在tophash[0] > mintophash时,说明他是存值状态,如果 < mintophash,说明他是处于记录状态,并且该bucket已经在更新过程中。
overflow以及hash冲突是如何体现的?
答:在map中key是唯一的,但是tophash不一定唯一。当相同的tophash在同一个bucket中出现时,就会发生hash冲突,继而创建新的bmap并通过overflow指向它。
golang map实现原理
文章图片均来自该链接,仅作为学习使用,侵权删!