golang map 操作,是map 实现中较复杂的逻辑。因为当赋值时,为了减少hash 冲突链的长度过长问题,会做map 的扩容以及数据的迁移。而map 的扩容以及数据的迁移也是关注的重点。
数据结构
首先,我们需要重新学习下map实现的数据结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type hmap struct {
count int
flags uint8
B uint8
noverflow uint16
hash0 uint32
buckets unsafe . Pointer
oldbuckets unsafe . Pointer
nevacuate uintptr
extra * mapextra
}
type mapextra struct {
overflow * [] * bmap
oldoverflow * [] * bmap
nextOverflow * bmap
}
hmap 是 map 实现的结构体。大部分字段在 第一节中已经学习过了。剩余的就是nevacuate 和extra 了。
首先需要了解搬迁的概念:当hash 中数据链太长,或者空的bucket 太多时,会操作数据搬迁,将数据挪到一个新的bucket 上,就的bucket数组成为了oldbuckets。bucket的搬迁不是一次就搬完的,是访问到对应的bucket时才可能会触发搬迁操作。(这一点是不是和redis 的扩容比较类似,将扩容放在多个访问上,减少了单次访问的延迟压力)
nevactuate 标识的是搬迁的位置(也可以考虑为搬迁的进度)。标识目前 oldbuckets 中 (一个 array)bucket 搬迁到哪里了。
extra 是一个map 的结构体,nextOverflow 标识的是申请的空的bucket,用于之后解决冲突时使用;overflow 和 oldoverflow 标识溢出的链表中正在使用的bucket 数据。old 和非old 的区别是,old 是为搬迁的数据。
理解了大概的数据结构,我们可以学习map的 赋值操作了。
map 赋值操作
map 的赋值操作写法如下:
1
2
data := mapExample [ "hello" ]
赋值的实现,golang 为了对不同类型k做了优化,下面时一些实现方法:
1
2
3
4
5
6
func mapassign ( t * maptype , h * hmap , key unsafe . Pointer ) unsafe . Pointer {}
func mapassign_fast32 ( t * maptype , h * hmap , key uint32 ) unsafe . Pointer {}
func mapassign_fast32ptr ( t * maptype , h * hmap , key unsafe . Pointer ) unsafe . Pointer {}
func mapassign_fast64 ( t * maptype , h * hmap , key uint64 ) unsafe . Pointer {}
func mapassign_fast64ptr ( t * maptype , h * hmap , key unsafe . Pointer ) unsafe . Pointer {}
func mapassign_faststr ( t * maptype , h * hmap , s string ) unsafe . Pointer {}
内容大同小异,我们主要学习mapassign 的实现。
mapassign
方法的实现是查找一个空的bucket,把key赋值到bucket上,然后把val的地址返回,然后直接通过汇编做内存拷贝。
那我们一步步看是如何找空闲bucket的:
① 在查找key之前,会做异常检测,校验map是否未初始化,或正在并发写操作,如果存在,则抛出异常:(这就是为什么map 并发写回panic的原因)
1
2
3
4
5
6
7
8
if h == nil {
panic ( plainError ( "assignment to entry in nil map" ))
}
// 竟态检查 和 内存扫描
if h . flags & hashWriting != 0 {
throw ( "concurrent map writes" )
}
② 需要计算key 对应的hash 值,如果buckets 为空(初始化的时候小于一定长度的map 不会初始化数据)还需要初始化一个bucket
1
2
3
4
5
6
7
8
9
alg := t . key . alg
hash := alg . hash ( key , uintptr ( h . hash0 ))
// 为什么需要在hash 后设置flags,因为 alg.hash可能会panic
h . flags ^= hashWriting
if h . buckets == nil {
h . buckets = newobject ( t . bucket ) // newarray(t.bucket, 1)
}
③ 通过hash 值,获取对应的bucket。如果map 还在迁移数据,还需要在oldbuckets中找对应的bucket,并搬迁到新的bucket。
1
2
3
4
5
6
7
8
9
10
11
12
// 通过hash 计算bucket的位置偏移
bucket := hash & bucketMask ( h . B )
// 此处是搬迁逻辑,我们后续详解
if h . growing () {
growWork ( t , h , bucket )
}
// 计算对应的bucket 位置,和top hash 值
b := ( * bmap )( unsafe . Pointer ( uintptr ( h . buckets ) + bucket * uintptr ( t . bucketsize )))
top := tophash ( hash )
④ 拿到bucket之后,还需要按照链表方式一个一个查,找到对应的key, 可能是已经存在的key,也可能需要新增。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
for {
for i := uintptr ( 0 ); i < bucketCnt ; i ++ {
// 若 tophash 就不相等,那就取tophash 中的下一个
if b . tophash [ i ] != top {
// 若是个空位置,把kv的指针拿到。
if isEmpty ( b . tophash [ i ]) && inserti == nil {
inserti = & b . tophash [ i ]
insertk = add ( unsafe . Pointer ( b ), dataOffset + i * uintptr ( t . keysize ))
val = add ( unsafe . Pointer ( b ), dataOffset + bucketCnt * uintptr ( t . keysize ) + i * uintptr ( t . valuesize ))
}
// 若后续无数据,那就不用再找坑了
if b . tophash [ i ] == emptyRest {
break bucketloop
}
continue
}
// 若tophash匹配时
k := add ( unsafe . Pointer ( b ), dataOffset + i * uintptr ( t . keysize ))
if t . indirectkey () {
k = * (( * unsafe . Pointer )( k ))
}
// 比较k不等,还需要继续找
if ! alg . equal ( key , k ) {
continue
}
// 如果key 也相等,说明之前有数据,直接更新k,并拿到v的地址就可以了
if t . needkeyupdate () {
typedmemmove ( t . key , k , key )
}
val = add ( unsafe . Pointer ( b ), dataOffset + bucketCnt * uintptr ( t . keysize ) + i * uintptr ( t . valuesize ))
goto done
}
// 取下一个overflow (链表指针)
ovf := b . overflow ( t )
if ovf == nil {
break
}
b = ovf
}
总结下这段程序,主要有几个部分:
a. map hash 不匹配的情况,会看是否是空kv 。如果调用了delete,会出现空kv的情况,那先把地址留下,如果后面也没找到对应的k(也就是说之前map 里面没有对应的Key),那就直接用空kv的位置即可。
b. 如果 map hash 是匹配的,需要判定key 的字面值是否匹配。如果不匹配,还需要查找。如果匹配了,那直接把key 更新(因为可能有引用),v的地址返回即可。
c. 如果上面都没有,那就看下一个bucket
⑤ 插入数据前,会先检查数据太多了,需要扩容,如果需要扩容,那就从第③开始拿到新的bucket,并查找对应的位置。
1
2
3
4
if ! h . growing () && ( overLoadFactor ( h . count + 1 , h . B ) || tooManyOverflowBuckets ( h . noverflow , h . B )) {
hashGrow ( t , h )
goto again // Growing the table invalidates everything, so try again
}
⑥ 如果刚才看没有有空的位置,那就需要在链表后追加一个bucket,拿到kv。
1
2
3
4
5
6
7
if inserti == nil {
// all current buckets are full, allocate a new one.
newb := h . newoverflow ( t , b )
inserti = & newb . tophash [ 0 ]
insertk = add ( unsafe . Pointer ( newb ), dataOffset )
val = add ( insertk , bucketCnt * uintptr ( t . keysize ))
}
⑦ 最后更新tophash 和 key 的字面值, 并解除hashWriting 约束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 如果非指针数据(也就是直接赋值的数据),还需要申请内存和拷贝
if t . indirectkey () {
kmem := newobject ( t . key )
* ( * unsafe . Pointer )( insertk ) = kmem
insertk = kmem
}
if t . indirectvalue () {
vmem := newobject ( t . elem )
* ( * unsafe . Pointer )( val ) = vmem
}
// 更新tophash, k
typedmemmove ( t . key , insertk , key )
* inserti = top
done :
if h . flags & hashWriting == 0 {
throw ( "concurrent map writes" )
}
h . flags &^= hashWriting
if t . indirectvalue () {
val = * (( * unsafe . Pointer )( val ))
}
return val
到这里,map的赋值基本就介绍完了。下面学习下步骤⑤中的map的扩容。
Map 的扩容
有两种情况下,需要做扩容。一种是存的kv数据太多了,已经超过了当前map的负载。还有一种是overflow的bucket过多了。这个阈值是一个定值,经验得出的结论,所以我们这里不考究。
当满足条件后,将开始扩容。如果满足条件二,扩容后的buckets 的数量和原来是一样的,说明可能是空kv占据的坑太多了,通过map扩容做内存整理。如果是因为kv 量多导致map负载过高,那就扩一倍的量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func hashGrow ( t * maptype , h * hmap ) {
bigger := uint8 ( 1 )
// 如果是第二种情况,扩容大小为0
if ! overLoadFactor ( h . count + 1 , h . B ) {
bigger = 0
h . flags |= sameSizeGrow
}
oldbuckets := h . buckets
// 申请一个大数组,作为新的buckets
newbuckets , nextOverflow := makeBucketArray ( t , h . B + bigger , nil )
flags := h . flags &^ ( iterator | oldIterator )
if h . flags & iterator != 0 {
flags |= oldIterator
}
// 然后重新赋值map的结构体,oldbuckets 被填充。之后将做搬迁操作
h . B += bigger
h . flags = flags
h . oldbuckets = oldbuckets
h . buckets = newbuckets
h . nevacuate = 0
h . noverflow = 0
// extra 结构体做赋值
if h . extra != nil && h . extra . overflow != nil {
// Promote current overflow buckets to the old generation.
if h . extra . oldoverflow != nil {
throw ( "oldoverflow is not nil" )
}
h . extra . oldoverflow = h . extra . overflow
h . extra . overflow = nil
}
if nextOverflow != nil {
if h . extra == nil {
h . extra = new ( mapextra )
}
h . extra . nextOverflow = nextOverflow
}
}
总结下map的扩容操作。首先拿到扩容的大小,然后申请大数组,然后做些初始化的操作,把老的buckets,以及overflow做切换即可。
map 数据的迁移
扩容完成后,需要做数据的迁移。数据的迁移不是一次完成的,是使用时才会做对应bucket的迁移。也就是逐步做到的数据迁移。下面我们来学习。
在数据赋值的第③步,会看需要操作的bucket是不是在旧的buckets里面,如果在就搬迁。下面是搬迁的具体操作:
1
2
3
4
5
6
7
8
9
func growWork ( t * maptype , h * hmap , bucket uintptr ) {
// 首先把需要操作的bucket 搬迁
evacuate ( t , h , bucket & h . oldbucketmask ())
// 再顺带搬迁一个bucket
if h . growing () {
evacuate ( t , h , h . nevacuate )
}
}
nevacuate 标识的是当前的进度,如果都搬迁完,应该和2^B的长度是一样的(这里说的B是oldbuckets 里面的B,毕竟新的buckets长度可能是2^(B+1))。
在evacuate 方法实现是把这个位置对应的bucket,以及其冲突链上的数据都转移到新的buckets上。
① 先要判断当前bucket是不是已经转移。 (oldbucket 标识需要搬迁的bucket 对应的位置)
1
2
3
4
5
b := ( * bmap )( add ( h . oldbuckets , oldbucket * uintptr ( t . bucketsize )))
// 判断
if ! evacuated ( b ) {
// 做转移操作
}
转移的判断直接通过tophash 就可以,判断tophash中第一个hash值即可 (tophash的作用可以参考第三讲)
1
2
3
4
5
func evacuated ( b * bmap ) bool {
h := b . tophash [ 0 ]
// 这个区间的flag 均是已被转移
return h > emptyOne && h < minTopHash
}
② 如果没有被转移,那就要迁移数据了。数据迁移时,可能是迁移到大小相同的buckets上,也可能迁移到2倍大的buckets上。这里xy 都是标记目标迁移位置的标记:x 标识的是迁移到相同的位置,y 标识的是迁移到2倍大的位置上。我们先看下目标位置的确定:
1
2
3
4
5
6
7
8
9
10
11
12
var xy [ 2 ] evacDst
x := & xy [ 0 ]
x . b = ( * bmap )( add ( h . buckets , oldbucket * uintptr ( t . bucketsize )))
x . k = add ( unsafe . Pointer ( x . b ), dataOffset )
x . v = add ( x . k , bucketCnt * uintptr ( t . keysize ))
if ! h . sameSizeGrow () {
// 如果是2倍的大小,就得算一次 y 的值
y := & xy [ 1 ]
y . b = ( * bmap )( add ( h . buckets , ( oldbucket + newbit ) * uintptr ( t . bucketsize )))
y . k = add ( unsafe . Pointer ( y . b ), dataOffset )
y . v = add ( y . k , bucketCnt * uintptr ( t . keysize ))
}
③ 确定bucket位置后,需要按照kv 一条一条做迁移。(目的就是清除空闲的kv)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 遍历每个bucket
for ; b != nil ; b = b . overflow ( t ) {
k := add ( unsafe . Pointer ( b ), dataOffset )
v := add ( k , bucketCnt * uintptr ( t . keysize ))
// 遍历bucket 里面的每个kv
for i := 0 ; i < bucketCnt ; i , k , v = i + 1 , add ( k , uintptr ( t . keysize )), add ( v , uintptr ( t . valuesize )) {
top := b . tophash [ i ]
// 空的不做迁移
if isEmpty ( top ) {
b . tophash [ i ] = evacuatedEmpty
continue
}
if top < minTopHash {
throw ( "bad map state" )
}
k2 := k
if t . indirectkey () {
k2 = * (( * unsafe . Pointer )( k2 ))
}
var useY uint8
if ! h . sameSizeGrow () {
// 2倍扩容的需要重新计算hash,
hash := t . key . alg . hash ( k2 , uintptr ( h . hash0 ))
if h . flags & iterator != 0 && ! t . reflexivekey () && ! t . key . alg . equal ( k2 , k2 ) {
useY = top & 1
top = tophash ( hash )
} else {
if hash & newbit != 0 {
useY = 1
}
}
}
// 这些是固定值的校验,可以忽略
if evacuatedX + 1 != evacuatedY || evacuatedX ^ 1 != evacuatedY {
throw ( "bad evacuatedN" )
}
// 设置oldbucket 的tophash 为已搬迁
b . tophash [ i ] = evacuatedX + useY // evacuatedX + 1 == evacuatedY
dst := & xy [ useY ] // evacuation destination
if dst . i == bucketCnt {
// 如果dst是bucket 里面的最后一个kv,则需要添加一个overflow
dst . b = h . newoverflow ( t , dst . b )
dst . i = 0
dst . k = add ( unsafe . Pointer ( dst . b ), dataOffset )
dst . v = add ( dst . k , bucketCnt * uintptr ( t . keysize ))
}
// 填充tophash值, kv 数据
dst . b . tophash [ dst . i & ( bucketCnt - 1 )] = top
if t . indirectkey () {
* ( * unsafe . Pointer )( dst . k ) = k2
} else {
typedmemmove ( t . key , dst . k , k )
}
if t . indirectvalue () {
* ( * unsafe . Pointer )( dst . v ) = * ( * unsafe . Pointer )( v )
} else {
typedmemmove ( t . elem , dst . v , v )
}
// 更新目标的bucket
dst . i ++
dst . k = add ( dst . k , uintptr ( t . keysize ))
dst . v = add ( dst . v , uintptr ( t . valuesize ))
}
}
对于key 非间接使用的数据(即非指针数据),做内存回收
1
2
3
4
5
6
7
8
if h . flags & oldIterator == 0 && t . bucket . kind & kindNoPointers == 0 {
b := add ( h . oldbuckets , oldbucket * uintptr ( t . bucketsize ))
ptr := add ( b , dataOffset )
n := uintptr ( t . bucketsize ) - dataOffset
// ptr 是kv的位置, 前面的topmap 保留,做迁移前的校验使用
memclrHasPointers ( ptr , n )
}
④ 如果当前搬迁的bucket 和 总体搬迁的bucket的位置是一样的,我们需要更新总体进度的标记 nevacuate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// newbit 是oldbuckets 的长度,也是nevacuate 的重点
func advanceEvacuationMark ( h * hmap , t * maptype , newbit uintptr ) {
// 首先更新标记
h . nevacuate ++
// 最多查看2^10 个bucket
stop := h . nevacuate + 1024
if stop > newbit {
stop = newbit
}
// 如果没有搬迁就停止了,等下次搬迁
for h . nevacuate != stop && bucketEvacuated ( t , h , h . nevacuate ) {
h . nevacuate ++
}
// 如果都已经搬迁完了,oldbukets 完全搬迁成功,清空oldbuckets
if h . nevacuate == newbit {
h . oldbuckets = nil
if h . extra != nil {
h . extra . oldoverflow = nil
}
h . flags &^= sameSizeGrow
}
}
总结
Map 的赋值难点在于数据的扩容和数据的搬迁操作。
bucket 搬迁是逐步进行的,每进行一次赋值,会做至少一次搬迁工作。
扩容不是一定会新增空间,也有可能是只是做了内存整理。
tophash 的标志即可以判断是否为空,还会判断是否搬迁,以及搬迁的位置为X or Y。
delete map 中的key,有可能出现很多空的kv,会导致搬迁操作。如果可以避免,尽量避免。