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: declare and assign of function literal escapes to heap #70171

Open
qdongxu opened this issue Nov 2, 2024 · 6 comments
Open

cmd/compile: declare and assign of function literal escapes to heap #70171

qdongxu opened this issue Nov 2, 2024 · 6 comments
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Performance
Milestone

Comments

@qdongxu
Copy link

qdongxu commented Nov 2, 2024

Go version

1.23.2

Output of go env in your module/workspace:

go env
GO111MODULE='on'
GOARCH='arm64'
GOBIN=''
GOCACHE='/Users/user/Library/Caches/go-build'
GOENV='/Users/user/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/user/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/user/go'
GOPRIVATE=''
GOPROXY='https://goproxy.cn,direct'
GOROOT='/opt/homebrew/opt/go/libexec'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='local'
GOTOOLDIR='/opt/homebrew/opt/go/libexec/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.23.2'
GODEBUG=''
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/user//Library/Application Support/go/telemetry'
GCCGO='gccgo'
GOARM64='v8.0'
AR='ar'
CC='cc'
CXX='c++'
CGO_ENABLED='1'
GOMOD='/Users/uesr/github/gorecycler/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/5b/483w2_yd7wn5x962q5hvrzkc0000gn/T/go-build271930357=/tmp/go-build -gno-record-gcc-switches -fno-common'

What did you do?

when declare an closure and assign in separate lines the local variable escape:

        var someFunc func(assignF func())
	someFunc = func(assignF func()) {
		assignF()
	}
	num := testing.AllocsPerRun(1, func() {
		i := 0 // i escaped
		someFunc(func() {
			x := i
			if x < 0 {
				// do nothing, just make x valid
			}
		})

	})

but when declare the closure in one statement, it does not escape:

       var someFunc = func(assignF func()) {
		assignF()
	}
	num := testing.AllocsPerRun(1, func() {
		i := 0 // i does not escaped
		someFunc(func() {
			x := i
			if x < 0 {
				// do nothing, just make x valid
			}
		})

	})

escape : https://go.dev/play/p/r36WtbRaJtf
not escape: https://go.dev/play/p/2u-ydl00_0i

What did you see happen?

The local variable i does not escape in one code block but escaped in another code block.

What did you expect to see?

The variable i should not escape in both code blocks.

@qdongxu qdongxu changed the title local variable escaped to the heap just for difference form of declaration local variable escaped to the heap just for difference form of closure declaration Nov 2, 2024
@seankhliao
Copy link
Member

If you look at the output of go build -gcflags '-m=2', it's the function literal that escapes, not the local variable.

@seankhliao seankhliao changed the title local variable escaped to the heap just for difference form of closure declaration cmd/compile: declare and assign of function literal escapes to heap Nov 2, 2024
@gopherbot gopherbot added the compiler/runtime Issues related to the Go compiler and/or runtime. label Nov 2, 2024
@randall77
Copy link
Contributor

I think this is probably the TODO here:

// TODO: handle initial declaration not including an assignment and

@randall77 randall77 added this to the Unplanned milestone Nov 2, 2024
@qdongxu
Copy link
Author

qdongxu commented Nov 3, 2024

Probable the local variable is a trigger. There's no allocs reported after moving the i into the assignF inline function : https://go.dev/play/p/R6203vmDmlo

And the original observation is from some real project code. I did not reproduced in pay ground yet after simplifying the code. but the allocs not happened after the local variable moved or removed.

Senario 1:

func NewNode() recycle.Recycler[producer.Node] {
	root := recycle.Get[producer.Node, node]()
	// i := 3 // the benchmark reports a alloc
	root.Assign(func(bt producer.Node) {
                 i := 3 // the benchmark does not reports allocs after `i` moved here
		newNode(bt.(*node), i)
	})

	return root
}

Senario 2:
Benchmark reports 2 allocs:

func newNode(n *node, layer int) {
	if layer <= 0 {
		return
	}

	lRecycler := recycle.Get[producer.Node, node]()
	rRecycler := recycle.Get[producer.Node, node]()
	var lNode, rNode producer.Node // benchmark reports 2 allocs 
	lRecycler.Assign(func(bt producer.Node) {
		bt.(*node).i = layer
		n.left = bt
		lNode = bt
	})
	rRecycler.Assign(func(bt producer.Node) {
		bt.(*node).i = layer 
		n.right = bt
		rNode = bt
	})

	newNode(lNode.(*node), layer-1)
	newNode(rNode.(*node), layer-1)
}

benchmark reports no allocs after removing the local variables lNode, rNode:

func newNode(n *node, layer int) {
	if layer <= 0 {
		return
	}

	lRecycler := recycle.Get[producer.Node, node]()
	rRecycler := recycle.Get[producer.Node, node]()
	lRecycler.Assign(func(bt producer.Node) {
		bt.(*node).i = layer
		n.left = bt
		newNode(bt.(*node), layer-1)
	})
	rRecycler.Assign(func(bt producer.Node) {
		bt.(*node).i = layer
		n.right = bt
		newNode(bt.(*node), layer-1)
	})
}

the allocs increase accordingly after adding additional variables lNode2 and rNode2:

func newNode(n *node, layer int) {
	if layer <= 0 {
		return
	}

	lRecycler := recycle.Get[producer.Node, node]()
	rRecycler := recycle.Get[producer.Node, node]()
	var lNode, rNode producer.Node
	var lNode2, rNode2 producer.Node
	lRecycler.Assign(func(bt producer.Node) {
		bt.(*node).i = layer + 1
		n.left = bt
		lNode = bt
		lNode2 = bt
	})
	rRecycler.Assign(func(bt producer.Node) {
		bt.(*node).i = layer + 1
		n.right = bt
		rNode = bt
		rNode2 = bt
	})

	newNode(lNode.(*node), layer-1)
	newNode(rNode.(*node), layer-1)
	newNode(lNode2.(*node), layer-1)
	newNode(rNode2.(*node), layer-1)
}

@qdongxu
Copy link
Author

qdongxu commented Nov 3, 2024

I verified with go build -gcflags "-m=2" . > 1.log 2>&1 and confirmed that the i does not escape to heap. It's the closure literal that escape to heap. But the lNode, rNode lNode2, rNode2 do escape:

./consumer.go:25:6: newNode capturing by ref: lNode (addr=false assign=true width=16)
./consumer.go:25:6: lNode escapes to heap:
./consumer.go:25:6:   flow: {storage for func literal} = &lNode:
./consumer.go:25:6:     from lNode (captured by a closure) at ./consumer.go:30:3
./consumer.go:25:6:     from lNode (reference) at ./consumer.go:30:3
./consumer.go:26:6: newNode capturing by ref: lNode2 (addr=false assign=true width=16)
./consumer.go:26:6: lNode2 escapes to heap:
./consumer.go:26:6:   flow: {storage for func literal} = &lNode2:
./consumer.go:26:6:     from lNode2 (captured by a closure) at ./consumer.go:31:3
./consumer.go:26:6:     from lNode2 (reference) at ./consumer.go:31:3
./consumer.go:18:23: newNode capturing by value: layer (addr=false assign=false width=8)
./consumer.go:18:14: newNode capturing by value: n (addr=false assign=false width=8)
./consumer.go:25:13: newNode capturing by ref: rNode (addr=false assign=true width=16)
./consumer.go:25:13: rNode escapes to heap:
./consumer.go:25:13:   flow: {storage for func literal} = &rNode:
./consumer.go:25:13:     from rNode (captured by a closure) at ./consumer.go:36:3
./consumer.go:25:13:     from rNode (reference) at ./consumer.go:36:3
./consumer.go:26:14: newNode capturing by ref: rNode2 (addr=false assign=true width=16)
./consumer.go:26:14: rNode2 escapes to heap:
./consumer.go:26:14:   flow: {storage for func literal} = &rNode2:
./consumer.go:26:14:     from rNode2 (captured by a closure) at ./consumer.go:37:3
./consumer.go:26:14:     from rNode2 (reference) at ./consumer.go:37:3

@cherrymui cherrymui added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Nov 11, 2024
@cherrymui
Copy link
Member

cc @dr2chase for how we handle closures (with a declaration and then assignment, similar to the autotmp case).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Performance
Projects
None yet
Development

No branches or pull requests

6 participants