Platform-independent SIMD in Go
Platform-independent SIMD in Go
Go 语言中的平台无关 SIMD
The Go Blog Platform-independent SIMD in Go David Chase and Junyang Shao 24 September 2026
Go 博客 Go 语言中的平台无关 SIMD David Chase 和 Junyang Shao 2026 年 9 月 24 日
Go 1.26 and 1.27 include experimental APIs for Single Instruction Multiple Data (SIMD) operations. SIMD is a native feature of many modern CPUs that allows software to perform uniform operations across vectors of data very quickly, such as adding 8 pairs of float64 values in a single instruction. It can significantly speed up many computationally-intensive tasks, ranging from cryptography to data processing to AI. In fact, Go’s Green Tea garbage collector even makes use of SIMD to accelerate scanning memory for live objects.
Go 1.26 和 1.27 版本包含了用于单指令多数据(SIMD)操作的实验性 API。SIMD 是许多现代 CPU 的原生功能,允许软件非常快速地对数据向量执行统一操作,例如在单条指令中相加 8 对 float64 值。它可以显著加速许多计算密集型任务,从密码学到数据处理再到人工智能。事实上,Go 的 Green Tea 垃圾回收器甚至利用 SIMD 来加速扫描内存中的存活对象。
Prior to these new experimental APIs, the only way to access this functionality from Go was by writing Go assembly. This was only worth it for truly performance-critical compute kernels, which meant plenty of software that could benefit from SIMD simply left a lot of the CPU unused. Go 1.26 introduced a SIMD API for amd64, and Go 1.27 added APIs for arm64 (specifically NEON) and wasm.
在这些新的实验性 API 出现之前,从 Go 访问此功能的唯一方法是编写 Go 汇编。这仅在真正对性能至关重要的计算内核中才值得,这意味着许多本可以从 SIMD 受益的软件白白浪费了大量的 CPU 资源。Go 1.26 为 amd64 引入了 SIMD API,而 Go 1.27 则增加了对 arm64(特别是 NEON)和 wasm 的 API 支持。
However, a basic challenge for a SIMD API is the enormous variation between platforms, not simply in what operations they support, but even in how vectors are represented. Some platforms provide fixed-size vectors, typically between 128 bits and 512 bits, while on others the vector size isn’t known at build time and must be queried when the program starts. To provide full access to the breadth of these platforms, these APIs live in an architecture-dependent archsimd package.
然而,SIMD API 面临的一个基本挑战是平台之间巨大的差异,这不仅体现在它们支持的操作上,甚至体现在向量的表示方式上。一些平台提供固定大小的向量(通常在 128 位到 512 位之间),而另一些平台的向量大小在构建时未知,必须在程序启动时进行查询。为了提供对这些平台广泛的访问能力,这些 API 位于与架构相关的 archsimd 包中。
But Go 1.27 goes beyond these architecture-dependent APIs and introduces an experimental, fully portable, platform- and size-agnostic SIMD interface, loosely based on Highway for C++. The goal is to support write-once near-asm-performance “simd” code on platforms with SIMD support, and to provide a competent emulation on those platforms that do not (yet) have SIMD support. The simd package currently supports AVX, AVX2, and AVX512 on amd64, NEON on arm64, and wasm’s SIMD instructions.
但 Go 1.27 超越了这些与架构相关的 API,引入了一个实验性的、完全可移植的、与平台和大小无关的 SIMD 接口,其设计大致基于 C++ 的 Highway 库。其目标是在支持 SIMD 的平台上支持“一次编写,近乎汇编性能”的 SIMD 代码,并在那些(尚未)支持 SIMD 的平台上提供有效的模拟。simd 包目前支持 amd64 上的 AVX、AVX2 和 AVX512,arm64 上的 NEON,以及 wasm 的 SIMD 指令。
Motivation: variation among SIMD architectures
动机:SIMD 架构间的差异
SIMD architectures vary in several dimensions. Some provide a single fixed vector size (wasm, PowerPC, and s390x, 128 bits). Some provide several fixed vector sizes (amd64, with 128, 256, and 512; loong64 with 128 and 256). Riscv64 supports vectors of unspecified size between 128 and 65536 bits, though the length is limited to powers of 2. Arm64 supports one fixed size (128 bits, NEON), and one variable size (128-2048 bits, powers of two only, SVE).
SIMD 架构在多个维度上存在差异。一些提供单一的固定向量大小(wasm、PowerPC 和 s390x,均为 128 位)。一些提供多种固定向量大小(amd64,支持 128、256 和 512 位;loong64 支持 128 和 256 位)。Riscv64 支持 128 到 65536 位之间未指定大小的向量,但长度限制为 2 的幂。Arm64 支持一种固定大小(128 位,NEON)和一种可变大小(128-2048 位,仅限 2 的幂,SVE)。
On a given instance of a particular architecture, determining what sizes that particular instance happens to support requires feature checks: amd64, but is it AVX, AVX2, or AVX512? Arm64, but is it NEON or SVE? If SVE, how large? Which variant of SVE: SVE, SVE2, or SVE2.1?
在特定架构的给定实例上,确定该实例支持哪些大小需要进行特性检查:例如 amd64,它是 AVX、AVX2 还是 AVX512?Arm64,它是 NEON 还是 SVE?如果是 SVE,大小是多少?是 SVE 的哪个变体:SVE、SVE2 还是 SVE2.1?
Different SIMD architectures vary in how they handle vector masking. For vectors, if-then-else across a vector can be implemented with masks; do the operation, but only assign the result (or load, or store) where the mask is “true”. Some SIMD variants do not provide masks; all operations work across all elements, and “masking” is done with vector bitmasks and vector boolean operations (wasm, AVX, AVX2, NEON). Some provide special mask registers, with one bit governing operations on one vector element (AVX512 and RVV). Others (SVE) allocate one bit per vector byte, but the least-significant bit of each element’s mask bits governs masked operations. AVX2 also supports masked loads and stores, but using a plain vector as the mask, and with the most-significant bit governing the operation.
不同的 SIMD 架构在处理向量掩码(masking)的方式上各不相同。对于向量而言,跨向量的 if-then-else 可以通过掩码实现;即执行操作,但仅在掩码为“真”的位置赋值(或加载、存储)。一些 SIMD 变体不提供掩码;所有操作作用于所有元素,而“掩码”是通过向量位掩码和向量布尔运算完成的(wasm、AVX、AVX2、NEON)。一些提供特殊的掩码寄存器,用一位控制一个向量元素的操作(AVX512 和 RVV)。另一些(SVE)为每个向量字节分配一位,但每个元素掩码位的最低有效位控制掩码操作。AVX2 也支持带掩码的加载和存储,但使用普通向量作为掩码,并由最高有效位控制操作。
A third source of variation is in the operations themselves. Each architecture provides its own primitives for rearranging vector elements; some require constant inputs, others support variable inputs. Different SIMD architectures support different crypto-related operations. Even basic arithmetic can have varying support; for example wasm lacks comparisons for vectors of 64-bit integers. Even for a given vector length on a particular architecture, instruction support depends on “features” that must be checked. Even though Go’s architecture-dependent archsimd package was designed to be as uniform as possible across architectures, many of these quirks remain, and make designing, writing, and testing code for multiplatform SIMD onerous. We could do more in the archsimd package to make the different architectures appear more similar, but we can only go so far without compromising efficiency.
第三个差异来源是操作本身。每个架构都提供自己的原语来重新排列向量元素;一些需要常量输入,另一些支持变量输入。不同的 SIMD 架构支持不同的加密相关操作。即使是基本算术运算的支持也可能不同;例如,wasm 缺乏对 64 位整数向量的比较支持。即使在特定架构的给定向量长度下,指令支持也取决于必须检查的“特性”。尽管 Go 的架构相关 archsimd 包旨在尽可能在各架构间保持统一,但许多这些怪癖依然存在,使得为多平台 SIMD 设计、编写和测试代码变得繁重。我们可以在 archsimd 包中做更多工作以使不同架构看起来更相似,但在不牺牲效率的情况下,我们能做的有限。
Overview
概述
The new simd package hides these differences by removing fixed-size vectors from the type system, and by only supporting those operations that are in the intersection of all the different platforms, and fills gaps in the intersection with efficient emulation in terms of other SIMD instructions. The goal is a set of operations that is adequate to support many data processing algorithms that benefit from a vectorized implementation (but are not tied to a particular vector size), is as efficient as assembly language when the source code operations match the underlying hardware, is otherwise emulated as well as possible, and is easy to read and understand (even/especially if an LLM ends up writing the code).
新的 simd 包通过从类型系统中移除固定大小向量,并仅支持所有不同平台交集中的操作来隐藏这些差异,并通过其他 SIMD 指令的高效模拟来填补交集中的空白。其目标是提供一套足以支持许多受益于向量化实现(但不绑定于特定向量大小)的数据处理算法的操作集,在源代码操作与底层硬件匹配时达到与汇编语言相同的效率,在其他情况下尽可能好地进行模拟,并且易于阅读和理解(即使/特别是当代码由 LLM 编写时)。
On platforms that lack SIMD instructions or that lack support in archsimd, all of the operations are emulated, so that code written using the simd package will always run. To use this experimental package, set GOEXPERIMENT=simd at build time, just like using the experimental archsimd package. The simd vector types are just capitalized, plural, primitive types, for example simd.Uint8s or simd.Float32s. Vectors are loaded from and stored to slices, for example:
在缺乏 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:])
// ...
}
}
// innerProduct 返回 x 和 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:]) // … } }