amd64 微架构级别对 Go 程序性能提升多少?

在 Go 1.17 之前,Go 编译器总是生成可由任何 64 位 x86 处理器执行的 x86 二进制文件。
Go 1.18 为 AMD64 引入了 4 个架构级别 。每个级别在编译器可以包含在生成的二进制文件中的 x86 指令集上有所不同:

  • GOAMD64=v1(默认值):基准模式。仅生成所有 64 位 x86 处理器都能执行的指令。
  • GOAMD64=v2:所有 v1 指令,加上 CMPXCHG16B、LAHF、SAHF、POPCNT、SSE3、SSE4.1、SSE4.2、SSSE3。
  • GOAMD64=v3:所有 v2 指令,加上 AVX、AVX2、BMI1、BMI2、F16C、FMA、LZCNT、MOVBE、OSXSAVE。
  • GOAMD64=v4:所有 v3 指令,加上 AVX512F、AVX512BW、AVX512CD、AVX512DQ、AVX512VL。

例如,设置 GOAMD64=v3 将允许 Go 编译器在生成的二进制文件中使用 AVX2 指令(这在某些情况下可能会提高性能);但是这些二进制文件将无法在不支持 AVX2 的旧 x86 处理器上运行。
Go 工具链也可能生成更新的指令,但会通过动态检查来确保它们只在支持的处理器上执行。例如,如果设置了 GOAMD64=v1,并且 CPUID 报告 POPCNT 指令可用,那么 math/bits.OnesCount 仍然会使用该指令。否则,它会回退到通用实现。
Go 工具链目前不生成任何 AVX512 指令。
不支持 SSE3 的平台不支持种族检测器。

64 位 Intel 和 AMD 处理器已经演进了几十年。当你为 64 位 Intel 或 AMD 处理器编译 Go 程序时,编译器默认面向的是一个将近 20 年前的指令集。生成的二进制文件几乎能在任何 x64 芯片上运行,但同时也放弃了自 2003 年以来添加的所有指令。

我们通常用微架构级别(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 并未要求。虽然 v1v4 是一种有用的通用语言,但今天一个现实的"用尽该 CPU 提供的一切"目标至少需要一个 v5,而且可以说整个方案应该被更细粒度的特性检测所取代。

无论如何,Go 工具链通过 GOAMD64 环境变量暴露了这个 v1v4 的阶梯。设置 GOAMD64=v3 告诉编译器可以使用直到 AVX2(含)的所有指令。默认值是 v1,即最低公共分母。

这就引出了一个显而易见的问题:如果我拿一个真实的、对性能敏感的库,在每个级别上重新编译,实际能获得多大收益?我选择了 Roaring Bitmaps,它是一种用于数据库和搜索引擎的压缩位集数据结构。

Roaring Bitmap 存储一组 32 位整数。它将 32 位空间按高 16 位划分为每块 65,536 个值的 chunk,每个 chunk 存储在一个仅保存低 16 位的容器(container)中。容器有三种形式,库始终保留最小的那种:

  • 数组容器(array container):一个已排序的 16 位值列表,当 chunk 稀疏时使用(最多几千个元素);
  • 位图容器(bitmap container):一个扁平的 8 KB 位向量(65,536 位,每个可能值对应一位),当 chunk 密集时使用;
  • run 容器(run container):一个 [start, length] 区间列表,当集合中的位聚集成连续区间时使用。简单说就是把连续的一大段 1 压缩成"从哪开始、有多长"——比如"第 3 到第 100 位全是 1"只记成 [3, 98],而不是逐位存 98 个 1。

我拉取了该库的最新版本,然后用它自带的基准测试套件运行了四次,每次一个级别,每个级别采集八个样本。我在一台 Intel Xeon Gold 6548N(Emerald Rapids,支持全部四个级别,包括 AVX-512)上完成测试,使用 Go 1.26.2 和 Roaring v2.18.2。

种群计数(population count,或称 popcount,也叫汉明重量)简单来说就是一个机器字中被置为 1 的位数。Roaring 大量依赖它:位图容器的基数——它持有多少个值——是其 1024 个 64 位字的种群计数之和。现代 x86 芯片有专门的 popcnt 指令可以在单次操作中完成这一计算,但该指令只在 v2 级别(SSE4.2,2008 年)才可用。没有它,编译器只能退回到多指令的位操作序列。

最清晰的单一结果是种群计数:计算位图容器中置位比特的数量。v1 基线无法使用 popcnt 指令,因此 Go 生成的是软件回退实现。一旦我们移到 v2popcnt 变得可用,耗时几乎减半:

这是 43% 的减少,而且是免费的:无需改动源码,只需一个编译器标志。不过请注意,v3v4 没有进一步改善。单条 popcnt 指令已经是最优的了;就 Go 编译器而言,AVX2 和 AVX-512 在此没有可添加的东西。

种群计数是容易获得的胜利。库中其他部分呢?

另一个明显的胜利是从密集位图构建容器。FromDense array 基准测试接收一个原始的 8 KB 位向量,并为其构造最紧凑的容器:它对每个字做 popcount 以获知基数,然后扫描出所有置位比特的位置。这种逐字的 popcount-扫描 循环正是编译器在 256 位寄存器可用后可以自动向量化的模式,因此收益在 v2 之后继续增长:

v2 通过使用标量 popcnt/tzcnt 指令已减少了 21%,而 v3(AVX2)几乎将其翻倍,达到 38% 的减少。与种群计数一样,v4 没有任何增益。

集合操作也呈现出相同的模式。IntersectionCardinality 基准测试计算两个位图共有多少个值:对于位图容器,它将字逐对做 AND 操作,然后对结果做种群计数,而不用物化出交集。在这里 v2 基本没有作用(标量 popcnt 已经在内部循环中了),但 v3 让编译器将 AND-计数 循环扩展到 256 位寄存器,将耗时减少了 22%:

要点总结:

  1. 在现代硬件上,每个人都应该使用 v2 或更高版本。生成的二进制文件可以在任何数据中心和任何非古董笔记本电脑上运行。
  2. v3 级别值得研究。
  3. v4 级别在我的一些基准测试中本应有所帮助,但实际上没有。我怀疑是 Go 编译器在这方面还不够好。

(显然:请运行你自己的基准测试。)


原文来源:Daniel Lemire, "How much do amd64 microarchitecture levels help in Go?," in Daniel Lemire's blog, June 6, 2026, https://lemire.me/blog/2026/06/06/how-much-do-amd64-microarchitecture-levels-help-in-go/


精选评论

Xarn(2026年6月8日):

关于 v4 没有任何作用,来自他们的文档:

Go 工具链目前不生成任何 AVX512 指令。

Marco(2026年6月9日):

我自己的使用 AVX512 指令的经验是,使用它们的逻辑(和数据结构)与常规使用的结构差异如此之大,编译器不太可能翻译并非专门为它们构建的代码。问题是编程语言何时会创建易于使用的语法来方便地使用这些扩展。