标签 GC 下的文章

Google I/O 2025 Go 语言进展:生产力、生产就绪与 AI 赋能

本文永久链接 – https://tonybai.com/2025/05/25/go-at-googleio-2025

大家好,我是Tony Bai。

在Google I/O 2025大会上,Go 产品负责人 Cameron Balahan 和开发者关系负责人 Marc Dougherty 详细阐述了 Go 语言在生产力、生产就绪度和开发者体验方面的最新进展及未来规划。演讲强调了 Go 语言以规模化为核心的设计理念及其三大指导原则:生产力、超越语言的完整体验和生产就绪。重点介绍了Go 1.23Go 1.24版本在生产力方面的革新,包括引入迭代器简化循环、gopls 的智能现代化能力以及通过 go get 管理 Go 工具链;在生产就绪性方面,突出了 WebAssembly 支持的增强、安全体系的持续深化(特别是后量子密码学的透明集成和 FIPS-140 支持的便捷启用)以及核心性能的显著提升(如全新的 map 实现)。此外,演讲还强调了 Go 语言在 AI 基础设施构建中的核心地位,并展望了 Go 1.25+ 在 SIMD 支持、多核硬件优化等方向的探索,同时重申了 Go 1.0 的兼容性承诺。

这里是基于演讲视频,借助AI整理的文字稿,我做了简单校对和格式调整,供大家参考。

原视频链接:https://www.youtube.com/watch?v=kj80m-umOxs 建议大家也都看一下。


我是 Cameron,我是 Google Go 编程语言的产品负责人。我是 Marc,我负责 Go 的开发者关系。

对于那些刚接触我们项目的人来说,Go 是一个由 Google 支持的开源编程语言,它能让开发者和软件工程团队快速构建更安全、可靠和可扩展的生产系统。

Google 在 15 年前将 Go 作为一个开源项目发布,在此之前两年,Google 为了应对自身在构建和维护大规模、关键任务系统方面面临的挑战而启动了这个项目。使用现有的工具,我们不得不在动态解释性语言的生产力和强类型编译语言的生产就绪性之间做出选择。但我们两者都想要,所以我们构建了Go

Go 的核心前提是开发工具从一开始就应该优先考虑可扩展性,这意味着要考虑到现代软件的架构方式、现代工作负载运行的环境,以及最重要的,编写、操作和维护这一切的团队。因此,考虑到这一点,我们围绕三个原则构建了Go,这些原则至今仍在指导着我们。

首先,Go 是高效的。它易于学习,易于维护,可读性强,并且能够很好地适应不同的团队、工作负载和用例。

其次,Go 不仅仅是一门语言,它是一个完整的开发者体验。从 IDE 到生产环境,我们提供端到端的解决方案,涵盖整个软件开发生命周期的所有接触点。我们提供所有这一切,开箱即用,并带有合理的、可自动调整的默认设置。

第三,Go 是生产就绪的。它可靠、高效、稳定且安全,这使得它非常适合从简单应用到企业系统和关键基础设施的各种场景。

多年来,Go 已经成为现代云计算的核心,并由此延伸到现代网络。世界上许多最知名的云技术都是用 Go 编写的,包括 Kubernetes、Docker、Terraform 等等。各种规模的公司,从个人到初创企业再到大型企业,都已采用 Go,尤其是在其基于云的工作负载方面。这在很大程度上是因为 Go 是为云计算而专门构建的。Go 所支持的库、集成和架构是为云而生的,而不是后来才为云进行改造的。这意味着你可以比使用其他语言更快、更容易地实现云计算的优势。

但你不必相信我的话。Go 用户一直给予我们非凡的反馈和客户满意度(注:93%)——这种水平在行业内几乎闻所未闻。使用情况也证明了这一点。如今,Go 比以往任何时候都更受欢迎,拥有数百万开发者,并且仍在快速增长。事实上,根据去年的NewStack的一项调查,Go 是仅有的两种增长速度超过开发者本身增长速度的语言之一。另一种是 Rust,我们认为它与 Go 配合得非常好,但这是另一个话题了。这样的迹象随处可见。Go 一直在 Stack Overflow 上被评为最受欢迎的技术之一。去年,Cloudflare 报告称,Go 是互联网上支持 API 调用的第一大语言

因此,无论你是个人开发者、企业,还是介于两者之间的组织,Go 都能让你快速、更可靠地构建和扩展你的项目。你可能会很高兴你这样做了。接下来,Marc 将深入探讨 Go 的所有最新进展。交给你了,Marc。

谢谢,Cameron。Go 每年发布两次新的主版本,分别在八月和二月。在过去的一年里,我们在 1.23 和 1.24 版本中发布了许多令人兴奋的新功能,以帮助你和你的团队提高工作效率。

在1.23 版本中,我们引入了带有 seq 和 seq2 类型的迭代器。相较于经典的 Go 风格,迭代器不仅仅是标准库中的一个新类型。它们是一种优雅的方式,可以使用已经熟悉的 for range 表达式来简化循环,并将迭代的机制与循环体分开。在迭代器出现之前,有几种不同的方法来遍历数据。一些方法会返回一个包含所有结果的切片,这对于大型集合来说可能效率低下。另一种方法是创建自己的迭代器对象,就像这段代码一样,它使用了 Google Cloud Storage 库。注意这里的复杂性。我们的循环中有流程控制和错误检查。并且该错误检查需要在每个循环中重复。使用迭代器,你可以使用熟悉的 for range 语法来执行循环。复杂的流程控制则保留在迭代器内部。这使得我们的循环体可以专注于处理文件或错误,而无需担心流程控制。

从 1.24 版本开始,标准库在 strings、slices 和 maps 包中包含了一系列迭代器。因为迭代器只是一个函数,所以你可以定义自己的迭代器,包括为其他地方定义的集合类型定义迭代器。这是我为 Cloud Storage 示例定义的迭代器。声明看起来有点复杂,但你可以看到这里的流程控制与之前具有相同的效果。这个迭代器让我们能够将流程控制处理从循环中分离出来,并使它们更具可读性。

随着像迭代器这样的新概念的引入,Go 的垂直集成工具可帮助你的代码库与最新的模式和习惯用法保持同步。Go 的语言服务器 gopls 可以与你的 IDE 集成,既可以通过大多数 IDE 中的语言服务器支持,也可以通过插件(如 VS Code Go 扩展)实现。Gopls 在常规的语言服务器功能方面提供帮助,例如类型检查、函数签名和引用。但 gopls 的功能远不止于此。还记得那个复杂的迭代器定义吗?由于 gopls 从第一天起就知道新功能,因此它可以帮助你在编写时避免错误。在这里,它注意到了一个错误,即我们的迭代器可能会在应该停止后调用我们的 yield 函数。gopls 包含一套现代化功能,这些常见模式后来已作为语言特性或标准库新增功能得到解决。虽然你可以在整个代码库上运行现代化工具,但 gopls 可以在你编辑的任何地方内联建议它们。这里有一些旧模式的例子在左边,以及它们现代化的替代方案在右边。

最后一个现代化工具展示了 JSON 解析器的一个新特性,称为omitzero。JSON 包从 Go 1.0 开始就是 Go 的一部分。它通过简化 Go 结构体的序列化,实现了 API 客户端和服务器的人性化开发。omitzero 选项的添加解决了一些在处理 Go 的零值(如空结构体和未初始化的 time.Time 对象)时常见的错误和令人意外的行为。这些新增功能让你能够更好地控制对象如何序列化为 JSON,并避免可能的错误和混淆来源。

你是否需要更新你的 Go 运行时以利用新功能?从 1.23 版本开始,你可以使用 go get 来管理 Go 工具链,就像管理任何其他依赖项一样。Go 会根据需要下载更新的工具链,让你的团队可以使用最新的功能,而无需停下来手动更新工具链。这也适用于依赖项。如果你依赖了需要 1.24 版本的代码,Go 会更新你模块的 go 指令以要求 1.24 版本,并自动获取 1.24 运行时。Go 语言和 Go 工具不断寻找新的方法来帮助你保持代码库的可读性和现代化,并让你的团队保持专注和高效。

Marc 刚刚向你介绍了让你更高效的一些新功能。但请记住,Go 关注的是生产力和生产就绪性。那么,让我们来谈谈 Go 1.23 和 1.24 中那些让你的应用程序更健壮、更安全、性能更高的最新功能。

正如我之前所说,Go 的创始原则部分集中在其可移植性和对现代工作负载运行的现代环境的关注上。这些环境在不断发展。随着它们的发展,我们希望确保 Go 能够跟上步伐。我们做到这一点的一种方式是在 Go 1.24 中显著改进了 Go 对 WebAssembly 的支持。WebAssembly,或称 Wasm,是一种二进制指令格式和沙盒化运行时环境,它开启了许多新的有趣用例,尤其是在云端。包括 Go 在内的几种语言都能够编译 Wasm 模块,这些模块包含可在所有 Wasm 主机上运行的可移植的、与体系结构无关的字节码。同一个 Wasm 主机应用程序可以调用来自多个不同 Wasm 模块的方法,这些模块可以根据需要用一种语言或多种语言混合编写。这些 Wasm 模块是可热加载的,并在内存安全的沙盒化运行时中运行,具有结构化的控制流和验证。任何系统调用都通过 Wasm 运行时进行路由,这提供了一个额外的安全层,有点像一个极其轻量级的容器。尽管存在这一层抽象,但 Wasm 应用程序效率极高,能够在主机上实现接近本机的性能。这使得它们特别适用于高性能、低延迟的用例,例如边缘计算。例如,你可以在 Google Cloud 服务扩展上运行你的 Wasm 代码,它在 200 多个国家的 200 多个边缘位置提供边缘计算。

Go 在 Go 1.11 版本中通过 JS Wasm 移植首次引入了对 Wasm 的支持。Wasm 本身最初是为浏览器设计的。JS Wasm 移植通过允许你通过 JavaScript 主机定位网页,从而启用了此用例。Go 开发者利用这个功能制作了一些非常有趣的东西,尤其是游戏。甚至还有一些利用 JS Wasm 移植的 Go 开源游戏引擎。Go 开发者可以使用这些项目轻松开发在浏览器中运行的令人印象深刻的 2D 游戏。随着 Wasm 的发展,Go 也在发展。在 Go 1.21 中,我们引入了对 WebAssembly 系统接口(WASI)预览版 1 的支持。WASI 提供了一个 POSIX 风格的接口,用于与系统资源进行交互,例如文件系统、系统时钟、数据实用程序等等。在这个例子中,你可以看到一个简单的“Hello, world!”程序,我们通过开头的编译标志将其编译为 Wasm。然后我们可以使用众多免费开源的 Wasm 运行时和库之一来运行该程序。在这种情况下,我们使用的是 wazero,一个用 Go 实现的开源项目。从 Go 1.21 开始,Go 开发者可以将 Wasm 模块构建为可执行文件,在 Wasm 运行时中启动它,并运行至完成。

这就引出了今天的内容。在 Go 1.24 中,我们通过两种主要方式扩展了 Go 的 Wasm 功能。首先,Go 1.24 允许你使用 go:wasmexport 编译器指令将 Go 函数导出到 Wasm 主机。当我们将这样的代码编译成 Wasm 模块时,我们可以在 Wasm 主机中导入它,Wasm 主机可以直接调用模块导出的函数。其次,Go 1.24 添加了对构建 WASI 反应器 (reactor) 的支持。当你使用此功能以 Reactor 模式构建 Wasm 模块时,即使模块执行完毕,它也可以保持初始化状态。这对于你希望无限期可用的长时间运行的插件或扩展非常有用。初始化一次,让它保持运行,它可以继续响应调用,包括通过维护状态。在这个例子中,我们使用 wazero 的库来创建一个 Wasm 主机,它将调用我们在上一个例子中导出的 add 函数。不过,这次我们将使用高亮显示的构建标志以反应器模式构建 Wasm 模块。现在,我们可以多次运行 add 函数而无需重新初始化它。

接下来,我们来谈谈 Go 如何让你的应用程序更安全。Go 一直在安全特性和功能方面处于领先地位。在 Go 1.13 中,我们引入了模块代理和校验和数据库,它们缓存并记录 Go 生态系统中所有依赖项的哈希值,保护你免受中间人攻击和其他对依赖项的篡改。然后,在 Go 1.18 中,我们引入了内置的模糊测试 (fuzz testing),这是第一个将原生模糊测试内置并集成到其标准工具链中的主流编程语言。你可以将模糊测试视为一种自动化测试形式,它智能地操纵程序的输入以找出错误,尤其是安全漏洞。2022 年,我们推出了 Go 的端到端漏洞管理系统,它可以在任何地方(从 IDE 到运行时)发现依赖项中的已知漏洞。通过分析从你的代码到依赖项的调用图,Go 的漏洞管理工具能够检测你是否实际调用了易受攻击的代码,从而消除了绝大多数的误报。

基于我们对安全的关注,在 Go 1.24 中,我们引入了对后量子密码学的支持,所有这些都在幕后透明地实现。我们还改进了对 FIPS-140 的支持,这是一项美国政府合规制度,其中包括用于加密应用的已批准算法。你可以在不更改任何代码的情况下启用 FIPS 模式,既可以在运行时使用高亮显示的调试标志,也可以在构建时使用高亮显示的构建 flag。

最后,我们继续专注于使 Go 更快、更高效。我们做到这一点的一个重要方式是引入了一个全新的内置 map 类型实现,它基于一种名为 Swiss Tables 的新哈希表设计。从 Go 1.24 开始,map 透明地使用新的 Swiss Table 实现。在微基准测试中,使用新实现的 map 操作比 Go 1.23 快了高达 60%,尤其是在处理大型 map 时。这一切都无缝集成在 Go 的内置 map 中。无需调整你的代码。只需升级即可。

还有更多,包括 Go 1.23 和 1.24 中许多新的底层工具,用于提高效率。例如,在 Go 1.23 中,我们引入了 Unique Package,可以高效地对值进行去重和比较。在 Go 1.24 中,我们引入了 weak.Pointers,它允许你安全地指向一个对象而不会阻止它被垃圾回收,以及 AddCleanup 函数,这是一种更灵活、更高效且更不容易出错的终结机制。还有更多,包括改进的内存分配速度和整体速度提升。所有这些都延续了我们保持 Go 既高效又生产就绪的重点。

接下来,让我们把话筒转回给 Marc,让他快速介绍一下 Go 在生成式 AI 中的最新应用。

正如你刚才听到的,Go 拥有许多特性,使其成为构建生产系统的绝佳语言。像高效的网络库和集成的结构体标签这样的特性,使其非常适合构建分布式系统。这也是 Go 在云基础设施和服务中如此普遍的重要原因。同样的这些原因也使得 Go 成为当今构建 AI 基础设施和服务的绝佳选择。流行的生成式 AI 工具和库,如 Ollama、Local AI、LangChain Go、Genkit 等等,都是用 Go 编写的。就像之前的主要基础设施项目一样,这些工具和库利用 Go 的生产力和生产就绪性来创建高度可扩展且更可靠的关键任务服务,数百万来自不同语言生态系统的开发者依赖这些服务来支持其 AI 驱动的工作负载。

事实上,云和 AI 系统之间的共同点比你想象的要多。由于 LLM 通常需要专用的、专门的计算资源,因此它们通常作为通过 API 调用的网络服务运行。让我们以 Go 博客最近一篇文章中概述的检索增强生成 (RAG) 系统为例。我们的 RAG 系统使用向量数据库来存储相关文档,以便在回答用户问题时提供给我们的 LLM。向量数据库依赖于专门的嵌入模型,因此我们可以高效地查询与用户问题相似的文档。我们将研究三种不同的框架,用于将这些服务连接在一起。

对于我们的第一个例子,我们将直接使用 Gemini 和 Weaviate 客户端库。这段代码来自用户查询处理程序。我们正在使用 Weaviate 的 GraphQL 接口来获取文档。查询本身有点长,所以我们使用了一个辅助函数。这种方法的一个缺点是,如果我们更改向量数据库,就必须重写辅助函数。

在这里,我们使用的是 LangChain Go,它为我们的 LLM 和向量数据库提供了接口抽象。如果我们替换这些组件,相似性搜索和从单个提示生成调用的代码将无需更改。

最后,我们来看看 Firebase Genkit for Go,目前处于测试阶段。它提供了与 LangChain Go 类似的抽象。Genkit 包含生产级功能,如提示管理和可观察性,这些功能可能在代码中不可见,但可以改善整体开发者体验。

随着你的 AI 系统的发展,Go 对简单性的强调意味着即使代码规模和复杂性增加,你的代码仍然保持可读性。Go 的特性,如对象嵌入和接口,使得在需求和技术发生变化时可以无缝迁移——而它们总是会发生变化。Go 在跟上快速变化方面的成熟能力使其在一些最知名的云基础设施组件中取得了成功。推动 Go 在云领域普及的相同特性,也使其成为我们构建未来 AI 基础设施的绝佳选择。

我希望我们已经在这个视频中证明了,Go 围绕生产力、开发者体验和生产就绪性的创始原则,仍然是我们今天优先考虑工作的依据。在结束之前,我想花几分钟时间让大家一窥 Go 1.25 及更高版本即将推出的内容。

首先,在 Marc 关于 AI 的讨论基础上,我们对围绕 SIMD 所做的工作感到非常兴奋。SIMD 使现代 CPU 能够执行向量化数组操作,并行运行某些类型的循环。这些功能对于许多类型的性能优化至关重要,包括某些类型的 AI 基础设施所需的优化。

在性能方面,我们在多核硬件方面有很多令人兴奋的机会,包括垃圾回收器和调度器的功能,这些功能可以更好地利用现代 CPU 架构中的非一致性内存访问。

切换到语言本身,在我们持续推动提高生产力方面,我们还有很多需要完善的地方,特别是在泛型操作的灵活性方面。有关该工作的更多信息,请查看我们在 GitHub 上 Go 项目的讨论。

在我们做所有这些以及更多事情的同时,你可以放心,我们现在和将来所做的任何更改都将继续履行 Go 的兼容性承诺。Go 仍然并将永远保持与 Go 1.0 的完全向后兼容。

在我们结束时,我们想花点时间感谢 Go 社区。我们,Go 团队,致力于在未来很长一段时间内保持 Go 的生产力和生产就绪性。但我们知道我们并不孤单。今天,我们的生态系统比以往任何时候都更大、更健全。我们继续看到许多非常高质量的工具和库涌现,尤其是在围绕生成式 AI 的新用例方面。我们看到世界各地成千上万的 Gopher 聚会、参加 Go 会议,并在网上协作,所有这些都是因为他们热爱 Go。所以,感谢 Go 社区。正是因为你们的贡献,Go 才得以发展,并且比以往任何时候都更具相关性。我们非常自豪能与你们一起参与这段旅程。

要开始使用,或获取有关本视频中讨论的任何内容的更多信息,请务必访问我们的主页 go.dev。感谢你参加今年的 Google I/O 大会。我们迫不及待地想看看你今年以及未来几年用 Go 构建的成果。


原「Gopher部落」已重装升级为「Go & AI 精进营」知识星球,快来加入星球,开启你的技术跃迁之旅吧!

我们致力于打造一个高品质的 Go 语言深度学习AI 应用探索 平台。在这里,你将获得:

  • 体系化 Go 核心进阶内容: 深入「Go原理课」、「Go进阶课」、「Go避坑课」等独家深度专栏,夯实你的 Go 内功。
  • 前沿 Go+AI 实战赋能: 紧跟时代步伐,学习「Go+AI应用实战」、「Agent开发实战课」,掌握 AI 时代新技能。
  • 星主 Tony Bai 亲自答疑: 遇到难题?星主第一时间为你深度解析,扫清学习障碍。
  • 高活跃 Gopher 交流圈: 与众多优秀 Gopher 分享心得、讨论技术,碰撞思想火花。
  • 独家资源与内容首发: 技术文章、课程更新、精选资源,第一时间触达。

衷心希望「Go & AI 精进营」能成为你学习、进步、交流的港湾。让我们在此相聚,享受技术精进的快乐!欢迎你的加入!

img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

原子操作的瓶颈与Go的多核扩展性之痛:深入剖析sync.ShardedValue及per-CPU提案

本文永久链接 – https://tonybai.com/2025/05/19/shardedvalue-per-cpu-proposal

大家好,我是Tony Bai。

在追求极致性能的道路上,Go 语言凭借其简洁的并发模型和高效的调度器,赢得了众多开发者的青睐。然而,随着现代服务器 CPU核心数量的不断攀升,一些我们曾经习以为常的“快速”操作,在高并发、多核环境下,也逐渐显露出其性能瓶颈。其中,原子操作 (atomic operations) 的扩展性问题,以及标准库中一些依赖原子操作的并发原语(如 sync.RWMutex)的性能表现,成为了社区热议的焦点。

最近,fasthttp 的作者及 VictoriaMetrics 数据库的联合创始人 Aliaksandr Valiakin (valyala) 在 X.com 上的一番“叹息”,更是将原子计数器的扩展性问题推向了前台:

Valyala 指出:“基于原子操作的计数器更新性能在多 CPU 核心上无法扩展,因为每个 CPU 核心在增量操作期间都需要从慢速内存中原子加载实际的计数器值。因此,实际性能受限于内存延迟(约 15ns,即每秒 6 千万次增量)。通过使用可缓存于 CPU L1 缓存的 per-CPU 计数器,可以将单 CPU 核心性能提升至每秒数十亿次增量。遗憾的是,Go 语言本身并未提供高效处理 per-CPU 数据的函数。”

这番话点出了一个残酷的现实:即使是看似轻量级的原子操作,在多核“混战”中也可能成为性能的阿喀琉斯之踵。那么,这背后的深层原因是什么?Go 社区又在如何探索解决之道呢?今天,我们就来深入剖析这个问题,并解读 Go 项目 issue 中几个重要的相关提案,同时看看社区是如何先行一步尝试解决这类问题的。

原子操作为何在高并发多核下“失速”?sync.RWMutex 的痛点

要理解原子操作的瓶颈,我们需要潜入到 CPU 缓存的微观世界。现代多核 CPU 为了加速内存访问,都配备了多级缓存(L1, L2, L3)。当多个核心同时读写同一块内存区域时,就需要缓存一致性协议 (Cache Coherence Protocols)(如 MESI,Modify-Exclusive-Shared-Invalid)来确保数据的一致性。

当我们对一个共享变量(即使是原子变量)进行写操作时,例如 atomic.AddInt64,会发生什么?

  1. 执行该操作的 CPU 核心需要获得对该变量所在缓存行 (Cache Line) 的独占访问权 (Exclusive state)。
  2. 如果其他核心的缓存中也存在这份缓存行的副本(即使是共享状态 Shared state),它们会被标记为无效 (Invalidate)。
  3. 当其他核心再次需要访问这个变量时,就会发生缓存未命中 (Cache Miss),需要从更高级别的缓存或主内存中重新加载数据,并可能再次引发缓存行在不同核心间的同步。

在高并发场景下,如果多个核心频繁地对同一个缓存行中的原子变量进行写操作,就会导致:

  • 缓存行在不同核心的 L1/L2 缓存之间频繁失效和同步,这个过程被称为“缓存行乒乓 (Cache Line Ping-Ponging)”。
  • 产生大量的总线流量和内存访问延迟

这就是所谓的真共享 (True Sharing) 争用。即使原子操作本身在单个核心上执行得非常快,这种跨核心的缓存同步开销也会让其整体性能急剧下降。

这个问题的典型体现之一,便是 Go 标准库中的 sync.RWMutex。正如 github.com/jonhoo/drwmutex 项目在其 README 中指出的:“Go 默认的 sync.RWMutex 在多核下扩展性不佳,因为所有读操作者在尝试原子性地增加同一个内存位置(用于读者计数)时会产生争用。” 对于读多写少的场景,本应高效的读锁操作,却因为内部共享计数器的原子更新而受到了性能限制。

社区的先行者:jonhoo/drwmutex 的分片读写锁实践

面对标准库 sync.RWMutex 在多核环境下的扩展性瓶颈,社区早已开始了积极的探索。一个显著的例子便是 jonhoo/drwmutex,一个 n 路分片读写锁(Distributed Read-Write Mutex)的实现,也被称为“大读者”锁。

其核心思想非常直观:为每个 CPU 核心提供其自己的 RWMutex 实例。读者只需要获取其核心本地的读锁,而写者则必须按顺序获取所有核心上的锁。 这种设计通过将读操作的争用分散到各个核心,从而显著提升了读多写少场景下的并发性能。

jonhoo/drwmutex 的实现也揭示了构建这类 per-CPU 优化方案的一些关键技术点和挑战:

  • 获取当前 CPU ID: 为了将操作路由到正确的本地锁,需要一种方法来确定当前 goroutine 正在哪个 CPU 核心上运行。drwmutex 在 Linux x86 平台上使用了 CPUID 汇编指令来获取 APICID,并在程序启动时构建 APICID 到 CPU 索引的映射。这突显了获取可靠且高效的 CPU/P 标识是实现此类优化的一个难点。
  • CPU 信息可能过时: README 中也坦诚地指出,goroutine 获取到的 CPU 信息可能是过时的(因为 goroutine 可能已被调度到其他核心),但这主要影响性能而非正确性(只要读者记住它获取的是哪个锁)。OS 内核通常会尽量将线程保持在同一核心以提高缓存命中率,这在一定程度上缓解了这个问题。
  • 性能表现与 NUMA 效应: jonhoo/drwmutex 的性能测试表明,在核心数较多,特别是写操作比例低于 1% 时,其性能远超 sync.RWMutex。有趣的是,其性能图表还揭示了 NUMA (Non-Uniform Memory Access) 效应的影响——在测试机器上每增加一个包含 10 个核心的 NUMA 节点,跨核心流量的成本就会增加,导致性能曲线出现波动。

jonhoo/drwmutex 的实践不仅提供了一个解决 sync.RWMutex 性能问题的有效方案,也为后续 Go 官方和社区在 per-CPU 数据结构方面的探索提供了宝贵的经验和参照。

官方的早期探索:sync.ShardedValue 的初心与挑战 (#18802)

在社区积极探索的同时,Go 核心团队也早已关注到这类问题。一个重要的早期官方提案便是由 Austin Clements 在 2017 年提出的 sync.ShardedValue (issue #18802)

sync.ShardedValue 的核心思想与 jonhoo/drwmutex 有异曲同工之妙:提供一种机制来创建和使用分片值,将一个逻辑上的共享值分散到多个独立的“分片”中,每个分片与一个 CPU 核心或更准确地说是 Go 调度器中的 P (Processor) 相关联。 这样,每个 P 上的 goroutine 优先访问其本地分片,从而大大减少对单一共享内存位置的争用。

该提案围绕 Get()、Put() 和 Do() 等核心 API 进行了深入讨论,涉及了诸多设计维度,例如 Get/Put 的阻塞性、溢出处理、Do 操作的一致性等。尽管因难以就“最重要的问题达成共识”而被搁置,但 sync.ShardedValue 提案为后续的探索奠定了重要的基础,并清晰地指明了通过“分片”来提升多核扩展性的方向。

新的尝试:valyala 的 sync.PLocalCache (#69229) 与 sync.MLocal (#73667)

近期,valyala 基于其在 fasthttp 和 VictoriaMetrics 等高性能项目中的实践经验,提出了两个更聚焦、API 更简洁的提案,试图从特定场景切入,解决 per-CPU/per-P/per-M 数据的高效访问问题。

1. sync.PLocalCache (issue #69229): Per-P 对象缓存

  • 设计目标: 为 CPU 密集型的算法提供一个高效且可随 CPU 核心数线性扩展的状态缓存机制
  • API 设计: 核心是 Get() (返回 P 本地对象,若无则返回 nil) 和 Put() (将对象放回 P 本地存储),保证 Get() 返回的对象只能被当前 goroutine 访问,无需额外同步。
  • 解决痛点: 旨在解决 sync.Pool 在作为严格 per-P 缓存时存在的问题,如跨 P 窃取、内存浪费和 GC 清理等。

2. sync.MLocal[T any] (issue #73667): Per-M (OS 线程) 泛型存储

  • 设计目标: 为需要在 OS 线程层面实现数据隔离以达到线性扩展性的并发代码,提供 M 本地存储。
  • API 设计 (泛型): 提供 Get() (返回当前 M 的 *T 项) 和 All() (返回所有 M 上的项)。
  • 解决痛点: 直接应对 valyala 在 VictoriaMetrics 中遇到的共享缓冲区互斥锁争用导致的扩展性瓶颈。

这些提案的共性、差异与启示

无论是社区的 jonhoo/drwmutex 实践,还是官方及 valyala 的提案,它们的核心目标都是一致的:通过数据的分片或本地化,最大限度地减少多核间的共享内存争用,从而提升高并发应用在多核处理器上的性能和可伸缩性。

然而,它们在具体实现、API 设计的通用性、易用性以及针对的场景上有所不同:

  • jonhoo/drwmutex 是一个针对特定问题(读写锁)的具体解决方案,它依赖平台相关的 CPUID 指令,并自己处理了核心映射和数据同步。
  • sync.ShardedValue 试图提供一个更通用的分片值抽象,但也因此面临更大的设计复杂性和社区共识挑战。Austin Clements 后续也反思了早期设计,并提出了更优的“检出/检入”模型。
  • sync.PLocalCache 和 sync.MLocal 则更为聚焦,API 更简洁,分别针对 per-P 缓存和 per-M 存储这两个具体场景。

这些探索过程也充满了 Go 社区对技术细节的极致追求和严谨思辨,例如关于命名(”sharding” vs “perCPU” vs “SplitValue”)、GOMAXPROCS 动态变化的影响、与 GC 的交互、API 语义的精确性(如 mknyszek 提出的包含 Merge 方法的 ShardedValue API 及其多种语义可能)以及泛型的应用等。

展望未来:Go 如何更好地拥抱多核时代?

原子操作的瓶颈、标准库并发原语的局限,以及社区和官方对 per-CPU/P/M 存储方案的持续探索,清晰地表明了 Go 语言在追求极致多核扩展性方面仍有提升空间。解决这类底层并发原语的性能问题,对于 Go 在高性能服务器、大规模分布式系统、数据库、监控系统等领域的持续领先至关重要。

未来,我们或许会看到:

  • 更底层的运行时支持: Go 运行时可能会暴露更底层的、与调度器(P、M)相关的亲和性原语,或提供高效获取当前 P/核心 ID 的标准方法,正如 jonhoo/drwmutex 所尝试的那样。
  • 标准库中出现新的同步原语: 借鉴这些提案和社区实践的精华,可能会有新的、经过精心设计的同步原语加入到 sync 或 sync/atomic 包中。
  • 社区持续贡献优秀的解决方案: 像 jonhoo/drwmutex 这样的项目,即使官方没有立即提供标准方案,社区也会基于现有技术孵化出优秀的第三方库。

小结

从 valyala 对原子操作性能的“叹息”,到 jonhoo/drwmutex 的巧妙实践,再到 Go 社区围绕 sync.ShardedValue、sync.PLocalCache、sync.MLocal 等提案的深入探讨,我们看到了 Go 语言在追求极致性能道路上永不停歇的脚步。这不仅仅是关于几个新的 API,更是关于 Go 如何在多核时代继续保持其并发优势和工程效率的战略思考。

作为 Gopher,关注这些讨论和提案的进展,理解其背后的设计哲学和技术挑战,不仅能让我们更深刻地认识 Go 语言,也能启发我们在自己的高性能项目中进行类似的性能优化思考和实践。

让我们共同期待 Go 在多核扩展性方面能迈出更坚实的步伐,为构建更高性能的未来系统提供更强大的动力!

参考资料


聊一聊,也帮个忙:

  • 在你的 Go 项目中,是否也曾遇到过原子操作或 sync.RWMutex 在高并发多核下的性能瓶颈?你是如何解决的?是否尝试过类似 jonhoo/drwmutex 的分片锁方案?
  • 对于 Go 社区提出的这些 per-CPU/P/M 存储提案,你认为哪种设计思路更具潜力?或者你有什么更好的建议?
  • 你认为 Go 语言在提升多核扩展性方面,未来最应该关注哪些方向?

欢迎在评论区留下你的经验、思考和问题。如果你觉得这篇文章对你有所启发,也请转发给你身边的 Gopher 朋友们,让更多人参与到这场关于 Go 性能未来的讨论中来!


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 AI原生开发工作流实战 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats