Build2 publishers3 min readPublished
Go 1.27 ships a size-agnostic simd package for loops that once needed per-architecture assembly
Go 1.27 adds an experimental, portable simd package covering AVX through AVX-512, Arm NEON and wasm SIMD instructions. The Go team's stated goal is near-assembly speed from code written once, and each fleet has to measure that on its own hardware.
The Engineer · Build desk
What happened
- Before Go 1.26, the only way to use SIMD from Go was hand-written Go assembly, which the Go team says was only worth it for truly performance-critical kernels.
- Go 1.26 added an amd64 SIMD API and Go 1.27 added arm64 NEON and wasm, all in an architecture-dependent package called archsimd.
- The portable simd package is modeled on Highway, Google's SIMD library for C++.
- The Go runtime already uses SIMD in its Green Tea garbage collector to speed up scanning memory for live objects.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision Data and inference loops that never justified per-architecture assembly can now get a portable SIMD rewrite, and a benchmark on each target decides whether that rewrite stays.
- cost Mixed amd64 and arm64 fleets need a benchmark run per architecture, because one source file executes with different lane counts on each.
- exposure Teams shipping to riscv64, loong64, PowerPC or s390x get emulated vectors from the same source, so a portable kernel can reach those builds without anyone having measured a speedup there.
The obstacle to a single SIMD API in Go is the vector itself. On wasm, PowerPC and s390x a vector is 128 bits, amd64 offers 128, 256 and 512, and loong64 offers 128 and 256 [7]. Riscv64 allows any power of two from 128 to 65536 bits. Arm64 pairs fixed 128-bit NEON with SVE at 128 to 2048 bits [8]. On some platforms the size is unknown at build time and has to be queried when the program starts [9].
Masking is worse. Wasm, AVX, AVX2 and NEON have no mask registers, so masking is done with vector bitmasks and boolean operations [10]. AVX512 and RVV have mask registers with one bit per element [10]. SVE allocates one bit per vector byte and reads the least-significant bit of each element's group. AVX2's masked loads and stores take a plain vector and read the most-significant bit [11]. Wasm also has no comparison for vectors of 64-bit integers [12].
The archsimd package leaves many of those quirks visible. The Go team wrote that they make designing, writing and testing multiplatform SIMD code "onerous" [13]. After the masking rules, that word seems restrained. The team also wrote that archsimd could be made more uniform, "but we can only go so far without compromising efficiency" [13]. The portable simd package takes the other path and removes fixed-size vectors from the type system [14]. A kernel's types no longer commit it to 128 or 512 bits [14].
That portability changes what a benchmark number means. A 512-bit AVX-512 vector holds 8 float64 values, the post's own example of what one instruction can add. A 128-bit NEON vector holds 2 [c18, c19]. So the same source can process four times as many elements per instruction on one machine as on another [18]. An amd64 speedup carries over to arm64 only if the loop is limited by something other than vector width, such as memory bandwidth or a scalar tail.
Where the package has no hardware backend, it emulates vectors, and the post calls that emulation "competent" [6]. The supported list is AVX, AVX2 and AVX512 on amd64, NEON on arm64, and wasm's SIMD instructions [5]. Riscv64, loong64, PowerPC and s390x all appear in the post's own survey of SIMD architectures, and they run on emulation for now [20]. On arm64 the package uses NEON's fixed 128 bits, and SVE's wider vectors are not yet supported [c5, c8].
I think the two-package split is the right design. Archsimd keeps each platform's instructions reachable for the kernels that need a specific one, and simd covers the common loop [c3, c14]. For a service that runs on both amd64 and arm64, I would write the portable kernel first and benchmark it on each fleet. Assembly would stay only for a loop where the numbers show the portable version falls short.
What to watch
- Whether Arm SVE support lands in Go 1.28, as Phoronix reports is planned, letting arm64 kernels use vectors wider than NEON's 128 bits.
- Whether the Go team publishes measurements of the portable package against hand-written assembly, which would test the near-assembly goal.
- Which operations are added before the package leaves experimental status, especially crypto primitives, which the Go post says differ from one architecture to the next.