金黄色仙人掌刺的微距特写,密集排列成阵列

以前想用 SIMD,只能写汇编

SIMD(单指令多数据)是现代 CPU 的原生能力:一条指令同时处理一批数据,比如一次把 8 对 float64 相加。Go 官方博客 9 月 24 日的文章说得很直白——在这套新 API 之前,想在 Go 里用上它,唯一的办法是写 Go 汇编。只有真正吃性能的核心代码才值得这么干,于是大量本可受益的软件,让 CPU 白白空着。值得一提的是,Go 自己的 Green Tea 垃圾回收器就用 SIMD 加速扫描存活对象。

难点不在指令,在平台差异

Go 1.26 先给了 amd64 的 SIMD API,1.27 补上 arm64(NEON)和 wasm。真正的麻烦是差异本身:有的平台只有一种固定位宽(wasm、PowerPC、s390x 是 128 位),有的有好几档(amd64 有 128/256/512,loong64 有 128/256);riscv64 的向量长度在编译期根本不知道,得程序启动时问出来;arm64 除了 128 位的 NEON,还有 128 到 2048 位、只取 2 的幂的 SVE。连「向量怎么表示」都不统一。

所以 archsimd 这类包只能按架构分别写。Go 1.27 另开一条路:引入实验性的、完全可移植的 simd 包,思路借鉴 C++ 的 Highway,目标是「写一次、接近汇编的性能」,在没有 SIMD 的平台上则给一个合格的模拟实现。

它怎么把差异抹平

做法是取交集:只保留所有平台都有的操作,缺口用其它 SIMD 指令高效补出来——标量移位用向量移位模拟,某些平台缺的无符号比较,用有符号比较加两次与常数的异或代替。位宽不进类型系统,类型就是 simd.Uint8s、simd.Float32s 这种「复数形式的基本类型」,长度在运行时问。

写起来很朴素:LoadFloat32s 从切片装载,做乘加,比较得到的掩码(Int8s 比较出 Mask8s)用于选择和过滤。需要架构专属能力时,用 ToArch() 转到 archsimd 类型,用完转回来。编译器前端会做一次 AST 重写,按位宽生成特化副本(@simd128、@simd256、@simd512,@simd0 表示模拟),把派发开销提到计算循环之外。

对普通项目意味着什么

一是门槛降了:不必为 x86 和 ARM 各维护一套汇编,同一份代码在多平台吃向量加速,没有硬件的机器上也不会崩,退化到模拟实现继续跑。这种「性能可移植」的取舍,正是多平台交付真正需要的东西。

二是测试有了抓手:GODEBUG 的 simd=0/128/256/+512 可以强制走模拟实现或指定向量宽度,等于在一台机器上验证多种硬件配置,边缘设备(文章提到树莓派这类只缺一两个指令的平台)的问题提前暴露。Go 1.28 计划给 archsimd 补 SVE 支持,并补齐 OnesCount、掩码、归约、向量重排等操作。

对做企业数字化的小团队,启发不是立刻去写 SIMD,而是先量化再优化:日志检索、批量对账、图片处理、加密这类重复计算密集的环节,先量出热点在哪个函数,再看能不能向量化。软件优化的成本常常只有加机器的一个零头,而它需要的前提,只是愿意花半天把瓶颈找出来。