这篇文章解释了为什么在 Go 1.24 版本中,你的程序可能因为 map 变慢了,以及 Go 团队是怎么计划修复这个问题的。
Golang 1.24 中最吸引我的功能就是 SwissMap,在以前的非官方实现中,有些场景能够提升50% 的性能,官方的实现中,也有不小的性能提升。
详情参考我以前的文章:
SwissTable 会成为 Golang std map嘛?
Go 1.24 的 Swiss Map:兼容性、扩展哈希与遗留问题
但是 如果你在使用 Go 1.24 时可能会发现SwissMap 没有达到预期的表现,甚至程序运行变慢了,特别是在 Map很大的时候,这不是你的幻觉,确实存在这个问题。
https://x.com/valyala/status/1879988053076504761
这个问题记录在 Issue #70835 中,开发者们正在努力解决它。
问题出在哪?
Go 的 map 在新版中使用了Swiss Table,它在小 map 和高并发的场景下非常快。但是当 map 很大、数据又不在 CPU 缓存里(也就是说,数据是“冷”的),就会变慢。
为什么会这样呢?
因为 SwissMap 的内部结构比较复杂,它会分几层来存储数据:
- 首先是一个 map 的头部结构
- 它指向一个目录,这个目录是多个 table 的指针组成的列表
- 每个 table 里面有控制信息、key 和 value
当你查找一个 key 的时候,可能需要进行 4 到 6 次跳转,每一次都可能遇到缓存未命中。这样就会导致 CPU 忙着从内存取数据,速度就慢了。
像 Prometheus 这样的大型项目就发现了这个问题。他们升级到 Go 1.24 后,CPU 使用率上升了很多,经调查发现就是因为 map 的查找变慢了。
是怎么发现的?
问题并不是在测试用例里发现的,而是在真实的线上场景中出现的。
- 使用了很大的 map(比如几兆字节大小)
- 经常读取 map 里的数据
- 数据不在 CPU 的高速缓存里
Go 团队的工程师 Michael Pratt 通过做很多测试,找到了 map 访问变慢的原因,并在 Issue #70835 中详细说明。
怎么修?
为了让 map 更快,他们计划做以下几件事:
- 简化目录结构:把原来存指针的列表改成直接存结构,减少一次跳转
- 控制信息更紧凑:把控制信息安排得更集中,这样更容易被 CPU 一次加载
- 分离 key 和 value:改成“key-key-key + value-value-value”的结构,这样可以优化加载顺序
- 对齐控制字节:把控制信息按照 CPU 缓存对齐,减少未命中
这些改动并不简单,因为会影响到 Go 的运行时核心:
- 要确保垃圾回收能正常工作
- map 扩容和缩容逻辑要更新
- 要确保对小 map 的性能没有影响
哪些地方在讨论这个?
这个问题被广泛讨论和跟踪,可以通过Issue #70835 了解更多的 细节。 Go Release Dashboard 中已经标记这个问题将会在 Go1.25 中解决
此外 Issue #71368 中也讨论了 另一个与内存布局的问题。
总结
Go 团队一直在努力让语言运行得更快更稳。SwissMap 是个好改进,但它也带来了新挑战,比如这次的冷缓存性能下降。
Issue #70835 展示了 Go 是如何通过社区反馈不断进步的。感谢像 Prometheus 这样的开源项目,他们的报告帮助 Go 做得更好。
如果一切顺利,Go 1.25 就能把速度和稳定性都带回来。
我们一起期待吧!