Skip to content

simd: enable portable 256-bit variants on amd64 - #3049

Merged
cpunion merged 9 commits into
xgo-dev:mainfrom
zhouguangyuan0718:codex/portable-simd256-20261110
Nov 11, 2026
Merged

cpunion merged 9 commits into
xgo-dev:mainfrom
zhouguangyuan0718:codex/portable-simd256-20261110

Conversation

@zhouguangyuan0718

@zhouguangyuan0718 zhouguangyuan0718 commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Enable portable simd programs to select 256-bit variants on amd64. Midway specializes declarations and cross-package references, shares dispatch between calls and global initialization, and preserves deferred recover through every generated variant. Runtime selection follows Go's effective AVX/AVX2 and VPCLMULQDQ policy, capped at 256 bits; ARM64 and Wasm retain their existing widths.

Adds four-target FileCheck coverage for dispatch, vector operations and recovery frames, plus 27 upstream Go tests and 6 examples in test/std/simd. Native CI also runs 72 qualified ARM64 and 125 qualified AMD64 instruction tests directly from GOROOT. Unsupported and hardware-gated cases are documented in dev/simd/README.md; the two inner-product tests explicitly skip LLGo's unsupported 256-bit reduction helper while retaining their emulated/128-bit coverage.

Validation: focused compiler tests, the full native ARM64 SIMD script, Linux AMD64 O0/O2 emulation at 0/128/256 widths and CPU overrides, and WASI/GoJS O2 execution passed. Hosted CI is rerunning on the latest commit; local emulation is not native x86 qualification.

@codecov

codecov Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

LLGo WebAssembly build benchmarks

f2b9e94a5d1e | workflow run | long-term charts

WebAssembly output sizes
Example, profile and compiler Wasm module vs base Generated JS glue vs base
cprintf/j32-emscripten/LLGo 155941 B 0 B / +0.0% 88932 B 0 B / +0.0%
cprintf/j32-goos-js/LLGo 155024 B 0 B / +0.0% 72985 B 0 B / +0.0%
cprintf/j64-emscripten-memory64/LLGo 142573 B 0 B / +0.0% 92103 B 0 B / +0.0%
cprintf/w32-goos-wasip1/LLGo 15937 B 0 B / +0.0% 0 B 0 B / 0.0%
cprintf/w32-wasi/LLGo 15937 B 0 B / +0.0% 0 B 0 B / 0.0%
fmtprintf/j32-emscripten/LLGo 1633563 B 0 B / +0.0% 119298 B 0 B / +0.0%
fmtprintf/j32-goos-js/Go 2526852 B 0 B / +0.0% 0 B 0 B / 0.0%
fmtprintf/j32-goos-js/LLGo 1698640 B 0 B / +0.0% 89416 B 0 B / +0.0%
fmtprintf/j64-emscripten-memory64/LLGo 1514512 B 0 B / +0.0% 122540 B 0 B / +0.0%
fmtprintf/w32-goos-wasip1/Go 2500019 B 0 B / +0.0% 0 B 0 B / 0.0%
fmtprintf/w32-goos-wasip1/LLGo 1075060 B 0 B / +0.0% 0 B 0 B / 0.0%
fmtprintf/w32-wasi/LLGo 1076347 B 0 B / +0.0% 0 B 0 B / 0.0%
j32-emscripten/LLGo 155131 B 0 B / +0.0% 88932 B 0 B / +0.0%
j32-goos-js/Go 1895533 B 0 B / +0.0% 0 B 0 B / 0.0%
j32-goos-js/LLGo 154420 B 0 B / +0.0% 72985 B 0 B / +0.0%
j64-emscripten-memory64/LLGo 141774 B 0 B / +0.0% 92103 B 0 B / +0.0%
reflectcall/j32-emscripten/LLGo 1372777 B 0 B / +0.0% 95650 B 0 B / +0.0%
reflectcall/j32-goos-js/Go 2191221 B 0 B / +0.0% 0 B 0 B / 0.0%
reflectcall/j32-goos-js/LLGo 1385609 B 0 B / +0.0% 79699 B 0 B / +0.0%
reflectcall/j64-emscripten-memory64/LLGo 1257043 B 0 B / +0.0% 99450 B 0 B / +0.0%
reflectcall/w32-goos-wasip1/Go 2205707 B 0 B / +0.0% 0 B 0 B / 0.0%
reflectcall/w32-goos-wasip1/LLGo 1042340 B 0 B / +0.0% 0 B 0 B / 0.0%
reflectcall/w32-wasi/LLGo 1043496 B 0 B / +0.0% 0 B 0 B / 0.0%
w32-goos-wasip1/Go 1909947 B 0 B / +0.0% 0 B 0 B / 0.0%
w32-goos-wasip1/LLGo 18855 B 0 B / +0.0% 0 B 0 B / 0.0%
w32-wasi/LLGo 18855 B 0 B / +0.0% 0 B 0 B / 0.0%
LLGo WebAssembly build measurements
Example and profile Build vs base
j32-emscripten 9.308 s +140.8 ms / +1.5% (worse)
j32-goos-js 9.098 s +212.5 ms / +2.4% (worse)
j64-emscripten-memory64 7.586 s -909.2 ms / -10.7% (better)
reflectcall/w32-wasi 28.988 s -1.793 s / -5.8% (better)
w32-goos-wasip1 4.384 s -203 ms / -4.4% (better)
w32-wasi 3.994 s -167.9 ms / -4.0% (better)

Compared with 717fbe4429cb measured in the same runner job.

@zhouguangyuan0718
zhouguangyuan0718 marked this pull request as ready for review November 11, 2026 01:07

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SIMD256 portable Midway review

This PR extends LLGo's Midway specialization stage from two width variants (emulated + 128-bit) to three on amd64 (adding a 256-bit / AVX2 variant), threads the target arch through Rewrite, generalizes the scalar-entry dispatch to select by simd.VectorBitSize(), updates the runtime width policy (archMaxVectorSize, checkMidwayWidth), and teaches the recover dispatcher about the @simd256 sibling.

Overall this is a clean, well-tested change. arch is threaded consistently end-to-end (conf.Goarch → rewritePortableSIMD → midway.Rewrite → targetWidths), the dispatch ordering (emulated → width-equality → 128 fallback) is correct given that emulation reports a logical width of 128, the width-selection trust boundary stays memory-safe (any non-256 width falls through to the 128 baseline, and only generated variants can ever be selected), and the new tests cover the positive/negative cases well (per-target variant presence/absence, FMV-composed 256 variants, the partialWide recover negative case, and runtime rejection of unsupported widths). No correctness or safety issues were found.

The notes below are all minor / maintainability.

Stale documentation outside the diff

test/simd/README.md:188-190 (just below the hunk this PR edits) still says an external storage binding "needs an ABI shared by both concrete representations" and that embedded strings/byte slices "retain their contents in both variants." With the 256-bit variant added, amd64 now has three concrete representations (@simd0, @simd128, @simd256), and global() replicates embed-bearing globals across every entry in f.widths. Consider generalizing "both" → "all" / "every" while this section is being updated. (Flagged here rather than inline because those exact lines fall outside the PR diff.)

Comment thread internal/midway/rewrite.go Outdated
stmts = append(stmts, &ast.IfStmt{If: pos, Cond: cond,
Body: &ast.BlockStmt{Lbrace: pos, Rbrace: pos, List: invoke(width)}})
}
return append(stmts, invoke(128)...)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dispatchWidth hardcodes 128 twice — as the case 128: continue skip and as the trailing append(stmts, invoke(128)...) fallback. This implicitly assumes 128 is always present in f.widths and is always the correct final fallback. Both hold today (targetWidths always includes 128), but the literal is disconnected from targetWidths: if a future target ever dropped 128 or used a different baseline, this would emit a dispatcher calling a non-existent @simd128 variant with no compile-time signal. Consider a named const fallbackWidth = 128 (shared with targetWidths) or a comment documenting the invariant that targetWidths must always contain the fallback width.

return 0, 0
if archsimd.X86.AVX2() {
size = 256
if cpu.X86.HasVPCLMULQDQ {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The else-less VPCLMULQDQ logic relies on the preceding AVX branch having already set allFeatureSize = 128, so that when AVX2 is present but HasVPCLMULQDQ is false, allFeatureSize correctly stays 128 rather than 0. This is safe (AVX2 implies AVX, so the AVX branch always runs here), but the dependency is implicit. A one-line comment — e.g. "AVX2 implies AVX, so allFeatureSize is already 128" — would make this self-documenting and guard against a later reorder/split of the two branches.

Comment thread runtime/_patch/simd/selection_llgo.go Outdated
func checkMidwayWidth() {
if maxVectorSize != 128 {
panic("LLGo portable SIMD supports emulation and 128-bit vectors only")
if maxVectorSize != 128 && (runtime.GOARCH != "amd64" || maxVectorSize != 256) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The amd64/256 width policy is encoded here (accept 256 only on amd64) and independently in internal/midway/analysis.go targetWidths ({0,128,256} for amd64). These must stay in lockstep — a runtime-selectable width must have a corresponding generated variant — but nothing links the two sites. A brief cross-reference comment in one pointing to the other would reduce drift risk if, e.g., arm64 later gains 256-bit variants. (Optional: the condition maxVectorSize != 128 && (runtime.GOARCH != "amd64" || maxVectorSize != 256) is correct but reads as an inverted De Morgan form; a positive supported := maxVectorSize == 128 || (GOARCH == "amd64" && maxVectorSize == 256) would be easier to read.)

@cpunion
cpunion merged commit 96331c1 into xgo-dev:main Nov 11, 2026
164 of 167 checks passed
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.

2 participants