Go 1.24 全面拥抱 Swiss Table:让内置 map 提速 60% 的秘密

本文来自公众号 猩猩程序员 欢迎关注

本文来自 go.dev/blog/swisst...

哈希表是计算机科学的核心数据结构,也是包括 Go 在内的许多语言中 map 类型的底层实现。

1953 年,Hans Peter Luhn 在一份 IBM 内部备忘录中首次提出哈希表概念:通过把元素放进"桶"并用链表处理溢出,来加速查找。今天我们把这种实现称为"链地址法"哈希表。

1954 年,Gene M. Amdahl、Elaine M. McGraw 与 Arthur L. Samuel 在为 IBM 701 编程时首次采用"开放寻址"策略:当一个桶已被占用时,就把新元素放进下一个空桶。1957 年,W. Wesley Peterson 在《Addressing for Random-Access Storage》中正式发表这一思想。今天我们把这种实现称为"线性探测开放寻址"哈希表。

历经七十多年,我们很容易以为这种"老"数据结构已经"定型"。事实并非如此!计算机科学仍在持续改进基础算法,既优化算法复杂度,也更好地利用现代 CPU 硬件。例如,Go 1.19 就把 sort 包从传统快排换成了 2015 年才提出的 Pattern-Defeating Quicksort。

与排序算法类似,哈希表也在演进。2017 年,Google 的 Sam Benzaquen、Alkis Evlogimenos、Matt Kulukundis 与 Roman Perepelitsa 提出了一种新的 C++ 哈希表设计------"Swiss Tables";2018 年,其开源实现随 Abseil C++ 库发布。

Go 1.24 基于 Swiss Table 设计,对内置 map 进行了彻底重写。本文将介绍 Swiss Table 如何优于传统哈希表,以及将其引入 Go map 时遇到的独特挑战。

开放寻址哈希表

Swiss Tables 属于开放寻址哈希表,我们先回顾一下基本实现方式。

在开放寻址哈希表中,所有元素都存放在同一块底层数组里,数组中的每个位置称为一个 slot。每个键所属 slot 由哈希函数 hash(key) 决定。若目标 slot 已被占用(冲突),就按"探测序列"继续查找,直到找到一个空 slot。

示例

假设底层数组有 16 个 slot:

Slot 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Key 56 32 21 78

插入新键时,先计算 hash(key) % 16 得到目标 slot。若 slot 为空,直接插入;否则按线性探测依次尝试后继 slot。

查找过程同理,遇到空 slot 即可停止。

当元素数量超过数组大小时,哈希表会扩容(通常翻倍),并把旧元素全部重新插入到新数组中。为避免平均探测长度过长,哈希表并不会等到数组全满才扩容,而是设定一个最大负载因子(通常 70--90%)。

Swiss Table

Swiss Table 仍是开放寻址,但做了关键改进:

  1. 把底层数组按 8 个 slot 一组划分为"group"(也可更大)。
  2. 每组配一个 64 位"控制字"(control word)。8 个字节分别对应组内 8 个 slot,记录该 slot 是空、已删除还是占用;若占用,则保存该键哈希低 7 位(h2)。

示意图:

Group 0 Group 1
slot 0--7 slot 0--7
Key: 56,32,21,... Key: 78,...
Ctrl: 23,89,50,... Ctrl: 47,...

插入流程:

  1. 计算 hash(key),拆分为高 57 位 h1 与低 7 位 h2。
  2. 用 h1 选起始 group:h1 % group_cnt
  3. 在组内并行比较控制字,筛选 h2 匹配的候选 slot;若命中则更新,否则找空 slot;若无空 slot,则继续探测下一组。
  4. 查找同理,可用 SIMD 一次并行比较 8 个 slot。

借助控制字,我们一次就能完成 8 次探测,大幅减少比较次数。Abseil 与 Go 的实现都因此提高了最大负载因子,降低了平均内存占用。

Go 独有的挑战

Go 的内置 map 有两个特殊需求,给 Swiss Table 落地带来额外复杂性:

  1. 增量扩容

    传统实现触发扩容时,需要一次性复制全部元素,最坏情况下延迟极高。Go map 用于高并发服务器,必须限制单次插入的最坏延迟。

    因此 Go map 采用"增量扩容":把一个大 map 拆成多个最多 1024 条目的独立 Swiss Table。哈希的高位决定落入哪个表。扩容时只需复制 1024 条元素,延迟可控。

  2. 边遍历边修改

    Go 语言规范允许在遍历 map 时增删键值,并给出明确定义:

    • 删除尚未遍历到的键不会被产出;
    • 更新尚未遍历到的键会产出新值;
    • 新增的键可能或可能不被产出。
      传统"顺序扫描数组"方式在扩容时会打乱内存布局,无法满足语义。
      Go 的解决方法是:迭代器持有旧表的引用;若表在遍历期间扩容,仍按旧表顺序产出键值,但在真正返回前查询新表,确保返回最新值或跳过已删除项。这使得遍历成为 Go map 实现中最复杂的部分。

未来工作

在微基准测试中,Go 1.24 的 map 操作相比 1.23 最高提速 60%。

整体应用级基准平均 CPU 时间下降约 1.5%。

后续计划:

  • 提升未命中 CPU cache 时的局部性;
  • 在更多架构上利用 SIMD 指令,把 group 扩大到 16 slot,进一步减少探测次数。

致谢

Swiss Table 版 Go map 历经多年、多人贡献。感谢 YunHao Zhang (@zhangyunhao116)、PJ Malloy (@thepudds)、@andy-wm-arthur 早期实现;Peter Mattis (@petermattis) 结合 Go 需求完成 github.com/cockroachdb/swiss 库;Go 1.24 官方实现大量基于其工作。感谢社区每一位贡献者!

本文来自公众号 猩猩程序员 欢迎关注

相关推荐
菜鸟‍16 小时前
【前端学习】仿Deepseek官网AI聊天网站React
前端·学习·react.js
小光学长17 小时前
基于Vue的保护动物信息管理系统r7zl6b88 (程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
前端·数据库·vue.js
huangql52017 小时前
截图功能技术详解:从原理到实现的完整指南
前端·html5
长空任鸟飞_阿康17 小时前
Node.js 核心模块详解:fs 模块原理与应用
前端·人工智能·ai·node.js
这儿有一堆花18 小时前
网站链接重定向原理
前端
cecyci18 小时前
如何实现AI聊天机器人的打字机效果?
前端·javascript
IT_陈寒18 小时前
Vite 5个隐藏技巧让你的项目构建速度提升50%,第3个太香了!
前端·人工智能·后端
詩句☾⋆᭄南笙18 小时前
HTML的盒子模型
前端·html·盒子模型
落言18 小时前
AI 时代的工程师:懂,却非懂的时代
前端·程序员·架构
一枚攻城狮18 小时前
前端知识点大汇总
前端