Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/compile: shallow copy of a struct does not always compile as MOVL #53810

Closed
LeGamerDc opened this issue Jul 12, 2022 · 6 comments
Closed
Labels
FrozenDueToAge NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Performance WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided.
Milestone

Comments

@LeGamerDc
Copy link

type Example struct {
  a, b int32
}
x := Example{x:1, y:1}
y := x

the y:=x will be compiled to two movl while the x:=Example{x:1, y:1} compiled to single movq.
it confuse me about the memory bevavior, assumpt we have one routine write x=Example{x:1, y:1} and another write x=Example{x:2, y:2}, according to the memory model, x will either be x:1,y:1 or x:2, y:2. how ever if we do

y:=x
fmt.Println(y.a, y.b)

then, we could get x:1, y:2 !!!

@gopherbot gopherbot added this to the Proposal milestone Jul 12, 2022
@mvdan
Copy link
Member

mvdan commented Jul 12, 2022

This doesn't need to be a proposal - it seems like an instance where the compiler could do a better job.

@mvdan mvdan changed the title proposal: copy struct of 64bit use one movq affected/package: 1.18.1 cmd/compile: shallow copy of a struct does not always compile as MOVL Jul 12, 2022
@mvdan mvdan added Performance NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. and removed Proposal labels Jul 12, 2022
@mvdan mvdan removed this from the Proposal milestone Jul 12, 2022
@mvdan
Copy link
Member

mvdan commented Jul 12, 2022

cc @randall77

@ianlancetaylor ianlancetaylor added this to the Backlog milestone Jul 13, 2022
@randall77
Copy link
Contributor

The compiler already does the optimization you're requesting:

type Example struct {
  a, b int32
}
func f(x, y *Example) {
	*x = *y
}

compiles to 2 MOVQs, at least on amd64. If you're seeing something different, can you please post a complete compilable example that shows the problem?

There is also the semantics question:

it confuse me about the memory behavior, assume we have one routine write x=Example{x:1, y:1} and another write x=Example{x:2, y:2}, according to the memory model, x will either be x:1,y:1 or x:2, y:2.

I don't think it is mentioned in the memory model spec, but each field of a struct is its own variable. The Go spec hints at that:

Structured variables of array, slice, and struct types have elements and fields that may be addressed individually. Each such element acts like a variable.

As a result, the memory model does not forbid a result of x:1,y:2 or x:2,y:1. In other words, struct reads and writes are not atomic. If you want to avoid tearing of a 64-bit value, you need to use a 64-bit type, not a struct with 2 32-bit fields.

@randall77 randall77 added the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Jul 18, 2022
@LeGamerDc
Copy link
Author

The compiler already does the optimization you're requesting:

type Example struct {
  a, b int32
}
func f(x, y *Example) {
	*x = *y
}

compiles to 2 MOVQs, at least on amd64. If you're seeing something different, can you please post a complete compilable example that shows the problem?

There is also the semantics question:

it confuse me about the memory behavior, assume we have one routine write x=Example{x:1, y:1} and another write x=Example{x:2, y:2}, according to the memory model, x will either be x:1,y:1 or x:2, y:2.

I don't think it is mentioned in the memory model spec, but each field of a struct is its own variable. The Go spec hints at that:

Structured variables of array, slice, and struct types have elements and fields that may be addressed individually. Each such element acts like a variable.

As a result, the memory model does not forbid a result of x:1,y:2 or x:2,y:1. In other words, struct reads and writes are not atomic. If you want to avoid tearing of a 64-bit value, you need to use a 64-bit type, not a struct with 2 32-bit fields.

yes, it's true that write a struct of two variable are not atomic. it confuse me that write value to struct(a := Example{...}) and copy value to struct(a:= b) behave not same.

code below:

1: package main
2: 
3: import "fmt"
4:
5: type Example struct {
6: 	x, y int32
7: }
8: 
9: func main() {
10:	var a = Example{x: 1, y: 2}
11:	go func() {
12:		a = Example{x: 2, y: 1}
13:	}()
14:	b := a
15:	fmt.Println(b)
16:}

will be compile to

// const init is movq
 0x0020 00032 (ff.go:10) CALL    runtime.newobject(SB)
 0x0025 00037 (ff.go:10) MOVQ    AX, "".&a+48(SP)
 0x002a 00042 (ff.go:10) MOVQ    $8589934593, CX
 0x0034 00052 (ff.go:10) MOVQ    CX, (AX)

// const init is movq
0x0004 00004 (ff.go:12) MOVQ    $4294967298, CX
0x000e 00014 (ff.go:12) MOVQ    CX, (AX)

// assign is two movl
0x0076 00118 (ff.go:14) MOVQ    "".&a+48(SP), CX
0x007b 00123 (ff.go:14) MOVL    (CX), DX
0x007d 00125 (ff.go:14) MOVL    4(CX), CX

go env:

GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPRIVATE=""
GOPROXY="https://goproxy.cn,direct"
GOROOT="/usr/local/go1.18"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go1.18/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.18.1"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build3818297273=/tmp/go-build -gno-record-gcc-switches"

@randall77
Copy link
Contributor

Ah, ok. Here's a simpler reproducer, using 2 loads/stores instead of one.

package main

type Example struct {
	x, y int32
}

func f(p *Example) interface{} {
	x := *p
	return x
}

I think a vardef for the temporary used to pass into convTnoptr is getting in the way of the combining rule.

@gopherbot
Copy link

Change https://go.dev/cl/419320 mentions this issue: cmd/compile: issue VarDef only for pointer-ful types

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Performance WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided.
Projects
None yet
Development

No branches or pull requests

5 participants