[RFC] SSA-level SLP autovectorization pass for go compiler

545 views
Skip to first unread message

Arseny Samoylov

unread,
May 26, 2026, 3:24:08 PMMay 26
to golang-dev
Hi,
I've been working on SSA-level SLP (Superword Level Parallelism) autovectorization pass for go compiler and would appreciate feedback on whether this direction seems worth pursuing upstream.

Thanks to the current work on GOEXPERIMENT=simd, the current prototype swiftly operates on architecture-independent SSA before lowering. I primarily work and test it on arm64, but it also works on amd64 (I expect it to also work without a problem on wasm, but I hadn't checked it yet).

The implementation is intentionally conservative for now:
  • basic-block local
  • no shuffles/permutations
  • no reductions
  • no profitability/cost model
  • fixed-width (128-bit) vectors only
At the moment the prototype successfully vectorizes several simple arithmetic kernels on amd64 and arm64.

As a simple example, consider:
```
type Vec struct {
    E [4]float64
}
 
func (v Vec) Dot(v2 Vec) float64 {
    return v.E[0]*v2.E[0] + v.E[1]*v2.E[1] + v.E[2]*v2.E[2] + v.E[3]*v2.E[3]
}
```
is vectorized to:
Screenshot from 2026-05-26 22-10-30.png
Screenshot from 2026-05-26 22-12-22.png
  • 4 vector loads
  • 2 vector multiplies
  • scalar extraction for the final reduction (the reduction itself is not currently vectorized)
Even with these limitations, the prototype already identifies several hundred vectorization regions in real codebases.:

runtime tests
```
# In the GOROOOT/src
GOEXPERIMENT=slp go test -a -c -o a.out -gcflags=all=-d=ssa/slp/debug=2 runtime/ &> runtime_build.log
grep "Commit point" runtime_build.log | wc -l
225
```

etcd
```
GOEXPERIMENT=slp go build -a -o a.out -gcflags=all=-d=ssa/slp/debug=2 &> etcd_build.log
grep "Commit point" etcd_build.log | wc -l
738
```

cmd/comile itself
```
# In the GOROOT/src
GOEXPERIMENT=slp go build -a -o a.out -gcflags=all=-d=ssa/slp/debug=2 cmd/compile/ &> compile_build.log
grep "Commit point" compile_build.log | wc -l                                                              
3407
```

For the compile-time overhead I plan to evaluate the impact using the sweet/go-build benchmark suite.

Testing currently relies on test/codegen assembly and compiler output checks. Also there some test wrappers for semantic correctness validation. I also plan to add SSA-level tests.

Since this is a big change, I would appreciate feedback on whether this direction aligns with the current optimization direction of go compiler.

This work is also part of my academic research, so I intend to continue iterating on correctness, benchmarks, and design. And early feedback would help me align that work with the compiler's needs and priorities.

I would also be glad to prepare a more detailed design document if this direction appears promising.

My current plans for the first prototype:
  • Write more extensive tests (current ones a rather shallow)
  • Write synthetic benchmarks for the compiler test suite
  • Enable other vector width
  • Enable scalar-to-vector packing support
    • Currently, I have only supported vector-to-scalar unpacking.
  • Vectorize slice with non-constant index access. 
    • For example: load s[0], load s[1] currently can be vectorized, while load s[i], load s[i+1] - are not.
Future plans:
  • More aggressively vectorize Load/Store. 
    • Current implementation is very conservative with combining operations that affect (or affected by) memory state
  • Cross block vectorization
  • Vectorize reductions.
Here is CL with current implementation for reference.

Thanks,
Arseny Samoylov

Goran

unread,
May 26, 2026, 4:35:22 PMMay 26
to golang-dev
Thanks for working on this — SLP comes up a lot and it's good to have a concrete prototype to look at.

> no profitability/cost model

> already identifies several hundred vectorization regions

The region count isn't really the number that matters; what matters is whether it's a net win after regalloc and lowering. Without a cost model SLP regresses pretty easily — unaligned loads, the insert/extract around the scalar reduction, extra vector pressure — and a bb-local packer is exactly where you have the least info to tell. So the first thing I'd want to see is benchstat, not region counts: a bent run (or at least the go1/x benchmarks) on amd64 and arm64, GOEXPERIMENT=slp vs off, -count high enough that the p-values mean something, run under perflock. And please call out the worst per-benchmark regression, not just the geomean — a +N% geomean hiding a -20% on one hot loop is the case everyone worries about.

Separately, compile time. The compiler is very sensitive here and SLP seed-finding/tree-building isn't free. compilebench against a toolstash baseline (-compile $(toolstash -n compile)) would give the per-package delta; the tail matters more than the average, since the pass will do the most work on the packages with the biggest functions. sweet/go-build is fine for the macro number, but I'd want the compilebench breakdown too.

And I'd pull the cost model forward in the roadmap. Right now it reads as "later," but it's the part that decides whether the rest is worth doing, and it's hard to evaluate the pass without it.

Will take a look at the CL.

Arseny Samoylov

unread,
May 27, 2026, 11:46:31 AMMay 27
to golang-dev
(Not sure where my reply is, so I'll duplicate it just in case it got lost)


> The region count isn't really the number that matters

Agreed. I used it mainly to demonstrate that the current version is functional and successfully vectorizes code -- for example, the compiler is able to bootstrap itself with vectorization enabled.

> Without a cost model SLP regresses pretty easily

Agreed. For that reason, the current implementation is intentionally conservative: no shuffles, no scalar-to-vector transforms, and very limited vector-to-scalar transforms.
For reference, see the comments on the analysis stage (link) and validation stage (link).

> So the first thing I'd want to see is benchstat

The end goal is definitely performance improvement. However, for an initial implementation, I think there are several other important concerns to address first:

  • Correctness -- preserving existing program semantics.
  • Portability -- ensuring generated code remains sufficiently portable.
  • Maintainability -- keeping the implementation simple, comprehensible, and robust.

Ideally, I see this happening in two stages: first, establishing groundwork with a simple and reliable implementation that prioritizes stability and clarity; second, iterating toward performance improvements.

> Separately, compile time. The compiler is very sensitive here and SLP seed-finding/tree-building isn't free.

Agreed.

For the current prototype, compile-time overhead has not been a primary focus. In a few places, I intentionally prioritized development speed and simplicity over performance, using more brute-force approaches to validate the design and iterate faster. There are several clearly suboptimal areas that would need optimization before considering production use.


> compilebench against a toolstash baseline (-compile $(toolstash -n compile)) would give the per-package delta

That is interesting — I had not come across this before, and I should definitely try it.


> the tail matters more than the average, since the pass will do the most work on the packages with the biggest functions

Agreed -- I have already run into this (see my comment here).

> sweet/go-build is fine for the macro number

Interestingly, sweet/go-build currently shows almost no degradation. That seems better than expected, so I want to investigate it further.

> And I'd pull the cost model forward in the roadmap. Right now it reads as "later," but it's the part that decides whether the rest is worth doing, and it's hard to evaluate the pass without it.

I agree that the cost model is important. My thinking is that, with the current conservative approach, it may not yet be strictly necessary for an initial version to have a cost model, though it should likely move earlier in the roadmap.

Arseny Samoylov

unread,
May 27, 2026, 11:46:39 AMMay 27
to golang-dev
> The region count isn't really the number that matters;

Agreed. I used it mainly to demonstrate that the current version is functional and successfully vectorizes code -- for example, the compiler is able to bootstrap itself with vectorization enabled.

> Without a cost model SLP regresses pretty easily

Agreed. For the reason, the current implementation is intentionally conservative: no shuffles, no scalar-to-vec transforms, and very limited vec-to-scalar transforms.
For reference, see the comments on analysis stage (link) and validation stage (link).

>  So the first thing I'd want to see is benchstat

The end goal is definitely performance improvement. However, for an initial implementation, I think there are several other important concerns to address first:
  • Correctness -- preserving existing program semantics.
  • Portability -- ensuring generated code remains sufficiently portable
  • Maintainability -- keeping the implementation simple, comprehensible, and robust.
Ideally, I see this happening in two stages: first, establishing groundwork with a simple and reliable implementation that prioritizes stability and clarity; second, iterating toward performance improvements.

> Separately, compile time. The compiler is very sensitive here and SLP seed-finding/tree-building isn't free.

Agreed. 
For the current prototype, compile time overhead has not been a primary focus. In a few places, I intentionally prioritized development speed and simplicity over performance, using more brute-force approaches to validate the design and iterate faster. There are several clearly suboptimal places that would need optimization. 

> compilebench against a toolstash baseline (-compile $(toolstash -n compile)) would give the per-package delta;

That's interesting, I had not come across this before, so I definitely should try it.

> the tail matters more than the average, since the pass will do the most work on the packages with the biggest functions.

Agreed -- I have already run into this (see my comment here).

> sweet/go-build is fine for the macro number,

Interestingly, sweet/go-build currently shows almost no degradation. That seems better than expected, so I want to investigate it further.

> And I'd pull the cost model forward in the roadmap. Right now it reads as "later," but it's the part that decides whether the rest is worth doing, and it's hard to evaluate the pass without it.

I agree that the cost model is important. My thinking is that, with the current conservative approach, it may not yet be strictly necessary for an initial version to have a cost model, though I agree it should likely move earlier in the roadmap.
On Tuesday, 26 May 2026 at 23:35:22 UTC+3 Goran wrote:

Arseny Samoylov

unread,
May 27, 2026, 1:59:32 PMMay 27
to Goran, golang-dev
(Not sure where my reply is, and this is third time I am trying to send it, this time, using gmail instead of Web Groups interface)


> The region count isn't really the number that matters

Agreed. I used it mainly to demonstrate that the current version is functional and successfully vectorizes code -- for example, the compiler is able to bootstrap itself with vectorization enabled.

> Without a cost model SLP regresses pretty easily

Agreed. For that reason, the current implementation is intentionally conservative: no shuffles, no scalar-to-vector transforms, and very limited vector-to-scalar transforms.
For reference, see the comments on the analysis stage (link) and validation stage (link).


> So the first thing I'd want to see is benchstat

The end goal is definitely performance improvement. However, for an initial implementation, I think there are several other important concerns to address first:

    Correctness -- preserving existing program semantics.
    Portability -- ensuring generated code remains sufficiently portable.

    Maintainability -- keeping the implementation simple, comprehensible, and robust.

Ideally, I see this happening in two stages: first, establishing groundwork with a simple and reliable implementation that prioritizes stability and clarity; second, iterating toward performance improvements.
> Separately, compile time. The compiler is very sensitive here and SLP seed-finding/tree-building isn't free.

Agreed.

For the current prototype, compile-time overhead has not been a primary focus. In a few places, I intentionally prioritized development speed and simplicity over performance, using more brute-force approaches to validate the design and iterate faster. There are several clearly suboptimal areas that would need optimization before considering production use.


> compilebench against a toolstash baseline (-compile $(toolstash -n compile)) would give the per-package delta

That is interesting — I had not come across this before, and I should definitely try it.


> the tail matters more than the average, since the pass will do the most work on the packages with the biggest functions
Agreed -- I have already run into this (see my comment here).

> sweet/go-build is fine for the macro number

Interestingly, sweet/go-build currently shows almost no degradation. That seems better than expected, so I want to investigate it further.

> And I'd pull the cost model forward in the roadmap. Right now it reads as "later," but it's the part that decides whether the rest is worth doing, and it's hard to evaluate the pass without it.

I agree that the cost model is important. My thinking is that, with the current conservative approach, it may not yet be strictly necessary for an initial version to have a cost model, though it should likely move earlier in the roadmap.

--
You received this message because you are subscribed to a topic in the Google Groups "golang-dev" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/golang-dev/4YKD-sMVQUU/unsubscribe.
To unsubscribe from this group and all its topics, send an email to golang-dev+...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/golang-dev/967af120-6644-4f25-bfe6-359f1be9fb2en%40googlegroups.com.

Arseny Samoylov

unread,
Jun 4, 2026, 4:57:30 AMJun 4
to Goran, golang-dev
Hi,
Here is a small update with some preliminary sweet benchmark results.

Intel(R) Xeon(R) CPU E5-2690 v3 @ 2.60GHz
│ base.results │ slp.results │
│ sec/op │ sec/op vs base │
BleveIndexBatch100-4 5.867 ± 2% 5.842 ± 2% ~ (p=0.971 n=10)
ESBuildThreeJS-4 685.0m ± 2% 686.9m ± 1% ~ (p=0.853 n=10)
ESBuildRomeTS-4 179.6m ± 1% 180.2m ± 1% ~ (p=0.353 n=10)
EtcdPut-4 138.7m ± 37% 134.6m ± 19% ~ (p=0.315 n=10)
EtcdSTM-4 526.0m ± 4% 531.9m ± 2% ~ (p=0.579 n=10)
GoBuildKubelet-4 145.5 ± 1% 146.5 ± 0% +0.73% (p=0.000 n=10)
GoBuildKubeletLink-4 9.647 ± 0% 9.670 ± 0% ~ (p=0.739 n=10)
GoBuildIstioctl-4 117.4 ± 1% 118.3 ± 0% +0.80% (p=0.000 n=10)
GoBuildIstioctlLink-4 10.13 ± 0% 10.09 ± 0% -0.41% (p=0.019 n=10)
GoBuildFrontend-4 41.29 ± 1% 41.80 ± 0% +1.22% (p=0.000 n=10)
GoBuildFrontendLink-4 1.613 ± 2% 1.619 ± 1% ~ (p=0.971 n=10)
GoBuildTsgo-4 66.31 ± 1% 66.02 ± 1% ~ (p=0.052 n=10)
GoBuildTsgoLink-4 842.0m ± 1% 838.3m ± 1% ~ (p=0.089 n=10)
GopherLuaKNucleotide-4 27.62 ± 1% 27.63 ± 1% ~ (p=0.739 n=10)
MarkdownRenderXHTML-4 237.6m ± 1% 239.7m ± 1% +0.89% (p=0.001 n=10)
Tile38QueryLoad-4 532.3µ ± 0% 523.3µ ± 0% -1.69% (p=0.000 n=10)
geomean 2.391 2.390 -0.03%

HiSilicon Kunpeng-920
│ base.results │ slp.results │
│ sec/op │ sec/op vs base │
BleveIndexBatch100-4 7.467 ± 1% 7.513 ± 2% ~ (p=0.165 n=10)
ESBuildThreeJS-4 755.1m ± 1% 754.5m ± 1% ~ (p=0.912 n=10)
ESBuildRomeTS-4 195.5m ± 2% 194.0m ± 1% ~ (p=0.123 n=10)
EtcdPut-4 55.08m ± 1% 54.69m ± 1% ~ (p=0.165 n=10)
EtcdSTM-4 292.2m ± 1% 291.4m ± 1% ~ (p=0.436 n=10)
GoBuildKubelet-4 157.7 ± 0% 158.7 ± 0% +0.59% (p=0.000 n=10)
GoBuildKubeletLink-4 12.54 ± 2% 12.51 ± 1% ~ (p=0.247 n=10)
GoBuildIstioctl-4 123.8 ± 0% 124.0 ± 0% +0.17% (p=0.011 n=10)
GoBuildIstioctlLink-4 8.517 ± 1% 8.525 ± 0% ~ (p=0.529 n=10)
GoBuildFrontend-4 45.04 ± 0% 45.55 ± 1% +1.14% (p=0.000 n=10)
GoBuildFrontendLink-4 2.134 ± 1% 2.135 ± 1% ~ (p=0.739 n=10)
GoBuildTsgo-4 75.66 ± 0% 75.74 ± 1% ~ (p=0.796 n=10)
GoBuildTsgoLink-4 1.162 ± 1% 1.165 ± 1% ~ (p=0.631 n=10)
GopherLuaKNucleotide-4 33.30 ± 3% 32.97 ± 1% ~ (p=0.075 n=10)
MarkdownRenderXHTML-4 266.0m ± 0% 267.2m ± 0% +0.45% (p=0.001 n=10)
Tile38QueryLoad-4 607.9µ ± 0% 608.0µ ± 0% ~ (p=0.739 n=10)
geomean 2.450 2.450 +0.03%

On both x86 and Arm64, the current prototype shows essentially no measurable change in overall performance.

There is small degradation in the go-build benchmark, which is expected, as the current implementation has not yet been optimized for the compile-time overhead.

One result that stood out is Tile38QueryLoad on x86, which shows a small improvement.
I reran the benchmark separately to verify that the improvement persists, and the result appear stable across the runs:
(rerun of Tile38QueryLoad benchmark on x86)
│ base.results │ slp.results │
│ sec/op │ sec/op vs base │
Tile38QueryLoad-4 531.3µ ± 0% 523.4µ ± 0% -1.49% (p=0.000 n=10)

│ base.results │ slp.results │
│ p50-latency-sec │ p50-latency-sec vs base │
Tile38QueryLoad-4 251.3µ ± 0% 250.6µ ± 0% -0.25% (p=0.005 n=10)

│ base.results │ slp.results │
│ p90-latency-sec │ p90-latency-sec vs base │
Tile38QueryLoad-4 862.6µ ± 0% 847.7µ ± 0% -1.73% (p=0.000 n=10)

│ base.results │ slp.results │
│ p99-latency-sec │ p99-latency-sec vs base │
Tile38QueryLoad-4 4.998m ± 1% 4.803m ± 1% -3.89% (p=0.000 n=10)

│ base.results │ slp.results │
│ ops/s │ ops/s vs base │
Tile38QueryLoad-4 5.646k ± 0% 5.731k ± 0% +1.51% (p=0.000 n=10)

Here is how I ran the benchmarks:
taskset -c 44-47 ./sweet run -shell -work-dir `pwd`/tmp config.toml 2>&1 | tee sweet.log
# Separate rerun for tile38 on x86
taskset -c 44-47 ./sweet run -run=tile38 -shell -work-dir `pwd`/tmp config.toml 2>&1 | tee sweet.log


config.toml:
[[config]]
name = "base"
goroot = "/home/asamoylov/go-upstream"

[[config]]
name = "slp"
goroot = "/home/asamoylov/go-slp"
envbuild = ["GOFLAGS=-d=ssa/slp/debug=2"]

Notes:
  • In my environment cocroachdb benchmark fails for some reason (regardless of the SLP)
  • For the go-build benchmark, I had to use a separate compiler build with SLP enabled by default (thats why "slp" config has different goroot and doesn't need GOEXPERIMENT=slp)
  • GOFLAGS=-d=ssa/slp/debug=2 is not particular useful without `=all`, which it does not accept

Also, my previous reply sent through Groups web interface appears to have gotten stuck and was only published after I replied via Gmail.
As a result, the thread now contains three copies of the same reply.
The Gmail version (the third one) does not contain proper links, so please refer to one of the first two copies.

I hope benchmarks tables will be displayed correctly. Pasting them as plain text looks rather messy.

Arseny Samoylov

unread,
Jul 17, 2026, 10:11:51 AM (9 days ago) Jul 17
to Goran, golang-dev
Hi, here is the update on the SLP vectorization work.

Now that majority of SIMD-related work has been merged to master, I was able to rebase the SLP onto master (CL 791740)

The biggest change is that the SLP vectorized is no longer basic block local (see CL 800480).
I also spent some time reducing compile time overhead, which is now down to 1-2%.

The sweet benchmarks show a performance improvement on Tile38 benchmark, with no statistically significant regressions elsewhere:
```
│ base.stat │ slp.stat │
│ sec/op │ sec/op vs base │
BleveIndexBatch100-4 5.404 ± 1% 5.323 ± 2% ~ (p=0.089 n=10)
ESBuildThreeJS-4 684.0m ± 1% 680.2m ± 1% ~ (p=0.218 n=10)
ESBuildRomeTS-4 180.3m ± 2% 179.1m ± 1% ~ (p=0.393 n=10)
EtcdPut-4 196.2m ± 3% 197.1m ± 3% ~ (p=0.853 n=10)
EtcdSTM-4 619.3m ± 2% 632.6m ± 5% ~ (p=0.165 n=10)
GopherLuaKNucleotide-4 25.70 ± 0% 25.90 ± 1% ~ (p=0.165 n=10)
MarkdownRenderXHTML-4 231.8m ± 0% 232.2m ± 0% ~ (p=0.052 n=10)
Tile38QueryLoad-4 529.5µ ± 1% 522.5µ ± 0% -1.32% (p=0.000 n=10)
geomean 355.6m 355.3m -0.06%
          
```

The compile time overhead also looks reasonable:
```
│ base.results │ slp.results │
│ sec/op │ sec/op vs base │
GoBuildKubelet-4 146.8 ± 1% 149.9 ± 1% +2.07% (p=0.000 n=10)
GoBuildKubeletLink-4 9.634 ± 0% 9.668 ± 0% +0.35% (p=0.009 n=10)
GoBuildIstioctl-4 118.5 ± 1% 120.7 ± 1% +1.86% (p=0.000 n=10)
GoBuildIstioctlLink-4 10.13 ± 0% 10.14 ± 0% ~ (p=0.247 n=10)
GoBuildFrontend-4 42.18 ± 1% 43.28 ± 2% +2.60% (p=0.000 n=10)
GoBuildFrontendLink-4 1.627 ± 1% 1.632 ± 1% ~ (p=0.123 n=10)
GoBuildTsgo-4 67.10 ± 1% 67.86 ± 1% +1.12% (p=0.000 n=10)
GoBuildTsgoLink-4 848.1m ± 0% 850.2m ± 1% +0.25% (p=0.009 n=10)
geomean 16.89 17.08 +1.08%

```

The complete benchmark results (sweet, bent, compilbench for x86-64 and arm64) you can find here -- https://gist.github.com/ArsenySamoylov/7f45d4c4267e08b1ba8d9dd7ef0795b3.

If these results look acceptable, I believe the initial SLP vectorization implementation is ready for review.

Arseny Samoylov

unread,
Jul 17, 2026, 10:25:52 AM (9 days ago) Jul 17
to Goran, golang-dev
> The complete benchmark results (Sweet, Bent, and Compilebench for amd64 and arm64) are available here: https://gist.github.com/ArsenySamoylov/7f45d4c4267e08b1ba8d9dd7ef0795b3

Sorry, that was the wrong link -- it points to an older set of results that had large compile-time regression.

The correct link is: https://gist.github.com/ArsenySamoylov/cd6f7cfe95aaecbbbaa598d0f3e4b03b 

(Note: You can compare the performance results with the previous gist -- they should be unchanged. Only the compile-time results differ, since the changes only reduce the compile-time overhead.)
Reply all
Reply to author
Forward
0 new messages