Go 1.24 与 Swiss Table 革命:Map 的新纪元
https://mp.weixin.qq.com/s/T7FLiFqdJF7p9S0QxfQaIw原创 Go Official Blog Go Official Blog
随着 Go 1.24 的发布,Golang 在性能优化方面取得了重大进展——这次针对的是其最基础的数据结构之一:map。受 Google 高性能 Swiss Table[1](在 C++ 的 Abseil 库中广受欢迎)的启发,Go 团队重新设计了 map 实现的内部结构,提供了更快的查找、更低的内存开销和更好的缓存利用率。这一变化标志着 Go 处理哈希表方式的重大转变,解决了长期存在的低效问题,同时采用了现代哈希表设计原则。
第一部分:旧版 Map(快速回顾)在 Go 1.24 之前,map 的工作方式如下:
- 桶(Buckets):键存储在桶中(每组8个条目)
- 溢出链(Overflow Chains):如果一个桶填满,新条目会进入溢出桶,像链表一样连接
- 顶部哈希(Tophash):每个条目都有一个微小的顶部哈希(键哈希的8位),用于快速跳过不匹配的键
这种方式可行,但存在一些问题:
- 查找慢:扫描溢出链意味着在内存中跳来跳去(产生缓存未命中)
- 内存膨胀:溢出桶占用额外空间
- 调整大小困难:扩展map意味着要重新哈希所有内容
让我们逐步分解 Swiss Table 的核心机制,通过具体示例来说明。关键在于智能元数据和缓存友好的分组。
步骤1:哈希分割(H1 vs H2)
假设你向 Go map 插入键price。哈希函数生成一个64位值,比如:H = 0x5F4A3B2C1D(为清晰起见简化)
Swiss Table 将这个哈希分为两部分:
- H2(7位):哈希的前7位。可以看作是键的"微型指纹"
- H1(57位):剩余位。决定键属于哪个组
例如:
- H2 =
0x5F4A3B2C1D的前7位 →0x5F(二进制:01011111) - H1 = 剩余位 → 用于计算组索引
步骤2:元数据——神奇的速查表
每个 map 条目都有 1 字节的元数据字段,存储:
- 7位H2值
- 1位状态标志:
- 0:空
- 1:占用
0x80(二进制10000000):墓碑(已删除)
步骤3:使用H1进行组查找
map 被划分为 16 个条目的组。查找"price"的过程:
- 使用H1计算组索引:
group_index = H1 % number_of_groups - 检查该组中所有16个条目的元数据
步骤4:处理冲突(无链表!)
如果组已满:
- 线性探测:检查下一组,直到找到空槽
- 墓碑:被标记为删除的槽位可用于插入,但在查找时保留
优势:
- 不再追踪链表——所有内容都在连续内存中
- CPU 缓存友好(更少的缓存未命中)
Swiss Table 的精妙之处在于:
- 元数据:用于跳过无用工作的微型速查表
- 分组:缓存友好的内存布局
- 线性探测:更简单、更快的冲突处理
Go 1.24 的新 map 不仅更快——而且更智能。而且你无需改变任何代码就能获得这些性能的提升!🎉
参考资料[1]swiss table design: https://www.dolthub.com/blog/2023-03-28-swiss-map/