go基础之map-增和改(二)
写在之前
在上篇文章《go基础之map-写在前面(一)》介绍了map的数据结构,本篇会详细介绍map的增和改的代码实现,由于增和改的实现基本上差不多,所以就纳到一起分析了。如果想详细查看源码的注释,可以查看我的GitHub,欢迎批评指正。我的打算是把一些常用的数据结构都分析一遍,如果有志同道合的人,可以联系我。
环境说明
我的具体调试环境在《go基础之map-写在前面(一)》已经说明的非常仔细了,现在只讲我分析增和改的调试代码。
package main
import (
"fmt"
"strconv"
)
func main() {
m1 := make(map[string]string, 9)
fmt.Println(m1)
for i := 0; i < 20; i++ {
str := strconv.Itoa(i)
m1[str] = str
}
}
老规矩编译一波,看看第9行的申明到底干了啥?
go tool compile -N -l -S main.go > main.txt
编译结果有点多,我只列举重点代码:
"".main STEXT size=394 args=0x0 locals=0x98
0x0000 00000 (main.go:8) TEXT "".main(SB), ABIInternal, $152-0
....
0x0036 00054 (main.go:9) LEAQ type.map[string]string(SB), AX
0x003d 00061 (main.go:9) PCDATA $0, $0
0x003d 00061 (main.go:9) MOVQ AX, (SP)
0x0041 00065 (main.go:9) MOVQ $9, 8(SP)
0x004a 00074 (main.go:9) MOVQ $0, 16(SP)
0x0053 00083 (main.go:9) CALL runtime.makemap(SB)
....
0x0107 00263 (main.go:13) PCDATA $0, $5
0x0107 00263 (main.go:13) LEAQ type.map[string]string(SB), DX
0x010e 00270 (main.go:13) PCDATA $0, $4
0x010e 00270 (main.go:13) MOVQ DX, (SP)
0x0112 00274 (main.go:13) PCDATA $0, $6
0x0112 00274 (main.go:13) MOVQ "".m1+56(SP), BX
0x0117 00279 (main.go:13) PCDATA $0, $4
0x0117 00279 (main.go:13) MOVQ BX, 8(SP)
0x011c 00284 (main.go:13) PCDATA $0, $0
0x011c 00284 (main.go:13) MOVQ CX, 16(SP)
0x0121 00289 (main.go:13) MOVQ AX, 24(SP)
0x0126 00294 (main.go:13) CALL runtime.mapassign_faststr(SB)
....
- 第9行调用了
runtime.makemap
方法做一些初始化操作,我把map的初始容量设为大于8底层才会走该方法,否则会调用runtime.makemap_small方法。
- 第22行调用了
runtime.mapassign_faststr方法
,该方法对应main.go
第13行的赋值方法m1[str] = str
。
我们找到了方法,在后面就可以在$go_sdk_path/src/runtime/map.go
和$go_sdk_path/src/runtime/map_faststr.go
找到方法,然后断点调试即可。
makemap_small和makemap的区别
makemap_small
的代码如下:
// makemap_small implements Go map creation for make(map[k]v) and
// make(map[k]v, hint) when hint is known to be at most bucketCnt
// at compile time and the map needs to be allocated on the heap.
func makemap_small() *hmap {
h := new(hmap)
h.hash0 = fastrand()
return h
}
该代码的实现十分简单,就设置了一个hash种子,其他的例如申通桶内存的操作只有在真正赋值数据的时候才会创建桶。该方法在什么情况会被调用呢?如注释说说"hint is known to be at most bucketCnt at compile time and the map needs to be allocated on the heap",bucketCnt就是8个,所以上面我的示例代码为何要设初始容量为9的原因就在这里。
我就直接略过这种情况,因为在实际应用场景下还是要指定容量,避免后面因为频繁扩容造成性能损失,makemap
的代码如下:
// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap {
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
// 是否超出了最大的可分配虚拟内存或者超出了uintptr表示的值
if overflow || mem > maxAlloc {
hint = 0
}
// initialize Hmap
if h == nil {
h = new(hmap)
}
// 随机hash因子
h.hash0 = fastrand()
// Find the size parameter B which will hold the requested # of elements.
// For hint < 0 overLoadFactor returns false since hint < bucketCnt.
// 计算B的值,桶的个数为 1 << B
B := uint8(0)
// 不断的循环得到最大的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
// 根据B的值去申请桶,包括逸出桶
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil {
h.extra = new(mapextra)
// nextOverflow指向逸出桶的内存地址
h.extra.nextOverflow = nextOverflow
}
}
return h
}
看程序注释大概明白该代码的作用就是得到B值和申请桶,overLoadFactor
方法是用了6.5的扩容因子去计算出最大的B值,保证你申请的容量count要大于 (1>> B) * 6.5, 这个扩容因子想必大家都不陌生,在java中是0.75,为什么在go中是0.65呢?在runtime/map.go
开头处有测试数据,综合考虑来说选择了6.5。大家可能注意到maptype
用来申请桶的内存块了,下面看看maptype
的代码,也有助于理解map的结构:
type maptype struct {
typ _type
// map的key的类型
key *_type
// map的value的类型
elem *_type
// 桶的类型
bucket *_type // internal type representing a hash bucket
// function for hashing keys (ptr to key, seed) -> hash
hasher func(unsafe.Pointer, uintptr) uintptr
// key的类型的大小
keysize uint8 // size of key slot
// value类型元素的大小
elemsize uint8 // size of elem slot
// 桶里面所有元素的大小
bucketsize uint16 // size of bucket
flags uint32
}
makemap
方法里面math.MulUintptr(uintptr(hint), t.bucket.size)
用到了bucket的size,这里这个size和maptype
的bucketsize一模一样都是272(《go基础之map-写在前面(一)》有介绍为什么是272),所以就能计算出需要分配的内存。仔细分析makemap
的字段,可以发现定义了map的基本数据结构,后面代码用来申请桶的内存块的时候都使用了这个数据结构。
makemap
第36行代码调用了方法makeBucketArray
方法来申请内存,我们简单看看它里面的细节:
// makeBucketArray initializes a backing array for map buckets.
// 1<<b is the minimum number of buckets to allocate.
// dirtyalloc should either be nil or a bucket array previously
// allocated by makeBucketArray with the same t and b parameters.
// If dirtyalloc is nil a new backing array will be alloced and
// otherwise dirtyalloc will be cleared and reused as backing array.
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
base := bucketShift(b)
nbuckets := base
// For small b, overflow buckets are unlikely.
// Avoid the overhead of the calculation.