Skip to content

cmd/compile: simd.Float32s.Neg rebuilds its sign-bit constant on every loop iteration #81846

Open

Description

@mauri870

Go version

go version go1.27.1 linux/amd64 / gotip

Output of go env in your module/workspace:

GOOS=linux
GOARCH=amd64
GOAMD64=v1
GOEXPERIMENT=simd

What did you do?

Found this while working on https://github.com/mauri870/loopvec.

Compiled and disassembled a loop that calls Float32s.Neg:

package main

import "simd"

func neg(dst, a []float32) {
	for i := 0; i < len(dst); {
		v, n := simd.LoadFloat32sPart(a[i:])
		v.Neg().StorePart(dst[i:])
		i += n
	}
}

func main() {
	a := make([]float32, 100)
	d := make([]float32, 100)
	neg(d, a)
}
GOEXPERIMENT=simd go build -o neg .
go tool objdump -s 'main\.neg@simd512$' neg

neg is a dispatcher that picks neg@simd128, neg@simd256 or neg@simd512
on every call; the loop body lives in the width-specific clone.

What did you see happen?

Neg lowers to an XOR with a sign-bit vector, but the instructions that
build that vector are emitted at the use site, inside the loop body, instead
of once before the loop. On go1.27.1 the relevant part of neg@simd512
is:

4a3898  ...                          ; loop head (the back edge jumps here)
4a38d6  VMOVDQU64 0(DX), Z1          ; full-width load
4a3922  MOVL $-0x80000000, DX        ; constant rebuilt every iteration
4a3927  VMOVD DX, X2
4a392b  VPBROADCASTD X2, Z2
4a3937  VPXORD Z2, Z1, Z1            ; the actual negation
4a3947  VMOVDQU64 Z1, 0(R9)          ; full-width store
4a394d  JMP 0x4a3898                 ; back edge, before the constant rebuild

On gotip (go1.28-devel_2ca679b1f1) the same sequence appears, at shifted
addresses:

49cfd2  ...                          ; loop head
49d016  VMOVDQU64 0(DX), Z0
49d05c  MOVL $-0x80000000, DX        ; constant rebuilt every iteration
49d061  VMOVD DX, X1
49d065  VPBROADCASTD X1, Z1
49d071  VPXORD Z1, Z0, Z0
49d07e  VMOVDQU64 Z0, 0(R9)
49d084  JMP 0x49cfd2                 ; back edge, before the constant rebuild

In both, the MOVL $-0x80000000 / VMOVD / VPBROADCASTD sequence sits
between the loop head and the back-edge jump target, so it re-executes every
iteration. The result is correct; it is three extra instructions and a
dependency chain per iteration for a value that never changes.

What did you expect to see?

The sign-bit constant materialized once before the loop and kept live in a
register across iterations, the way a user-hoisted constant already is.

Activity

  1. added
    NeedsFixThe path to resolution is known, but the work has not been done.
    compiler/runtimeIssues related to the Go compiler and/or runtime.
    on Sep 29, 2026
  2. mauri870 commented on Sep 29, 2026

    @mauri870
    MemberAuthor

    cc @golang/compiler

  3. prattmic commented on Sep 29, 2026

    @prattmic
    Member
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsFixThe path to resolution is known, but the work has not been done.Performancecompiler/runtimeIssues related to the Go compiler and/or runtime.

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions