Golang - Map 内部实现原理解析

Golang - Map 内部实现原理解析

一.前言

  • Golang中Map存储的是kv键值对,采用哈希表作为底层实现,用拉链法解决hash冲突

本文Go版本:gov1.14.4,源码位于src/runtime/map.go

二.Map的内存模型

在源码中,表示map的结构体是hmap,是hashmap的缩写

const (
	// 一个桶(bucket)内 可容纳kv键值对 的最大数量
	bucketCntBits = 3
	bucketCnt     = 1 << bucketCntBits
)

// map的底层结构
type hmap struct {
	count     int    // map中kv键值对的数量
	flags     uint8  // 状态标识符,比如正在被写,buckets和oldbuckets正在被遍历或扩容
	B         uint8  // 2^B=len(buckets)
	noverflow uint16 // 溢出桶的大概数量,当B小于16时是准确值,大于等于16时是大概的值
	hash0     uint32 // hash因子

	buckets    unsafe.Pointer // 指针,指向一个[]bmap类型的数组,数组大小为2^B,我们将一个bmap叫做一个桶,buckets字段我们称之为正常桶,正常桶存满8个元素后,正常桶指向的下一个桶,我们将其叫做溢出桶(拉链法)
	oldbuckets unsafe.Pointer // 类型同上,用途不同,用于在扩容时存放之前的buckets
	nevacuate  uintptr        // 计数器,表示扩容进度

	extra *mapextra // 用于gc,指向所有的溢出桶,正常桶里面某个bmap存满了,会使用这里面的内存空间存放键值对
}

// 溢出桶结构
type mapextra struct {
	overflow    *[]*bmap // 指针数组,指向所有溢出桶
	oldoverflow *[]*bmap // 指针数组,发生扩容时,指向所有旧的溢出桶

	nextOverflow *bmap // 指向 所有溢出桶中 下一个可以使用的溢出桶
}

// 桶结构
type bmap struct {
	tophash  [bucketCnt]uint8     // 存放key哈希值的高8位,用于决定kv键值对放在桶内的哪个位置

	// 以下属性,编译时动态生成,在源码中不存在
	keys     [bucketCnt]keytype   // 存放key的数组
	values   [bucketCnt]valuetype // 存放value的数组
	pad      uintptr              // 用于对齐内存
	overflow uintptr              // 指向下一个桶,即溢出桶,拉链法
}

用图表示一下map底层的内存模型:

Golang - Map 内部实现原理解析_第1张图片

解析:

  • map的内存模型中,其实总共就三种结构,hmap,bmap,mapextra

  • hmap表示整个map,bmap表示hmap中的一个桶,map底层其实是由很多个桶组成的

  • 当一个桶存满之后,指向的下一个桶,就叫做溢出桶,溢出桶就是拉链法的具体表现

  • mapextra表示所有的溢出桶,之所以还要重新的指向,目的是为了用于gc,避免gc时扫描整个map,仅扫描所有溢出桶就足够了

  • 桶结构的很多字段得在编译时才会动态生成,比如key和values等

  • 桶结构中,之所以所有的key放一起,所有的value放一起,而不是key/value一对对的一起存放,目的便是在某些情况下可以省去pad字段,节省内存空间

  • golang中的map使用的内存是不会收缩的,只会越用越多。

三.Map的设计原理

1.hash值的使用

通过哈希函数,key可以得到一个唯一值,map将这个唯一值,分成高8位和低8位,分别有不同的用途

  • 低8位:用于寻找当前key属于哪个bucket
  • 高8位:用于寻找当前key在bucket中的位置,bucket有个tohash字段,便是存储的高8位的值,用来声明当前bucket中有哪些key,这样搜索查找时就不用遍历bucket中的每个key,只要先看看tohash数组值即可,提高搜索查找效率

map其使用的hash算法会根据硬件选择,比如如果cpu是否支持aes,那么采用aes哈希,并且将hash值映射到bucket时,会采用位运算来规避mod的开销

2.桶的细节设计

bmap结构,即桶,是map中最重要的底层实现之一,其设计要点如下:

  • 桶是map中最小的挂载粒度:map中不是每一个key都申请一个结构通过链表串联,而是每8个kv键值对存放在一个桶中,然后桶再通以链表的形式串联起来,这样做的原因就是减少对象的数量,减轻gc的负担。

  • 桶串联实现拉链法:当某个桶数量满了,会申请一个新桶,挂在这个桶后面形成链表,新桶优先使用预分配的桶。

  • 哈希高8位优化桶查找key : 将key哈希值的高8位存储在桶的tohash数组中,这样查找时不用比较完整的key就能过滤掉不符合要求的key,tohash中的值相等,再去比较key值

  • 桶中key/value分开存放 : 桶中所有的key存一起,所有的value存一起,目的是为了方便内存对齐

  • 根据k/v大小存储不同值 : 当k或v大于128字节时,其存储的字段为指针,指向k或v的实际内容,小于等于128字节,其存储的字段为原值

  • 桶的搬迁状态 : 可以根据tohash字段的值,是否小于minTopHash,来表示桶是否处于搬迁状态

3.map的扩容与搬迁策略

map底层扩容策略如下:

  • map的扩容策略是新分配一个更大的数组,然后在插入和删除key的时候,将对应桶中的数据迁移到新分配的桶中去

map的搬迁策略如下:

  • 由于map扩容需要将原有的kv键值对搬迁到新的内存地址,直接一下子全部搬完会非常的影响性能
  • 采用渐进式的搬迁策略,将搬迁的O(N)开销均摊到O(1)的赋值和删除操作上

以下两种情况时,会进行扩容:

  • 当装载因子超过6.5时,扩容一倍,属于增量扩容

  • 当使用的溢出桶过多时间,重新分配一样大的内存空间,属于等量扩容,实际上没有扩容,主要是为了回收空闲的溢出桶

装载因子等于 map中元素的个数 / map的容量,即len(m

你可能感兴趣的:(python,数据结构,java,hashmap,面试)