本文是针对** go1.24 版本**之前 map 的源码解读。1.24 版本开始,go map 使用了不同的结构swiss map。
我认为直接上来阅读源码的难度十分困难,最好是先看下别的文章中对于 map 的总体讲解。大概了解 map 的数据结构以及原理,再深入阅读源码就会相对轻松一些。
好了,让我们开始吧!
原始代码
package main
func main() {
_ = make(map[string]string, 10)
}
汇编代码
使用go build -gcflags="-S -l -N" main.go 2> main.s进行编译
在 go 1.17+之后,使用寄存器传参。之前使用的是栈传参
如果不懂汇编代码,可以看克里斯叮大佬的文章juejin.cn/post/731948…
main.main STEXT size=71 args=0x0 locals=0x50 funcid=0x0 align=0x0
0x0000 00000 (/Users/awesomeProject1/main.go:3) TEXT main.main(SB), ABIInternal, $80-0
0x0000 00000 (/Users/awesomeProject1/main.go:3) CMPQ SP, 16(R14)
0x0004 00004 (/Users/awesomeProject1/main.go:3) PCDATA $0, $-2
0x0004 00004 (/Users/awesomeProject1/main.go:3) JLS 60
0x0006 00006 (/Users/awesomeProject1/main.go:3) PCDATA $0, $-1
0x0006 00006 (/Users/awesomeProject1/main.go:3) PUSHQ BP
0x0007 00007 (/Users/awesomeProject1/main.go:3) MOVQ SP, BP
0x000a 00010 (/Users/awesomeProject1/main.go:3) SUBQ $72, SP
0x000e 00014 (/Users/awesomeProject1/main.go:3) FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
0x000e 00014 (/Users/awesomeProject1/main.go:3) FUNCDATA $1, gclocals·/ydTHfVJHvKeH/UP4dRKSQ==(SB)
0x000e 00014 (/Users/awesomeProject1/main.go:3) FUNCDATA $2, main.main.stkobj(SB)
0x000e 00014 (/Users/awesomeProject1/main.go:4) MOVUPS X15, main..autotmp_0+24(SP) //清零 hmap 的临时空间
0x0014 00020 (/Users/awesomeProject1/main.go:4) MOVUPS X15, main..autotmp_0+40(SP)
0x001a 00026 (/Users/awesomeProject1/main.go:4) MOVUPS X15, main..autotmp_0+56(SP)
0x0020 00032 (/Users/awesomeProject1/main.go:4) LEAQ type:map[string]string(SB), AX //传入*maptype
0x0027 00039 (/Users/awesomeProject1/main.go:4) MOVL $10, BX //传入 hint
0x002c 00044 (/Users/awesomeProject1/main.go:4) LEAQ main..autotmp_0+24(SP), CX //取栈顶向栈基移动 24 个字节后的地址。传入*hmap,即 map对象占用的内存空间
0x0031 00049 (/Users/awesomeProject1/main.go:4) PCDATA $1, $0
0x0031 00049 (/Users/awesomeProject1/main.go:4) CALL runtime.makemap(SB) //调用 makemap 函数
0x0036 00054 (/Users/awesomeProject1/main.go:5) ADDQ $72, SP
0x003a 00058 (/Users/awesomeProject1/main.go:5) POPQ BP
0x003b 00059 (/Users/awesomeProject1/main.go:5) RET
0x003c 00060 (/Users/awesomeProject1/main.go:5) NOP
0x003c 00060 (/Users/awesomeProject1/main.go:3) PCDATA $1, $-1
0x003c 00060 (/Users/awesomeProject1/main.go:3) PCDATA $0, $-2
0x003c 00060 (/Users/awesomeProject1/main.go:3) NOP
0x0040 00064 (/Users/awesomeProject1/main.go:3) CALL runtime.morestack_noctxt(SB)
0x0045 00069 (/Users/awesomeProject1/main.go:3) PCDATA $0, $-1
0x0045 00069 (/Users/awesomeProject1/main.go:3) JMP 0
可以看到 map 的创建是调用了 makemap 函数,通过寄存器传入了三个参数,分别是 type:map[string]string、10、以及main..autotmp_0+24(SP)。
makeup源码 makeup 为创建 map 的调用函数,接收 map 类型,容量以及 hmap 指针三个参数。其中 map type 用于获取 map 的 key val 类型,容量用来计算初始的 bucket 数量,hmap为创建 map 的内存地址。
下面是创建map 的步骤
- 在传入的 hmap 内存地址上初始化 hmap 结构体
- 根据 hint 容量计算出 bucket数量。
- 在连续内存中初始化 bucket 数组,在此过程中会在基础的 bucket 之后添加 overflow bucket 用于应对hash 冲突。
- 将初始化后的 bucket 数组保存在 hmap 中。
- 返回 hmap
func makemap(t *maptype, hint int, h *hmap) *hmap {
mem, overflow := math.MulUintptr(uintptr(hint), t.Bucket.Size_) //hint 为预估的元素个数,考虑到最坏的情况,每个桶里面仅有一个元素。所以使用 hint*BucktSize 来计算内存是否溢出。
//如果内存溢出了,直接为设置 hint 为 0,使用懒加载。
if overflow || mem > maxAlloc {
hint = 0
}
// initialize Hmap
if h == nil {
h = new(hmap)
}
h.hash0 = uint32(rand())
// Find the size parameter B which will hold the requested # of elements.
// For hint < 0 overLoadFactor returns false since hint < bucketCnt.
B := uint8(0)
//根据 80% 的负载,每个桶里有 8 个元素。计算出满足当前 hint 容量的桶数量 B
for overLoadFactor(hint, B) {
B++
}
h.B = B
// allocate initial hash table
// if B == 0, the buckets field is allocated lazily later (in mapassign) 懒加载
// If hint is large zeroing this memory could take a while.
if h.B != 0 {
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil {
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
makeBucketArray
改函数为创建 bucket 数组函数。bucket 在内存中是连续的,这有利于通过偏移量的快速访问。在极多 key 的情况下可以减少 cpu 内存的数据页置换。(这个问题反而在 go1.24 使用 swiss map 之后会凸显。)
当桶的数量大于等于 4 的时候设置 overflow bucket,其数量为 1<<(bucket 数量-4)。 将最后一个 overflow bucket 桶的 overflow 标志位指向第一个桶,以说明后续没有可用的 overflow bucket 桶。
这段代码中高强度的使用了内存地址运算!细细品味
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
base := bucketShift(b) //根据 b 计算桶的数量
nbuckets := base
// For small b, overflow buckets are unlikely.
// Avoid the overhead of the calculation.
if b >= 4 {
// Add on the estimated number of overflow buckets
// required to insert the median number of elements
// used with this value of b.
nbuckets += bucketShift(b - 4)
sz := t.Bucket.Size_ * nbuckets //根据编译时候的 Size 来计算内存空间
up := roundupsize(sz, !t.Bucket.Pointers())
if up != sz {
nbuckets = up / t.Bucket.Size_
}
}
//是否能够有 dirtyalloc可以复用
if dirtyalloc == nil {
buckets = newarray(t.Bucket, int(nbuckets))
} else {
// dirtyalloc was previously generated by
// the above newarray(t.Bucket, int(nbuckets))
// but may not be empty.
buckets = dirtyalloc
size := t.Bucket.Size_ * nbuckets
if t.Bucket.Pointers() {
memclrHasPointers(buckets, size)
} else {
memclrNoHeapPointers(buckets, size)
}
}
//溢出桶设置
if base != nbuckets {
// We preallocated some overflow buckets.
// To keep the overhead of tracking these overflow buckets to a minimum,
// we use the convention that if a preallocated overflow bucket's overflow
// pointer is nil, then there are more available by bumping the pointer.
// We need a safe non-nil pointer for the last overflow bucket; just use buckets.
//将最后一个溢出桶的overflow设置为第一个桶,
//如果桶的 overflow 为 nil,可以直接移动指针访问下一个桶。当非 nil 的时候说明已经在最后一个溢出桶了。
nextOverflow = (*bmap)(add(buckets, base*uintptr(t.BucketSize)))
last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.BucketSize)))
last.setoverflow(t, (*bmap)(buckets))
}
return buckets, nextOverflow
}
bmap
我一开始看 bmap 的时候有些疑问,为什么 bmap 里面只有 tophash 它的 key 和 val 存放在了哪里?
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 [abi.MapBucketCount]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.
}
实际的bmap 结构如下所示(TODO 添加图)
编译的时候 go 会根据存储的类型计算 bucketSize 的大小,可以看到只有 tophash 的内存大小是固定的。存储的 key与 val 的内存大小会随着其类型变化。
这里真的很巧妙,利用 bucketSize 以及指针就实现所有 map 类型。(这就是泛型吗?)
bucketSize = unsafe.Sizeof(bmap{}) + // tophash 大小
bucketCnt * keySize + // 所有键的大小
bucketCnt * valueSize + // 所有值的大小
unsafe.Sizeof(uintptr(0)) // overflow 指针大小
反思
既然已经存储了 key,为什么需要hashtop?
8 位的比较肯定比字符串的更快。
hashtop 中还可以存放一些约定好的状态,可以不遍历key 内存空间。获知 bucket 的状态。(可以看第二张map 的插入)