Skip to content

Commit cc258e6

Browse files
committed
cmd/compile: add "deadlocals" pass to remove unused locals
This CL adds a "deadlocals" pass, which runs after inlining and before escape analysis, to prune any unneeded local variables and assignments. In particular, this helps avoid unnecessary Addrtaken markings from unreachable closures. Deadlocals is sensitive to "_ = ..." as a signal of explicit use for testing. This signal occurs only if the entire left-hand-side is "_" targets; if it is `_, ok := someInlinedFunc(args)` then the first return value is eligible for dead code elimination. Use this (`_ = x`) to fix tests broken by deadlocals elimination. Includes a test, based on one of the tests that required modification. Matthew Dempsky wrote this, changing ownership to allow rebases, commits, tweaks. Fixes #65158. Old-Change-Id: I723fb69ccd7baadaae04d415702ce6c8901eaf4e Change-Id: I1f25f4293b19527f305c18c3680b214237a7714c Reviewed-on: https://go-review.googlesource.com/c/go/+/600498 Reviewed-by: Keith Randall <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Keith Randall <[email protected]> Auto-Submit: David Chase <[email protected]> Commit-Queue: David Chase <[email protected]>
1 parent 7b867b9 commit cc258e6

File tree

8 files changed

+235
-4
lines changed

8 files changed

+235
-4
lines changed

src/cmd/compile/internal/base/debug.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type DebugFlags struct {
4848
MergeLocalsTrace int `help:"trace debug output for locals merging"`
4949
MergeLocalsHTrace int `help:"hash-selected trace debug output for locals merging"`
5050
Nil int `help:"print information about nil checks"`
51+
NoDeadLocals int `help:"disable deadlocals pass" concurrent:"ok"`
5152
NoOpenDefer int `help:"disable open-coded defers" concurrent:"ok"`
5253
NoRefName int `help:"do not include referenced symbol names in object file" concurrent:"ok"`
5354
PCTab string `help:"print named pc-value table\nOne of: pctospadj, pctofile, pctoline, pctoinline, pctopcdata"`
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// The deadlocals pass removes assignments to unused local variables.
6+
package deadlocals
7+
8+
import (
9+
"cmd/compile/internal/base"
10+
"cmd/compile/internal/ir"
11+
"cmd/compile/internal/types"
12+
"cmd/internal/src"
13+
"fmt"
14+
"go/constant"
15+
)
16+
17+
// Funcs applies the deadlocals pass to fns.
18+
func Funcs(fns []*ir.Func) {
19+
if base.Flag.N != 0 || base.Debug.NoDeadLocals != 0 {
20+
return
21+
}
22+
23+
zero := ir.NewBasicLit(base.AutogeneratedPos, types.Types[types.TINT], constant.MakeInt64(0))
24+
25+
for _, fn := range fns {
26+
if fn.IsClosure() {
27+
continue
28+
}
29+
30+
v := newVisitor(fn)
31+
v.nodes(fn.Body)
32+
33+
for _, assigns := range v.defs {
34+
for _, as := range assigns {
35+
// Kludge for "missing func info" linker panic.
36+
// See also closureInitLSym in inline/inl.go.
37+
if clo, ok := (*as.rhs).(*ir.ClosureExpr); ok && clo.Op() == ir.OCLOSURE {
38+
if !ir.IsTrivialClosure(clo) {
39+
ir.InitLSym(clo.Func, true)
40+
}
41+
}
42+
43+
*as.lhs = ir.BlankNode
44+
*as.rhs = zero
45+
}
46+
}
47+
}
48+
}
49+
50+
type visitor struct {
51+
curfn *ir.Func
52+
// defs[name] contains assignments that can be discarded if name can be discarded.
53+
// if defs[name] is defined nil, then name is actually used.
54+
defs map[*ir.Name][]assign
55+
56+
doNode func(ir.Node) bool
57+
}
58+
59+
type assign struct {
60+
pos src.XPos
61+
lhs, rhs *ir.Node
62+
}
63+
64+
func newVisitor(fn *ir.Func) *visitor {
65+
v := &visitor{
66+
curfn: fn,
67+
defs: make(map[*ir.Name][]assign),
68+
}
69+
v.doNode = func(n ir.Node) bool {
70+
v.node(n)
71+
return false
72+
}
73+
return v
74+
}
75+
76+
func (v *visitor) node(n ir.Node) {
77+
if n == nil {
78+
return
79+
}
80+
81+
switch n.Op() {
82+
default:
83+
ir.DoChildrenWithHidden(n, v.doNode)
84+
case ir.OCLOSURE:
85+
n := n.(*ir.ClosureExpr)
86+
v.nodes(n.Init())
87+
for _, cv := range n.Func.ClosureVars {
88+
v.node(cv)
89+
}
90+
v.nodes(n.Func.Body)
91+
92+
case ir.ODCL:
93+
// ignore
94+
case ir.ONAME:
95+
n := n.(*ir.Name)
96+
n = n.Canonical()
97+
if isLocal(n, false) {
98+
// Force any lazy definitions.
99+
s := v.defs[n]
100+
v.defs[n] = nil
101+
102+
for _, as := range s {
103+
// do the visit that was skipped in v.assign when as was appended to v.defs[n]
104+
v.node(*as.rhs)
105+
}
106+
}
107+
108+
case ir.OAS:
109+
n := n.(*ir.AssignStmt)
110+
v.assign(n.Pos(), &n.X, &n.Y, false)
111+
case ir.OAS2:
112+
n := n.(*ir.AssignListStmt)
113+
114+
// If all LHS vars are blank, treat them as intentional
115+
// uses of corresponding RHS vars. If any are non-blank
116+
// then any blanks are discards.
117+
hasNonBlank := false
118+
for i := range n.Lhs {
119+
if !ir.IsBlank(n.Lhs[i]) {
120+
hasNonBlank = true
121+
break
122+
}
123+
}
124+
for i := range n.Lhs {
125+
v.assign(n.Pos(), &n.Lhs[i], &n.Rhs[i], hasNonBlank)
126+
}
127+
}
128+
}
129+
130+
func (v *visitor) nodes(list ir.Nodes) {
131+
for _, n := range list {
132+
v.node(n)
133+
}
134+
}
135+
136+
func hasEffects(n ir.Node) bool {
137+
if n == nil {
138+
return false
139+
}
140+
if len(n.Init()) != 0 {
141+
return true
142+
}
143+
144+
switch n.Op() {
145+
// TODO(mdempsky): More.
146+
case ir.ONAME, ir.OLITERAL, ir.ONIL, ir.OCLOSURE:
147+
return false
148+
}
149+
return true
150+
}
151+
152+
func (v *visitor) assign(pos src.XPos, lhs, rhs *ir.Node, blankIsNotUse bool) {
153+
name, ok := (*lhs).(*ir.Name)
154+
if !ok {
155+
v.node(*lhs) // XXX: Interpret as variable, not value.
156+
v.node(*rhs)
157+
return
158+
}
159+
name = name.Canonical()
160+
161+
if isLocal(name, blankIsNotUse) && !hasEffects(*rhs) {
162+
if s, ok := v.defs[name]; !ok || s != nil {
163+
// !ok || s != nil is FALSE if previously "v.defs[name] = nil" -- that marks a use.
164+
v.defs[name] = append(s, assign{pos, lhs, rhs})
165+
return // don't visit rhs unless that node ends up live, later.
166+
}
167+
}
168+
169+
v.node(*rhs)
170+
}
171+
172+
func isLocal(n *ir.Name, blankIsNotUse bool) bool {
173+
if ir.IsBlank(n) {
174+
// Treat single assignments as intentional use (false), anything else is a discard (true).
175+
return blankIsNotUse
176+
}
177+
178+
switch n.Class {
179+
case ir.PAUTO, ir.PPARAM:
180+
return true
181+
case ir.PPARAMOUT:
182+
return false
183+
case ir.PEXTERN, ir.PFUNC:
184+
return false
185+
}
186+
panic(fmt.Sprintf("unexpected Class: %+v", n))
187+
}

src/cmd/compile/internal/gc/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"bytes"
1010
"cmd/compile/internal/base"
1111
"cmd/compile/internal/coverage"
12+
"cmd/compile/internal/deadlocals"
1213
"cmd/compile/internal/dwarfgen"
1314
"cmd/compile/internal/escape"
1415
"cmd/compile/internal/inline"
@@ -247,6 +248,8 @@ func Main(archInit func(*ssagen.ArchInfo)) {
247248
// and doesn't benefit from dead-coding or inlining.
248249
symABIs.GenABIWrappers()
249250

251+
deadlocals.Funcs(typecheck.Target.Funcs)
252+
250253
// Escape analysis.
251254
// Required for moving heap allocations onto stack,
252255
// which in turn is required by the closure implementation,

src/runtime/race/testdata/mop_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,8 @@ func TestNoRaceEnoughRegisters(t *testing.T) {
612612
}
613613

614614
// emptyFunc should not be inlined.
615+
//
616+
//go:noinline
615617
func emptyFunc(x int) {
616618
if false {
617619
fmt.Println(x)
@@ -1176,7 +1178,7 @@ func TestNoRaceHeapReallocation(t *testing.T) {
11761178
// others.
11771179
const n = 2
11781180
done := make(chan bool, n)
1179-
empty := func(p *int) {}
1181+
empty := func(p *int) { _ = p }
11801182
for i := 0; i < n; i++ {
11811183
ms := i
11821184
go func() {
@@ -1417,7 +1419,7 @@ func TestRaceInterCall2(t *testing.T) {
14171419

14181420
func TestRaceFuncCall(t *testing.T) {
14191421
c := make(chan bool, 1)
1420-
f := func(x, y int) {}
1422+
f := func(x, y int) { _ = y }
14211423
x, y := 0, 0
14221424
go func() {
14231425
y = 42
@@ -1804,6 +1806,7 @@ func TestRaceAsFunc2(t *testing.T) {
18041806
x := 0
18051807
go func() {
18061808
func(x int) {
1809+
_ = x
18071810
}(x)
18081811
c <- true
18091812
}()
@@ -1817,6 +1820,7 @@ func TestRaceAsFunc3(t *testing.T) {
18171820
x := 0
18181821
go func() {
18191822
func(x int) {
1823+
_ = x
18201824
mu.Lock()
18211825
}(x) // Read of x must be outside of the mutex.
18221826
mu.Unlock()

test/closure3.dir/main.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func main() {
2020
if x := func() int { // ERROR "can inline main.func2" "func literal does not escape"
2121
return 1
2222
}; x() != 1 { // ERROR "inlining call to main.func2"
23+
_ = x // prevent simple deadcode elimination after inlining
2324
ppanic("x() != 1")
2425
}
2526
}
@@ -33,6 +34,7 @@ func main() {
3334
if y := func(x int) int { // ERROR "can inline main.func4" "func literal does not escape"
3435
return x + 2
3536
}; y(40) != 42 { // ERROR "inlining call to main.func4"
37+
_ = y // prevent simple deadcode elimination after inlining
3638
ppanic("y(40) != 42")
3739
}
3840
}
@@ -181,6 +183,7 @@ func main() {
181183
if y := func() int { // ERROR "can inline main.func21" "func literal does not escape"
182184
return x
183185
}; y() != 42 { // ERROR "inlining call to main.func21"
186+
_ = y // prevent simple deadcode elimination after inlining
184187
ppanic("y() != 42")
185188
}
186189
}
@@ -199,6 +202,7 @@ func main() {
199202
return x + y
200203
}() // ERROR "inlining call to main.func23.1"
201204
}; z(1) != 43 { // ERROR "inlining call to main.func23" "inlining call to main.main.func23.func31"
205+
_ = z // prevent simple deadcode elimination after inlining
202206
ppanic("z(1) != 43")
203207
}
204208
}
@@ -287,6 +291,25 @@ func main() {
287291
}
288292
}
289293

294+
//go:noinline
295+
func notmain() {
296+
{
297+
// This duplicates the first block in main, but without the "_ = x" for closure x.
298+
// This allows dead code elimination of x before escape analysis,
299+
// thus "func literal does not escape" should not appear.
300+
if x := func() int { // ERROR "can inline notmain.func1"
301+
return 1
302+
}(); x != 1 { // ERROR "inlining call to notmain.func1"
303+
ppanic("x != 1")
304+
}
305+
if x := func() int { // ERROR "can inline notmain.func2"
306+
return 1
307+
}; x() != 1 { // ERROR "inlining call to notmain.func2"
308+
ppanic("x() != 1")
309+
}
310+
}
311+
}
312+
290313
//go:noinline
291314
func ppanic(s string) { // ERROR "leaking param: s"
292315
panic(s) // ERROR "s escapes to heap"

test/fixedbugs/issue54159.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ func run() { // ERROR "cannot inline run: recursive"
1111
g() // ERROR "inlining call to g"
1212
}
1313
f() // ERROR "inlining call to run.func1" "inlining call to g"
14+
_ = f
1415
run()
1516
}
1617

test/inline.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func l(x, y int) (int, int, error) { // ERROR "can inline l"
7373
f := e
7474
f(nil) // ERROR "inlining call to l.func1"
7575
}
76+
_ = e // prevent simple deadcode elimination after inlining
7677
return y, x, nil
7778
}
7879

@@ -109,6 +110,7 @@ func p() int { // ERROR "can inline p"
109110

110111
func q(x int) int { // ERROR "can inline q"
111112
foo := func() int { return x * 2 } // ERROR "can inline q.func1" "func literal does not escape"
113+
_ = foo // prevent simple deadcode elimination after inlining
112114
return foo() // ERROR "inlining call to q.func1"
113115
}
114116

@@ -121,14 +123,17 @@ func r(z int) int {
121123
return 2*y + x*z
122124
}(x) // ERROR "inlining call to r.func2.1"
123125
}
126+
_, _ = foo, bar // prevent simple deadcode elimination after inlining
127+
124128
return foo(42) + bar(42) // ERROR "inlining call to r.func1" "inlining call to r.func2" "inlining call to r.r.func2.func3"
125129
}
126130

127131
func s0(x int) int { // ERROR "can inline s0"
128132
foo := func() { // ERROR "can inline s0.func1" "func literal does not escape"
129133
x = x + 1
130134
}
131-
foo() // ERROR "inlining call to s0.func1"
135+
foo() // ERROR "inlining call to s0.func1"
136+
_ = foo // prevent simple deadcode elimination after inlining
132137
return x
133138
}
134139

@@ -137,6 +142,7 @@ func s1(x int) int { // ERROR "can inline s1"
137142
return x
138143
}
139144
x = x + 1
145+
_ = foo // prevent simple deadcode elimination after inlining
140146
return foo() // ERROR "inlining call to s1.func1"
141147
}
142148

0 commit comments

Comments
 (0)