Featured image of post Go 的 map 为什么变慢了?.zh-cn

Go 的 map 为什么变慢了?.zh-cn

 

这篇文章解释了为什么在 Go 1.24 版本中,你的程序可能因为 map 变慢了,以及 Go 团队是怎么计划修复这个问题的。

Golang 1.24 中最吸引我的功能就是 SwissMap,在以前的非官方实现中,有些场景能够提升50% 的性能,官方的实现中,也有不小的性能提升。
详情参考我以前的文章:
SwissTable 会成为 Golang std map嘛?
Go 1.24 的 Swiss Map:兼容性、扩展哈希与遗留问题

但是 如果你在使用 Go 1.24 时可能会发现SwissMap 没有达到预期的表现,甚至程序运行变慢了,特别是在 Map很大的时候,这不是你的幻觉,确实存在这个问题。
Pasted image 20250520225045
https://x.com/valyala/status/1879988053076504761

这个问题记录在 Issue #70835 中,开发者们正在努力解决它。

问题出在哪?

Go 的 map 在新版中使用了Swiss Table,它在小 map 和高并发的场景下非常快。但是当 map 很大、数据又不在 CPU 缓存里(也就是说,数据是“冷”的),就会变慢。

为什么会这样呢?

因为 SwissMap 的内部结构比较复杂,它会分几层来存储数据:

  1. 首先是一个 map 的头部结构
  2. 它指向一个目录,这个目录是多个 table 的指针组成的列表
  3. 每个 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 就能把速度和稳定性都带回来。
我们一起期待吧!

Licensed under CC BY-NC-SA 4.0
最后更新于 May 20, 2025 22:18 CST
使用 Hugo 构建
主题 StackJimmy 设计