amd64 微架构级别对 Go 有多大帮助?

Lobsters Hottest 论文

摘要

使用 Roaring Bitmap 库对不同 amd64 微架构级别(GOAMD64)编译的 Go 程序进行性能评估,结果表明启用诸如 popcnt (v2) 或 AVX-512 等新指令集可以显著提升性能。

<p><a href="https://lobste.rs/s/cuh5an/how_much_do_amd64_microarchitecture">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/08 07:15

# amd64 微架构级别在 Go 中能带来多少帮助? 来源: https://lemire.me/blog/2026/06/06/how-much-do-amd64-microarchitecture-levels-help-in-go/ 我们的 64 位 Intel 和 AMD 处理器在过去几十年间不断演进。当你为 64 位 Intel 或 AMD 处理器编译 Go 程序时,编译器默认针对的指令集已有近 20 年历史。这样生成的二进制文件几乎能在所有 x64 芯片上运行,但同时也放弃了自 2003 年以来加入的所有新指令。 我们常会提及 *微架构级别* (https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels) 。每个级别打包了一组可假定存在的指令集扩展: | 级别 | 大致新增内容 | |------|-------------| | **v1** | 原始的 AMD64 基线 (SSE2) | | **v2** | `popcnt`, SSE4.2 | | **v3** | AVX2 | | **v4** | AVX-512 (F/BW/DQ/VL) | 在我看来,这个层级划分已经有些过时了。它在 2020 年左右被固定下来,而硬件已经向前发展。我们还需要加入最新的 AVX-512 子扩展(如 VBMI、VBMI2、VNNI、BF16、FP16、VPOPCNTDQ 等),现代服务器和消费级芯片均已支持它们,但 `v4` 并未要求这些。虽然 `v1` 到 `v4` 是一种实用的通用语言,但如今想要一个“使用当前 CPU 所有特性”的编译目标,至少需要 `v5`,而且或许整个方案应该被更细粒度的特性检测所取代。 无论如何,Go 工具链通过 `GOAMD64` (https://go.dev/wiki/MinimumRequirements#amd64) 环境变量暴露了 `v1` 到 `v4` 这个层级。设置 `GOAMD64=v3` 告诉编译器它可以使用包括 AVX2 在内的所有指令。默认是 `v1`,即最低公共分母。 这就引出了一个显而易见的问题:如果我拿一个真实的、对性能敏感的库,并在每个级别下重新编译它,我实际能获得多少提升?我选择了 Roaring Bitmaps (https://github.com/RoaringBitmap/roaring),这是一个用于数据库和搜索引擎的压缩位集数据结构。 Roaring Bitmap 存储一组 32 位整数。它将 32 位空间分割成大小为 65,536 的块,以高 16 位为键,并将每个块存储在一个仅保存低 16 位的 *容器* 中。一个容器有三种形式,库始终使用最小的那一款: - 数组容器:一个排序的 16 位值列表,用于稀疏块(最多几千个元素); - 位图容器:一个平坦的 8 KB 位向量(65,536 位,每个可能的值占一位),用于稠密块; - 运行容器:一个 `[start, length]` 区间的列表,用于设定位聚集成连续运行的情况。 我获取了该库的最新发布版本,然后对其自带的基准测试套件运行了四次,每个级别一次,每次收集八个样本。我在一颗 Intel Xeon Gold 6548N(Emerald Rapids,支持全部四个级别,包括 AVX-512)上使用 Go 1.26.2 和 Roaring v2.18.2 进行了测试。 *种群计数*(或 *popcount*,也称为汉明重量)就是计算机器字中设为 1 的位的数量。Roaring 大量依赖它:位图容器的基数(即它包含多少个值)是其 1024 个 64 位字的 popcount 之和。现代 x86 芯片有一条专用的 `popcnt` 指令,可以在单个操作中完成这一计算,但它只在 `v2` 级别(SSE4.2,2008)才可用。没有它,编译器必须退回到多条指令的位操作序列。 最清晰的一个结果是种群计数:计算位图容器中设定位的数量。`v1` 基线无法使用 `popcnt` 指令,因此 Go 会发出一个软件回退。一旦我们升级到 `v2`,`popcnt` 变得可用,时间几乎减少了一半: ![](https://lemire.me/blog/wp-content/uploads/2026/06/popcount_levels.svg) 这减少了 43%,而且是免费的:无需修改源代码,只需一个编译器标志。注意,`v3` 和 `v4` 并没有带来更多提升。单条 `popcnt` 指令已经是最优的了;就 Go 编译器而言,AVX2 和 AVX-512 没有什么可贡献的。 种群计数是轻松的胜利。库的其余部分呢? 另一个明显的胜利是从稠密位图构建容器。`FromDense array` 基准测试接收一个原始的 8 KB 位向量,并为其构建最紧凑的容器:它对每个字进行 popcount 以了解基数,然后扫描出设定位的位置。这个逐字 popcount 并扫描的循环正是编译器在 256 位寄存器可用时可以自动向量化的地方,因此收益在 `v2` 之后仍在继续: ![](https://lemire.me/blog/wp-content/uploads/2026/06/fromdense_levels.svg) `v2` 已经通过使用标量 `popcnt`/`tzcnt` 指令减少了 21%,而 `v3`(AVX2)几乎使收益翻倍,达到 38% 的减少。与 popcount 一样,`v4` 没有带来任何提升。 集合操作也呈现出相同的模式。`IntersectionCardinality` 基准测试计算两个位图有多少共同值:对于位图容器,它逐字进行 AND 操作并对结果进行 popcount,而无需实际生成交集。这里 `v2` 基本上没有作用(标量 `popcnt` 已经在内部循环中),但 `v3` 让编译器将 AND 并计数的循环扩展到 256 位寄存器,将时间减少了 22%: ![](https://lemire.me/blog/wp-content/uploads/2026/06/intersectcard_levels.svg) 要点总结: 1. 在现代硬件上,每个人都应该使用 `v2` 或更高版本。生成的二进制文件可以在任何数据中心和任何非远古的笔记本上运行。 2. `v3` 级别可能值得研究。 3. `v4` 级别本应在我的一些基准测试中有所帮助,但事实并非如此。我怀疑 Go 编译器在这方面并不擅长。 (显然:请运行你自己的基准测试。)

相似文章

优化CPU密集型Go热路径的笔记

Hacker News Top

本文讨论了CPU密集型Go代码的性能优化技术,指出了泛型和接口抽象因无法内联而产生的局限性,并主张在热路径中使用代码复制。文章通过一个Brotli移植示例和深入基准测试进行了说明。

深入理解Go运行时:性能分析

Hacker News Top

深入探讨Go的性能分析机制,解释运行时如何收集CPU、堆、阻塞、互斥锁和协程的性能分析数据,以及这些数据在pprof格式中的表示方式。