Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Bench

on:
push:
branches: [bench-amd64-row]

jobs:
bench:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: CPU
run: |
go install golang.org/x/perf/cmd/benchstat@latest
lscpu | grep -E 'Model name'
grep -o -w -E 'avx2|avx512f' /proc/cpuinfo | sort -u
- name: Build
run: |
go test -c -o scalar.test ./bench/
GOEXPERIMENT=simd go test -c -o stacked.test ./bench/
git show origin/offset-store:bench/ops_simd.go > bench/ops_simd.go
GOEXPERIMENT=simd go test -c -o partial.test ./bench/
git checkout bench/ops_simd.go
- name: Interleaved runs
run: |
for r in 1 2 3 4 5 6 7 8; do
for b in scalar partial stacked; do
GODEBUG=asyncpreemptoff=1 ./$b.test -test.run '^$' -test.bench 'StoreRow|AccumRow|AddFloat32s|AxpyFloat32s' -test.benchtime=20ms -test.count=1 >> $b.txt
done
done
- uses: actions/upload-artifact@v4
with:
name: bench
path: "*.txt"
35 changes: 21 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,21 @@ safety argument stays structural:
the loop runs at least once (`x` may be a nil pointer the scalar loop never reads), and
the overlap check and the vector loop use the locals; the fallback is the original loop.
Arrays, pointers to slices, maps, and the result of a call stay rejected.
- A store is `ident[i]` where `i` is *exactly* the loop variable (`index`). A load
may also be `ident[i+e]`, `ident[e+i]` or `ident[i-e]` for a loop-invariant integer
`e` (`indexAt`, `Load.Off`). A slice that is also stored may be read only at a
constant positive offset in a forward loop: iteration i then reads what a later
iteration writes, so the scalar loop sees the old value and the emitter loads it
before any store of the chunk (`statements` hoists those loads). A negative
offset is a recurrence (`ReasonCarriedDependence`); a variable distance or a
loop that counts down is `ReasonOffsetOfStored`. The length check is
against the last index read (`lastIndex`), and offset loads are not capped at the
limit. Every other store and load is `ident[i]` (`index`) — `dst[i+1]`, `dst[2*i]` are rejected outright, regardless of
whether they're actually dependent. That is what guarantees no two
iterations touch the same element.
- A store or a load is `ident[i]`, `ident[i+e]`, `ident[e+i]` or `ident[i-e]` where
`i` is *exactly* the loop variable and `e` a loop-invariant integer (`indexAt`,
`Store.Off`, `Load.Off`). Every store to one slice is at the same offset
(`ReasonStoreOffsets` otherwise), so each iteration writes its own element. A slice
that is also stored may be read at that offset, or at a constant positive distance
past it in a forward loop: iteration i then reads what a later iteration writes, so
the scalar loop sees the old value and the emitter loads it before any store of the
chunk (`statements` hoists those loads). A negative distance is a recurrence
(`ReasonCarriedDependence`); a variable distance or a loop that counts down is
`ReasonOffsetOfStored`. The length check is against the last index touched
(`lastIndex`); offset loads are not capped at the limit, and a store at an offset
is capped at the limit plus the offset (`destination`), so it writes nothing the
scalar loop does not. `dst[2*i]` and `dst[i][j]` are rejected outright, regardless
of whether they're actually dependent. That is what guarantees no two iterations
touch the same element.
- A non-slice operand becomes an `Invariant` only if it is a variable, a
constant, a conversion or negation of one, or a sum, difference, product or
bitwise combination of two (`invariant`). It is evaluated
Expand Down Expand Up @@ -159,8 +162,12 @@ reversal), no new dependence checking is needed, just a wider clause match.

`internal/rewrite/emit.go` never looks at the original statement's clauses —
it only uses `plan.Loop.Node.Pos()/.End()` to know what text span to replace,
and generates a **fixed forward** `for _i := 0; _i < bound; { ... }` template
driven entirely by the `Plan` (`Root`, `Dst`, `Bound`, ...).
and generates a **fixed forward** template driven entirely by the `Plan`
(`Root`, `Dst`, `Bound`, ...): whole vectors while
`_i <= bound-_lanes`, then one partial vector for what is left. Keep that
condition in that form: with the length checks before the loop, the compiler
proves every operand slice long enough and drops the per-iteration bounds
checks, which `_i+_lanes <= bound` does not let it do (about 2x on arm64).
`Induction.Step` records the direction but the emitter ignores it. This means:
extending lowering to accept a new *loop clause shape* that still produces
the same kind of `Loop` (same body semantics) needs **zero** emitter changes.
Expand Down
12 changes: 7 additions & 5 deletions BUGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ gotip, "1.27.1" is the release toolchain loopvec is tested against.
| [#81845](https://github.com/golang/go/issues/81845) calls left unresolved when inlined into a package initializer | closed as a duplicate of [#80689](https://github.com/golang/go/issues/80689), which is open | loops in package-level `var x = func() {...}()` | not rewritten (the generated initializer fails to link), see `package_initializer_no_rewrite.txt` and `toolexec_package_initializer.txt` |
| [#81846](https://github.com/golang/go/issues/81846) `Float32s.Neg` rebuilds its sign constant every iteration | open | speed of negation loops | none; the loop is correct, only slower than it should be |
| [#81847](https://github.com/golang/go/issues/81847) `GOEXPERIMENT=simd` alone slows async preemption on AVX-512 | open | any benchmark comparing a scalar and a simd build | benchmark with `GODEBUG=asyncpreemptoff=1` |
| [#82001](https://github.com/golang/go/issues/82001) emulated `Load{Float64,Int64,Uint64}sPart` return `len(s)` | open | generated loops under `GODEBUG=simd=0` | none; the loops use the count `Load*Part` returns, as the package documents |
| [#82001](https://github.com/golang/go/issues/82001) emulated `Load{Float64,Int64,Uint64}sPart` return `len(s)` | open | nothing loopvec generates | the loops use `Load*Part` only for what is left after the last whole vector, where `len(s)` is the count loaded |
| [#82002](https://github.com/golang/go/issues/82002) `Min` and `Max` on floats differ from Go's for NaN and -0 | open | float `min`/`max` | not rewritten (`float min and max differ from Go's ...`) |

## #80657: simd in a method
Expand Down Expand Up @@ -76,10 +76,12 @@ regressions.
## #82001: emulated partial loads

With `GODEBUG=simd=0` the emulated `Load{Float64,Int64,Uint64}sPart` return `len(s)`
instead of the number of elements loaded. loopvec's loops advance by that count (the loop
shape the `simd` package documents), so in that mode a copy of ten `float64`s produces
`[1 2 0 0 0 0 0 0 0 0]` instead of the input. The hardware-backed mode gives the right
answer.
instead of the number of elements loaded. A loop that advances by that count on every
iteration (the loop shape the `simd` package documents) skips elements in that mode: a
copy of ten `float64`s produces `[1 2 0 0 0 0 0 0 0 0]` instead of the input. loopvec's
loops load whole vectors while one fits and use `Load*Part` only for what is left, fewer
elements than a vector holds, where `len(s)` is the number loaded, so they are not
affected. The hardware-backed mode gives the right answer either way.

## #82002: float `Min` and `Max`

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ operations that lower to **AVX-512/AVX2/NEON** depending on the target CPU:
| `for i := range x { if x[i] < 0 { x[i] = 0 } }`, `if c { dst[i] = u } else if d { dst[i] = v } else { dst[i] = w }` | compare, then select (`IfElse`): ReLU, leaky ReLU, ReLU6, hard tanh, clamp, sign, step; also when the branches assign a temporary (`t := x[i]+3; if t < 0 { t = 0 } else if t > 6 { t = 6 }`: hard sigmoid, hard swish, cubic SiLU) |
| `for i := range v.Values { v.Values[i] *= a }`, `dst.f[i] += src.f[i]` | a slice reached through a field (`x.f`, `x.f.g`); the root must not be assigned in the loop, and the field is copied into a local once |
| `for i := range dst { dst[i] = a[i+1] - a[i] }`, `work[j] += a[row*lda+j]` | read at an offset from the index (stencil, row of a 2-D slice); a slice the loop also writes is read only at a constant positive offset (`a[i] = a[i+1] + b[i]`) |
| `b[i*ldb+j] = a[i*lda+j]`, `c[row+j] += alpha * x[j]`, `line[off+x] = v` | store at an offset from the index (row of a 2-D slice); every store to a slice is at the same offset, and the slice is read only there or at a constant positive distance past it |

Supported element types: `int8`, `int16`, `int32`, `int64`, `uint8`, `uint16`,
`uint32`, `uint64`, `float32`, `float64`.
Expand Down
15 changes: 15 additions & 0 deletions bench/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,21 @@ func BenchmarkAccumRowInt32s(b *testing.B) {
}
}

// StoreRowFloat32s adds alpha*x into one row of a two-dimensional slice.
func BenchmarkStoreRowFloat32s(b *testing.B) {
for _, n := range benchSizes {
b.Run(fmt.Sprintf("%d", n), func(b *testing.B) {
x := make([]float32, n)
c := make([]float32, 2*n)
setBytes(b, n, 4, 3)
b.ResetTimer()
for b.Loop() {
StoreRowFloat32s(c, x, 2, 1, n)
}
})
}
}

func BenchmarkMinUint16s(b *testing.B) {
for _, n := range benchSizes {
b.Run(fmt.Sprintf("%d", n), func(b *testing.B) {
Expand Down
6 changes: 6 additions & 0 deletions bench/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,3 +332,9 @@ func HardSigmoidFloat32s(dst, x []float32) {
dst[i] = t / 6
}
}

func StoreRowFloat32s(c, x []float32, alpha float32, row, ldc int) {
for j := range x {
c[row*ldc+j] += alpha * x[j]
}
}
Loading
Loading