Go 1.27 SIMD Benchmark: Can It Replace GoAT Generated AVX512?
Go 1.27 SIMD Benchmark: Can It Replace GoAT Generated AVX512?
Go 1.27 introduced the experimental simd package, which provides portable, vector-width-independent SIMD types and operations for integers and floating-point numbers. To accelerate vector computations during model training and inference, Gorse has used GoAT to convert SIMD instructions compiled from C into Go assembly. The simd package makes it possible to run the same Go code across different instruction sets, but can its performance approach the assembly generated by GoAT with LLVM?
We implemented several vector operations with Go SIMD in Gorse's common/floats package and added the corresponding benchmarks. This post compares the performance of four implementations:
- A baseline implementation that does not use SIMD features—in other words, an ordinary scalar loop in Go.
- AVX and AVX512 assembly compiled from C and converted by GoAT.
- An implementation written with Go 1.27's experimental
simdpackage.
Test Environment and Methodology
The test environment was configured as follows:
| Item | Configuration |
|---|---|
| Operating system | Windows 11 Pro 25H2 |
| CPU | 11th Gen Intel Core i7-11370H @ 3.30 GHz |
| Go version | 1.27.0 |
| Input length | 16, 32, 64, or 128 float32 values |
The following command ran the complete benchmark suite:
$env:GOEXPERIMENT = "simd"
go test ./common/floats -run '^$' -bench . -count=1The Intel Core i7-11370H supports AVX512, so Go SIMD uses AVX512 instructions for the vector operations.
Warning
These results come from a single complete run. They are useful for observing orders of magnitude and overall trends, but they do not include confidence intervals. Differences of a few nanoseconds or less can easily be affected by CPU frequency, temperature, and operating-system scheduling.
Go SIMD Implementation
Using in-place vector subtraction as an example, the Go SIMD implementation processes complete vectors first, then uses partial loads and stores for a tail of any length:
func simdSub(a, b []float32) {
vectorLen := simd.VectorBitSize() / 32
for len(a) >= vectorLen {
simd.LoadFloat32s(a).Sub(simd.LoadFloat32s(b)).Store(a)
a = a[vectorLen:]
b = b[vectorLen:]
}
if len(a) > 0 {
x, n := simd.LoadFloat32sPart(a)
y, _ := simd.LoadFloat32sPart(b)
x.Sub(y).StorePart(a[:n])
}
}The Go compiler generates versions of the same function for different vector widths, such as simdSub@simd128, simdSub@simd256, and simdSub@simd512, then selects the appropriate implementation at runtime. On the Intel Core i7-11370H, it uses simdSub@simd512, which maps to AVX512 instructions.
Performance Comparison
- An FP32 vector of length 16, exactly the number of values held by one 512-bit AVX512 register.
| Operation | Scalar loop | AVX | AVX512 | Go SIMD | vs. scalar loop | vs. AVX512 |
|---|---|---|---|---|---|---|
| Dot | 10.94 ns | 8.81 ns | 8.55 ns | 14.55 ns | 0.75x | 0.59x |
| Euclidean | 33.99 ns | 10.06 ns | 8.94 ns | 19.96 ns | 1.70x | 0.45x |
| SubTo | 14.98 ns | 7.07 ns | 7.04 ns | 8.66 ns | 1.73x | 0.81x |
| MulTo | 27.82 ns | 7.59 ns | 7.62 ns | 7.70 ns | 3.61x | 0.99x |
| DivTo | 42.36 ns | 7.17 ns | 8.19 ns | 7.95 ns | 5.33x | 1.03x |
| SqrtTo | 124.10 ns | 6.36 ns | 6.38 ns | 6.04 ns | 20.55x | 1.06x |
- An FP32 vector of length 32.
| Operation | Scalar loop | AVX | AVX512 | Go SIMD | vs. scalar loop | vs. AVX512 |
|---|---|---|---|---|---|---|
| Dot | 45.94 ns | 8.82 ns | 10.23 ns | 14.03 ns | 3.27x | 0.73x |
| Euclidean | 45.66 ns | 10.44 ns | 9.56 ns | 24.15 ns | 1.89x | 0.40x |
| SubTo | 33.50 ns | 7.87 ns | 7.59 ns | 11.00 ns | 3.05x | 0.69x |
| MulTo | 49.98 ns | 8.23 ns | 8.43 ns | 12.71 ns | 3.93x | 0.66x |
| DivTo | 46.86 ns | 9.24 ns | 8.54 ns | 11.50 ns | 4.07x | 0.74x |
| SqrtTo | 93.70 ns | 8.06 ns | 9.45 ns | 10.80 ns | 8.68x | 0.88x |
- An FP32 vector of length 64.
| Operation | Scalar loop | AVX | AVX512 | Go SIMD | vs. scalar loop | vs. AVX512 |
|---|---|---|---|---|---|---|
| Dot | 76.25 ns | 11.68 ns | 10.80 ns | 19.17 ns | 3.98x | 0.56x |
| Euclidean | 73.29 ns | 11.96 ns | 11.40 ns | 27.50 ns | 2.67x | 0.41x |
| SubTo | 58.41 ns | 10.21 ns | 9.67 ns | 16.69 ns | 3.50x | 0.58x |
| MulTo | 79.18 ns | 8.39 ns | 8.15 ns | 18.48 ns | 4.28x | 0.44x |
| DivTo | 73.70 ns | 13.45 ns | 13.36 ns | 18.36 ns | 4.01x | 0.73x |
| SqrtTo | 339.00 ns | 16.11 ns | 15.30 ns | 16.86 ns | 20.11x | 0.91x |
- An FP32 vector of length 128.
| Operation | Scalar loop | AVX | AVX512 | Go SIMD | vs. scalar loop | vs. AVX512 |
|---|---|---|---|---|---|---|
| Dot | 124.60 ns | 13.81 ns | 11.43 ns | 23.84 ns | 5.23x | 0.48x |
| Euclidean | 161.60 ns | 23.16 ns | 17.24 ns | 33.68 ns | 4.80x | 0.51x |
| SubTo | 93.39 ns | 13.07 ns | 13.46 ns | 27.84 ns | 3.35x | 0.48x |
| MulTo | 125.90 ns | 13.66 ns | 9.21 ns | 29.52 ns | 4.26x | 0.31x |
| DivTo | 260.20 ns | 28.74 ns | 28.51 ns | 32.93 ns | 7.90x | 0.87x |
| SqrtTo | 484.00 ns | 27.73 ns | 31.07 ns | 36.24 ns | 13.36x | 0.86x |
The results lead to the following conclusions:
- Go SIMD provides substantial overall gains over ordinary Go loops. Across the six operation types, the mean speedup over scalar loops rises from 3.09x to 5.79x as the vector length increases from 16 to 128. Longer vectors amortize fixed call overhead more effectively and make SIMD parallelism more beneficial.
- For short vectors, Go SIMD can approach or even surpass GoAT-generated AVX512 assembly on high-latency operations. At length 16, Go SIMD is approximately 3% faster on
DivToand 5% faster onSqrtTo, whileMulTois essentially tied with the AVX512 assembly implementation. These differences are all below 0.4 ns, however, so they are not enough to establish that Go SIMD is consistently faster. - For long vectors, GoAT-generated AVX512 assembly remains clearly faster overall. At length 128, the average performance ratio of Go SIMD to AVX512 assembly is 0.55, meaning that Go SIMD takes about 1.82 times as long overall.
MulTohas the largest gap, with Go SIMD about 3.21 times slower;DotandSubToare also about 2.1 times slower. - High-latency instructions hide loop-management overhead more effectively. At length 128, Go SIMD is only about 16% slower than AVX512 assembly on
DivToand 17% slower onSqrtTo. Division and square-root instructions have relatively high latency, so re-slicing, bounds checks, and loop branches account for a smaller share of the total time. - Reduction operations remain a clear weakness.
DotandEuclideanmust store the SIMD accumulator in a temporary array and then perform a horizontal sum with scalar code. At length 16,Dotis about 33% slower than the ordinary Go loop. At the other lengths, both operations outperform scalar loops but still trail AVX512 assembly by a wide margin.
The 512-bit Go SIMD version does generate AVX512 instructions such as VMOVDQU64 and VSUBPS, but the current Go implementation must still maintain slice length and capacity, evaluate loop conditions, and retain bounds checks and a tail path for every 16 elements it processes. By comparison, the AVX512 assembly generated by GoAT with LLVM uses tighter pointer loops and may also unroll them. This explains why “using AVX512 instructions” does not automatically mean “matching the performance of GoAT-generated AVX512 assembly.”
Which Implementation Should You Choose?
Based on this experiment, a reasonably clear order of preference emerges:
- Go SIMD is attractive when maintainability and cross-architecture support are priorities. A single implementation can select 128-, 256-, or 512-bit vectors according to runtime capabilities and can automatically handle an emulated path when the hardware lacks support. In these benchmarks, it was commonly several times faster than ordinary Go loops.
- For maximum performance on a fixed platform, GoAT-generated AVX512 assembly still leads. The AVX512 loops generated by GoAT with LLVM can control pointers, unrolling, and reductions more precisely. With 128-element vectors, Go SIMD still takes nearly twice as long on average.
- The experimental API is suitable for validation, but mature assembly paths should not be removed yet.
simdstill requiresGOEXPERIMENT=simd. A safer path forward is to retain the existing assembly as a performance baseline while continuing to track compiler improvements to loops, bounds checks, and horizontal reductions.
Conclusion
Go 1.27's experimental simd package fills a longstanding gap in Go by providing a portable SIMD API. On the test machine, it does generate and execute AVX512 instructions and accelerates ordinary Go vector loops. This is a valuable feature for projects that need to balance performance, readability, and cross-architecture support.
Portability is not free, however. The current Go SIMD implementation still trails the AVX512 assembly generated by GoAT on long vectors and horizontal reductions. At this stage, the right conclusion is not that “Go SIMD has replaced GoAT-generated AVX512 assembly,” but that “Go SIMD has become a practical new option between ordinary Go and the high-performance assembly generated by GoAT.”
