mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
* gcasm: fuse the interleaved repack kernels' cross-chain dots on amd64; F16C scale conversion The ggml q8_0x4 repack gemv/gemm keeps two i32x4_add chains per block — all extend_low half dots in one, all extend_high in the other, over the same byte-source pairs — and combines them with the even/odd dword-select shuffles. The existing (low, high) pairing walk never fires on that shape, so amd64 ran it as literal SSE pair ops while arm64's a64CrossSdotRewrite turned it into raw SDOT chains. x64CrossDotRewrite is the amd64 twin: it matches the combine on the raw node forms and rebuilds it as a chain of synthetic raw block dots. Emission computes each 16-byte leaf in the 8-lane VPMADDWD domain across one ymm and collapses to the four-consecutive-byte grouping in-register (VPHADDD + VEXTRACTI128 + VPUNPCKLQDQ), then VPADDD-accumulates. i16 products of i8 lanes cannot overflow and VPMADDWD widens into i32, so every step is exact for ALL inputs — unlike the VPSIGNB/VPMADDUBSW route native kernels use, which wraps on -128 — and i32 wraparound addition is associative, so regrouping is bit-identical. Feature-body only; portable twins keep the SSE lowering via the existing dual-body dispatch. f16x4_cvt feature bodies now convert with mask + VPACKUSDW + VCVTPH2PS (F16C, which the HasAVX2 gate already requires) instead of the 15-op SSE2 bit trick — the x64 mirror of the arm64 xtn+fcvtl pair. Both sequences are all-VEX-128, so x64TreeVexClean treats them as clean and no ymm upper state is dirtied. Co-Authored-By: Claude Fable 5 <[email protected]> * gcasm: pair-domain wide raw-dot chains; skip the dead f16 mask after zero-extending loads The per-leaf raw-dot emission collapsed every 16-byte leaf through VPHADDD-ymm + VEXTRACTI128 — three lane-crossing shuffles per leaf, the exact shape the earlier chunk-widening experiment measured as a loss on Zen 3. The chain now accumulates in the 8-lane VPMADDWD pair domain across a full ymm (four ops per leaf, only the two unavoidable VPMOVSXBW crossings) and collapses ONCE per chain at the combine slot. The running value's ymm upper must survive between links, so the wide form is selected only for fully-VEX trees (region-exit VZEROUPPER, every other write VEX); anything else falls back to the self-contained per-leaf form. Exactness is unchanged: same integer products, i32 add reassociation only. f16x4_cvt over v128_load16x4_u drops its VPAND: the load zero-extends, so the lanes' upper halves are already empty and VPACKUSDW packs them exactly. Co-Authored-By: Claude Fable 5 <[email protected]> * codegen: fuse loops whose leftover interveners derive from bumped arguments or died into the tree The interleaved repack kernels' inner loops matched the countdown shape, window-fused their whole body, and carried their accumulators — then failed the final leftover-intervener invariance check on two statement kinds and fell back to the per-iteration window path, whose region boundaries (result parking, VZEROUPPER, argument re-materialization, scalar glue) dominate the prompt-processing profile on both arches: - a derived address ("q = p + off", one or two levels deep) whose argument slot can simply take p's own bump: the hoisted initial reads p's pre-loop value, which is exactly the slot's base, and the slot then advances in lockstep. The existing reassociation only covered bump targets that are not arguments themselves. - a value whose only readers were CONSUMED into the fused tree (the tree recomputes it through its own scalar chain — the f16 LUT index load): dead once the loop fuses, droppable. On the llama engine this upgrades the four hot repack gemv loops (plus one more) to fused loops; previously only one loop in the whole module fused. Co-Authored-By: Claude Fable 5 <[email protected]> * gcasm: hoist fused-loop window checks for floor loops and chain-addressed narrow loads The loop-level window-check hoist only covered pretest power-of-two loops and rng-windowed loads with plain scalar bases. The interleaved repack loops are do-while floor loops (unit step, signed threshold backedge) whose loads address through scalar-chain sums (bumpedBase + columnBase + const) — every load kept its 7-op per-iteration bounds/address dance, which the profile puts at the top of what remains after loop fusion. - Iteration-count form 2: a do-while ExitGT unit-step loop runs max(counter - thresh, 1) iterations, so iters-1 = counter - thresh - 1 clamped at zero — computable at entry. - Address resolution generalizes to affine chains: ArgScalar/ArgSum or a scalar_i32_add tree over them; the stride is the sum of the terms' bump deltas, and one check per (terms, end, stride) family covers every load in it. - Narrow loads join the rewrite through unchecked twins (v128_load32_splat_nc / load16x4_u_nc / load32_zero_nc / load64_zero_nc, x64-splicer-local), all emitting single folded indexed instructions; a full-width chain already terminating in R12 also drops its redundant self-move. Co-Authored-By: Claude Fable 5 <[email protected]> * gcasm: retarget the repack GEMM to native 4x4 tile kernels The engine's wasm-side q8_0x4 GEMM (exported as dbg_gemm_q8_0_4x4 exactly so the transpiler can find it) is shaped as four sequential gemv passes — the form the loop fusion is proven to claim — which gives up the tile's 4x weight reuse: prompt processing runs at token-generation speed per token. The whole feature body now becomes a native 4x4 tile kernel through the existing dual-body dispatch (portable twin = the transformed wasm body): - arm64: by-element SDOT — the activation block interleaves the four rows' 4-byte groups contiguously, so each 16-byte weight group is one load + four `sdot vAcc.4s, vW.16b, vA.4b[row]`, the native dotprod kernel's own shape. Gated on CPUDotProd. - amd64: AVX2 nest sign-extends each weight group once per ymm and runs four broadcast-activation VPMADDWD accumulations against it; a VNNI entry branch uses VPDPBUSD with the exact +128 bias identity, with the per-row byte sums accumulated by ONE extra VPDPBUSD per group (ones dot the activation block yields all four rows' group sums). Gated on HasAVX2 / gcasmHasAVX512VNNI. Integer arithmetic is exact; the per-block scale accumulation keeps the sequential form's per-row block order but fuses mul+add, so the retarget is fast-math-only, like the nrc2 tile kernel. Every span the loop nest will touch is bounds-checked against memSize at entry. Both kernels assemble and link on every host in the new gate tests; the numeric comparison against a scalar reference executes on matching hosts (the arm64 kernel passes natively on Apple Silicon; CI's amd64 runner executes the AVX2 arm). Co-Authored-By: Claude Fable 5 <[email protected]> * gcasm: repack GEMM kernels keep the wasm's own mul/add rounding; opt-out env The tile kernels' per-block scale accumulation switches from fused multiply-add to the exact multiply/multiply/add sequence the wasm gemm (and the fused GEMV loops) execute, so the batched path rounds identically to the wasm semantics it replaces — keeping the batched and single-row prompt paths as close as the engine's own contract. (The prompt batch-size divergence observed with qwen on arm64 reproduces byte-for-byte on the PREVIOUS engine as well: it is the known batched-vs-incremental near-tie flip, not a kernel effect.) WASM2GO_NO_REPACK_GEMM=1 disables the retarget for A/B bisection, mirroring the other WASM2GO_* iteration toggles. Co-Authored-By: Claude Fable 5 <[email protected]> * lower: pin the exported repack GEMM out-of-line so its retarget stays live wasm2go's own function inliner (leaf, <=4096 bytes) was splicing the exported dbg_gemm_q8_0_4x4 body into its caller (forward_mul_mat_one_chunk), so the standalone body the gcasm backend retargets to the native 4x4 tile kernel was dead — the tile never ran, and pp went through the inlined per-pass GEMV shape instead. (Binaryen was not involved: -Oz leaves the wasm-level calls intact.) The inliner now pins any dbg_gemm_-exported function out-of-line regardless of size. GEMV is deliberately left inlinable: it runs at nrows=1 in the decode hot path, where per-call overhead outweighs the tile — pinning it out-of-line measured a tg regression, so only the batched GEMM is pinned. Co-Authored-By: Claude Fable 5 <[email protected]> * gcasm: deduplicate shared emission shapes in the amd64 repack work Pure refactor — emitted assembly is byte-identical (verified by dumping every touched emission path before and after): - Promote the even/odd dword-select shuffle classifier out of a64CrossSdotRewrite into package-level crossSelKind and drop the verbatim x64 copy; the patterns are wasm-level constants shared by both arch rewrites. - Extract x64PairFold for the VPHADDD/VEXTRACTI128/VPUNPCKLQDQ pair-domain collapse emitted from three sites. - Merge the four unchecked narrow-load cases in x64FusedLoad into one table-shaped case (VMOVSS/VMOVQ plus the op's own widen). - Replace the hand-rolled insertion sort in x64CrossDotRewrite with sort.Slice over the leaf records used directly. - Share the repack GEMM ABI0 argument frame layout between the a64 and x64 emitters (repackGemmArgs). - Build the per-arch trapstub once in writeGemmRunTree instead of writing the amd64 kernel file twice. Co-Authored-By: Claude Opus 4.8 <[email protected]> * gcasm: make the repack GEMM opt-out a config option, not an env var Replace the WASM2GO_NO_REPACK_GEMM environment check with DisableRepackGemm threaded through the option structs — the project's configuration pattern (codegen.Options -> gcasm.Config, CLI flag -no-repack-gemm). The retarget-target scan moves into repackGemmExport with a unit test over the gate combinations. Co-Authored-By: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: Claude Fable 5 <[email protected]>
1734 lines
60 KiB
Go
1734 lines
60 KiB
Go
package gcasm
|
||
|
||
// Fused-region splicing, amd64 — the SSE twin of
|
||
// simdsplice_fuse_a64.go. Same synthesis: table bodies verbatim
|
||
// (operands in X0..X2, result in X0; scratch reaches up to X7 in the
|
||
// wider bodies), operand builds and result moves replaced by MOVOU
|
||
// register copies, parked values in X4..X14 (X15 is the ABI zero
|
||
// register) — any park still live when a canned body is pasted is
|
||
// first relocated above that body's measured reach. Fused arguments
|
||
// ride AX,BX,CX,DI,SI,R8,R9 (fusedMaxIntRegs=7 exists exactly so the
|
||
// load scratch R10–R12 stays free); m is saved to R13 because table
|
||
// cores stage scalars through AX.
|
||
|
||
import (
|
||
"fmt"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/goccy/wasm2go/internal/simdfuse"
|
||
)
|
||
|
||
var x64FusedArgRegs = []string{"AX", "BX", "CX", "DI", "SI", "R8", "R9", "R10", "R11"}
|
||
|
||
// x64XRegRe matches vector-register operands in canned op lines; the
|
||
// fused emitter uses the highest mention to size an op's scratch reach.
|
||
var x64XRegRe = regexp.MustCompile(`\bX(\d+)\b`)
|
||
|
||
// x64FusedResultRegs is the full ABIInternal integer RESULT sequence:
|
||
// results run past the argument cap because the epilogue is the last
|
||
// thing in the splice, when every scratch register is dead.
|
||
var x64FusedResultRegs = []string{"AX", "BX", "CX", "DI", "SI", "R8", "R9", "R10", "R11"}
|
||
|
||
// x64FusedIntArg names integer-argument slot pos: a register, or the
|
||
// ABIInternal stack-sequence operand. Overflow slots here are always
|
||
// 8-byte pair halves — scalars, the counter and stride deltas are
|
||
// register-resident by the codegen caps — so the sequence is uniform.
|
||
// maxOut is the transform's outgoing-area shift (stack operands sit
|
||
// above it after the frame rewrite).
|
||
func x64FusedIntArg(pos, maxOut int) string {
|
||
if pos < len(x64FusedArgRegs) {
|
||
return x64FusedArgRegs[pos]
|
||
}
|
||
return fmt.Sprintf("%d(SP)", 8*(pos-len(x64FusedArgRegs))+maxOut)
|
||
}
|
||
|
||
// x64FusedResultLoc names integer-result slot pos (sizes sz in bytes,
|
||
// natural alignment). The stack results area follows the stack
|
||
// arguments, pointer-aligned.
|
||
func x64FusedResultLocs(sizes []int, argStackBytes, maxOut int) []string {
|
||
locs := make([]string, len(sizes))
|
||
seq := (argStackBytes + 7) &^ 7
|
||
intN := 0
|
||
for i, sz := range sizes {
|
||
if intN < len(x64FusedResultRegs) {
|
||
locs[i] = x64FusedResultRegs[intN]
|
||
intN++
|
||
continue
|
||
}
|
||
al := sz
|
||
seq = (seq + al - 1) &^ (al - 1)
|
||
locs[i] = fmt.Sprintf("%d(SP)", seq+maxOut)
|
||
seq += sz
|
||
}
|
||
return locs
|
||
}
|
||
|
||
var (
|
||
x64BuildLoRe = regexp.MustCompile(`^MOVQ ([A-Z0-9]+), X(\d+)$`)
|
||
x64BuildHiRe = regexp.MustCompile(`^PINSRQ \$1, ([A-Z0-9]+), X(\d+)$`)
|
||
x64StagingRe = regexp.MustCompile(`^MOVQ [A-Z][A-Z0-9]*, AX$`)
|
||
x64OutHiRe = regexp.MustCompile(`^PEXTRQ \$1, X0, BX$`)
|
||
x64OutLoRe = regexp.MustCompile(`^MOVQ X0, AX$`)
|
||
x64CoreGprRe = regexp.MustCompile(`\b(AX|BX|CX|DX|DI|SI|BP|R\d+)\b`)
|
||
x64ConstRefRe = regexp.MustCompile(`·simdConst`)
|
||
)
|
||
|
||
func x64VecCopy(b *strings.Builder, dst, src int) {
|
||
if dst != src {
|
||
fmt.Fprintf(b, "\tVMOVDQU X%d, X%d\n", src, dst)
|
||
}
|
||
}
|
||
|
||
// x64ClassifyPair splits an amd64 pair-table body like the arm64
|
||
// twin, arity-driven: exactly nV128 operand builds (MOVQ+PINSRQ
|
||
// pairs), an optional scalar staging move into AX (dropped — the
|
||
// fused emitter stages scalars itself), core, with the trailing
|
||
// result moves dropped.
|
||
func x64ClassifyPair(lines []string, nV128 int) (opTargets []int, core []string, err error) {
|
||
i := 0
|
||
for ; i < len(lines) && len(opTargets) < nV128; i++ {
|
||
l := strings.TrimSpace(lines[i])
|
||
m := x64BuildLoRe.FindStringSubmatch(l)
|
||
if m == nil || x64ConstRefRe.MatchString(l) {
|
||
return nil, nil, fmt.Errorf("fuse classify: expected %d operand builds, found %d before %q", nV128, len(opTargets), l)
|
||
}
|
||
xk := pfNum(m[2])
|
||
if xk < 0 {
|
||
return nil, nil, fmt.Errorf("fuse classify: bad register capture in %q", l)
|
||
}
|
||
if i+1 >= len(lines) || x64BuildHiRe.FindStringSubmatch(strings.TrimSpace(lines[i+1])) == nil {
|
||
return nil, nil, fmt.Errorf("fuse classify: build-lo without PINSRQ: %q", l)
|
||
}
|
||
opTargets = append(opTargets, xk)
|
||
i++
|
||
}
|
||
if i < len(lines) && x64StagingRe.MatchString(strings.TrimSpace(lines[i])) {
|
||
i++ // the fused emitter stages the scalar into AX itself
|
||
}
|
||
for ; i < len(lines); i++ {
|
||
l := strings.TrimSpace(lines[i])
|
||
if x64OutHiRe.MatchString(l) || x64OutLoRe.MatchString(l) {
|
||
continue
|
||
}
|
||
// Live fused arguments sit in BX..R9; only AX (scalar staging)
|
||
// may appear in a core. See the arm64 twin for why this is a
|
||
// hard error rather than a silent clobber.
|
||
for _, g := range x64CoreGprRe.FindAllStringSubmatch(l, -1) {
|
||
if g[1] != "AX" {
|
||
return nil, nil, fmt.Errorf("fuse classify: core line touches %s: %q", g[1], l)
|
||
}
|
||
}
|
||
core = append(core, l)
|
||
}
|
||
return opTargets, core, nil
|
||
}
|
||
|
||
// x64SpliceFused synthesizes the inline amd64 body of a fused-region
|
||
// call. Results in (AX, BX).
|
||
func x64SpliceFused(b *strings.Builder, tree *simdfuse.Tree, pool *ConstPool, offs *ModuleOffsets, portable bool, maxOut int) (bool, bool, error) {
|
||
tree, usedAVX := x64Dot8Rewrite(tree, portable)
|
||
// Scalars (addresses the body indexes with) must be
|
||
// register-resident here; only pair halves may ride the stack
|
||
// sequence. A wider window keeps the helper CALL.
|
||
if 1+tree.NumScalars > len(x64FusedArgRegs) {
|
||
return false, false, fmt.Errorf("fused splice %s: %w: scalars past the register file", tree.Name, errFusedCapacity)
|
||
}
|
||
if err := x64FusedProlog(b, tree, offs); err != nil {
|
||
return false, false, err
|
||
}
|
||
hoffs := x64HoistMem(b, offs, tree, 1+tree.NumScalars+2*tree.NumPairs)
|
||
loc, needsTrap, err := x64SpliceFusedCore(b, tree, pool, hoffs, nil, nil, 0, true, portable, maxOut)
|
||
if err != nil {
|
||
return false, false, err
|
||
}
|
||
for k, r := range tree.RootList() {
|
||
src := 0
|
||
if !loc[r].chained {
|
||
src = loc[r].pool
|
||
}
|
||
fmt.Fprintf(b, "\tMOVQ X%d, %s\n", src, x64FusedResultRegs[2*k])
|
||
fmt.Fprintf(b, "\tPEXTRQ $1, X%d, %s\n", src, x64FusedResultRegs[2*k+1])
|
||
}
|
||
if usedAVX && x64TreeVexClean(tree) {
|
||
// Fully VEX region: one deferred upper clear at the exit.
|
||
b.WriteString("\tVZEROUPPER\n")
|
||
}
|
||
return true, needsTrap, nil
|
||
}
|
||
|
||
// x64FusedProlog emits the shared prologue: m saved to R13 and the
|
||
// leaf float arguments packed into X14 lanes.
|
||
func x64FusedProlog(b *strings.Builder, tree *simdfuse.Tree, offs *ModuleOffsets) error {
|
||
if tree.NeedsMem && offs == nil {
|
||
return fmt.Errorf("fused splice %s: no Module offsets", tree.Name)
|
||
}
|
||
b.WriteString("\tMOVQ AX, R13\n")
|
||
if tree.NumFloats > 0 {
|
||
b.WriteString("\tVMOVDQU X0, X14\n")
|
||
for i := 1; i < tree.NumFloats; i++ {
|
||
fmt.Fprintf(b, "\tVINSERTPS $%d, X%d, X14, X14\n", i<<4, i)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// x64SpliceFusedCore is the parameterized node-body emitter; see the
|
||
// arm64 twin for the carried/reserve/extraIntArgs contract.
|
||
func x64SpliceFusedCore(b *strings.Builder, tree *simdfuse.Tree, pool *ConstPool, offs *ModuleOffsets,
|
||
carried map[int]int, reserve []int, extraIntArgs int, skipProlog bool, portable bool, maxOut int) ([]fusedLoc, bool, error) {
|
||
if !skipProlog {
|
||
if err := x64FusedProlog(b, tree, offs); err != nil {
|
||
return nil, false, err
|
||
}
|
||
} else if tree.NeedsMem && offs == nil {
|
||
return nil, false, fmt.Errorf("fused splice %s: no Module offsets", tree.Name)
|
||
}
|
||
uses := make([]int, len(tree.Nodes))
|
||
for _, n := range tree.Nodes {
|
||
for _, a := range n.Args {
|
||
if a.Kind == simdfuse.ArgNode {
|
||
uses[a.Index]++
|
||
}
|
||
}
|
||
}
|
||
// Each root is one extra consumer (the result epilogue); see the
|
||
// arm64 twin.
|
||
roots := tree.RootList()
|
||
for _, r := range roots {
|
||
uses[r]++
|
||
}
|
||
|
||
scalarReg := func(a simdfuse.Arg) string {
|
||
return x64FusedArgRegs[1+a.Index]
|
||
}
|
||
pairRegs := func(a simdfuse.Arg) (string, string) {
|
||
base := 1 + tree.NumScalars + extraIntArgs + 2*a.Index
|
||
return x64FusedIntArg(base, maxOut), x64FusedIntArg(base+1, maxOut)
|
||
}
|
||
|
||
loc := make([]fusedLoc, len(tree.Nodes))
|
||
var freePool []int
|
||
poolTop := 14
|
||
if tree.NumFloats > 0 {
|
||
poolTop = 13 // X14 holds the packed float lanes
|
||
}
|
||
reservedX := map[int]bool{}
|
||
for _, r := range reserve {
|
||
reservedX[r] = true
|
||
}
|
||
// Canned pair-table op bodies (pasted verbatim by the ClassV128
|
||
// default arm below) use X0–X7 as inputs and scratch, so a pool
|
||
// value parked there is silently clobbered by the next canned op
|
||
// — visible as corrupt vectors whenever a window keeps a value
|
||
// live across a shuffle. The arm64 twin pools V16–V30 above its
|
||
// canned v0–v4 scratch; amd64 has no such headroom, so X4–X7 stay
|
||
// in the pool for capacity and any value still live when a canned
|
||
// op is pasted is relocated to X8+ first (see relocateForCanned).
|
||
for x := poolTop; x >= 4; x-- {
|
||
if !reservedX[x] {
|
||
freePool = append(freePool, x)
|
||
}
|
||
}
|
||
// Loop-carried accumulators are live across every canned op and
|
||
// are never relocated; they reserve downward from the pool top, so
|
||
// reaching the canned-scratch range means the window is over
|
||
// budget, not silently corrupt.
|
||
for _, r := range reserve {
|
||
if r < 8 {
|
||
return nil, false, fmt.Errorf("fused splice %s: %w: carried value in canned-scratch range", tree.Name, errFusedCapacity)
|
||
}
|
||
}
|
||
needsTrap := false
|
||
vexClean := !portable && x64TreeVexClean(tree)
|
||
|
||
chains := newX64ScalarChain(b, tree, scalarReg, offs, uses)
|
||
|
||
// memAddrReg: see the arm64 twin — hands a chain-computed address
|
||
// terminal to the memory emitters.
|
||
memAddrReg := func(n simdfuse.Node) (string, error) {
|
||
if len(n.Args) == 0 || n.Args[0].Kind != simdfuse.ArgNode {
|
||
return "", nil
|
||
}
|
||
return chains.takeAddrSource(n.Args[0].Index)
|
||
}
|
||
|
||
for i, n := range tree.Nodes {
|
||
if n.Op == a64OpElided {
|
||
// Absorbed into a synthetic AVX2 block dot: nothing to emit,
|
||
// nothing referenced, nothing to free.
|
||
loc[i] = fusedLoc{chained: true}
|
||
continue
|
||
}
|
||
if n.Class() != simdfuse.ClassV128 {
|
||
// Inline scalar-chain node (see the arm64 twin).
|
||
if err := chains.emitNode(i, &tree.Nodes[i]); err != nil {
|
||
return nil, false, err
|
||
}
|
||
loc[i] = fusedLoc{chained: true}
|
||
continue
|
||
}
|
||
// Placement first; see the arm64 twin.
|
||
for _, a := range n.Args {
|
||
if a.Kind != simdfuse.ArgNode {
|
||
continue
|
||
}
|
||
uses[a.Index]--
|
||
if uses[a.Index] == 0 && !loc[a.Index].chained {
|
||
freePool = append(freePool, loc[a.Index].pool)
|
||
}
|
||
}
|
||
if simdfuse.IsStore(n.Op) {
|
||
areg, aerr := memAddrReg(n)
|
||
if aerr != nil {
|
||
return nil, false, aerr
|
||
}
|
||
if err := x64FusedStore(b, n, scalarReg, pairRegs, offs, pool, tree.Addr64, dst0Src(loc, n), areg); err != nil {
|
||
return nil, false, err
|
||
}
|
||
needsTrap = true
|
||
loc[i] = fusedLoc{chained: true}
|
||
continue
|
||
}
|
||
willChain := i == len(tree.Nodes)-1 || (uses[i] == 1 && a64ChainsIntoNext(tree, i))
|
||
dst := 0
|
||
if !willChain {
|
||
if len(freePool) == 0 {
|
||
return nil, false, fmt.Errorf("fused splice %s: %w: vector pool exhausted", tree.Name, errFusedCapacity)
|
||
}
|
||
dst = freePool[len(freePool)-1]
|
||
freePool = freePool[:len(freePool)-1]
|
||
}
|
||
if n.Op == "v128_load32_lane" {
|
||
if src := dst0Src(loc, n); src < 0 {
|
||
lo, hi := pairRegs(n.Args[3])
|
||
fmt.Fprintf(b, "\tMOVQ %s, X%d\n", lo, dst)
|
||
fmt.Fprintf(b, "\tPINSRQ $1, %s, X%d\n", hi, dst)
|
||
} else {
|
||
x64VecCopy(b, dst, src)
|
||
}
|
||
if n.Args[2].Kind != simdfuse.ArgConst {
|
||
return nil, false, fmt.Errorf("fused splice %s: non-constant lane", tree.Name)
|
||
}
|
||
areg, aerr := memAddrReg(n)
|
||
if aerr != nil {
|
||
return nil, false, aerr
|
||
}
|
||
mb, lerr := x64FusedLaneAddr(b, n, scalarReg, offs, tree.Addr64, areg)
|
||
if lerr != nil {
|
||
return nil, false, lerr
|
||
}
|
||
fmt.Fprintf(b, "\tPINSRD $%d, (%s)(R12*1), X%d\n", uint32(n.Args[2].Const)&3, mb, dst)
|
||
needsTrap = true
|
||
loc[i] = fusedLoc{chained: willChain, pool: dst}
|
||
continue
|
||
}
|
||
if _, isMem := fusedMemOpsA64[n.Op]; isMem {
|
||
areg, aerr := memAddrReg(n)
|
||
if aerr != nil {
|
||
return nil, false, aerr
|
||
}
|
||
if err := x64FusedLoad(b, n, scalarReg, offs, dst, tree.Addr64, areg); err != nil {
|
||
return nil, false, err
|
||
}
|
||
needsTrap = true
|
||
loc[i] = fusedLoc{chained: willChain, pool: dst}
|
||
continue
|
||
} else if n.Op == "f16x4_cvt" {
|
||
// Four f16 values in the low 16 bits of each i32 lane -> f32
|
||
// lanes, computed with the SSE2 bit trick (no F16C at the
|
||
// x86-64-v2 baseline): f = as_float(mag<<13) * 0x1p+112 is
|
||
// exact for normals and denormals; inf/NaN lanes blend in
|
||
// sh|0x70000000 (exponent forced to 0xFF, payload<<13
|
||
// preserved); the sign rides (h<<16)&0x80000000. Bit-exact
|
||
// against the verified conversion table.
|
||
var src int
|
||
switch a := n.Args[0]; a.Kind {
|
||
case simdfuse.ArgNode:
|
||
if loc[a.Index].chained {
|
||
src = 0
|
||
} else {
|
||
src = loc[a.Index].pool
|
||
}
|
||
case simdfuse.ArgPairIn:
|
||
// A loop-carried pair lives in its reserved register
|
||
// across iterations (the tail writes it there); the
|
||
// incoming argument registers only hold the first
|
||
// iteration's value. Read the reserved register so
|
||
// later iterations see the accumulated value.
|
||
if cr, isCarried := carried[a.Index]; isCarried {
|
||
src = cr
|
||
} else {
|
||
lo, hi := pairRegs(a)
|
||
fmt.Fprintf(b, "\tMOVQ %s, X1\n", lo)
|
||
fmt.Fprintf(b, "\tPINSRQ $1, %s, X1\n", hi)
|
||
src = 1
|
||
}
|
||
default:
|
||
return nil, false, fmt.Errorf("fused splice %s: malformed %s args", tree.Name, n.Op)
|
||
}
|
||
if pool == nil {
|
||
return nil, false, fmt.Errorf("fused splice %s: %s needs a const pool", tree.Name, n.Op)
|
||
}
|
||
c4 := func(w uint32) string {
|
||
blob := make([]byte, 16)
|
||
for i := 0; i < 4; i++ {
|
||
blob[i*4] = byte(w)
|
||
blob[i*4+1] = byte(w >> 8)
|
||
blob[i*4+2] = byte(w >> 16)
|
||
blob[i*4+3] = byte(w >> 24)
|
||
}
|
||
return pool.addBlob(blob)
|
||
}
|
||
if !portable {
|
||
// Feature body: hardware conversion, the x64 mirror of
|
||
// the arm64 xtn+fcvtl pair. Mask each lane to its low
|
||
// 16 bits (the conversion reads nothing else), pack the
|
||
// four u32 lanes into four contiguous u16s (exact — the
|
||
// masked values fit unsaturated), and convert with
|
||
// VCVTPH2PS (F16C, which the HasAVX2 gate requires).
|
||
// All VEX-128, so no ymm upper state is dirtied. The
|
||
// marker routes non-AVX2 hosts to the portable twin.
|
||
// A zero-extending 16x4 load already has empty upper
|
||
// lane halves, so its mask is dead weight.
|
||
if a := n.Args[0]; a.Kind == simdfuse.ArgNode && tree.Nodes[a.Index].Op == "v128_load16x4_u" {
|
||
fmt.Fprintf(b, "\tVPACKUSDW X%d, X%d, X1\n", src, src)
|
||
} else {
|
||
fmt.Fprintf(b, "\tVPAND ·%s(SB), X%d, X1\n", c4(0xFFFF), src)
|
||
b.WriteString("\tVPACKUSDW X1, X1, X1\n")
|
||
}
|
||
fmt.Fprintf(b, "\tVCVTPH2PS X1, X%d\n", dst)
|
||
b.WriteString("\t// avx2 dot\n")
|
||
loc[i] = fusedLoc{chained: willChain, pool: dst}
|
||
continue
|
||
}
|
||
fmt.Fprintf(b, "\tMOVOU X%d, X3\n", src)
|
||
b.WriteString("\tPSLLL $16, X3\n")
|
||
fmt.Fprintf(b, "\tPAND \u00b7%s(SB), X3\n", c4(0x80000000))
|
||
fmt.Fprintf(b, "\tMOVOU X%d, X1\n", src)
|
||
fmt.Fprintf(b, "\tPAND \u00b7%s(SB), X1\n", c4(0x7FFF))
|
||
b.WriteString("\tMOVOU X1, X2\n")
|
||
fmt.Fprintf(b, "\tPCMPGTL \u00b7%s(SB), X2\n", c4(0x7BFF))
|
||
b.WriteString("\tPSLLL $13, X1\n")
|
||
fmt.Fprintf(b, "\tMOVOU X1, X%d\n", dst)
|
||
fmt.Fprintf(b, "\tMULPS \u00b7%s(SB), X%d\n", c4(0x77800000), dst)
|
||
fmt.Fprintf(b, "\tPOR \u00b7%s(SB), X1\n", c4(0x70000000))
|
||
b.WriteString("\tPAND X2, X1\n")
|
||
fmt.Fprintf(b, "\tPANDN X%d, X2\n", dst)
|
||
b.WriteString("\tPOR X1, X2\n")
|
||
b.WriteString("\tPOR X3, X2\n")
|
||
fmt.Fprintf(b, "\tMOVOU X2, X%d\n", dst)
|
||
loc[i] = fusedLoc{chained: willChain, pool: dst}
|
||
continue
|
||
} else if n.Op == "f32x4_splat" {
|
||
// Broadcast a lane of the packed float register: a leaf
|
||
// float argument's lane, or a computed scalar terminal's.
|
||
switch {
|
||
case len(n.Args) == 1 && n.Args[0].Kind == simdfuse.ArgFloat:
|
||
fmt.Fprintf(b, "\tVPSHUFD $%d, X14, X%d\n", n.Args[0].Index*0x55, dst)
|
||
case len(n.Args) == 1 && n.Args[0].Kind == simdfuse.ArgNode && tree.Nodes[n.Args[0].Index].Class() == simdfuse.ClassF32:
|
||
f, err := chains.takeSplatSource(n.Args[0].Index)
|
||
if err != nil {
|
||
return nil, false, err
|
||
}
|
||
fmt.Fprintf(b, "\tVPSHUFD $0, X%d, X%d\n", f, dst)
|
||
default:
|
||
return nil, false, fmt.Errorf("fused splice %s: malformed f32x4_splat args", tree.Name)
|
||
}
|
||
loc[i] = fusedLoc{chained: willChain, pool: dst}
|
||
continue
|
||
} else if n.Op == x64OpBlockDot {
|
||
// AVX2 8-bit block dot (see x64Dot8Rewrite): sign-extend both
|
||
// 16-byte byte sources to i16 across a ymm, VPMADDWD into 8
|
||
// i32 lanes (0..3 = low-half dot, 4..7 = high-half), then fold
|
||
// the high 128 onto the low with VPADDD to yield the four i32
|
||
// sums. Y2/Y3 (X2/X3) are scratch the vector pool never
|
||
// occupies. BOTH sources are staged before either extend runs:
|
||
// X2 is Y2's own low half, so writing a staging register after
|
||
// Y2 is produced would corrupt it. Every source is read before
|
||
// dst is written, so dst may safely alias a source register.
|
||
// The "// avx2 dot" marker triggers the dual-body dispatch
|
||
// (see the amd64 archSpec entry): a non-AVX2 host runs the
|
||
// portable SSE twin.
|
||
blockDotSrc := func(a simdfuse.Arg, stage int) (string, error) {
|
||
switch a.Kind {
|
||
case simdfuse.ArgNode:
|
||
if loc[a.Index].chained {
|
||
return "X0", nil
|
||
}
|
||
return fmt.Sprintf("X%d", loc[a.Index].pool), nil
|
||
case simdfuse.ArgPairIn:
|
||
if cr, isCarried := carried[a.Index]; isCarried {
|
||
return fmt.Sprintf("X%d", cr), nil
|
||
}
|
||
lo, hi := pairRegs(a)
|
||
fmt.Fprintf(b, "\tMOVQ %s, X%d\n", lo, stage)
|
||
fmt.Fprintf(b, "\tPINSRQ $1, %s, X%d\n", hi, stage)
|
||
return fmt.Sprintf("X%d", stage), nil
|
||
default:
|
||
return "", fmt.Errorf("fused splice %s: malformed %s args", tree.Name, n.Op)
|
||
}
|
||
}
|
||
ra, err := blockDotSrc(n.Args[0], 2)
|
||
if err != nil {
|
||
return nil, false, err
|
||
}
|
||
rb, err := blockDotSrc(n.Args[1], 3)
|
||
if err != nil {
|
||
return nil, false, err
|
||
}
|
||
fmt.Fprintf(b, "\tVPMOVSXBW %s, Y2\n", ra)
|
||
fmt.Fprintf(b, "\tVPMOVSXBW %s, Y3\n", rb)
|
||
b.WriteString("\tVPMADDWD Y3, Y2, Y2\n")
|
||
b.WriteString("\tVEXTRACTI128 $1, Y2, X3\n")
|
||
fmt.Fprintf(b, "\tVPADDD X3, X2, X%d\n", dst)
|
||
if !vexClean {
|
||
// Clear the ymm upper halves IMMEDIATELY: legacy SSE
|
||
// follows in this region, and on Intel a dirty upper
|
||
// state gives every legacy SSE instruction a false
|
||
// dependency that serializes the loop (measured 30x on
|
||
// Granite Rapids). A fully VEX region defers this to
|
||
// one clear at the exit instead.
|
||
b.WriteString("\tVZEROUPPER\n")
|
||
}
|
||
b.WriteString("\t// avx2 dot\n")
|
||
loc[i] = fusedLoc{chained: willChain, pool: dst}
|
||
continue
|
||
} else if n.Op == x64OpRawDot || n.Op == x64OpRawDotAcc ||
|
||
n.Op == x64OpRawDotWide || n.Op == x64OpRawDotWideAcc {
|
||
// Raw 16-byte 8-bit dots (see x64CrossDotRewrite). The
|
||
// self-contained forms collapse to the four-byte grouping
|
||
// in place; the wide forms leave the value in the 8-lane
|
||
// pair domain across a full ymm for x64OpRawDotFold.
|
||
// Y2/Y3 (X2/X3) are scratch the pool never occupies; byte
|
||
// sources stage BEFORE either extend runs (X2 is Y2's own
|
||
// low half), and every source is read before dst is
|
||
// written, so dst may alias a source.
|
||
isWide := n.Op == x64OpRawDotWide || n.Op == x64OpRawDotWideAcc
|
||
hasAcc := n.Op == x64OpRawDotAcc || n.Op == x64OpRawDotWideAcc
|
||
args := n.Args
|
||
accReg := -1
|
||
if hasAcc {
|
||
a := args[0]
|
||
if a.Kind != simdfuse.ArgNode {
|
||
return nil, false, fmt.Errorf("fused splice %s: %s accumulator is not a node", tree.Name, n.Op)
|
||
}
|
||
if loc[a.Index].chained {
|
||
accReg = 0
|
||
} else {
|
||
accReg = loc[a.Index].pool
|
||
}
|
||
args = args[1:]
|
||
}
|
||
rawSrc := func(a simdfuse.Arg, stage int) (string, error) {
|
||
switch a.Kind {
|
||
case simdfuse.ArgNode:
|
||
if loc[a.Index].chained {
|
||
return "X0", nil
|
||
}
|
||
return fmt.Sprintf("X%d", loc[a.Index].pool), nil
|
||
case simdfuse.ArgPairIn:
|
||
if cr, isCarried := carried[a.Index]; isCarried {
|
||
return fmt.Sprintf("X%d", cr), nil
|
||
}
|
||
lo, hi := pairRegs(a)
|
||
fmt.Fprintf(b, "\tMOVQ %s, X%d\n", lo, stage)
|
||
fmt.Fprintf(b, "\tPINSRQ $1, %s, X%d\n", hi, stage)
|
||
return fmt.Sprintf("X%d", stage), nil
|
||
default:
|
||
return "", fmt.Errorf("fused splice %s: malformed %s args", tree.Name, n.Op)
|
||
}
|
||
}
|
||
ra, err := rawSrc(args[0], 2)
|
||
if err != nil {
|
||
return nil, false, err
|
||
}
|
||
rb, err := rawSrc(args[1], 3)
|
||
if err != nil {
|
||
return nil, false, err
|
||
}
|
||
fmt.Fprintf(b, "\tVPMOVSXBW %s, Y2\n", ra)
|
||
fmt.Fprintf(b, "\tVPMOVSXBW %s, Y3\n", rb)
|
||
if isWide {
|
||
if hasAcc {
|
||
b.WriteString("\tVPMADDWD Y3, Y2, Y2\n")
|
||
fmt.Fprintf(b, "\tVPADDD Y%d, Y2, Y%d\n", accReg, dst)
|
||
} else {
|
||
fmt.Fprintf(b, "\tVPMADDWD Y3, Y2, Y%d\n", dst)
|
||
}
|
||
} else {
|
||
b.WriteString("\tVPMADDWD Y3, Y2, Y2\n")
|
||
if !hasAcc {
|
||
x64PairFold(b, 2, dst)
|
||
} else {
|
||
x64PairFold(b, 2, 2)
|
||
fmt.Fprintf(b, "\tVPADDD X%d, X2, X%d\n", accReg, dst)
|
||
}
|
||
if !vexClean {
|
||
// See the block dot above: uppers must not stay
|
||
// dirty into legacy SSE code. (The wide forms are
|
||
// never selected outside fully-VEX trees.)
|
||
b.WriteString("\tVZEROUPPER\n")
|
||
}
|
||
}
|
||
b.WriteString("\t// avx2 dot\n")
|
||
loc[i] = fusedLoc{chained: willChain, pool: dst}
|
||
continue
|
||
} else if n.Op == x64OpRawDotFold {
|
||
// Collapse the pair-domain chain value: lane j of the
|
||
// result is P2j + P2j+1 — the four-consecutive-byte
|
||
// grouping the consumers were proven to want.
|
||
a := n.Args[0]
|
||
if a.Kind != simdfuse.ArgNode {
|
||
return nil, false, fmt.Errorf("fused splice %s: %s input is not a node", tree.Name, n.Op)
|
||
}
|
||
v := 0
|
||
if !loc[a.Index].chained {
|
||
v = loc[a.Index].pool
|
||
}
|
||
x64PairFold(b, v, dst)
|
||
b.WriteString("\t// avx2 dot\n")
|
||
loc[i] = fusedLoc{chained: willChain, pool: dst}
|
||
continue
|
||
} else if vex, isVex := x64VexOps[n.Op]; isVex && !portable && x64VexArgsOK(n) {
|
||
// VEX three-operand form with an explicit destination: no
|
||
// staging copies into the canned X0/X1 slots and no parking
|
||
// copy out — the op reads its sources where they live and
|
||
// writes the pool register directly. VEX-128 zeroes the ymm
|
||
// upper halves, so no dirty-upper hazard arises. Scratch is
|
||
// X2/X3 (never pool homes); sources are read before dst is
|
||
// written, so dst may alias a source.
|
||
srcs := make([]string, 0, 2)
|
||
stage := 2
|
||
okSrc := true
|
||
for _, a := range n.Args {
|
||
switch a.Kind {
|
||
case simdfuse.ArgNode:
|
||
if loc[a.Index].chained {
|
||
srcs = append(srcs, "X0")
|
||
} else {
|
||
srcs = append(srcs, fmt.Sprintf("X%d", loc[a.Index].pool))
|
||
}
|
||
case simdfuse.ArgPairIn:
|
||
if cr, isCarried := carried[a.Index]; isCarried {
|
||
srcs = append(srcs, fmt.Sprintf("X%d", cr))
|
||
} else {
|
||
lo, hi := pairRegs(a)
|
||
fmt.Fprintf(b, "\tMOVQ %s, X%d\n", lo, stage)
|
||
fmt.Fprintf(b, "\tPINSRQ $1, %s, X%d\n", hi, stage)
|
||
srcs = append(srcs, fmt.Sprintf("X%d", stage))
|
||
stage++
|
||
}
|
||
default:
|
||
okSrc = false
|
||
}
|
||
}
|
||
if !okSrc {
|
||
return nil, false, fmt.Errorf("fused splice %s: malformed %s args", tree.Name, n.Op)
|
||
}
|
||
d := dst
|
||
if willChain {
|
||
d = 0
|
||
}
|
||
vex(b, srcs, d)
|
||
loc[i] = fusedLoc{chained: willChain, pool: d}
|
||
continue
|
||
} else if n.Op == "i8x16_shuffle_const" {
|
||
// Shuffle with a descriptor-constant control vector: the
|
||
// pattern comes from the const pool (no ABI slot), the two
|
||
// sources route to X0/X1 and the standard two-source
|
||
// PSHUFB emulation runs with the pattern in X2.
|
||
if len(n.Args) != 6 {
|
||
return nil, false, fmt.Errorf("fused splice %s: malformed %s args", tree.Name, n.Op)
|
||
}
|
||
if pool == nil {
|
||
return nil, false, fmt.Errorf("fused splice %s: %s needs a const pool", tree.Name, n.Op)
|
||
}
|
||
for k := 0; k < 2; k++ {
|
||
a := n.Args[k]
|
||
switch a.Kind {
|
||
case simdfuse.ArgNode:
|
||
if loc[a.Index].chained {
|
||
if k != 0 {
|
||
return nil, false, fmt.Errorf("fused splice %s: chained value consumed at operand %d", tree.Name, k)
|
||
}
|
||
} else {
|
||
x64VecCopy(b, k, loc[a.Index].pool)
|
||
}
|
||
case simdfuse.ArgPairIn:
|
||
if cr, isCarried := carried[a.Index]; isCarried {
|
||
x64VecCopy(b, k, cr)
|
||
} else {
|
||
lo, hi := pairRegs(a)
|
||
fmt.Fprintf(b, "\tMOVQ %s, X%d\n", lo, k)
|
||
fmt.Fprintf(b, "\tPINSRQ $1, %s, X%d\n", hi, k)
|
||
}
|
||
default:
|
||
return nil, false, fmt.Errorf("fused splice %s: malformed %s args", tree.Name, n.Op)
|
||
}
|
||
}
|
||
u32c := func(a simdfuse.Arg) uint64 { return uint64(uint32(a.Const)) }
|
||
patLo := u32c(n.Args[2]) | u32c(n.Args[3])<<32
|
||
patHi := u32c(n.Args[4]) | u32c(n.Args[5])<<32
|
||
blob := make([]byte, 16)
|
||
for bi := 0; bi < 8; bi++ {
|
||
blob[bi] = byte(patLo >> (8 * bi))
|
||
blob[8+bi] = byte(patHi >> (8 * bi))
|
||
}
|
||
// The emulation must stay inside the splicer's scratch set
|
||
// (X0-X3): pool values live in X4+ for the whole loop
|
||
// body, and the window path's relocate-out-of-reach scan
|
||
// does not run here. The pattern reloads from the pool
|
||
// instead of holding a second biased copy, and the bias
|
||
// constants apply as memory operands, so three registers
|
||
// suffice.
|
||
patBlob := pool.addBlob(blob)
|
||
shufLines, cok := simdSpliceRewriteConsts([]string{
|
||
"MOVOU ·" + patBlob + "(SB), X2",
|
||
"PADDUSB ·simdConst70(SB), X2", "PSHUFB X2, X0",
|
||
"MOVOU ·" + patBlob + "(SB), X2",
|
||
"PSUBB ·simdConst16(SB), X2", "PADDUSB ·simdConst70(SB), X2", "PSHUFB X2, X1",
|
||
"POR X1, X0",
|
||
}, pool)
|
||
if !cok {
|
||
return nil, false, fmt.Errorf("fused splice %s: shuffle const table", tree.Name)
|
||
}
|
||
for _, ln := range shufLines {
|
||
fmt.Fprintf(b, "\t%s\n", ln)
|
||
}
|
||
d := dst
|
||
if willChain {
|
||
d = 0
|
||
}
|
||
if d != 0 {
|
||
x64VecCopy(b, d, 0)
|
||
}
|
||
loc[i] = fusedLoc{chained: willChain, pool: d}
|
||
continue
|
||
} else {
|
||
lines, ok := x64SimdPairSpliceTab[n.Op]
|
||
if !ok {
|
||
return nil, false, fmt.Errorf("fused splice %s: no pair table entry for %q", tree.Name, n.Op)
|
||
}
|
||
lines, ok = simdSpliceRewriteConsts(lines, pool)
|
||
if !ok {
|
||
return nil, false, fmt.Errorf("fused splice %s: unknown const reference in %q", tree.Name, n.Op)
|
||
}
|
||
vArgs := make([]simdfuse.Arg, 0, len(n.Args))
|
||
var sArg *simdfuse.Arg
|
||
for ai := range n.Args {
|
||
a := n.Args[ai]
|
||
if a.Kind == simdfuse.ArgNode || a.Kind == simdfuse.ArgPairIn {
|
||
vArgs = append(vArgs, a)
|
||
} else {
|
||
aa := a
|
||
sArg = &aa
|
||
}
|
||
}
|
||
opTargets, core, err := x64ClassifyPair(lines, len(vArgs))
|
||
if err != nil {
|
||
return nil, false, fmt.Errorf("fused splice %s, op %q: %w", tree.Name, n.Op, err)
|
||
}
|
||
for k, a := range vArgs {
|
||
dst := opTargets[k]
|
||
switch a.Kind {
|
||
case simdfuse.ArgNode:
|
||
src := loc[a.Index]
|
||
if src.chained {
|
||
if dst != 0 || k != 0 {
|
||
return nil, false, fmt.Errorf("fused splice %s: chained value consumed at operand %d", tree.Name, k)
|
||
}
|
||
continue
|
||
}
|
||
x64VecCopy(b, dst, src.pool)
|
||
case simdfuse.ArgPairIn:
|
||
// Loop-carried pair: copy from its reserved
|
||
// register (the tail wrote the running value
|
||
// there); only the first iteration's value ever
|
||
// reaches the argument registers.
|
||
if cr, isCarried := carried[a.Index]; isCarried {
|
||
x64VecCopy(b, dst, cr)
|
||
} else {
|
||
lo, hi := pairRegs(a)
|
||
fmt.Fprintf(b, "\tMOVQ %s, X%d\n", lo, dst)
|
||
fmt.Fprintf(b, "\tPINSRQ $1, %s, X%d\n", hi, dst)
|
||
}
|
||
}
|
||
}
|
||
if sArg != nil {
|
||
if sArg.Kind == simdfuse.ArgConst {
|
||
fmt.Fprintf(b, "\tMOVQ $%d, AX\n", int64(sArg.Const))
|
||
} else {
|
||
fmt.Fprintf(b, "\tMOVQ %s, AX\n", scalarReg(*sArg))
|
||
}
|
||
}
|
||
// Relocate pool values that must survive this op out of
|
||
// the canned core's reach before it is pasted; every
|
||
// later consumer reads the updated loc. The reach differs
|
||
// per op — a plain PADDW touches only its X0/X1 inputs
|
||
// while the shuffle body scratches up to X5 — so scan the
|
||
// core for the highest register it mentions and move only
|
||
// what that range would corrupt. This runs after the
|
||
// input copies above: registers freed by this op's own
|
||
// consumption are dead by now and safe to hand out as
|
||
// relocation homes.
|
||
maxX := 1
|
||
for _, l := range core {
|
||
for _, m := range x64XRegRe.FindAllStringSubmatch(l, -1) {
|
||
if x, aerr := strconv.Atoi(m[1]); aerr == nil && x > maxX {
|
||
maxX = x
|
||
}
|
||
}
|
||
}
|
||
for j := range loc {
|
||
// pool < 4: not parked (chained through X0, inline
|
||
// scalar chain, or not yet emitted).
|
||
if loc[j].chained || loc[j].pool < 4 || loc[j].pool > maxX || uses[j] <= 0 {
|
||
continue
|
||
}
|
||
moved := -1
|
||
for k := range freePool {
|
||
if freePool[k] > maxX {
|
||
moved = freePool[k]
|
||
freePool = append(freePool[:k], freePool[k+1:]...)
|
||
break
|
||
}
|
||
}
|
||
if moved < 0 {
|
||
return nil, false, fmt.Errorf("fused splice %s: %w: no register outside canned scratch", tree.Name, errFusedCapacity)
|
||
}
|
||
x64VecCopy(b, moved, loc[j].pool)
|
||
freePool = append(freePool, loc[j].pool)
|
||
loc[j].pool = moved
|
||
}
|
||
for _, l := range core {
|
||
fmt.Fprintf(b, "\t%s\n", l)
|
||
}
|
||
}
|
||
if !willChain {
|
||
x64VecCopy(b, dst, 0)
|
||
}
|
||
loc[i] = fusedLoc{chained: willChain, pool: dst}
|
||
}
|
||
return loc, needsTrap, nil
|
||
}
|
||
|
||
// x64MemBase names the register holding m.M for indexed accesses: the
|
||
// hoisted register when the region pinned one (see x64HoistMem), else
|
||
// AX freshly loaded from the Module struct.
|
||
func x64MemBase(b *strings.Builder, offs *ModuleOffsets) string {
|
||
if offs != nil && offs.HoistM != "" {
|
||
return offs.HoistM
|
||
}
|
||
fmt.Fprintf(b, "\tMOVQ %d(R13), AX\n", offs.M)
|
||
return "AX"
|
||
}
|
||
|
||
// x64MemSizeCheck emits the bounds compare of R12 (holding the access
|
||
// END address) against memSize, trapping on overrun: one CMPQ against
|
||
// the hoisted value, or the pointer-load + dereference pair.
|
||
func x64MemSizeCheck(b *strings.Builder, offs *ModuleOffsets) {
|
||
if offs != nil && offs.HoistMemSizeVal != "" {
|
||
fmt.Fprintf(b, "\tCMPQ %s, R12\n", offs.HoistMemSizeVal)
|
||
} else {
|
||
fmt.Fprintf(b, "\tMOVQ %d(R13), AX\n", offs.MemSize)
|
||
b.WriteString("\tMOVQ (AX), AX\n")
|
||
b.WriteString("\tCMPQ AX, R12\n")
|
||
}
|
||
fmt.Fprintf(b, "\tJCS %s\n", x64SimdMemTrapLabel)
|
||
}
|
||
|
||
// x64HoistMem pins m.M (and the memSize value) into free argument-tail
|
||
// registers for one fused region or loop, returning the offsets the
|
||
// member emitters should use. Free means beyond the region's argument
|
||
// usage; a splice contains no calls, so both values stay valid for its
|
||
// whole extent, and the epilogue may clobber the registers freely.
|
||
// Regions with fewer than two memory ops keep the per-site loads (the
|
||
// prologue would not pay for itself).
|
||
func x64HoistMem(b *strings.Builder, offs *ModuleOffsets, tree *simdfuse.Tree, usedIntArgs int) *ModuleOffsets {
|
||
if offs == nil || !tree.NeedsMem {
|
||
return offs
|
||
}
|
||
memOps := 0
|
||
for _, n := range tree.Nodes {
|
||
if _, ok := fusedMemOpsA64[n.Op]; ok || simdfuse.IsStore(n.Op) || n.Op == "v128_load32_lane" {
|
||
memOps++
|
||
}
|
||
}
|
||
if memOps < 2 || usedIntArgs >= len(x64FusedArgRegs) {
|
||
return offs
|
||
}
|
||
free := x64FusedArgRegs[usedIntArgs:]
|
||
o := *offs
|
||
o.HoistM = free[0]
|
||
fmt.Fprintf(b, "\tMOVQ %d(R13), %s\n", offs.M, free[0])
|
||
if len(free) >= 2 {
|
||
o.HoistMemSizeVal = free[1]
|
||
fmt.Fprintf(b, "\tMOVQ %d(R13), %s\n", offs.MemSize, free[1])
|
||
fmt.Fprintf(b, "\tMOVQ (%s), %s\n", free[1], free[1])
|
||
}
|
||
return &o
|
||
}
|
||
|
||
// x64FusedLoad emits one member load, result in X0. m lives in R13;
|
||
// scratch is R12 plus AX (free once m is saved) so every integer
|
||
// argument register stays live. The bounds compare runs against the
|
||
// END address accumulated in R12 (add, compare, subtract back) to
|
||
// avoid a third scratch register; non-constant rlo/span windows fall
|
||
// back to AX between address steps.
|
||
func x64FusedLoad(b *strings.Builder, n simdfuse.Node, scalarReg func(simdfuse.Arg) string, offs *ModuleOffsets, dst int, addr64 bool, addrReg string) error {
|
||
want := fusedMemOpsA64[n.Op]
|
||
if len(n.Args) != want {
|
||
return fmt.Errorf("fused load %s: %d args, want %d", n.Op, len(n.Args), want)
|
||
}
|
||
arg := func(i int) (reg string, c int32, isConst bool) {
|
||
a := n.Args[i]
|
||
if a.Kind == simdfuse.ArgConst {
|
||
return "", a.Const, true
|
||
}
|
||
return scalarReg(a), 0, false
|
||
}
|
||
// The address materializes into R12 as u32 — or full-width with
|
||
// carry-checked sums on a memory64 tree (whose addr/offset are
|
||
// always opaque scalars; a wrapped u64 ea could land back inside
|
||
// bounds, so every sum traps on carry there).
|
||
addOffErr := error(nil)
|
||
// Fully-folded unchecked load: on a memory64 tree a register base
|
||
// plus constant terms is ONE indexed instruction — decided here,
|
||
// before any base materialization, so no dead address arithmetic
|
||
// is emitted.
|
||
if n.Op == "v128_load_nc" && addr64 && addrReg == "" {
|
||
if a := n.Args[0]; a.Kind == simdfuse.ArgScalar || a.Kind == simdfuse.ArgSum {
|
||
base := int64(0)
|
||
if a.Kind == simdfuse.ArgSum {
|
||
base = int64(a.Const)
|
||
}
|
||
if off := n.Args[1]; off.Kind == simdfuse.ArgConst {
|
||
disp := base + int64(off.Const)
|
||
if disp >= 0 && disp < 1<<31 {
|
||
fmt.Fprintf(b, "\tVMOVDQU %d(%s)(%s*1), X%d\n", disp, x64MemBase(b, offs), scalarReg(a), dst)
|
||
return nil
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if addrReg != "" {
|
||
// Chain-computed address: move the terminal into the address
|
||
// register first (see the arm64 twin). MOVL re-zero-extends
|
||
// the i32-wrapped chain result on wasm32; at full width a
|
||
// chain already terminating in R12 needs no move at all.
|
||
if addr64 {
|
||
if addrReg != "R12" {
|
||
fmt.Fprintf(b, "\tMOVQ %s, R12\n", addrReg)
|
||
}
|
||
} else {
|
||
fmt.Fprintf(b, "\tMOVL %s, R12\n", addrReg)
|
||
}
|
||
} else if addr64 {
|
||
switch a := n.Args[0]; a.Kind {
|
||
case simdfuse.ArgScalar:
|
||
fmt.Fprintf(b, "\tMOVQ %s, R12\n", scalarReg(a))
|
||
case simdfuse.ArgSum:
|
||
// base+const at full width: the descriptor constant is a
|
||
// small reassociated memarg delta added as i64 — no wrap
|
||
// semantics needed under the 2^48 memory cap.
|
||
fmt.Fprintf(b, "\tMOVQ %s, R12\n", scalarReg(a))
|
||
if a.Const != 0 {
|
||
fmt.Fprintf(b, "\tADDQ $%d, R12\n", int64(a.Const))
|
||
}
|
||
default:
|
||
return fmt.Errorf("fused addr64 load %s: folded address form", n.Op)
|
||
}
|
||
} else {
|
||
switch a := n.Args[0]; a.Kind {
|
||
case simdfuse.ArgConst:
|
||
fmt.Fprintf(b, "\tMOVQ $%d, R12\n", int64(uint32(a.Const)))
|
||
case simdfuse.ArgSum:
|
||
// MOVL zero-extends and ADDL wraps in 32 bits with the upper
|
||
// half staying zero — exactly Add32.
|
||
fmt.Fprintf(b, "\tMOVL %s, R12\n", scalarReg(a))
|
||
if a.Const != 0 {
|
||
fmt.Fprintf(b, "\tADDL $%d, R12\n", a.Const)
|
||
}
|
||
default:
|
||
fmt.Fprintf(b, "\tMOVL %s, R12\n", scalarReg(a))
|
||
}
|
||
}
|
||
addOff := func(i int) {
|
||
if addr64 {
|
||
// Add the memarg offset (small const, or a scalar) into the
|
||
// effective address; same arithmetic as wasm32, no wrap
|
||
// guard (see x64FusedMemCheck).
|
||
a := n.Args[i]
|
||
switch a.Kind {
|
||
case simdfuse.ArgConst:
|
||
if a.Const != 0 {
|
||
fmt.Fprintf(b, "\tADDQ $%d, R12\n", int64(a.Const))
|
||
}
|
||
case simdfuse.ArgScalar:
|
||
fmt.Fprintf(b, "\tMOVQ %s, AX\n", scalarReg(a))
|
||
b.WriteString("\tADDQ AX, R12\n")
|
||
default:
|
||
addOffErr = fmt.Errorf("fused addr64 load %s: bad offset form", n.Op)
|
||
}
|
||
return
|
||
}
|
||
reg, c, isConst := arg(i)
|
||
if isConst {
|
||
if c != 0 {
|
||
fmt.Fprintf(b, "\tADDQ $%d, R12\n", int64(uint32(c)))
|
||
}
|
||
return
|
||
}
|
||
fmt.Fprintf(b, "\tMOVL %s, AX\n", reg)
|
||
b.WriteString("\tADDQ AX, R12\n")
|
||
}
|
||
checked := func(size int) {
|
||
fmt.Fprintf(b, "\tADDQ $%d, R12\n", size)
|
||
x64MemSizeCheck(b, offs)
|
||
fmt.Fprintf(b, "\tSUBQ $%d, R12\n", size)
|
||
}
|
||
switch n.Op {
|
||
case "v128_load":
|
||
addOff(1)
|
||
checked(16)
|
||
case "v128_load32_zero":
|
||
addOff(1)
|
||
checked(4)
|
||
fmt.Fprintf(b, "\tVMOVSS (%s)(R12*1), X%d\n", x64MemBase(b, offs), dst)
|
||
return addOffErr
|
||
case "v128_load64_zero":
|
||
addOff(1)
|
||
checked(8)
|
||
fmt.Fprintf(b, "\tVMOVQ (%s)(R12*1), X%d\n", x64MemBase(b, offs), dst)
|
||
return addOffErr
|
||
case "v128_load32_splat":
|
||
addOff(1)
|
||
checked(4)
|
||
fmt.Fprintf(b, "\tVMOVSS (%s)(R12*1), X%d\n", x64MemBase(b, offs), dst)
|
||
fmt.Fprintf(b, "\tVPSHUFD $0x00, X%d, X%d\n", dst, dst)
|
||
return addOffErr
|
||
case "v128_load16x4_u":
|
||
addOff(1)
|
||
checked(8)
|
||
fmt.Fprintf(b, "\tVMOVQ (%s)(R12*1), X%d\n", x64MemBase(b, offs), dst)
|
||
fmt.Fprintf(b, "\tVPMOVZXWD X%d, X%d\n", dst, dst)
|
||
return addOffErr
|
||
case "v128_load_nc":
|
||
if d, ok := x64DeferOff(n.Args[1]); ok {
|
||
fmt.Fprintf(b, "\tVMOVDQU %s(%s)(R12*1), X%d\n", d, x64MemBase(b, offs), dst)
|
||
return addOffErr
|
||
}
|
||
addOff(1)
|
||
case "v128_load32_splat_nc", "v128_load16x4_u_nc", "v128_load32_zero_nc", "v128_load64_zero_nc":
|
||
// Unchecked narrow loads (window check hoisted at the loop
|
||
// head): a 4-byte VMOVSS or 8-byte VMOVQ, then the op's own
|
||
// widen where it has one.
|
||
mov := "VMOVSS"
|
||
if n.Op == "v128_load16x4_u_nc" || n.Op == "v128_load64_zero_nc" {
|
||
mov = "VMOVQ"
|
||
}
|
||
if d, ok := x64DeferOff(n.Args[1]); ok {
|
||
fmt.Fprintf(b, "\t%s %s(%s)(R12*1), X%d\n", mov, d, x64MemBase(b, offs), dst)
|
||
} else {
|
||
addOff(1)
|
||
fmt.Fprintf(b, "\t%s (%s)(R12*1), X%d\n", mov, x64MemBase(b, offs), dst)
|
||
}
|
||
switch n.Op {
|
||
case "v128_load32_splat_nc":
|
||
fmt.Fprintf(b, "\tVPSHUFD $0x00, X%d, X%d\n", dst, dst)
|
||
case "v128_load16x4_u_nc":
|
||
fmt.Fprintf(b, "\tVPMOVZXWD X%d, X%d\n", dst, dst)
|
||
}
|
||
return addOffErr
|
||
case "v128_load_rng":
|
||
// start/end accumulate in R12 and are undone afterwards, so
|
||
// the address survives without a third register. The window is
|
||
// constant by the walker's contract (checked before emitting
|
||
// anything).
|
||
_, rloC, rloConst := arg(2)
|
||
_, spanC, spanConst := arg(3)
|
||
if !rloConst || !spanConst {
|
||
return fmt.Errorf("fused load %s: non-constant rng window (walker contract violated)", n.Op)
|
||
}
|
||
// A non-negative rlo cannot make start negative (the base is
|
||
// ≥ 0 either as a u32 or under the 2^48 memory64 cap), so the
|
||
// sign trap is emitted only for a negative rlo.
|
||
undo := int64(uint32(spanC))
|
||
if rloC != 0 {
|
||
fmt.Fprintf(b, "\tADDQ $%d, R12\n", int64(rloC))
|
||
undo += int64(rloC)
|
||
}
|
||
if rloC < 0 {
|
||
fmt.Fprintf(b, "\tJS %s\n", x64SimdMemTrapLabel)
|
||
}
|
||
fmt.Fprintf(b, "\tADDQ $%d, R12\n", int64(uint32(spanC)))
|
||
x64MemSizeCheck(b, offs)
|
||
fmt.Fprintf(b, "\tSUBQ $%d, R12\n", undo)
|
||
if d, ok := x64DeferOff(n.Args[1]); ok {
|
||
fmt.Fprintf(b, "\tVMOVDQU %s(%s)(R12*1), X%d\n", d, x64MemBase(b, offs), dst)
|
||
return addOffErr
|
||
}
|
||
addOff(1)
|
||
}
|
||
fmt.Fprintf(b, "\tVMOVDQU (%s)(R12*1), X%d\n", x64MemBase(b, offs), dst)
|
||
return addOffErr
|
||
}
|
||
|
||
// x64DeferOff reports whether a memarg offset can ride the load's
|
||
// displacement instead of an ADDQ: a small constant (disp32 is
|
||
// sign-extended, so it must fit in 31 bits). Callers may fold it only
|
||
// when no bounds check consumes the summed address afterwards — the
|
||
// unchecked and range-pre-checked load forms.
|
||
func x64DeferOff(a simdfuse.Arg) (string, bool) {
|
||
if a.Kind != simdfuse.ArgConst {
|
||
return "", false
|
||
}
|
||
c := int64(uint32(a.Const))
|
||
if c == 0 {
|
||
return "", true
|
||
}
|
||
if c >= 1<<31 {
|
||
return "", false
|
||
}
|
||
return fmt.Sprintf("%d", c), true
|
||
}
|
||
|
||
// x64FusedLaneAddr materializes a lane load's checked address:
|
||
// R12 = u32 effective address bounds-checked against a 4-byte window,
|
||
// AX = the memory base, ready for an indexed lane insert.
|
||
// x64FusedEA64 materializes a memory64 member's checked effective
|
||
// address into R12 (base pointer + memarg offset, then the ea+size
|
||
// range check against memSize) and leaves the M base in AX. The
|
||
// arithmetic and range check are identical to the wasm32 fused-store
|
||
// path; the only difference is the full-width base load. No wrap guard
|
||
// is needed — see a64FusedMemCheck's note (the arm64 twin).
|
||
func x64FusedEA64(b *strings.Builder, n simdfuse.Node, scalarReg func(simdfuse.Arg) string, offs *ModuleOffsets, size int, addrReg string) (string, error) {
|
||
if addrReg != "" {
|
||
fmt.Fprintf(b, "\tMOVQ %s, R12\n", addrReg)
|
||
} else {
|
||
switch a := n.Args[0]; a.Kind {
|
||
case simdfuse.ArgScalar:
|
||
fmt.Fprintf(b, "\tMOVQ %s, R12\n", scalarReg(a))
|
||
case simdfuse.ArgSum:
|
||
// base+const at full width (see x64FusedLoad's addr64 arm).
|
||
fmt.Fprintf(b, "\tMOVQ %s, R12\n", scalarReg(a))
|
||
if a.Const != 0 {
|
||
fmt.Fprintf(b, "\tADDQ $%d, R12\n", int64(a.Const))
|
||
}
|
||
default:
|
||
return "", fmt.Errorf("fused addr64 %s: folded base form", n.Op)
|
||
}
|
||
}
|
||
switch off := n.Args[1]; off.Kind {
|
||
case simdfuse.ArgConst:
|
||
if off.Const != 0 {
|
||
fmt.Fprintf(b, "\tADDQ $%d, R12\n", int64(off.Const))
|
||
}
|
||
case simdfuse.ArgScalar:
|
||
fmt.Fprintf(b, "\tMOVQ %s, AX\n", scalarReg(off))
|
||
b.WriteString("\tADDQ AX, R12\n")
|
||
default:
|
||
return "", fmt.Errorf("fused addr64 %s: bad offset form", n.Op)
|
||
}
|
||
fmt.Fprintf(b, "\tADDQ $%d, R12\n", size)
|
||
x64MemSizeCheck(b, offs)
|
||
fmt.Fprintf(b, "\tSUBQ $%d, R12\n", size)
|
||
return x64MemBase(b, offs), nil
|
||
}
|
||
|
||
func x64FusedLaneAddr(b *strings.Builder, n simdfuse.Node, scalarReg func(simdfuse.Arg) string, offs *ModuleOffsets, addr64 bool, addrReg string) (string, error) {
|
||
if addr64 {
|
||
return x64FusedEA64(b, n, scalarReg, offs, 4, addrReg)
|
||
}
|
||
if addrReg != "" {
|
||
// Chain-computed address (see x64FusedLoad).
|
||
fmt.Fprintf(b, "\tMOVL %s, R12\n", addrReg)
|
||
} else {
|
||
switch a := n.Args[0]; a.Kind {
|
||
case simdfuse.ArgConst:
|
||
fmt.Fprintf(b, "\tMOVQ $%d, R12\n", int64(uint32(a.Const)))
|
||
case simdfuse.ArgSum:
|
||
fmt.Fprintf(b, "\tMOVL %s, R12\n", scalarReg(a))
|
||
if a.Const != 0 {
|
||
fmt.Fprintf(b, "\tADDL $%d, R12\n", a.Const)
|
||
}
|
||
default:
|
||
fmt.Fprintf(b, "\tMOVL %s, R12\n", scalarReg(a))
|
||
}
|
||
}
|
||
if off := n.Args[1]; off.Kind == simdfuse.ArgConst {
|
||
if off.Const != 0 {
|
||
fmt.Fprintf(b, "\tADDQ $%d, R12\n", int64(uint32(off.Const)))
|
||
}
|
||
} else {
|
||
fmt.Fprintf(b, "\tMOVL %s, AX\n", scalarReg(n.Args[1]))
|
||
b.WriteString("\tADDQ AX, R12\n")
|
||
}
|
||
b.WriteString("\tADDQ $4, R12\n")
|
||
x64MemSizeCheck(b, offs)
|
||
b.WriteString("\tSUBQ $4, R12\n")
|
||
return x64MemBase(b, offs), nil
|
||
}
|
||
|
||
// x64FusedStore emits one member store: checked address in R12, value
|
||
// from X0/pool/a pair build into X1 (dead between node bodies), then
|
||
// the indexed MOVOU store.
|
||
func x64FusedStore(b *strings.Builder, n simdfuse.Node, scalarReg func(simdfuse.Arg) string,
|
||
pairRegs func(simdfuse.Arg) (string, string), offs *ModuleOffsets, pool *ConstPool, addr64 bool, src int, addrReg string) error {
|
||
if len(n.Args) != 3 {
|
||
return fmt.Errorf("fused store: %d args, want 3", len(n.Args))
|
||
}
|
||
if addr64 {
|
||
span := 16
|
||
if n.Op == "v128_f16x4_cvt_store" {
|
||
span = 8
|
||
}
|
||
if _, eerr := x64FusedEA64(b, n, scalarReg, offs, span, addrReg); eerr != nil {
|
||
return eerr
|
||
}
|
||
return x64FusedStoreTail(b, n, pairRegs, offs, pool, src)
|
||
}
|
||
if addrReg != "" {
|
||
// Chain-computed address (see x64FusedLoad).
|
||
fmt.Fprintf(b, "\tMOVL %s, R12\n", addrReg)
|
||
} else {
|
||
switch a := n.Args[0]; a.Kind {
|
||
case simdfuse.ArgConst:
|
||
fmt.Fprintf(b, "\tMOVQ $%d, R12\n", int64(uint32(a.Const)))
|
||
case simdfuse.ArgSum:
|
||
fmt.Fprintf(b, "\tMOVL %s, R12\n", scalarReg(a))
|
||
if a.Const != 0 {
|
||
fmt.Fprintf(b, "\tADDL $%d, R12\n", a.Const)
|
||
}
|
||
default:
|
||
fmt.Fprintf(b, "\tMOVL %s, R12\n", scalarReg(a))
|
||
}
|
||
}
|
||
if off := n.Args[1]; off.Kind == simdfuse.ArgConst {
|
||
if off.Const != 0 {
|
||
fmt.Fprintf(b, "\tADDQ $%d, R12\n", int64(uint32(off.Const)))
|
||
}
|
||
} else {
|
||
fmt.Fprintf(b, "\tMOVL %s, AX\n", scalarReg(n.Args[1]))
|
||
b.WriteString("\tADDQ AX, R12\n")
|
||
}
|
||
span := 16
|
||
if n.Op == "v128_f16x4_cvt_store" {
|
||
span = 8
|
||
}
|
||
fmt.Fprintf(b, "\tADDQ $%d, R12\n", span)
|
||
x64MemSizeCheck(b, offs)
|
||
fmt.Fprintf(b, "\tSUBQ $%d, R12\n", span)
|
||
return x64FusedStoreTail(b, n, pairRegs, offs, pool, src)
|
||
}
|
||
|
||
// x64FusedStoreTail stages the store's v128 value and performs the
|
||
// store proper (the f16 conversion chain included); the caller has
|
||
// already materialized the checked effective address in R12. Shared by
|
||
// the wasm32 and memory64 address paths.
|
||
func x64FusedStoreTail(b *strings.Builder, n simdfuse.Node,
|
||
pairRegs func(simdfuse.Arg) (string, string), offs *ModuleOffsets, pool *ConstPool, src int) error {
|
||
if src < 0 {
|
||
lo, hi := pairRegs(n.Args[2])
|
||
fmt.Fprintf(b, "\tMOVQ %s, X1\n", lo)
|
||
fmt.Fprintf(b, "\tPINSRQ $1, %s, X1\n", hi)
|
||
src = 1
|
||
}
|
||
if n.Op == "v128_f16x4_cvt_store" {
|
||
// f32x4 -> packed f16x4 with the software idiom's semantics
|
||
// (see simd_f16x4_cvt_bits_scalar): NaN forces sign|0x7E00,
|
||
// non-NaN goes through the bias/multiply rounding chain the
|
||
// wasm code performs, reproduced lane-wise in SSE. Scratch
|
||
// X0..X3; w lives in X0 (copied there when the source sits
|
||
// elsewhere).
|
||
if pool == nil {
|
||
return fmt.Errorf("fused store: %s needs a const pool", n.Op)
|
||
}
|
||
c4 := func(w uint32) string {
|
||
blob := make([]byte, 16)
|
||
for i := 0; i < 4; i++ {
|
||
blob[i*4] = byte(w)
|
||
blob[i*4+1] = byte(w >> 8)
|
||
blob[i*4+2] = byte(w >> 16)
|
||
blob[i*4+3] = byte(w >> 24)
|
||
}
|
||
return pool.addBlob(blob)
|
||
}
|
||
if src != 0 {
|
||
fmt.Fprintf(b, "\tMOVOU X%d, X0\n", src)
|
||
}
|
||
// NaN mask: |w| >u 0x7F800000, via the signed-compare bias trick.
|
||
b.WriteString("\tMOVOU X0, X1\n")
|
||
fmt.Fprintf(b, "\tPAND ·%s(SB), X1\n", c4(0x7FFFFFFF))
|
||
fmt.Fprintf(b, "\tPXOR ·%s(SB), X1\n", c4(0x80000000))
|
||
fmt.Fprintf(b, "\tMOVOU ·%s(SB), X2\n", c4(0xFF800000))
|
||
b.WriteString("\tPCMPGTL X2, X1\n")
|
||
// A = sign | (mask & 0x7E00)
|
||
b.WriteString("\tMOVOU X0, X2\n")
|
||
b.WriteString("\tPSRLL $16, X2\n")
|
||
fmt.Fprintf(b, "\tPAND ·%s(SB), X2\n", c4(0x8000))
|
||
b.WriteString("\tMOVOU X1, X3\n")
|
||
fmt.Fprintf(b, "\tPAND ·%s(SB), X3\n", c4(0x7E00))
|
||
b.WriteString("\tPOR X3, X2\n")
|
||
// bias = max(shl1 & 0xFF000000, 0x71000000)>>1 + 0x07800000
|
||
b.WriteString("\tMOVOU X0, X3\n")
|
||
b.WriteString("\tPSLLL $1, X3\n")
|
||
fmt.Fprintf(b, "\tPAND ·%s(SB), X3\n", c4(0xFF000000))
|
||
fmt.Fprintf(b, "\tPMAXUD ·%s(SB), X3\n", c4(0x71000000))
|
||
b.WriteString("\tPSRLL $1, X3\n")
|
||
fmt.Fprintf(b, "\tPADDL ·%s(SB), X3\n", c4(0x07800000))
|
||
// f = |w| * 0x1p+112 * 0x1p-110 + as_float(bias)
|
||
fmt.Fprintf(b, "\tPAND ·%s(SB), X0\n", c4(0x7FFFFFFF))
|
||
fmt.Fprintf(b, "\tMULPS ·%s(SB), X0\n", c4(0x77800000))
|
||
fmt.Fprintf(b, "\tMULPS ·%s(SB), X0\n", c4(0x08800000))
|
||
b.WriteString("\tADDPS X3, X0\n")
|
||
// h_nonnan = ((fbits>>13) & 0x7C00) + (fbits & 0xFFF)
|
||
b.WriteString("\tMOVOU X0, X3\n")
|
||
b.WriteString("\tPSRLL $13, X3\n")
|
||
fmt.Fprintf(b, "\tPAND ·%s(SB), X3\n", c4(0x7C00))
|
||
fmt.Fprintf(b, "\tPAND ·%s(SB), X0\n", c4(0xFFF))
|
||
b.WriteString("\tPADDL X0, X3\n")
|
||
// h = A | (~mask & h_nonnan); pack to 4 u16; 8-byte store.
|
||
b.WriteString("\tPANDN X3, X1\n")
|
||
b.WriteString("\tPOR X1, X2\n")
|
||
b.WriteString("\tPACKUSDW X2, X2\n")
|
||
fmt.Fprintf(b, "\tMOVQ X2, (%s)(R12*1)\n", x64MemBase(b, offs))
|
||
return nil
|
||
}
|
||
fmt.Fprintf(b, "\tMOVOU X%d, (%s)(R12*1)\n", src, x64MemBase(b, offs))
|
||
return nil
|
||
}
|
||
|
||
// x64SpliceLoop is the amd64 fused-loop synthesizer; see the arm64
|
||
// twin for the structure.
|
||
func x64SpliceLoop(b *strings.Builder, loop *simdfuse.Loop, pool *ConstPool, offs *ModuleOffsets, site string, portable bool, maxOut int) (bool, bool, error) {
|
||
// Scalars, the counter, and stride deltas must be
|
||
// register-resident (the loop indexes and bumps them in place);
|
||
// a wider signature keeps the helper CALL.
|
||
if 1+loop.Tree.NumScalars+1+loop.NumDeltas > len(x64FusedArgRegs) {
|
||
return false, false, fmt.Errorf("fused loop %s: %w: scalars past the register file", loop.Tree.Name, errFusedCapacity)
|
||
}
|
||
// x64Dot8Rewrite preserves node indices (absorbed nodes become
|
||
// elided placeholders), so CarriedPairs/roots/bumps stay valid.
|
||
tree, usedAVX := x64Dot8Rewrite(loop.Tree, portable)
|
||
if err := x64FusedProlog(b, tree, offs); err != nil {
|
||
return false, false, err
|
||
}
|
||
// Pin m.M / memSize for the whole loop: every iteration's member
|
||
// accesses then skip the per-site Module loads.
|
||
offs = x64HoistMem(b, offs, tree, 1+tree.NumScalars+1+loop.NumDeltas+2*tree.NumPairs)
|
||
// Hoist the strided window checks out of the loop: when the
|
||
// iteration count is computable at entry, ONE check of
|
||
// maxAddress = base + end + (iters-1)*delta
|
||
// against memSize covers every iteration's accesses and the loads
|
||
// inside the loop drop to single indexed instructions. Trapping at
|
||
// the loop head instead of the precise access is the same
|
||
// relaxation the windowed range check already makes. memory64
|
||
// trees only (all address terms add at full width there). Two loop
|
||
// forms qualify: the pretest power-of-two-step loop (iters =
|
||
// counter>>log2(Dec)) and the do-while floor loop with a unit step
|
||
// (iters = max(counter - thresh, 1) — the body runs at least
|
||
// once). Addresses may be a scalar argument, a scalar+const sum,
|
||
// or a scalar-chain ADD tree over those — the interleaved kernels
|
||
// derive every load address as bumpedBase + columnBase + const.
|
||
hoistedTrap := false
|
||
hoistForm := 0 // 1 = pretest pow2, 2 = do-while ExitGT unit-step
|
||
sh := 0
|
||
if tree.Addr64 && loop.Dec > 0 {
|
||
switch {
|
||
case loop.PreTest && loop.Dec&(loop.Dec-1) == 0:
|
||
hoistForm = 1
|
||
for d := loop.Dec; d > 1; d >>= 1 {
|
||
sh++
|
||
}
|
||
case !loop.PreTest && loop.ExitGT && loop.Dec == 1 && loop.ExitThresh >= 0:
|
||
hoistForm = 2
|
||
}
|
||
}
|
||
if hoistForm != 0 {
|
||
deltaOf := map[int]int64{}
|
||
dynamic := map[int]bool{}
|
||
for _, bump := range loop.Bumps {
|
||
if bump.DeltaScalar >= 0 {
|
||
dynamic[bump.Scalar] = true
|
||
} else {
|
||
deltaOf[bump.Scalar] += int64(bump.Delta)
|
||
}
|
||
}
|
||
nodes := append([]simdfuse.Node(nil), tree.Nodes...)
|
||
ctrReg := x64FusedArgRegs[1+loop.CounterScalar]
|
||
// resolveAffine flattens a load address into scalar-argument
|
||
// terms plus a constant: ArgScalar/ArgSum directly, or a
|
||
// scalar-chain add tree over those.
|
||
var resolveAffine func(a simdfuse.Arg, depth int) ([]int, int64, bool)
|
||
resolveAffine = func(a simdfuse.Arg, depth int) ([]int, int64, bool) {
|
||
if depth > 6 {
|
||
return nil, 0, false
|
||
}
|
||
switch a.Kind {
|
||
case simdfuse.ArgScalar:
|
||
return []int{a.Index}, 0, true
|
||
case simdfuse.ArgSum:
|
||
return []int{a.Index}, int64(a.Const), true
|
||
case simdfuse.ArgConst:
|
||
return nil, int64(a.Const), true
|
||
case simdfuse.ArgNode:
|
||
nd := &nodes[a.Index]
|
||
if nd.Op != "scalar_i32_add" || len(nd.Args) != 2 {
|
||
return nil, 0, false
|
||
}
|
||
t1, c1, ok1 := resolveAffine(nd.Args[0], depth+1)
|
||
if !ok1 {
|
||
return nil, 0, false
|
||
}
|
||
t2, c2, ok2 := resolveAffine(nd.Args[1], depth+1)
|
||
if !ok2 {
|
||
return nil, 0, false
|
||
}
|
||
return append(append([]int{}, t1...), t2...), c1 + c2, true
|
||
}
|
||
return nil, 0, false
|
||
}
|
||
// emitIters leaves (iters-1) in R12. Form 1 branches to after
|
||
// the check when the loop runs zero times; form 2 always runs
|
||
// at least once and clamps (counter - thresh - 1) at zero.
|
||
ctrMov := "MOVL"
|
||
if loop.CounterWide {
|
||
ctrMov = "MOVQ"
|
||
}
|
||
emitIters := func(lbl string) (post string) {
|
||
fmt.Fprintf(b, "\t%s %s, R12\n", ctrMov, ctrReg)
|
||
if hoistForm == 1 {
|
||
if sh > 0 {
|
||
fmt.Fprintf(b, "\tSHRQ $%d, R12\n", sh)
|
||
}
|
||
b.WriteString("\tTESTQ R12, R12\n")
|
||
fmt.Fprintf(b, "\tJZ %s\n", lbl)
|
||
b.WriteString("\tSUBQ $1, R12\n")
|
||
return lbl
|
||
}
|
||
fmt.Fprintf(b, "\tSUBQ $%d, R12\n", int64(loop.ExitThresh)+1)
|
||
fmt.Fprintf(b, "\tJGE %s\n", lbl)
|
||
b.WriteString("\tXORL R12, R12\n")
|
||
fmt.Fprintf(b, "%s:\n", lbl)
|
||
return ""
|
||
}
|
||
checkedSpans := map[string]bool{}
|
||
rewrote := false
|
||
for i := range nodes {
|
||
n := &nodes[i]
|
||
var width int64
|
||
var ncOp string
|
||
switch n.Op {
|
||
case "v128_load_rng":
|
||
ncOp = "v128_load_nc"
|
||
case "v128_load":
|
||
width, ncOp = 16, "v128_load_nc"
|
||
case "v128_load32_splat":
|
||
width, ncOp = 4, "v128_load32_splat_nc"
|
||
case "v128_load16x4_u":
|
||
width, ncOp = 8, "v128_load16x4_u_nc"
|
||
case "v128_load32_zero":
|
||
width, ncOp = 4, "v128_load32_zero_nc"
|
||
case "v128_load64_zero":
|
||
width, ncOp = 8, "v128_load64_zero_nc"
|
||
default:
|
||
continue
|
||
}
|
||
terms, base, aok := resolveAffine(n.Args[0], 0)
|
||
if !aok {
|
||
continue
|
||
}
|
||
var d int64
|
||
bad := false
|
||
for _, tm := range terms {
|
||
if dynamic[tm] {
|
||
bad = true
|
||
}
|
||
d += deltaOf[tm]
|
||
}
|
||
if bad || d < 0 || d > 1<<20 {
|
||
continue
|
||
}
|
||
var end int64
|
||
if n.Op == "v128_load_rng" {
|
||
rlo, span := n.Args[2], n.Args[3]
|
||
if rlo.Kind != simdfuse.ArgConst || span.Kind != simdfuse.ArgConst || rlo.Const < 0 {
|
||
continue
|
||
}
|
||
end = base + int64(rlo.Const) + int64(uint32(span.Const))
|
||
} else {
|
||
off := n.Args[1]
|
||
if off.Kind != simdfuse.ArgConst || off.Const < 0 {
|
||
continue
|
||
}
|
||
end = base + int64(off.Const) + width
|
||
}
|
||
key := fmt.Sprintf("%v/%d/%d", terms, end, d)
|
||
if !checkedSpans[key] {
|
||
checkedSpans[key] = true
|
||
post := emitIters(fmt.Sprintf("gcasmfxh%s_%d", site, i))
|
||
if d != 0 {
|
||
fmt.Fprintf(b, "\tIMUL3Q $%d, R12, R12\n", d)
|
||
}
|
||
for _, tm := range terms {
|
||
fmt.Fprintf(b, "\tADDQ %s, R12\n", x64FusedArgRegs[1+tm])
|
||
}
|
||
fmt.Fprintf(b, "\tADDQ $%d, R12\n", end)
|
||
x64MemSizeCheck(b, offs)
|
||
if post != "" {
|
||
fmt.Fprintf(b, "%s:\n", post)
|
||
}
|
||
hoistedTrap = true
|
||
}
|
||
n.Op = ncOp
|
||
n.Args = n.Args[:2]
|
||
rewrote = true
|
||
}
|
||
if rewrote {
|
||
nt := *tree
|
||
nt.Nodes = nodes
|
||
tree = &nt
|
||
}
|
||
}
|
||
if loop.Dec <= 0 {
|
||
return false, false, fmt.Errorf("fused loop %s: bad dec %d", tree.Name, loop.Dec)
|
||
}
|
||
if loop.ExitGT && (loop.PreTest || loop.ExitThresh < 0) {
|
||
return false, false, fmt.Errorf("fused loop %s: bad exit threshold %d", tree.Name, loop.ExitThresh)
|
||
}
|
||
poolTop := 14
|
||
if tree.NumFloats > 0 {
|
||
poolTop = 13
|
||
}
|
||
carried := map[int]int{}
|
||
var reserve []int
|
||
for j, cp := range loop.CarriedPairs {
|
||
r := poolTop - j
|
||
carried[cp[0]] = r
|
||
reserve = append(reserve, r)
|
||
base := 1 + tree.NumScalars + 1 + loop.NumDeltas + 2*cp[0]
|
||
fmt.Fprintf(b, "\tMOVQ %s, X%d\n", x64FusedIntArg(base, maxOut), r)
|
||
fmt.Fprintf(b, "\tPINSRQ $1, %s, X%d\n", x64FusedIntArg(base+1, maxOut), r)
|
||
}
|
||
counterReg := x64FusedArgRegs[1+loop.CounterScalar]
|
||
cmpOp, subOp := "CMPL", "SUBL"
|
||
if loop.CounterWide {
|
||
cmpOp, subOp = "CMPQ", "SUBQ"
|
||
}
|
||
loopLabel := "gcasmfxl" + site
|
||
exitLabel := "gcasmfxlx" + site
|
||
fmt.Fprintf(b, "%s:\n", loopLabel)
|
||
if loop.PreTest {
|
||
fmt.Fprintf(b, "\t%s %s, $%d\n", cmpOp, counterReg, loop.Dec)
|
||
fmt.Fprintf(b, "\tJB %s\n", exitLabel)
|
||
}
|
||
loc, needsTrap, err := x64SpliceFusedCore(b, tree, pool, offs, carried, reserve, 1+loop.NumDeltas, true, portable, maxOut)
|
||
if err != nil {
|
||
return false, false, err
|
||
}
|
||
for j, cp := range loop.CarriedPairs {
|
||
src := 0
|
||
if !loc[cp[1]].chained {
|
||
src = loc[cp[1]].pool
|
||
}
|
||
x64VecCopy(b, poolTop-j, src)
|
||
}
|
||
// Bumps wrap mod 2^32 on wasm32; an Addr64 loop's pointer scalars
|
||
// advance at full width (the body's per-node bounds checks keep a
|
||
// runaway pointer from being dereferenced).
|
||
bumpAdd := "ADDL"
|
||
if tree.Addr64 {
|
||
bumpAdd = "ADDQ"
|
||
}
|
||
for _, bump := range loop.Bumps {
|
||
if bump.DeltaScalar >= 0 {
|
||
fmt.Fprintf(b, "\t%s %s, %s\n", bumpAdd,
|
||
x64FusedArgRegs[1+tree.NumScalars+1+bump.DeltaScalar], x64FusedArgRegs[1+bump.Scalar])
|
||
continue
|
||
}
|
||
fmt.Fprintf(b, "\t%s $%d, %s\n", bumpAdd, bump.Delta, x64FusedArgRegs[1+bump.Scalar])
|
||
}
|
||
fmt.Fprintf(b, "\t%s $%d, %s\n", subOp, loop.Dec, counterReg)
|
||
switch {
|
||
case loop.PreTest:
|
||
fmt.Fprintf(b, "\tJMP %s\n", loopLabel)
|
||
fmt.Fprintf(b, "%s:\n", exitLabel)
|
||
case loop.ExitGT:
|
||
// Signed floor test: repeat while counter > ExitThresh.
|
||
fmt.Fprintf(b, "\t%s %s, $%d\n", cmpOp, counterReg, loop.ExitThresh)
|
||
fmt.Fprintf(b, "\tJGT %s\n", loopLabel)
|
||
default:
|
||
fmt.Fprintf(b, "\tJNE %s\n", loopLabel)
|
||
}
|
||
roots := tree.RootList()
|
||
moves := map[int]int{}
|
||
for j, xs := range loop.ExitScalars {
|
||
moves[2*len(roots)+j] = 1 + xs
|
||
}
|
||
wideDst := map[int]bool{}
|
||
for j, xs := range loop.ExitScalars {
|
||
if (xs == loop.CounterScalar && loop.CounterWide) || (tree.Addr64 && xs != loop.CounterScalar) {
|
||
// Addr64 exit scalars are the int64 pointer parameters
|
||
// (the counter keeps its own declared width).
|
||
wideDst[2*len(roots)+j] = true
|
||
}
|
||
}
|
||
movFor := func(dst int) string {
|
||
if wideDst[dst] {
|
||
return "MOVQ"
|
||
}
|
||
return "MOVL"
|
||
}
|
||
// Result slot locations: root pair halves (8 bytes each), then the
|
||
// exit scalars (4, or 8 for a wide counter). Slots past the
|
||
// register file land in the stack results area, which follows the
|
||
// integer argument overflow.
|
||
var resSizes []int
|
||
for range roots {
|
||
resSizes = append(resSizes, 8, 8)
|
||
}
|
||
for j := range loop.ExitScalars {
|
||
if wideDst[2*len(roots)+j] {
|
||
resSizes = append(resSizes, 8)
|
||
} else {
|
||
resSizes = append(resSizes, 4)
|
||
}
|
||
}
|
||
totalIntArgs := 1 + tree.NumScalars + 1 + loop.NumDeltas + 2*tree.NumPairs
|
||
argStackBytes := 0
|
||
if over := totalIntArgs - len(x64FusedArgRegs); over > 0 {
|
||
argStackBytes = 8 * over
|
||
}
|
||
resLocs := x64FusedResultLocs(resSizes, argStackBytes, maxOut)
|
||
emitParallelMoves(b, moves, func(dst, src int) string {
|
||
return fmt.Sprintf("\t%s %s, %s\n", movFor(dst), x64FusedArgRegs[src], resLocs[dst])
|
||
}, func(dst, src int) (string, string) {
|
||
return fmt.Sprintf("\t%s %s, R12\n", movFor(dst), x64FusedArgRegs[src]),
|
||
fmt.Sprintf("\t%s R12, %s\n", movFor(dst), resLocs[dst])
|
||
})
|
||
carriedOf := map[int]int{}
|
||
for j, cp := range loop.CarriedPairs {
|
||
carriedOf[cp[1]] = poolTop - j
|
||
}
|
||
for k, r := range roots {
|
||
src, ok := carriedOf[r]
|
||
if !ok {
|
||
src = 0
|
||
if !loc[r].chained {
|
||
src = loc[r].pool
|
||
}
|
||
}
|
||
fmt.Fprintf(b, "\tMOVQ X%d, %s\n", src, resLocs[2*k])
|
||
if lo := resLocs[2*k+1]; strings.HasSuffix(lo, "(SP)") {
|
||
fmt.Fprintf(b, "\tPEXTRQ $1, X%d, R12\n", src)
|
||
fmt.Fprintf(b, "\tMOVQ R12, %s\n", lo)
|
||
} else {
|
||
fmt.Fprintf(b, "\tPEXTRQ $1, X%d, %s\n", src, lo)
|
||
}
|
||
}
|
||
if usedAVX && x64TreeVexClean(tree) {
|
||
// Fully VEX region: one deferred upper clear at the exit.
|
||
b.WriteString("\tVZEROUPPER\n")
|
||
}
|
||
return true, needsTrap || hoistedTrap, nil
|
||
}
|
||
|
||
// x64VexOps maps ops with a VEX three-operand register form to their
|
||
// direct-destination emitters. Only all-vector-argument shapes qualify
|
||
// (x64VexArgsOK); everything else keeps the canned SSE paste.
|
||
var x64VexOps = map[string]func(b *strings.Builder, srcs []string, dst int){
|
||
"i32x4_add": func(b *strings.Builder, s []string, d int) {
|
||
fmt.Fprintf(b, "\tVPADDD %s, %s, X%d\n", s[1], s[0], d)
|
||
},
|
||
"i16x8_add": func(b *strings.Builder, s []string, d int) {
|
||
fmt.Fprintf(b, "\tVPADDW %s, %s, X%d\n", s[1], s[0], d)
|
||
},
|
||
"i32x4_sub": func(b *strings.Builder, s []string, d int) {
|
||
fmt.Fprintf(b, "\tVPSUBD %s, %s, X%d\n", s[1], s[0], d)
|
||
},
|
||
"f32x4_add": func(b *strings.Builder, s []string, d int) {
|
||
fmt.Fprintf(b, "\tVADDPS %s, %s, X%d\n", s[1], s[0], d)
|
||
},
|
||
"f32x4_mul": func(b *strings.Builder, s []string, d int) {
|
||
fmt.Fprintf(b, "\tVMULPS %s, %s, X%d\n", s[1], s[0], d)
|
||
},
|
||
"f32x4_sub": func(b *strings.Builder, s []string, d int) {
|
||
fmt.Fprintf(b, "\tVSUBPS %s, %s, X%d\n", s[1], s[0], d)
|
||
},
|
||
"f32x4_convert_i32x4_s": func(b *strings.Builder, s []string, d int) {
|
||
fmt.Fprintf(b, "\tVCVTDQ2PS %s, X%d\n", s[0], d)
|
||
},
|
||
"i32x4_dot_i16x8_s": func(b *strings.Builder, s []string, d int) {
|
||
fmt.Fprintf(b, "\tVPMADDWD %s, %s, X%d\n", s[1], s[0], d)
|
||
},
|
||
}
|
||
|
||
// x64TreeVexClean reports whether a rewritten tree's emission is
|
||
// entirely VEX-encoded (loads, the VEX op table, block dots, splats,
|
||
// the scalar chains): then legacy-SSE code never executes between a
|
||
// 256-bit op and the region exit, so ONE VZEROUPPER at the exit
|
||
// replaces the per-block-dot clears (the Intel dirty-upper hazard
|
||
// applies to legacy SSE only; VEX-128 ops are safe under dirty
|
||
// uppers).
|
||
func x64TreeVexClean(tree *simdfuse.Tree) bool {
|
||
return x64NodesVexClean(tree.Nodes)
|
||
}
|
||
|
||
func x64NodesVexClean(nodes []simdfuse.Node) bool {
|
||
for _, n := range nodes {
|
||
if n.Op == a64OpElided || n.Op == x64OpBlockDot ||
|
||
n.Op == x64OpRawDot || n.Op == x64OpRawDotAcc ||
|
||
n.Op == x64OpRawDotWide || n.Op == x64OpRawDotWideAcc ||
|
||
n.Op == x64OpRawDotFold || n.Op == "f32x4_splat" {
|
||
continue
|
||
}
|
||
if n.Op == "f16x4_cvt" {
|
||
// VEX-128 VCVTPH2PS sequence in feature bodies — and the
|
||
// caller only consults this in feature bodies (usedAVX
|
||
// and vexClean are both !portable-gated).
|
||
continue
|
||
}
|
||
if n.Class() != simdfuse.ClassV128 {
|
||
continue // scalar chains emit VEX FP ops
|
||
}
|
||
if simdfuse.IsStore(n.Op) || n.Op == "v128_load32_lane" {
|
||
return false
|
||
}
|
||
if _, isMem := fusedMemOpsA64[n.Op]; isMem {
|
||
continue
|
||
}
|
||
if _, isVex := x64VexOps[n.Op]; isVex && x64VexArgsOK(n) {
|
||
continue
|
||
}
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
// x64PairFold collapses an 8-lane VPMADDWD pair-domain ymm value to
|
||
// the four-consecutive-byte i32x4 grouping: lane j of the result is
|
||
// P2j + P2j+1. Y2/X3 are scratch; dst may equal src.
|
||
func x64PairFold(b *strings.Builder, src, dst int) {
|
||
fmt.Fprintf(b, "\tVPHADDD Y%d, Y%d, Y2\n", src, src)
|
||
b.WriteString("\tVEXTRACTI128 $1, Y2, X3\n")
|
||
fmt.Fprintf(b, "\tVPUNPCKLQDQ X3, X2, X%d\n", dst)
|
||
}
|
||
|
||
// x64VexArgsOK reports whether every argument is a register-resident
|
||
// vector (node result or pair input) — the shapes the VEX emitters
|
||
// handle without staging beyond X2/X3.
|
||
func x64VexArgsOK(n simdfuse.Node) bool {
|
||
if len(n.Args) == 0 {
|
||
return false
|
||
}
|
||
for _, a := range n.Args {
|
||
if a.Kind != simdfuse.ArgNode && a.Kind != simdfuse.ArgPairIn {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|