Go 1.27 引入了实验性的平台无关 SIMD 包

TL;DR

Go 1.27 发布了一个实验性的、平台无关的 simd 包,它抽象了固定大小的向量类型,实现了“一次编写,到处运行”的向量代码。该代码在 AVX/AVX2/AVX512 (amd64)、NEON (arm64) 和 WebAssembly 上可达到接近汇编的性能,同时在不支持 SIMD 的 CPU 上会自动回退到快速的软件模拟实现。


为什么需要一个可移植的 SIMD 层?

不同的 CPU 系列以截然不同的方式暴露 SIMD 功能:

  • 向量宽度可能是固定的(例如 wasm、PowerPC、s390x 上的 128 位)或可变的(RISC-V V 扩展、ARM SVE)。
  • 掩码(Masking)语义各不相同:一些架构有专用的掩码寄存器(AVX-512、RVV),另一些使用向量布尔值(NEON、AVX2),还有一些完全没有掩码。
  • 指令集不仅因系列而异,还因功能标志而异(AVX 与 AVX2 与 AVX-512,NEON 与 SVE 等)。

由于 Go 之前的 archsimd 包必须直接暴露这些差异,编写可移植的 SIMD 代码需要针对每个架构编写条件判断和手写汇编。新的 simd 包从类型系统中移除了固定大小的向量,仅实现了所有支持平台共有的操作交集,并通过高效的模拟来填补空白。

如何启用实验性 API

在构建或运行程序时设置环境变量 GOEXPERIMENT=simd,就像使用旧的 archsimd 实验一样。编译器会将对 simd 类型的调用重写为特定架构的实现,或者根据程序启动时检测到的硬件回退到模拟实现。

核心设计原则

  1. 交集优先的 API – 仅暴露所有目标平台共有的操作。缺失的原语通过映射到现有 SIMD 指令的模拟来提供。
  2. 接近汇编的性能 – 当源操作与硬件能力匹配时,生成的代码与手写汇编一样快。
  3. 优雅的回退 – 在没有 SIMD 支持的 CPU 上(或设置了 GODEBUG=simd=0 时),相同的代码将使用纯 Go 实现运行。
  4. 可读性 – 类型命名为 simd.Uint8s、simd.Float32s 等,方法读起来就像普通的 Go 方法,使代码对人类和大型语言模型都易于理解。

示例:向量化内积

func innerProduct(x, y []float32) float32 {
    var acc simd.Float32s
    var i int
    for i = 0; i < len(x)-acc.Len()+1; i += acc.Len() {
        u := simd.LoadFloat32s(x[i : i+acc.Len()])
        v := simd.LoadFloat32s(y[i : i+acc.Len()])
        acc = u.MulAdd(v, acc) // acc += u*v
    }
    if i < len(x) {
        u, _ := simd.LoadFloat32sPart(x[i:])
        v, _ := simd.LoadFloat32sPart(y[i:])
        acc = u.MulAdd(v, acc)
    }
    return simd.ReduceSum(acc) // added in Go 1.28
}

该循环每次迭代处理 acc.Len() 个元素,其中 acc.Len() 是运行时检测到的向量宽度(128、256 或 512 位)。相同的源代码在现代 x86 上编译为 AVX-512,在 Apple Silicon 上编译为 NEON,或者在 SIMD 不可用时编译为纯 Go 循环。


支持的操作 (Go 1.27)

下表总结了当前可用的方法。V 表示向量类型,M 表示掩码类型,E 表示标量元素类型。

加载 / 广播

  • LoadV([]E) V – 将完整切片加载到向量中。
  • LoadVPart([]E) (V, int) – 加载部分切片,返回实际加载的元素数量。
  • BroadcastV(E) V – 将标量广播到所有通道。

存储 / 字符串

  • Store([]E) – 将向量写回切片。
  • StorePart([]E) int – 存储尽可能多的通道。
  • String() string – 人类可读的表示形式。

算术(精选)

  • Add, Sub, Mul, Div (仅限浮点数), Neg, Abs (仅限浮点数)。
  • MulAdd – 浮点数的乘加运算。
  • IfElse(mask, y) – 当 mask 为真时从 y 中选择元素。
  • Masked(mask) – 将掩码为假的通道清零。

布尔与掩码

  • 向量上的 And, Or, Xor, AndNot。
  • Mask 类型 (Mask8s, Mask16s, …) 支持 And, Or, String, ToIntWs。

比较

  • Equal, NotEqual, Greater, GreaterEqual, Less, LessEqual – 生成相同元素宽度的掩码。

转换与重塑

  • ConvertToFloatW, ConvertToIntW, ConvertToUintW。
  • ToBits, ReshapeToUint*, BitsToFloatW, BitsToIntW – 零成本重解释。

移位与旋转(仅限整数向量)

  • 针对 16、32 和 64 位通道的 ShiftAllLeft/Right, RotateAllLeft/Right。

注意: 第一个版本缺少通用归约 (ReduceSum) 和一些以掩码为中心的功能;这些计划在 Go 1.28 中加入。


桥接到特定架构的代码

当所需的某些操作尚未包含在可移植 API 中时,开发者可以通过 ToArch() 和 FromArch 辅助函数回退到 archsimd 包。以下是 amd64 上缺失 Int8s.OnesCount 实现的示例:

//go:build goexperiment.simd && amd64
func OnesCount(v simd.Int8s) simd.Int8s {
    switch x := v.ToArch().(type) {
    case archsimd.Int8x16:
        // Use a lookup-table trick for AVX/AVX2.
        lut := archsimd.LoadInt8x16Array(&popcnt4x16)
        mask := archsimd.BroadcastInt8x16(0x0f)
        lo := x.And(mask)
        hi := x.ToBits().ReshapeToUint16s().ShiftAllRight(4).
                ReshapeToUint8s().BitsToInt8().And(mask)
        return simd.Int8sFromArch(lut.PermuteOrZero(lo).Add(lut.PermuteOrZero(hi)))
    default:
        return OnesCountEmulated(v)
    }
}

编译器会将类型 switch 专门化,因为二进制文件在编译时就知道目标平台存在哪些 archsimd 类型。


使用 GODEBUG 进行运行时控制

开发者可以强制指定向量宽度或进行全模拟以进行测试:

  • GODEBUG=simd=0 – 始终使用模拟路径。
  • GODEBUG=simd=128, 256, 512 – 要求该宽度;如果不可用则 panic。
  • GODEBUG=simd=+128 (或 +256, +512) – 即使缺少某些可选指令也允许该宽度;使用不支持的指令将在运行时 panic。

实现说明

编译器执行 AST 重写,为每个提到 simd 类型的函数生成特定大小的副本。这些副本位于 simd/internal/bridge 中,并被实例化为具体的 archsimd 类型(例如 Int8x16)。调度在程序启动时发生一次,之后热循环执行专门的版本,无需进一步的分支判断。添加对 simd 类型的虚拟引用(例如 var _ simd.Uint64s)可以将调度点提升到外层循环,确保使用专门的版本。


社区反应(精选 HN 评论)

"在这种情况下,可移植 SIMD 比非可移植 SIMD 慢约 11%,但两者都比非 SIMD 快约 5 倍。" – ImJasonH (wasm 上的基准测试)

"C++ 正在引入 std::simd;我非常支持用最少的内在函数编写向量代码。" – beached_whale

"没人要求这样做,但他们花时间把它做对了,并继续推动 Go 成为一种内存安全的高级系统语言。" – u8

"这为已经在运行多核的 Go 项目的底层性能打开了许多大门。很少有语言拥有标准库 SIMD 支持。" – qprofyeh

"接口转换和类型 switch 看起来效率低下,但编译器将它们专门化了;switch 在启动时解析,而不是在每次迭代时解析。" – vlovich123


下一步计划

  • Go 1.28 将向 archsimd 添加 SVE 支持,并最终添加到可移植的 simd 包中。
  • 计划加入新的操作,如 OnesCount、更丰富的掩码工具、归约原语和向量洗牌。
  • 功能变体标志将允许代码在仅缺少少量可选指令的硬件上运行(例如没有 PMULL 的 Raspberry Pi 的 NEON)。
  • 即将发布关于 archsimd 内部原理的专题博客文章。

总结

Go 的实验性 SIMD 层消除了历史上针对每个架构编写汇编的障碍,提供了一个简洁、与大小无关的 API,在广泛的 CPU 上提供接近原生的性能,同时保证在任何平台上都有功能性的回退。该设计反映了 C++ Highway 库的理念,并预示了 SVE 等未来的扩展,使 Go 成为高性能、跨平台工作负载更具吸引力的选择。

Sources

相关

  • 项目
  • 项目
  • 项目
  • 项目