前言
- go版本: go version go1.17.3 linux/amd64
- 建议阅读注释
- 如果没有实现过朴素的哈希表,推荐写一下leetcode 706.设计哈希映射,连最简单的哈希表都没写过就想来看实际工程实践中经过大量性能考量、折衷和优化的哈希表,还是比较困难的。
map总览
map相关的结构体定义在 runtime/map.go
中:
|
|
结构示意图:
-
注意红色箭头
-
Go的map和朴素的下标数组+相同哈希值链表形式的哈希表不同,但是思想类似。
-
在Go中,map的header(类型为hmap)用一个
hmap.buckets
指针,指向一个bucket数组,每个bucket对应的结构体是bmap
,一个bmap
只能存放bucketCnt
个键值对,多余的将会新申请一个bmap,用overflow
指针连接。 -
而
hmap.buckets
中存放的即哈希值取模后有相同结果的key的头结点,例如hmap.buckets[0]
存放的是hash(key)%len=0
的key
的头结点 -
此外,溢出bucket全部集中在一个
hmap.extra.overflow
的bucket数组(溢出数组)中存放。(实际上是用overflow数组实现链表) -
需要注意的是每个bmap中键值对存放方式:为了减少内存对齐时额外的开销,把key全部放一块,然后value全部放一块。
创建:
一些比较琐碎的细节
查找:
- 对要查找的
key
调用哈希算法后,得到哈希值,将哈希值和hmap.buckets
的长度求模,得到key
在hmap.buckets
数组中的下标index
- 再根据哈希值二进制形式的高八位构成的下标
high
(对应的十进制), 在以hmap.bucket[index]
为头结点的bmap
链表(数组实现的链表)中依次判断每个bmap
的tophash
数组中是否有值和high
相等,和high
相等的情况下还要进一步判断其对应的key
和要查找的key
是不是相等。
插入:
查找过程和上面类似。插入会面临当前溢出桶没有空余位置的情况,此时会使用hmap.newoverflow()
方法,先判断是否可以使用hmap.extra.nextoverflow
做为新的溢出桶,不行则创建(new)桶。
删除:
- 找到对应的key,如果key和value是指针类型还需要进行对应的内存释放操作,将
bmap
中key
数组和elem
数组对应下标的元素清空,并将tophash
数组对应位置标记为emptyOne
- 如果在删除期间哈希表正在扩容,那么会分流桶中的键值对,分流结束之后会找到桶中的目标元素完成删除工作
扩容:
在runtime/map.go/hashGrow()
中可以知道,map在两种情形下触发扩容:
- map的装载因子
LoadFactor>6.5
, 使用overLoadFactor()
方法判断。这种情形下进行扩容,扩容涉及哈希值重新计算。 - 溢出桶数量
noverflow>=2^15
,使用tooManyOverflowBuckets()
方法判断。这种情形下,不扩容,称为sameSizeGrow
上面两种情形都会将hmap.oldbuckets
设置成hmap.buckets
, 然后开辟新的buckets
数组并赋值给hmap.buckets
。
map的"扩容"是渐进式的,分流的实际操作在evacute()
中完成。
分流的大致流程是,将hmap.buckets
数组中每一项(逻辑含义是哈希值取模后有相同结果的链表的头结点),对这个链表中的节点分流成x和y两部分,核心原理举例说明:
原来hmap的容量为8,哈希值为15、23的两个key计算的下标:
15%8=7
23%8=7
扩容后hmap
的容量为16,哈希值计算不变,仍旧为15、23,但key
在hmap.buckets
中的下标将发生变化:
15%16=15 = 7+8 = OldIndex + OldCapacity
23%16=7
则15分入下标为15的y桶中,23分入下标为7的x桶中。
即,对于两倍扩容(不是sameSizeGrow
)分流只可能产生两种结果,将hmap.buckets[]
中原来下标为idx
的key
分流到现在下标为idx
或idx+oldCapacity
的桶中。
而sameSizeGrow
,只是将桶中的数据进行整合,例如频繁插入和删除key导致的大量"空"位置。而sameSizeGrow
只迁移未被删除的数据,使得新的hmap.buckets
中每个哈希值的链表更短,也使得hmap.extra.overflow
数组更小
map的创建
|
|
map 查找和插入
|
|
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {}
返回的是指针func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) {}
返回的是指针,布尔变量标识是否找到
map 插入
|
|
map 删除
|
|
map for range 迭代
|
|
map 扩容
|
|