From: Matthew Dempsky Date: Wed, 9 Aug 2023 11:05:35 +0000 (-0700) Subject: cmd/compile: move early deadcode into unified writer X-Git-Tag: go1.22rc1~1324 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=59037ac93a49889eb6a7d6b3b8fbc70321615f1f;p=gostls13.git cmd/compile: move early deadcode into unified writer This CL moves the early deadcode elimination pass into the unified writer. This allows shrinking the export data, by simplifying expressions and removing unreachable statements. It also means we don't need to repeatedly apply deadcode elimination on inlined calls or instantiated generics. Change-Id: I19bdb04861e50815fccdab39790f4aaa076121fd Reviewed-on: https://go-review.googlesource.com/c/go/+/517775 Reviewed-by: Michael Knyszek Reviewed-by: Cuong Manh Le Auto-Submit: Matthew Dempsky Run-TryBot: Matthew Dempsky Reviewed-by: Than McIntosh TryBot-Result: Gopher Robot --- diff --git a/src/cmd/compile/internal/deadcode/deadcode.go b/src/cmd/compile/internal/deadcode/deadcode.go deleted file mode 100644 index 46a2239f48..0000000000 --- a/src/cmd/compile/internal/deadcode/deadcode.go +++ /dev/null @@ -1,247 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package deadcode - -import ( - "go/constant" - "go/token" - - "cmd/compile/internal/base" - "cmd/compile/internal/ir" -) - -func Func(fn *ir.Func) { - stmts(&fn.Body) - - if len(fn.Body) == 0 { - return - } - - for _, n := range fn.Body { - if len(n.Init()) > 0 { - return - } - switch n.Op() { - case ir.OIF: - n := n.(*ir.IfStmt) - if !ir.IsConst(n.Cond, constant.Bool) || len(n.Body) > 0 || len(n.Else) > 0 { - return - } - case ir.OFOR: - n := n.(*ir.ForStmt) - if !ir.IsConst(n.Cond, constant.Bool) || ir.BoolVal(n.Cond) { - return - } - default: - return - } - } - - ir.VisitList(fn.Body, markHiddenClosureDead) - fn.Body = []ir.Node{ir.NewBlockStmt(base.Pos, nil)} -} - -func stmts(nn *ir.Nodes) { - var lastLabel = -1 - for i, n := range *nn { - if n != nil && n.Op() == ir.OLABEL { - lastLabel = i - } - } - for i, n := range *nn { - // Cut is set to true when all nodes after i'th position - // should be removed. - // In other words, it marks whole slice "tail" as dead. - cut := false - if n == nil { - continue - } - if n.Op() == ir.OIF { - n := n.(*ir.IfStmt) - n.Cond = expr(n.Cond) - if ir.IsConst(n.Cond, constant.Bool) { - var body ir.Nodes - if ir.BoolVal(n.Cond) { - ir.VisitList(n.Else, markHiddenClosureDead) - n.Else = ir.Nodes{} - body = n.Body - } else { - ir.VisitList(n.Body, markHiddenClosureDead) - n.Body = ir.Nodes{} - body = n.Else - } - // If "then" or "else" branch ends with panic or return statement, - // it is safe to remove all statements after this node. - // isterminating is not used to avoid goto-related complications. - // We must be careful not to deadcode-remove labels, as they - // might be the target of a goto. See issue 28616. - if body := body; len(body) != 0 { - switch body[(len(body) - 1)].Op() { - case ir.ORETURN, ir.OTAILCALL, ir.OPANIC: - if i > lastLabel { - cut = true - } - } - } - } - } - if n.Op() == ir.OSWITCH { - n := n.(*ir.SwitchStmt) - // Use a closure wrapper here so we can use "return" to abort the analysis. - func() { - if n.Tag != nil && n.Tag.Op() == ir.OTYPESW { - return // no special type-switch case yet. - } - var x constant.Value // value we're switching on - if n.Tag != nil { - if ir.ConstType(n.Tag) == constant.Unknown { - return - } - x = n.Tag.Val() - } else { - x = constant.MakeBool(true) // switch { ... } => switch true { ... } - } - var def *ir.CaseClause - for _, cas := range n.Cases { - if len(cas.List) == 0 { // default case - def = cas - continue - } - for _, c := range cas.List { - if ir.ConstType(c) == constant.Unknown { - return // can't statically tell if it matches or not - give up. - } - if constant.Compare(x, token.EQL, c.Val()) { - for _, n := range cas.Body { - if n.Op() == ir.OFALL { - return // fallthrough makes it complicated - abort. - } - } - // This switch entry is the one that always triggers. - for _, cas2 := range n.Cases { - for _, c2 := range cas2.List { - ir.Visit(c2, markHiddenClosureDead) - } - if cas2 != cas { - ir.VisitList(cas2.Body, markHiddenClosureDead) - } - } - - // Rewrite to switch { case true: ... } - n.Tag = nil - cas.List[0] = ir.NewBool(c.Pos(), true) - cas.List = cas.List[:1] - n.Cases[0] = cas - n.Cases = n.Cases[:1] - return - } - } - } - if def != nil { - for _, n := range def.Body { - if n.Op() == ir.OFALL { - return // fallthrough makes it complicated - abort. - } - } - for _, cas := range n.Cases { - if cas != def { - ir.VisitList(cas.List, markHiddenClosureDead) - ir.VisitList(cas.Body, markHiddenClosureDead) - } - } - n.Cases[0] = def - n.Cases = n.Cases[:1] - return - } - - // TODO: handle case bodies ending with panic/return as we do in the IF case above. - - // entire switch is a nop - no case ever triggers - for _, cas := range n.Cases { - ir.VisitList(cas.List, markHiddenClosureDead) - ir.VisitList(cas.Body, markHiddenClosureDead) - } - n.Cases = n.Cases[:0] - }() - } - - if len(n.Init()) != 0 { - stmts(n.(ir.InitNode).PtrInit()) - } - switch n.Op() { - case ir.OBLOCK: - n := n.(*ir.BlockStmt) - stmts(&n.List) - case ir.OFOR: - n := n.(*ir.ForStmt) - stmts(&n.Body) - case ir.OIF: - n := n.(*ir.IfStmt) - stmts(&n.Body) - stmts(&n.Else) - case ir.ORANGE: - n := n.(*ir.RangeStmt) - stmts(&n.Body) - case ir.OSELECT: - n := n.(*ir.SelectStmt) - for _, cas := range n.Cases { - stmts(&cas.Body) - } - case ir.OSWITCH: - n := n.(*ir.SwitchStmt) - for _, cas := range n.Cases { - stmts(&cas.Body) - } - } - - if cut { - ir.VisitList((*nn)[i+1:len(*nn)], markHiddenClosureDead) - *nn = (*nn)[:i+1] - break - } - } -} - -func expr(n ir.Node) ir.Node { - // Perform dead-code elimination on short-circuited boolean - // expressions involving constants with the intent of - // producing a constant 'if' condition. - switch n.Op() { - case ir.OANDAND: - n := n.(*ir.LogicalExpr) - n.X = expr(n.X) - n.Y = expr(n.Y) - if ir.IsConst(n.X, constant.Bool) { - if ir.BoolVal(n.X) { - return n.Y // true && x => x - } else { - return n.X // false && x => false - } - } - case ir.OOROR: - n := n.(*ir.LogicalExpr) - n.X = expr(n.X) - n.Y = expr(n.Y) - if ir.IsConst(n.X, constant.Bool) { - if ir.BoolVal(n.X) { - return n.X // true || x => true - } else { - return n.Y // false || x => x - } - } - } - return n -} - -func markHiddenClosureDead(n ir.Node) { - if n.Op() != ir.OCLOSURE { - return - } - clo := n.(*ir.ClosureExpr) - if clo.Func.IsHiddenClosure() { - clo.Func.SetIsDeadcodeClosure(true) - } - ir.VisitList(clo.Func.Body, markHiddenClosureDead) -} diff --git a/src/cmd/compile/internal/gc/main.go b/src/cmd/compile/internal/gc/main.go index 937d1c4751..e9af8aa325 100644 --- a/src/cmd/compile/internal/gc/main.go +++ b/src/cmd/compile/internal/gc/main.go @@ -9,7 +9,6 @@ import ( "bytes" "cmd/compile/internal/base" "cmd/compile/internal/coverage" - "cmd/compile/internal/deadcode" "cmd/compile/internal/devirtualize" "cmd/compile/internal/dwarfgen" "cmd/compile/internal/escape" @@ -220,11 +219,6 @@ func Main(archInit func(*ssagen.ArchInfo)) { // Create "init" function for package-scope variable initialization // statements, if any. - // - // Note: This needs to happen early, before any optimizations. The - // Go spec defines a precise order than initialization should be - // carried out in, and even mundane optimizations like dead code - // removal can skew the results (e.g., #43444). pkginit.MakeInit() // Second part of code coverage fixup (init func modification), @@ -233,14 +227,6 @@ func Main(archInit func(*ssagen.ArchInfo)) { coverage.FixupInit(cnames) } - // Eliminate some obviously dead code. - // Must happen after typechecking. - for _, n := range typecheck.Target.Decls { - if n.Op() == ir.ODCLFUNC { - deadcode.Func(n.(*ir.Func)) - } - } - // Compute Addrtaken for names. // We need to wait until typechecking is done so that when we see &x[i] // we know that x has its address taken if x is an array, but not if x is a slice. diff --git a/src/cmd/compile/internal/noder/reader.go b/src/cmd/compile/internal/noder/reader.go index f63040ae13..a92a890437 100644 --- a/src/cmd/compile/internal/noder/reader.go +++ b/src/cmd/compile/internal/noder/reader.go @@ -13,7 +13,6 @@ import ( "strings" "cmd/compile/internal/base" - "cmd/compile/internal/deadcode" "cmd/compile/internal/dwarfgen" "cmd/compile/internal/inline" "cmd/compile/internal/ir" @@ -1900,6 +1899,10 @@ func (r *reader) forStmt(label *types.Sym) ir.Node { perLoopVars := r.Bool() r.closeAnotherScope() + if ir.IsConst(cond, constant.Bool) && !ir.BoolVal(cond) { + return init // simplify "for init; false; post { ... }" into "init" + } + stmt := ir.NewForStmt(pos, init, cond, post, body, perLoopVars) stmt.Label = label return stmt @@ -1913,9 +1916,14 @@ func (r *reader) ifStmt() ir.Node { cond := r.expr() then := r.blockStmt() els := r.stmts() + r.closeAnotherScope() + + if ir.IsConst(cond, constant.Bool) && len(init)+len(then)+len(els) == 0 { + return nil // drop empty if statement + } + n := ir.NewIfStmt(pos, cond, then, els) n.SetInit(init) - r.closeAnotherScope() return n } @@ -3531,8 +3539,6 @@ func unifiedInlineCall(call *ir.CallExpr, fn *ir.Func, inlIndex int) *ir.Inlined // Note issue 28603. init.Append(ir.NewInlineMarkStmt(call.Pos().WithIsStmt(), int64(r.inlTreeIndex))) - nparams := len(r.curfn.Dcl) - ir.WithFunc(r.curfn, func() { if !r.syntheticBody(call.Pos()) { assert(r.Bool()) // have body @@ -3548,8 +3554,6 @@ func unifiedInlineCall(call *ir.CallExpr, fn *ir.Func, inlIndex int) *ir.Inlined // themselves. But currently it's an easy fix to #50552. readBodies(typecheck.Target, true) - deadcode.Func(r.curfn) - // Replace any "return" statements within the function body. var edit func(ir.Node) ir.Node edit = func(n ir.Node) ir.Node { @@ -3564,22 +3568,14 @@ func unifiedInlineCall(call *ir.CallExpr, fn *ir.Func, inlIndex int) *ir.Inlined body := ir.Nodes(r.curfn.Body) - // Quirkish: We need to eagerly prune variables added during - // inlining, but removed by deadcode.FuncBody above. Unused - // variables will get removed during stack frame layout anyway, but - // len(fn.Dcl) ends up influencing things like autotmp naming. - - used := usedLocals(body) - - for i, name := range r.curfn.Dcl { - if i < nparams || used.Has(name) { - name.Curfn = callerfn - callerfn.Dcl = append(callerfn.Dcl, name) + // Reparent any declarations into the caller function. + for _, name := range r.curfn.Dcl { + name.Curfn = callerfn + callerfn.Dcl = append(callerfn.Dcl, name) - if name.AutoTemp() { - name.SetEsc(ir.EscUnknown) - name.SetInlLocal(true) - } + if name.AutoTemp() { + name.SetEsc(ir.EscUnknown) + name.SetInlLocal(true) } } @@ -3647,10 +3643,6 @@ func expandInline(fn *ir.Func, pri pkgReaderIndex) { r.funarghack = true r.funcBody(tmpfn) - - ir.WithFunc(tmpfn, func() { - deadcode.Func(tmpfn) - }) } used := usedLocals(tmpfn.Body) diff --git a/src/cmd/compile/internal/noder/writer.go b/src/cmd/compile/internal/noder/writer.go index 77708245ae..f5b83c6402 100644 --- a/src/cmd/compile/internal/noder/writer.go +++ b/src/cmd/compile/internal/noder/writer.go @@ -6,6 +6,8 @@ package noder import ( "fmt" + "go/constant" + "go/token" "internal/buildcfg" "internal/pkgbits" @@ -1206,9 +1208,18 @@ func (w *writer) stmt(stmt syntax.Stmt) { } func (w *writer) stmts(stmts []syntax.Stmt) { + dead := false w.Sync(pkgbits.SyncStmts) for _, stmt := range stmts { + if dead { + // Any statements after a terminating statement are safe to + // omit, at least until the next labeled statement. + if _, ok := stmt.(*syntax.LabeledStmt); !ok { + continue + } + } w.stmt1(stmt) + dead = w.p.terminates(stmt) } w.Code(stmtEnd) w.Sync(pkgbits.SyncStmtsEnd) @@ -1449,6 +1460,11 @@ func (w *writer) forStmt(stmt *syntax.ForStmt) { } } else { + if stmt.Cond != nil && w.p.staticBool(&stmt.Cond) < 0 { // always false + stmt.Post = nil + stmt.Body.List = nil + } + w.pos(stmt) w.stmt(stmt.Init) w.optExpr(stmt.Cond) @@ -1504,6 +1520,13 @@ func (pw *pkgWriter) rangeTypes(expr syntax.Expr) (key, value types2.Type) { } func (w *writer) ifStmt(stmt *syntax.IfStmt) { + switch cond := w.p.staticBool(&stmt.Cond); { + case cond > 0: // always true + stmt.Else = nil + case cond < 0: // always false + stmt.Then.List = nil + } + w.Sync(pkgbits.SyncIfStmt) w.openScope(stmt.Pos()) w.pos(stmt) @@ -1558,10 +1581,56 @@ func (w *writer) switchStmt(stmt *syntax.SwitchStmt) { } else { tag := stmt.Tag + var tagValue constant.Value if tag != nil { - tagType = w.p.typeOf(tag) + tv := w.p.typeAndValue(tag) + tagType = tv.Type + tagValue = tv.Value } else { tagType = types2.Typ[types2.Bool] + tagValue = constant.MakeBool(true) + } + + if tagValue != nil { + // If the switch tag has a constant value, look for a case + // clause that we always branch to. + func() { + var target *syntax.CaseClause + Outer: + for _, clause := range stmt.Body { + if clause.Cases == nil { + target = clause + } + for _, cas := range unpackListExpr(clause.Cases) { + tv := w.p.typeAndValue(cas) + if tv.Value == nil { + return // non-constant case; give up + } + if constant.Compare(tagValue, token.EQL, tv.Value) { + target = clause + break Outer + } + } + } + // We've found the target clause, if any. + + if target != nil { + if hasFallthrough(target.Body) { + return // fallthrough is tricky; give up + } + + // Rewrite as single "default" case. + target.Cases = nil + stmt.Body = []*syntax.CaseClause{target} + } else { + stmt.Body = nil + } + + // Clear switch tag (i.e., replace with implicit "true"). + tag = nil + stmt.Tag = nil + tagType = types2.Typ[types2.Bool] + }() } // Walk is going to emit comparisons between the tag value and @@ -2629,6 +2698,61 @@ func (w *writer) pkgObjs(names ...*syntax.Name) { // @@@ Helpers +// staticBool analyzes a boolean expression and reports whether it's +// always true (positive result), always false (negative result), or +// unknown (zero). +// +// It also simplifies the expression while preserving semantics, if +// possible. +func (pw *pkgWriter) staticBool(ep *syntax.Expr) int { + if val := pw.typeAndValue(*ep).Value; val != nil { + if constant.BoolVal(val) { + return +1 + } else { + return -1 + } + } + + if e, ok := (*ep).(*syntax.Operation); ok { + switch e.Op { + case syntax.Not: + return pw.staticBool(&e.X) + + case syntax.AndAnd: + x := pw.staticBool(&e.X) + if x < 0 { + *ep = e.X + return x + } + + y := pw.staticBool(&e.Y) + if x > 0 || y < 0 { + if pw.typeAndValue(e.X).Value != nil { + *ep = e.Y + } + return y + } + + case syntax.OrOr: + x := pw.staticBool(&e.X) + if x > 0 { + *ep = e.X + return x + } + + y := pw.staticBool(&e.Y) + if x < 0 || y > 0 { + if pw.typeAndValue(e.X).Value != nil { + *ep = e.Y + } + return y + } + } + } + + return 0 +} + // hasImplicitTypeParams reports whether obj is a defined type with // implicit type parameters (e.g., declared within a generic function // or method). @@ -2706,6 +2830,15 @@ func isNil(p *pkgWriter, expr syntax.Expr) bool { return tv.IsNil() } +// isBuiltin reports whether expr is a (possibly parenthesized) +// referenced to the specified built-in function. +func (p *pkgWriter) isBuiltin(expr syntax.Expr, builtin string) bool { + if name, ok := unparen(expr).(*syntax.Name); ok && name.Value == builtin { + return p.typeAndValue(name).IsBuiltin() + } + return false +} + // recvBase returns the base type for the given receiver parameter. func recvBase(recv *types2.Var) *types2.Named { typ := recv.Type() @@ -2789,3 +2922,56 @@ func isPtrTo(from, to types2.Type) bool { ptr, ok := from.(*types2.Pointer) return ok && types2.Identical(ptr.Elem(), to) } + +// hasFallthrough reports whether stmts ends in a fallthrough +// statement. +func hasFallthrough(stmts []syntax.Stmt) bool { + last, ok := lastNonEmptyStmt(stmts).(*syntax.BranchStmt) + return ok && last.Tok == syntax.Fallthrough +} + +// lastNonEmptyStmt returns the last non-empty statement in list, if +// any. +func lastNonEmptyStmt(stmts []syntax.Stmt) syntax.Stmt { + for i := len(stmts) - 1; i >= 0; i-- { + stmt := stmts[i] + if _, ok := stmt.(*syntax.EmptyStmt); !ok { + return stmt + } + } + return nil +} + +// terminates reports whether stmt terminates normal control flow +// (i.e., does not merely advance to the following statement). +func (p *pkgWriter) terminates(stmt syntax.Stmt) bool { + switch stmt := stmt.(type) { + case *syntax.BranchStmt: + if stmt.Tok == syntax.Goto { + return true + } + case *syntax.ReturnStmt: + return true + case *syntax.ExprStmt: + if call, ok := unparen(stmt.X).(*syntax.CallExpr); ok { + if p.isBuiltin(call.Fun, "panic") { + return true + } + } + + // The handling of BlockStmt here is approximate, but it serves to + // allow dead-code elimination for: + // + // if true { + // return x + // } + // unreachable + case *syntax.IfStmt: + cond := p.staticBool(&stmt.Cond) + return (cond < 0 || p.terminates(stmt.Then)) && (cond > 0 || p.terminates(stmt.Else)) + case *syntax.BlockStmt: + return p.terminates(lastNonEmptyStmt(stmt.List)) + } + + return false +}