一个 map 长什么样
对于一个 map 来说,当我们操作一个元素,会通过对应的 hash 函数,散列到对应的存储桶数组 bucket,如果这个数组的空间不够,则会出现溢出,溢出的部分一般通过链表或者红黑树来存放。
在 go 中对字典 map 的类型抽象定义如下,其中 Map
中的 Key
和 Value
分别用于确定 Map
的键值是哪种类型,而 Bucket
字段则指向哈希表的存储桶,存储真实数据:
创建 map 会返回一个 hmap
指针对象, hmap
中有若干个 buckets
存储桶,存储桶又由若干个 bmap
组成,hmap
和 bmap
的关系大致如下:
如何创建一个 map
我们都知道在 golang 中,创建一个 map
可以通过 make(map[string]int)
方式,但实际底层是如何创建的呢?让我们来一探究竟。
搜索源码,平时使用 new(map[string]int)
, make(map[string])
, append
, delete
等操作 map
的关键字,背后其实调用的是这些方法:
关于如何创建一个 map
,我们可以留意这几个方法:
- makemap()
当编译期(runtime compile)能确定 map 或 map 的第一个存储桶 bucket 可以在栈上面进行创建,并且哈希表或存储桶不为空,会调用该方法进行创建:
- makemakp_small()
当调用 make(map[k]v)
或 make(map[k]v, hint)
进行 map
实例化时,如果期望值 hint
的值是在编译期间(runtime compile)确定的值,判定 map
被分配在堆上,会调用 makemap_small()
方法进行创建 map:
- makemap64()
逻辑同上,在 64-bit 下调用该方法进行实例化;
需要注意的是,当期望值 hint 可以转为 int 类型时,使用
makemap
来替代 makemap64
方式在 32-bit 的机型下能够可以更快、占用空间更小。
深入了解 map 结构
hmap
hmap
结构如下,其中的 extras
字段指向了一个溢出桶数组(具体来说,extras.overflow
字段指向 hmap.buckets
,extra.oldoverflow
字段指向 hmap.oldbuckets
):
oldbuckets
会在哈希表扩容的过程中被使用,扩容时将原有的 buckets
赋值给 oldbuckets
,buckets
再指向一个更大的数组;扩容时哈希表中的所有元素都需要从 oldbuckets
再搬迁回新的 buckets
,就需要重新计算哈希值,这个过程被称为 rehashing
1。为了避免迁移过程影响性能,整个过程采取的渐进式迁移。
mapextra
在 hmap
中是一个指针,指向包含溢出桶的结构;
overflow
: 指向当前哈希表的溢出桶数组;哈希表在插入时可能会产生哈希冲突,冲突的元素就会存放在溢出桶中;
oldoverflow
: 指向旧哈希表的溢出桶数组;在哈希表扩容的过程中,因旧的存储桶需要迁移到新存储桶,此时 oldoverflow
指向的就是旧哈希表的溢出桶;
nextoverflow
: 指向下一个即将被使用的溢出桶;当插入新元素产生哈希冲突时,就会存放在 nextoverflow
所指向的桶;
bmap
bmap
中的 tophash
用于优化 map 的快速查找,每个 bmap
可以存储多个键值对,为了快速判断某个键是否在存储桶中,每个键都有对应的 tophash
值,当 tophash
值匹配后,才会进行完整的键值比较:
tophash
字段的 abi.MapBucketCount
值在 src/internal/abi/map.go
中进行定义,MapBucketCount
为 8(1<<MapBucketCountBits)
,因此一个存储桶的长度为 8。
我们上面分析了 bmap
存储桶的实际结构,很明显仅有一个 tophash
字段没有任何实际作用。因为在编译期间,还会动态增加一些字段,结构如下所示:
深入了解 map 创建过程
上面提到了,创建 map 时会调用 makemap
函数,函数具体实现如下:
哈希表的负载因子
选择一个合适的负载因子 loadFactor 很重要:
- 太大会导致过多的溢出桶;
- 太小会浪费很多的存储空间;
负载因子的一般计算方式如下:
loadFactor(α)=mn
在源码中负载因子的具体实现:
在不同的负载因子下的表现情况如下(64bit,map 的 key 和 value 都是 8 字节):
指标 | 描述 |
---|
loadFactor | 负载因子 |
overflow | 溢出桶的占比 |
bytes/entry | 每个键值对(key/elem pair)额外的存储空间浪费 |
hitprobe | 访问一个存在的 key 所需要的查找次数 |
missprobe | 访问一个不存在的 key 所需要的查找次数 |
loadFactor | %overflow | bytes/entry | hitprobe | missprobe |
---|
4.00 | 2.13 | 20.77 | 3.00 | 4.00 |
4.50 | 4.05 | 17.30 | 3.25 | 4.50 |
5.00 | 6.85 | 14.77 | 3.50 | 5.00 |
5.50 | 10.55 | 12.94 | 3.75 | 5.50 |
6.00 | 15.27 | 11.67 | 4.00 | 6.00 |
6.50 | 20.90 | 10.79 | 4.25 | 6.50 |
7.00 | 27.14 | 10.15 | 4.50 | 7.00 |
7.50 | 34.03 | 9.73 | 4.75 | 7.50 |
8.00 | 41.10 | 9.40 | 5.00 | 8.00 |
哈希表的桶是如何创建的
在下一篇,我们会介绍下 map
为什么不是并发安全的,以及如何才能保证并发读写是安全的。
参考链接: