Skip to content

[not for merge] amd64 benchmark of #3's generated code - #5

Closed
thesyncim wants to merge 6 commits into
masterfrom
bench-amd64
Closed

thesyncim wants to merge 6 commits into
masterfrom
bench-amd64

Conversation

@thesyncim

Copy link
Copy Markdown
Owner

Measurement harness only. Close it once the numbers are recorded.

It runs bench/ with master's generated code (bench/ops_simd.go from master) and with #3's, interleaved for 8 rounds. The job runs on three runners, because GitHub assigns different AMD EPYC generations (Zen 3 AVX2, Zen 4, Zen 5 AVX-512). GODEBUG=asyncpreemptoff=1 is set per BUGS.md #81847.

ubuntu-latest is now ubuntu-26.04, which has no qemu-user-static
package, so every run fails at the install step and skips the arm64
tsvc run, the golden check, fmt and lint. qemu-user ships the static
emulators there; link qemu-aarch64-static to it when the name is
missing, as the Makefile runs that name.
Go tip writes export data in iexport version 4, which encodes float
constants exactly. x/tools v0.51.0, and the v0.52.0 release, read only
up to version 3, so on tip go/packages fails for every package:

  cannot import "math" (unstable iexport format version 4, just rebuild
  compiler and std library), export data is newer version - update tool

and every script that runs loopvec fails in the tip job. x/tools master
reads version 4 (golang/tools@966056e2637c). Update to it until a release
includes it. The pinned toolchain is unaffected.
A read at a negative constant offset in a loop with a constant limit, such
as dst[i] = a[i-20] + 1 in a loop that stops before 16, was rewritten into
code that does not compile: the length check of the vector loop is
_ = a[16-21], a negative constant index. -split wrote that file, and the
toolexec wrapper fell back to the original package.

Such a loop panics as soon as it runs, as does one whose first index is
a negative constant (a[i-1] in a loop from 0). Both are now left alone
with their own reason, the way a constant limit of zero or less already
is. A read whose indexes can all be zero or more (a[i-1] from 1) is
still rewritten.

The vectorized set of std, gonum and github.com/thesyncim/goav1 does not
change.
…an prove

Every element-wise loop advanced one partial vector at a time: LoadPart
and StorePart on every iteration, each working out how many elements
remain. On arm64, which has no masked loads, that costs more than the
operation itself (about 50 instructions per 4 lanes of AccumRowInt32s),
and the bounds checks of slicing each operand at _i stay in the loop on
every target.

The loop now cuts every slice it touches to its window before it starts:
the elements from the first iteration at that slice's offset, as many as
the loop runs, with a capacity of as many, indexed from zero. _count is
the first window's length, so the compiler knows it is not negative.
Under conditions of the form _i <= _count-k*_lanes it then proves every
slice the loop takes of a window in bounds and drops all bounds and
length checks from the loop.

The main loop runs four copies of the body per iteration over
consecutive vectors, each copy all the statements in order, so the
chunks run in the same order as with one vector per iteration; each
window is cut to the iteration's four vectors, so every copy addresses
its vector at a constant offset. Whole vectors then run one at a time,
and one partial vector takes what is left. Integer reductions get the
four accumulators float reductions have: an integer vector add takes two
cycles on arm64, where they make SumInt32s three times as fast, and
integer folds wrap, so regrouping them gives the same result.

Because a partial load now only ever sees fewer elements than a vector
holds, the emulated Load*Part of golang/go#82001, which returns len(s),
returns the right count, and GODEBUG=simd=0 builds produce the scalar
result.
Every rewritten loop with two or more slices calls _loopvecOverlap for
each pair before it runs. The helper took the windows as ...int and
inlining it cost 114, over the compiler's budget of 80, so each call was
a real call: AddFloat32s made two before it loaded anything, which for
a short loop costs about as much as the loop.

The helper now takes the windows as four plain arguments, and a whole
slice is passed as 0 and its length. It inlines (cost 55). The
comparison is the same. A file written by an earlier loopvec, whose
helper takes the window as ...int, accepts the same calls, so a package
that mixes the two still compiles.
@thesyncim

Copy link
Copy Markdown
Owner Author

Closing: measurement harness only; its numbers are in #3 and #11.

@thesyncim thesyncim closed this Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant