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 官方实现大量基于其工作。感谢社区每一位贡献者!

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

相关推荐
C澒2 分钟前
多场景多角色前端架构方案:基于页面协议化与模块标准化的通用能力沉淀
前端·架构·系统架构·前端框架
崔庆才丨静觅4 分钟前
稳定好用的 ADSL 拨号代理,就这家了!
前端
江湖有缘5 分钟前
Docker部署music-tag-web音乐标签编辑器
前端·docker·编辑器
恋猫de小郭1 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端