编程 Go 1.27 的 simd 包:一份向量代码同时跑 AVX、NEON 和 wasm

2026-10-10 21:02:48

Go 1.27 的 simd 包:一份向量代码同时跑 AVX、NEON 和 wasm

Go 1.26 和 1.27 引入了用于 SIMD(单指令多数据)操作的实验性 API。SIMD 是很多现代 CPU 的原生能力,Go 的 Green Tea 垃圾回收器本身就用 SIMD 来加速扫描内存中的存活对象。

在这套 API 出现之前,从 Go 里用上 SIMD 的唯一办法是写 Go 汇编。Go 1.26 提供了 amd64 的 SIMD API,Go 1.27 补上了 arm64(NEON)和 wasm。但这里有个绕不开的问题:平台之间差异巨大。要提供完整能力,这些 API 只能放在架构相关的 archsimd 包里。

Go 1.27 不止于此,还引入了一个实验性的、完全可移植、与平台和向量宽度无关的 SIMD 接口,大致参考了 C++ 的 Highway。simd 包目前支持 amd64 上的 AVX、AVX2、AVX512,arm64 上的 NEON,以及 wasm 的 SIMD 指令。

动机:SIMD 架构之间的差异

  • 向量宽度:有的平台只有一种固定宽度(wasm、PowerPC、s390x 都是 128 位);有的提供多种固定宽度(amd64:128、256、512;loong64:128 和 256);riscv64 支持 128 到 65536 位之间长度未指定的向量;arm64 同时有 NEON(128 位)和 SVE(128–2048 位)。
  • 掩码:wasm、AVX、AVX2、NEON 没有掩码;AVX512 和 RVV 有掩码寄存器;SVE 每个字节分配一位;AVX2 的掩码 load/store 直接用向量当掩码,由最高位决定是否生效。
  • 操作:重排元素的原语各不相同;加密操作各不相同;wasm 缺少 64 位整数向量的比较。

概览

simd 包把这些差异藏起来:类型系统里不再出现固定宽度向量,只支持所有平台交集内的操作,缺的部分用高效的模拟补齐。构建时加 GOEXPERIMENT=simd 启用。

向量类型是首字母大写、复数形式的原生类型,例如 simd.Uint8s 或 simd.Float32s,从切片做 load/store。

一个例子:

// innerProduct returns the inner product of x and y.
func innerProduct(x, y []float32) float32 {
var a simd.Float32s
var i int
for i = 0; i  `Mask8s`)。

## Go 1.27 支持的操作

包级别的构造函数 `LoadV([]E) V`、`LoadVPart([]E) (V, int)`、`BroadcastV(E) V` 覆盖 `Int8s` 到 `Float64s`;此外还有 `Store`/`StorePart`/`String`。

| 类别 | 方法 |
| --- | --- |
| 算术 | `Abs`、`Add`、`AddSaturated`、`Average`、`Div`、`IfElse`、`Len`、`Masked`、`Max`、`Min`、`Mul`、`MulAdd`、`Neg`、`Not`、`Or`、`Sqrt`、`Sub`、`SubSaturated`、`Xor` |
| 布尔 | `And`、`AndNot`、`CarrylessMultiplyEven`/`Odd` |
| 比较 | `Equal`、`Greater`、`GreaterEqual`、`Less`、`LessEqual`、`NotEqual` |
| 转换 | `ConvertToFloatW`/`IntW`/`UintW`、`ToMask` |
| 掩码 | `And`、`Or`、`String`、`ToIntWs` |
| 移位/旋转 | `RotateAllLeft`/`Right`、`ShiftAllLeft`/`Right` |
| 零开销重塑 | `ToBits`、`ReshapeToUint8s`..`64s`、`BitsToFloatW`/`IntW` |

## 与平台特定代码互转

每个 `simd` 向量类型都有 `ToArch()`,返回 `any`,可以断言成 `archsimd` 类型;反向用 `simd.FromArch` 转回来。

一个完整的例子是给 `Int8s` 补上缺失的 `OnesCount()`:amd64 版本用构建标签 `goexperiment.simd && amd64`,NEON/Wasm 版本更简单,再加一份回退模拟。amd64 分支的调用链大致是:

```go
archsimd.LoadInt8x16Array(...)
archsimd.BroadcastInt8x16(...)
// 用 And 做掩码,再用查表做逐半字节计数
ToBits().ReshapeToUint16s().ShiftAllRight(4).ReshapeToUint8s().BitsToInt8()
PermuteOrZero(...)   // 或 PermuteOrZeroGrouped(...)
OnesCount(...)

回退实现用 SWAR 位计数,掩码是 0x5555... 这类常量。

API 交集与方法模拟

simd 在 archsimd 的 API 上补模拟实现,把空洞填上。进位无关乘法(crypto/CRC 用)的模拟与输入无关,在运行时完成。下个版本会把「成对相加」换成真正的求和归约。

GODEBUG 设置

  • GODEBUG=simd=0:即使硬件支持也走模拟。
  • simd=128:使用 128 位向量;如果特性不可用则 panic。
  • simd=256、simd=512:同理。
  • simd=+128 / +256 / +512:即使部分特性不被支持也照样使用;只有真正用到了不支持的指令才 panic。比如不带 PMULL 的树莓派 NEON;Apple Silicon 上跑 amd64 模拟时没有 VPCLMULQDQ 的 AVX2。

实现细节

simd 同时是一个包、一个内部实现包,以及编译器前端的 AST 重写。特化出的副本带 @simdNNN 后缀(128/256/512/0)。分派逻辑被提升到了外层。

后续

会有一篇专门讲 archsimd 的博客文章。Go 1.28 计划支持 SVE,增加更多操作(OnesCount、mask、reduction、shuffling),并为树莓派这类平台提供特性变体。

复制全文 生成海报 Go SIMD 性能

推荐文章

程序员茄子在线接单