1、设计原理
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func fastrand() uint32 {
mp := getg().m
// Implement xorshift64+: 2 32-bit xorshift sequences added together.
// Shift triplet [17,7,16] was calculated as indicated in Marsaglia's
// Xorshift paper: https://www.jstatsoft.org/article/view/v008i14/xorshift.pdf
// This generator passes the SmallCrush suite, part of TestU01 framework:
// http://simul.iro.umontreal.ca/testu01/tu01.html
s1, s0 := mp.fastrand[0], mp.fastrand[1]
s1 ^= s1 << 17
s1 = s1 ^ s0 ^ s1>>7 ^ s0>>16
mp.fastrand[0], mp.fastrand[1] = s0, s1
return s0 + s1
}
|
2、数据结构
map仅仅是一个hash表,数据被存放到一个桶数组中,每个桶包含8个键值对。低位的hash用于选择桶。每个桶包含每个hash的几个高阶位,以区分单个桶中的条目。
如果一个桶超过了8条,我们链接到extra的桶。
2.1 hmap
哈希表 runtime.hmap 的桶是 runtime.bmap每一个 runtime.bmap 都能存储 8 个键值对,当哈希表中存储的数据过多,单个桶已经装满时就会使用 extra.nextOverflow 中桶存储溢出的数据。
Go 语言运行时同时使用了多个数据结构组合表示哈希表,其中runtime.hmap是最核心的结构体,我们先来了解一下该结构体的内部字段:
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
|
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/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 // buckets数量为2^B 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种子 hash seed
buckets unsafe.Pointer //长度为2^B的buckets数组 array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer //前一个桶数组,大小是现在的一半,只有在扩容过程中才不为nil 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
}
// mapextra holds fields that are not present on all maps.
type mapextra struct {
// If both key and elem do not contain pointers and are inline, then we mark bucket
// type as containing no pointers. This avoids scanning such maps.
// However, bmap.overflow is a pointer. In order to keep overflow buckets
// alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow.
// overflow and oldoverflow are only used if key and elem do not contain pointers.
// overflow contains overflow buckets for hmap.buckets.
// oldoverflow contains overflow buckets for hmap.oldbuckets.
// The indirection allows to store a pointer to the slice in hiter.
overflow *[]*bmap //已经使用的溢出桶
oldoverflow *[]*bmap //扩容阶段旧桶用到的那些桶
// nextOverflow holds a pointer to a free overflow bucket.
nextOverflow *bmap //下一个未使用的空桶
}
// 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
|
- count 长度
- flags 由四个bit位组成,1.有迭代器在使用buckets,2. 有迭代器在使用oldbuckets,4.hashWriting 正在向map写入,8、sameSizeGrow 等量扩容
- B buckets数量,2^B次方,如果B>4,则申请bucket数组的过程中,多申请2^(B-4)个作为overflow bucket,并将nextOverflow指向2^B处的bucket
- noverflow 溢出桶的数量,主要用于触发等量扩容条件当noverflow>uint16(1)«(B&15),其中B最大为15。
- hash0 hash随机种子
- buckets 桶,长度为2^B的buckets数组
- oldbuckets 旧桶,为了渐进式扩容,先将旧桶保存到这里,扩容完成后即消除。
- nevacuate 当前扩容的进度,指向下一个需要迁移的桶编号
- extra 扩展字段,主要拥有溢出的场景如下:
- overflow 已经使用的溢出桶
- oldoverflow 扩容阶段旧桶用到的那些溢出桶
- **nextOverflow **下一个可用的溢出桶
2.2 bmap
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
|
// A bucket for a Go map.
type bmap struct {
// tophash generally contains the top byte of the hash value
// for each key in this bucket. If tophash[0] < minTopHash,
// tophash[0] is a bucket evacuation state instead.
tophash [bucketCnt]uint8
// Followed by bucketCnt keys and then bucketCnt elems.
// NOTE: packing all the keys together and then all the elems together makes the
// code a bit more complicated than alternating key/elem/key/elem/... but it allows
// us to eliminate padding which would be needed for, e.g., map[int64]int8.
// Followed by an overflow pointer.
}
type bmap struct {
tophash [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}
emptyRest = 0 // this cell is empty, and there are no more non-empty cells at higher indexes or overflows.
emptyOne = 1 // this cell is empty
evacuatedX = 2 // key/elem is valid. Entry has been evacuated to first half of larger table.
evacuatedY = 3 // same as above, but evacuated to second half of larger table.
evacuatedEmpty = 4 // cell is empty, bucket is evacuated.
minTopHash = 5 // minimum tophash for a normal filled cell.
|
哈希表 runtime.hmap 的桶是 runtime.bmap。每一个 runtime.bmap 都能存储 8 个键值对,当哈希表中存储的数据过多,单个桶已经装满时就会使用 extra.nextOverflow 中桶存储溢出的数据。
在运行期间,runtime.bmap 结构体其实不止包含 tophash 字段,因为哈希表中可能存储不同类型的键值对,而且 Go 语言也不支持泛型,所以键值对占据的内存空间大小只能在编译时进行推导。runtime.bmap 中的其他字段在运行时也都是通过计算内存地址的方式访问的,所以它的定义中就不包含这些字段,不过我们能根据编译期间的 cmd/compile/internal/gc.bmap 函数重建它的结构:
- tophash 存储高位的hash值,用于快速定位,
tophash=hash>>(PtrSize*8 - 8)64位系统下将hash右移56位,刚好采用高8位,如果小于5,则加5,因为5以下都有特殊的定义。
- keys 键存放的地方,键与键存放一起主要是为了方便计算偏移量和遍历。
- values 存放值数据的地方
- pad ,内存对齐?我也不知
- overflow 指向溢出桶的指针,形成链表
2.3 hiter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// A hash iteration structure.
// If you modify hiter, also change cmd/compile/internal/gc/reflect.go to indicate
// the layout of this structure.
type hiter struct {
key unsafe.Pointer // 每次迭代结果的key Must be in first position. Write nil to indicate iteration end (see cmd/compile/internal/gc/range.go).
elem unsafe.Pointer // 每次迭代结果的value Must be in second position (see cmd/compile/internal/gc/range.go).
t *maptype // map的类型
h *hmap // map指针
buckets unsafe.Pointer // 初始化是指向的桶指针,bucket ptr at hash_iter initialization time
bptr *bmap // 遍历当前桶,current bucket
overflow *[]*bmap // 当前map溢出桶, keeps overflow buckets of hmap.buckets alive
oldoverflow *[]*bmap // 扩容之前map溢出桶, keeps overflow buckets of hmap.oldbuckets alive
startBucket uintptr // 开始桶偏移量,从这个桶开始遍历的, bucket iteration started at
offset uint8 // 桶内偏移量,表示第几个(key,value),intra-bucket offset to start from during iteration (should be big enough to hold bucketCnt-1)
wrapped bool // 尾部已经遍历完成,开始折返从头开始遍历,already wrapped around from end of bucket array to beginning
B uint8 // h.B
i uint8 // 当前桶已经遍历数目,i=8时,bptr指向下一个
bucket uintptr // 当前hash桶偏移量
checkBucket uintptr // 不为 noCheck(1<<(8*sys.PtrSize) - 1)的话,表示当前桶还没有搬迁到新 map,需要对旧桶检查跳过迁移到新扩容的hash桶的元素
}
|
2、初始化
1
2
3
4
5
|
hash := map[string]int{
"1": 2,
"3": 4,
"5": 6,
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func maplit(n *Node, m *Node, init *Nodes) {
a := nod(OMAKE, nil, nil)
a.Esc = n.Esc
a.List.Set2(typenod(n.Type), nodintconst(int64(n.List.Len())))
litas(m, a, init)
entries := n.List.Slice()
if len(entries) > 25 {
// ...
return
}
// Build list of var[c] = expr.
// Use temporaries so that mapassign1 can have addressable key, elem.
// ...
}
|
3、 读写操作
3、扩容
4.1、等量扩容
4.2、增量扩容
5、访问
5.1 遍历
mapiterinit()函数主要是决定我们从哪个位置开始迭代,为什么是从哪个位置,而不是直接从 hash 数组头部开始呢?hash 表中数据每次插入的位置是变化的,这是因为实现的原因,一方面 hash 种子是随机的,这导致相同的数据在不同的 map 变量内的 hash 值不同;另一方面即使同一个 map 变量内,数据删除再添加的位置也有可能变化,因为在同一个桶及溢出链表中数据的位置不分先后,所以为了防止用户错误的依赖于每次迭代的顺序,map 作者干脆让相同的 map 每次迭代的顺序也是随机的。
1
2
3
4
5
6
7
8
9
10
|
for ; i < bucketCnt; i++ {
offi := (i + it.offset) & (bucketCnt - 1) //offset开始遍历
it.bucket = bucket
if it.bptr != b { // avoid unnecessary write barrier; see issue 14921
it.bptr = b
}
it.i = i + 1
it.checkBucket = checkBucket
return
}
|
map 的遍历由函数mapiternext()完成,过程如下:
(1)从 hash 数组中第 it.startBucket 个桶开始,先遍历 hash 桶,然后是这个桶的溢出链表;
(2)之后 hash 数组偏移量+1,继续前一步动作;
(3)遍历每一个桶,无论是正常桶还是溢出桶,都从 it.offset 偏移量开始;
(4)当迭代器经过一轮循环回到 it.startBucket 的位置,结束遍历。
注意:
(1)map 如果在遍历开始时发现处于写入状态,那么报并发读写异常,终止程序。
(2)迭代还需要关注扩容的情况:如果是在迭代开始后才 growing,那么迭代初始状态如 it.buckets 和 it.B 等将被改变,迭代有可能出现异常。如果是先 growing,再开始迭代。这种情况下,不会出现异常,会先到旧 hash 表中检查 key 对应的桶有没有被迁移,未迁移则遍历旧桶,已迁移则遍历新 hash 表里对应的桶。
6、删除
渐进式扩容:

桶的数据结构:
tophash
key与key放一起
value与value放一起
一个桶放不下之后,会新建一个新的桶存放。
如果hash表要分配的桶的数量大于2^4就认为桶的溢出概率很大,就会预分配2^(B-4)个溢出桶备用,溢出桶和常规桶内存上面是连续的,常规桶在前2^B次方,后面的用着溢出桶,举个例子:B=14,前面4k个用着存储常规桶,后面1k个桶用着存储溢出桶。


2、扩容
2.1、增量扩容
从赋值函数mapassign()可以看出,触发扩容有两个条件:
(1)当前不处在 growing 状态;
(2.1)元素个数 count 大于 hash 桶数量(2^B)*6.5。注意这里的 hash 桶指的是 hash 数组中的桶,不包括溢出的桶;
(2.2)或溢出的桶数量 noverflow>=32768(1«15) 或者 noverflow>=hash 数组中桶数量。
go map 的扩容预处理由函数 hashGrow() 来完成,主要完成两个操作:
(1)判断扩容类型;
(2)申请新的 hash 桶。
新申请的 hash 桶数组指针由 h.buckets 保存,h.oldbuckets 则指向旧 hash 桶数组。map 是否处于扩容状态是根据 h.oldbuckets 是否为空来判断的。
Go map 有两种扩容类型:
(1)一种是真扩容,扩到 hash 桶数量为原来的两倍,针对元素数量过多的情况;
(2)一种是假扩容,hash 桶数量不变,只是把元素搬迁到新的 map,针对溢出桶过多的情况。如果是假扩容,那么 hmap.flags 会被打上 sameSizeGrow 标识。
2.2、等量扩容
目的是合并已经删除很多keys的map。






