Skip to content

cmd/compile: struct literal assignment to non-local targets generates unnecessary stack temporary #78597

Description

@chabbimilind

Go version

go version devel go1.27-5d6aa23e5b (tip)

Also reproducible on go1.24.6 and earlier — this has been the behavior since the optimization was first introduced.

What operating system and processor architecture are you using?

darwin/arm64 (Apple M1 Max). Also affects amd64 and other architectures.

What did you do?

A common pattern in Go services is to populate a struct and store it through a pointer, into a slice, or into a global:

package p

type User struct {
	ID          int
	Name        string
	Email       string
	Age         int
	IsActive    bool
	Balance     float64
	CreatedAt   int64
	UpdatedAt   int64
	Country     string
	City        string
	PhoneNumber string
	Department  string
}

var ptr *User

//go:noinline
func storeUserWithStruct(id int, name string, email string, age int, isActive bool, balance float64, createdAt int64, updatedAt int64, country string, city string, phoneNumber string, department string) {
	*ptr = User{
		ID:          id,
		Name:        name,
		Email:       email,
		Age:         age,
		IsActive:    isActive,
		Balance:     balance,
		CreatedAt:   createdAt,
		UpdatedAt:   updatedAt,
		Country:     country,
		City:        city,
		PhoneNumber: phoneNumber,
		Department:  department,
	}
}

//go:noinline
func storeUserFieldByField(id int, name string, email string, age int, isActive bool, balance float64, createdAt int64, updatedAt int64, country string, city string, phoneNumber string, department string) {
	ptr.ID = id
	ptr.Name = name
	ptr.Email = email
	ptr.Age = age
	ptr.IsActive = isActive
	ptr.Balance = balance
	ptr.CreatedAt = createdAt
	ptr.UpdatedAt = updatedAt
	ptr.Country = country
	ptr.City = city
	ptr.PhoneNumber = phoneNumber
	ptr.Department = department
}

These two functions are semantically equivalent. Compiled with go build -gcflags='-S' repro.go.

What did you expect to see?

Both functions should generate similar code — direct field stores to the destination.

For a local variable, oaslit in walk/complit.go already decomposes x := S{A: a, B: b} into x.A = a; x.B = b (avoiding a temporary). The same decomposition should apply when the LHS is any addressable target.

What did you see?

storeUserWithStruct allocates a large stack frame with a stack temporary for the entire struct. It zeroes the temporary, fills each field into the temporary, then does a single wbMove (bulk copy with write barrier) to copy the entire struct to *ptr:

; storeUserWithStruct — current compiler output (arm64)
TEXT  storeUserWithStruct(SB), $192-144   ; ← 192-byte frame (144 bytes for stack temp)

; Step 1: Zero the 144-byte stack temporary (9 STP instructions)
MOVD  $autotmp_12-144(SP), R15
STP   (ZR, ZR), (R15)
STP   (ZR, ZR), 16(R15)
STP   (ZR, ZR), 32(R15)
STP   (ZR, ZR), 48(R15)
STP   (ZR, ZR), 64(R15)
STP   (ZR, ZR), 80(R15)
STP   (ZR, ZR), 96(R15)
STP   (ZR, ZR), 112(R15)
STP   (ZR, ZR), 128(R15)

; Step 2: Fill all 12 fields into the temporary
STP   (R0, R1), autotmp_12-144(SP)   ; temp.ID, temp.Name.ptr
STP   (R2, R3), autotmp_12-128(SP)   ; temp.Name.len, temp.Email.ptr
; ... 8 more stores to temp ...

; Step 3: Bulk copy temp → *ptr (144 bytes with write barrier)
MOVD  ptr(SB), R1
CALL  runtime.wbMove(SB)            ; ← expensive bulk copy of entire struct

Meanwhile storeUserFieldByField stores directly to *ptr with individual write barriers — no temporary, no bulk copy:

; storeUserFieldByField — current compiler output (arm64)
TEXT  storeUserFieldByField(SB), $16-144   ; ← 16-byte frame, no temporary

MOVD  ptr(SB), R14                ; load pointer once
MOVD  R0, (R14)                   ; ptr.ID = id (direct store)
; ... individual field stores with per-field write barriers ...

The struct literal form is ~2x slower than the field-by-field form in benchmarks. On an 8-field struct:

BenchmarkStructLitAssign/SliceLiteral-10      651735466    9.042 ns/op
BenchmarkStructLitAssign/SliceFieldByField-10  1000000000  4.553 ns/op

The same gap applies to all non-local addressable LHS targets (globals, pointer deref, struct fields through pointer).

Root cause

In cmd/compile/internal/walk/complit.go, oaslit() decomposes composite literal assignments into field-by-field stores, but guards this optimization with isSimpleName(n.X) which requires the LHS to be an ir.ONAME node that is OnStack(). This rejects all non-local-variable targets:

  • s[i] = S{...} — slice index (OINDEX)
  • globalVar = S{...} — global (ONAME but not OnStack())
  • *ptr = S{...} — pointer dereference (ODEREF)
  • ptr.Field = S{...} — struct field through pointer (ODOT / OXDOT)

When oaslit returns false, walkCompLit creates a stack temporary via typecheck.TempAt, and the assignment becomes temp = S{...}; dst = temp — with the first part decomposed (since temp is a simple name) and the second being a full struct copy.

Proposed solution

Extend oaslit with a second path for addressable non-name targets: take the address of the destination once (evaluating any index/bounds check exactly once), then pass the dereferenced pointer to anylit which decomposes the literal into field-by-field stores through that pointer.

Safety is ensured by checking that all RHS values are constants, nil, or stack-local non-addrtaken names — these cannot alias with any addressable destination, so writing field-by-field is safe.

For partially-initialized literals (e.g., s[i] = S{A: a}), anylit zeroes the destination in-place before filling the specified fields. This still saves the bulk copy — the temp path would zero a temp + copy the temp, while the direct path just zeroes the destination.

I have a working implementation with benchmarks and codegen tests that shows:

                                 │  baseline   │             optimized              │
                                 │   sec/op    │   sec/op     vs base               │
StructLitAssign/SliceLiteral       8.91n ± 1%    4.57n ± 2%  -48.75% (p=0.000 n=8)
StructLitAssign/Global             8.04n ± 1%    3.97n ± 1%  -50.55% (p=0.000 n=8)
StructLitAssign/PtrDeref           8.22n ± 0%    3.84n ± 1%  -53.37% (p=0.000 n=8)
StructLitAssign/Nested             3.99n ± 1%    2.93n ± 2%  -26.62% (p=0.000 n=8)
StructLitAssign/Embedded           4.01n ± 1%    2.91n ± 1%  -27.32% (p=0.000 n=8)
StructLitAssign/FuncCallRHS        9.17n ± 1%    5.62n ± 1%  -38.75% (p=0.000 n=8)
StructLitAssign/PartialHalf        5.79n ± 1%    4.12n ± 1%  -28.74% (p=0.000 n=8)

Cases where the RHS values may alias the destination (e.g., a global variable on the RHS) correctly fall back to the original path with no regression.

Happy to send a CL.

Metadata

Metadata

Assignees

Labels

ImplementationIssues describing a semantics-preserving change to the Go implementation.NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.Performancecompiler/runtimeIssues related to the Go compiler and/or runtime.

Type

No type

Fields

No fields configured for issues without a type.

Projects

Status
In Progress

Relationships

None yet

Development

No branches or pull requests

Issue actions