2026 年 9 月 24 日

Go 1.26 和 Go 1.27 版本计划引入针对单指令多数据 (SIMD) 操作的实验性 API。SIMD 作为现代 CPU 的核心特性,允许软件对数据向量执行统一的并行操作,例如在单条指令中同时处理 8 对 float64 数值。这一技术能显著提升加密、数据处理及人工智能等计算密集型任务的效率。事实上,Go 语言现有的垃圾回收器(GC)已利用 SIMD 加速活跃对象的内存扫描。
在此前,若要在 Go 中调用底层硬件的 SIMD 能力,开发者必须编写汇编代码。这种方式仅适用于对性能极度敏感的关键内核,导致大量本可受益于 SIMD 加速的软件无法充分利用 CPU 算力。
Go 1.26 率先为 amd64 架构引入了 SIMD API,随后 Go 1.27 扩展至 arm64(特别是 NEON 指令集)和 wasm 平台。然而,跨平台 SIMD 开发面临的主要挑战在于各架构间的巨大差异,这不仅体现在支持的操作类型上,更反映在向量的表示方式上。部分平台提供固定大小的向量(通常在 128 位至 512 位之间),而其他平台的向量大小可能在构建时未知,需在程序运行时动态查询。为了全面覆盖这些平台特性,相关 API 被放置在依赖具体架构的 archsimd包中。
Go 1.27 在此基础上更进一步,引入了一个完全可移植、无平台依赖且与向量大小无关的实验性 SIMD 接口,其设计灵感源自 C++ 的 Highway 库。该接口的目标是让开发者只需编写一次“simd”代码,即可在支持 SIMD 的平台上获得接近汇编的性能,同时在尚未原生支持的平台或特定指令缺失的情况下提供高效模拟。目前,simd 包支持 AMD64 上的 AVX、AVX2 和 AVX512,arm64 上的 NEON,以及 wasm 的 SIMD 指令。
动机:SIMD 架构间的差异性
SIMD 架构在多个维度存在显著差异。首先,部分架构仅提供单一固定向量长度(如 wasm、PowerPC 和 s390x 均为 128 位)。其次,部分架构支持多种固定长度(amd64 支持 128、256 和 512 位;loong64 支持 128 和 256 位)。Riscv64 支持未指定长度但介于 128 至 65536 位之间的向量,且长度限制为 2 的幂次。Arm64 则同时支持一种固定长度(128 位 NEON)和一种可变长度(128-2048 位 SVE,仅限 2 的幂次)。对于特定架构的具体实例,确定其实际支持的向量长度需要进行特性检查:例如在 amd64 上需区分是 AVX、AVX2 还是 AVX512;在 arm64 上需区分是 NEON 还是 SVE,若是 SVE 还需确认具体变体(SVE、SVE2 或 SVE2.1)及其长度。
不同 SIMD 架构在处理向量掩码(Masking)的方式上也各不相同。向量化的 if-then-else 逻辑通常通过掩码实现,即执行操作但仅在掩码为“真”时赋值结果(或进行加载/存储)。一些 SIMD 变种不提供硬件掩码,所有操作均对所有元素运行,“掩码”效果需通过向量位掩码和布尔运算模拟(如 wasm、AVX、AVX2、NEON)。另一些变种提供特殊的掩码寄存器,其中一位控制一个向量元素的操作(如 AVX512 和 RVV)。SVE 则为每个向量字节分配一位,但由每个元素掩码位的最低有效位控制操作。AVX2 也支持带掩码的加载和存储,但使用纯向量作为掩码,并由最高有效位控制操作。
第三个差异来源在于指令本身。每种架构都提供了独特的原始函数来重新排列向量元素;有些要求常量输入,而另一些支持可变输入。不同的 SIMD 架构支持的加密相关操作各异。即使是基本算术运算,支持情况也不尽相同;例如,wasm 缺乏对 64 位整数向量比较的支持。即使在特定架构的给定向量长度下,指令支持也取决于必须检查的“特性”。
尽管 Go 的依赖架构的 archsimd 包旨在尽可能统一跨架构体验,但上述怪癖依然存在,使得多平台 SIMD 代码的设计、编写和测试变得繁重。archsimd 包内部可以做更多工作以缩小架构间的视觉差异。
概述
新的 simd 包通过从类型系统中移除固定大小向量,并仅支持所有不同平台交集处的操作,同时通过高效模拟填补空白,从而隐藏这些差异。该包的目标是一组简单的操作,遵循以下原则:
- 足以支持许多从矢量化实现中获益的数据处理算法(但不依赖于特定矢量大小)。
- 当源代码操作与底层硬件相匹配时,效率等同于汇编语言。
- 在不匹配或缺失时,尽可能通过其他方式模拟。
- 易于阅读和理解(即使是由 LLM 生成的代码)。
在缺乏 SIMD 指令或缺乏 archsimd 支持的平台上,所有操作均由软件模拟,因此使用 simd 包编写的代码始终可以运行。
要使用这个实验包,需在构建时设置环境变量 GOEXPERIMENT=simd,这与使用实验包 archsimd 的方式相同。
simd 向量类型仅是大写的、复数形式的原始类型,例如 simd.Uint8s 或 simd.Float32s。向量从切片中加载并存储回切片中,示例如下:
// 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 < len(x)-a.Len()+1; i += a.Len() {
u := simd.LoadFloat32s(x[i : i+a.Len()])
v := simd.LoadFloat32s(y[i : i+a.Len()])
a = u.MulAdd(v, a)
}
if i < len(x) {
u, _ := simd.LoadFloat32sPart(x[i:])
v, _ := simd.LoadFloat32sPart(y[i:])
a = u.MulAdd(v, a)
}
return sum(a)
}
// sum returns scalar sum of elements of x.
func sum(x simd.Float32s) float32 {
s := make([]float32, x.Len())
x.Store(s)
var r float32
for _, e := range s {
r += e
}
return r
}
此示例也展示了该包第一个实验版本的局限性:由于没有通用的方法对向量的所有元素求和,simd 在 Go 1.27 中不支持此功能。不过,ReduceSum 将出现在下一个版本中,届时可用 simd.ReduceSum 替换自定义的 sum 函数。
SIMD 比较产生掩码值,这些值的宽度与相应的向量元素宽度一致。例如,Int8s 的比较会产生 Mask8s。掩码值可用于选择和过滤向量。
从 Go 1.27 开始支持的 simd 包操作
在下表中,V 代表向量类型,M 代表掩码类型,E 代表标量类型,W 代表宽度。
包级负载/广播功能
| 功能 | Int8s |
Int16s |
Int32s |
Int64s |
Uint8s |
Uint16s |
Uint32s |
Uint64s |
Float32s |
Float64s |
|---|---|---|---|---|---|---|---|---|---|---|
Load V([]E) V |
Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
LoadPart ([]E) (V, int) |
Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
Broadcast V(E) V |
Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
存储/字符串操作
| (x V) 方法 | Int8s |
Int16s |
Int32s |
Int64s |
Uint8s |
Uint16s |
Uint32s |
Uint64s |
Float32s |
Float64s |
|---|---|---|---|---|---|---|---|---|---|---|
Store |
Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
StorePart |
Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
String |
Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
算术运算
| (x V) 方法 | Int8s |
Int16s |
Int32s |
Int64s |
Uint8s |
Uint16s |
Uint32s |
Uint64s |
Float32s |
Float64s |
|---|---|---|---|---|---|---|---|---|---|---|
Abs() V |
Y | Y | Y | Y | Y | |||||
Add(y V) V |
Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
AddSat(y V) V |
Y | Y | Y | Y | ||||||
Average(y V) V |
Y | Y | ||||||||
Div(y V) V |
Y | Y | ||||||||
IfElse(mask MaskWs, y V) V |
Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
Max(y V) V |
Y | Y | Y | Y | Y | Y | Y | Y | ||
Min(y V) V |
Y | Y | Y | Y | Y | Y | Y | Y | ||
Mul(y V) V |
Y | Y | Y | Y | Y | Y | Y | Y | ||
MulAdd(y V, z V) V |
Y | Y | ||||||||
Neg() V |
Y | Y | Y | Y | Y | Y | ||||
Not() V |
Y | Y | Y | Y | Y | Y | Y | Y | ||
Or(y V) V |





