博客 记录生活,记录工作。

golang基础-06-map的存储结构

2019-04-05
gongmh
go

在开发过程中,经常通过map实现kv数据的处理。源码src/runtime/map.go中可以看到map的底层结构,网上也有很多对map的分析,此处不再赘述。本文先综述map的结构,然后根据具体的一个map变量分析map底层是怎么存储的。

1. 内存中的map的结构

map_mem_struct

map的实现是通过hash来实现,底层是一个hmap和若干个bmap组成,基本结构见上图。

总结下来就是以下几点:

1. map变量是一个指针,指向hmap结构体;
2. hmap中包含map中元素的个数count,桶的个数(2^B),桶数组的地址`buckets`等,
3. hmap的元素`buckets`指向bmap的数组,根据hash确定key落入哪个bmap中;
4. hmap的元素`oldbuckets`指向扩容阶段之前的bmap的数组;
5. bmap中tophash用来快速定位key是否在这个bmap中;
6. bmap中8个kv槽位用来存储key和value;
7. bmap中overflow用来拉链指向新的桶,解决添加超过key时,超过8个槽位的情况;

2. 实例分析

接下来通过一个具体的例子,分析下map变量的内存结构,感兴趣的可以自己试下

map_var_eg

图中列出变量的实际地址,以及每个字段的值,接下来分步解析。

2.1 定义map变量

	mapLen := 9
	var mapVar = make(map[int]int, mapLen)
	for i := 0; i < mapLen; i++ {
		mapVar[i] = i + 1
	}

首先,定义一个map[int]int的变量mapVar,由于map的一个桶中只有8个槽位,此例中设置map中有9对kv。

2.2 变量自身结构

	fmt.Printf("mapVar 内容:%v\n", mapVar)
	//output: mapVar 内容:map[0:1 1:2 2:3 3:4 4:5 5:6 6:7 7:8 8:9]

	fmt.Printf("mapVar 占用字节数:%v\n", unsafe.Sizeof(mapVar))
	//output: mapVar 占用字节数:8

	mapVarValue := uintptr(*(*int)(unsafe.Pointer(&mapVar)))
	fmt.Printf("mapVar 地址:%p,值:%x\n", &mapVar, mapVarValue)
	//output: mapVar 地址:0xc00000e028,值:c000054150

变量mapVar是一个指针,指向hmap结构体。mapVar变量本身占用字节数为8,所在的地址为0xc00000e028,存储的值为c000054150。 变量mapVar对应的map为map[0:1 1:2 2:3 3:4 4:5 5:6 6:7 7:8 8:9]

2.3 hmap结构

接下来继续分析hmap每个字段的内存结构。

	//hmap 结构体首地址
	hmapBaseAddr := mapVarValue

hmap结构体的首地址就是mapVar的值,获取到hamp首地址hmapBaseAddr

	//count address:  base
	countPtr := (*int)(unsafe.Pointer(hmapBaseAddr))
	fmt.Printf("hmap count 地址:%p,值:%d\n", countPtr, *countPtr)
	//output: hmap count 地址:0xc000054150,值:9

hmap的count字段存储map中包含的元素个数,本例中就是9。

	//hmap B address:  base + count(int,8) + flags(uint8,1)
	bPtr := (*uint8)(unsafe.Pointer(hmapBaseAddr + 9))
	fmt.Printf("hmap B 地址:%p,值:%d\n", bPtr, *bPtr)
	//output: hmap B 地址:0xc000054159,值:1

hmap的B字段值为1,表明mapVar中一个有2(2^B)个桶(bmap)。

	//hmap buckets address:  base + count(int,8) + flags(uint8,1) + B(uint,1) + noverflow(uint16,2) + hash(uint32,4)
	bucketsPtr := (*int)(unsafe.Pointer(hmapBaseAddr + 16))
	fmt.Printf("hmap buckets 地址:%p,值:%x\n", bucketsPtr, *bucketsPtr)
	//output: hmap buckets 地址:0xc000054160,值:c000070000

hmap的buckets字段是一个指针,指向bmap结构体数组,也就是[2]bmap。

	//hmap oldbuckets address:  base + count(int,8) + flags(uint8,1) + B(uint,1) + noverflow(uint16,2) + hash(uint32,4) + bucktes(unsafe.Pointer,8)
	oldbuckets := (*int)(unsafe.Pointer(hmapBaseAddr + 24))
	fmt.Printf("hmap oldbuckets 地址:%p,值:%x\n", oldbuckets, *oldbuckets)
	//output: hmap oldbuckets 地址:0xc000054168,值:0

hmap的oldbuckets字段同样也是一个指针,指向bmap结构体数组,存储map扩缩容时的桶数组指针。此时没有扩缩容过程,oldbuckets的值为0。

2.4 bmap数组分析

找到bmap数组的首地址,即hmap字段buckets的值c000070000

	//hmap buckets 指向的bmap数组首地址
	bmapArrPtr := uintptr(*(*int)(unsafe.Pointer(bucketsPtr)))

	//bmap数组第一个bmap结构体首地址
	firstBucketPtr := bmapArrPtr

得到bmap数组的第一个结构体bmap[0]的指针。

	//bmap[0] tophash
	tophash0 := (*[8]uint8)(unsafe.Pointer(firstBucketPtr))
	fmt.Printf("bmap[0] tophash 地址:%p,值:%d\n", tophash0, *tophash0)
	//output: bmap[0] tophash 地址:0xc000070000,值:[26 125 177 0 0 0 0 0]

bmap[0]的tophash记录,对应key的hash值高位以及key的状态标识。可以看到桶(bmap[0])的前三个key有值,即第一个桶存放3个kv。

	//bmap[0] keys
	keys0 := (*[8]int)(unsafe.Pointer(firstBucketPtr + 8))
	fmt.Printf("bmap[0] keys 地址:%p,值:%d\n", keys0, *keys0)
	//output: bmap[0] keys 地址:0xc000070008,值:[1 4 6 0 0 0 0 0]

	//bmap[0] values
	values0 := (*[8]int)(unsafe.Pointer(firstBucketPtr + 8 + 8*8))
	fmt.Printf("bmap[0] values 地址:%p,值:%d\n", values0, *values0)
	//output: bmap[0] values 地址:0xc000070048,值:[2 5 7 0 0 0 0 0]

	//bmap[0] overflow
	overflow0 := (*int)(unsafe.Pointer(firstBucketPtr + 8 + 8*8 + 8*8))
	fmt.Printf("bmap[0] overflow 地址:%p,值:%d\n", overflow0, *overflow0)
	//output: bmap[0] overflow 地址:0xc000070088,值:0

bmap[0]中继续往下找,找到8个keys和8个values。看到map的3对kv值存储在第一个桶中。overflow的值为0,说明没有碰撞的key被拉链出去。

	//bmap数组第二个bmap结构体首地址
	secondBucketPtr := bmapArrPtr + 8 + 8*8 + 8*8 + 8

继续往下找,找到bmap数组的第二个结构体bmap[1]的指针。

	//bmap[1] tophash
	tophash := (*[8]uint8)(unsafe.Pointer(secondBucketPtr))
	fmt.Printf("bmap[1] tophash 地址:%p,值:%d\n", tophash, *tophash)
	//output: bmap[1] tophash 地址:0xc000070090,值:[88 156 97 100 191 59 0 0]

同样,bmap[1]可以看出第二个桶存放6对map的kv。

	//bmap[1] keys
	keys := (*[8]int)(unsafe.Pointer(secondBucketPtr + 8))
	fmt.Printf("bmap[1] keys 地址:%p,值:%d\n", keys, *keys)
	//output: bmap[1] keys 地址:0xc000070098,值:[0 2 3 5 7 8 0 0]

	//bmap[1] values
	values := (*[8]int)(unsafe.Pointer(secondBucketPtr + 8 + 8*8))
	fmt.Printf("bmap[1] values 地址:%p,值:%d\n", values, *values)
	//output: bmap[1] values 地址:0xc0000700d8,值:[1 3 4 6 8 9 0 0]

	//bmap[1] overflow
	overflow := (*int)(unsafe.Pointer(secondBucketPtr + 8 + 8*8 + 8*8))
	fmt.Printf("bmap[1] overflow 地址:%p,值:%d\n", overflow, *overflow)
	//output: bmap[1] overflow 地址:0xc000070118,值:0

bmap[1]中,找到8个keys和8个values。看到map的6对kv值存储在第二个桶中。overflow的值为0,说明没有碰撞的key被拉链出去。

至此,mapVar变量的内存分析完毕,详细内容可以参考本节的图及本例源码,感兴趣的同学可以继续分析map其他情况~

注:本文基于go1.11版本分析,其他版本请基于对应版本的源码分析。


相关博文

评论