go基础之map-增和改(二)

本文深入探讨了Go语言中map的增加和修改操作的实现细节,包括makemap_small和makemap的区别,以及在不同情况下如何进行内存分配和扩容。文章分析了map在添加元素时不触发扩容时的内存结构,并详细讲解了扩容过程中的数据迁移机制。通过对源码的分析,揭示了Go中0.65扩容因子的选取原因以及如何优化存储和查找效率。
摘要由CSDN通过智能技术生成

写在之前

在上篇文章《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.
	
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhangshen023

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值