Skip to content

Commit

Permalink
cmd/compile/internal/inline: no-return flag analysis for inline heuri…
Browse files Browse the repository at this point in the history
…stics

Add code to compute whether a given function appears to
unconditionally call panic or exit, as a means of driving inlining
decisions. Note that this determination is based on
heuristics/guesses, as opposed to strict safety analysis; in some
cases we may miss a function that does indeed always panic, or mark a
function as always invoking panic when it doesn't; the intent is get
the right answer in "most" cases.

Updates #61502.

Change-Id: Ibba3e60c06c2e54cf29b3ffa0f816518aaacb9a3
Reviewed-on: https://go-review.googlesource.com/c/go/+/511558
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
  • Loading branch information
thanm committed Sep 6, 2023
1 parent 5cdb132 commit e844d72
Show file tree
Hide file tree
Showing 6 changed files with 703 additions and 110 deletions.
41 changes: 39 additions & 2 deletions src/cmd/compile/internal/inline/inlheur/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,24 @@ import (

const (
debugTraceFuncs = 1 << iota
debugTraceFuncFlags
)

// propAnalyzer interface is used for defining one or more analyzer
// helper objects, each tasked with computing some specific subset of
// the properties we're interested in. The assumption is that
// properties are independent, so each new analyzer that implements
// this interface can operate entirely on its own. For a given analyzer
// there will be a sequence of calls to nodeVisitPre and nodeVisitPost
// as the nodes within a function are visited, then a followup call to
// setResults so that the analyzer can transfer its results into the
// final properties object.
type propAnalyzer interface {
nodeVisitPre(n ir.Node)
nodeVisitPost(n ir.Node)
setResults(fp *FuncProps)
}

// fnInlHeur contains inline heuristics state information about
// a specific Go function being analyzed/considered by the inliner.
type fnInlHeur struct {
Expand All @@ -37,8 +53,29 @@ func computeFuncProps(fn *ir.Func) *FuncProps {
fmt.Fprintf(os.Stderr, "=-= starting analysis of func %v:\n%+v\n",
fn.Sym().Name, fn)
}
// implementation stubbed out for now
return &FuncProps{}
ffa := makeFuncFlagsAnalyzer(fn)
analyzers := []propAnalyzer{ffa}
fp := new(FuncProps)
runAnalyzersOnFunction(fn, analyzers)
for _, a := range analyzers {
a.setResults(fp)
}
return fp
}

func runAnalyzersOnFunction(fn *ir.Func, analyzers []propAnalyzer) {
var doNode func(ir.Node) bool
doNode = func(n ir.Node) bool {
for _, a := range analyzers {
a.nodeVisitPre(n)
}
ir.DoChildren(n, doNode)
for _, a := range analyzers {
a.nodeVisitPost(n)
}
return false
}
doNode(fn)
}

func fnFileLine(fn *ir.Func) (string, uint) {
Expand Down
338 changes: 338 additions & 0 deletions src/cmd/compile/internal/inline/inlheur/analyze_func_flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
// Copyright 2023 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 inlheur

import (
"cmd/compile/internal/base"
"cmd/compile/internal/ir"
"cmd/compile/internal/types"
"fmt"
"os"
)

// funcFlagsAnalyzer computes the "Flags" value for the FuncProps
// object we're computing. The main item of interest here is "nstate",
// which stores the disposition of a given ir Node with respect to the
// flags/properties we're trying to compute.
type funcFlagsAnalyzer struct {
fn *ir.Func
nstate map[ir.Node]pstate
noInfo bool // set if we see something inscrutable/un-analyzable
}

// pstate keeps track of the disposition of a given node and its
// children with respect to panic/exit calls.
type pstate int

const (
psNoInfo pstate = iota // nothing interesting about this node
psCallsPanic // node causes call to panic or os.Exit
psMayReturn // executing node may trigger a "return" stmt
psTop // dataflow lattice "top" element
)

func makeFuncFlagsAnalyzer(fn *ir.Func) *funcFlagsAnalyzer {
return &funcFlagsAnalyzer{
fn: fn,
nstate: make(map[ir.Node]pstate),
}
}

// setResults transfers func flag results to 'fp'.
func (ffa *funcFlagsAnalyzer) setResults(fp *FuncProps) {
var rv FuncPropBits
if !ffa.noInfo && ffa.stateForList(ffa.fn.Body) == psCallsPanic {
rv = FuncPropNeverReturns
}
// This is slightly hacky and not at all required, but include a
// special case for main.main, which often ends in a call to
// os.Exit. People who write code like this (very common I
// imagine)
//
// func main() {
// rc = perform()
// ...
// foo()
// os.Exit(rc)
// }
//
// will be constantly surprised when foo() is inlined in many
// other spots in the program but not in main().
if isMainMain(ffa.fn) {
rv &^= FuncPropNeverReturns
}
fp.Flags = rv
}

func (ffa *funcFlagsAnalyzer) getstate(n ir.Node) pstate {
val, ok := ffa.nstate[n]
if !ok {
base.Fatalf("funcFlagsAnalyzer: fn %q node %s line %s: internal error, no setting for node:\n%+v\n", ffa.fn.Sym().Name, n.Op().String(), ir.Line(n), n)
}
return val
}

func (ffa *funcFlagsAnalyzer) setstate(n ir.Node, st pstate) {
if _, ok := ffa.nstate[n]; ok {
base.Fatalf("funcFlagsAnalyzer: fn %q internal error, existing setting for node:\n%+v\n", ffa.fn.Sym().Name, n)
} else {
ffa.nstate[n] = st
}
}

func (ffa *funcFlagsAnalyzer) setstateSoft(n ir.Node, st pstate) {
ffa.nstate[n] = st
}

// blockCombine merges together states as part of a linear sequence of
// statements, where 'pred' and 'succ' are analysis results for a pair
// of consecutive statements. Examples:
//
// case 1: case 2:
// panic("foo") if q { return x } <-pred
// return x panic("boo") <-succ
//
// In case 1, since the pred state is "always panic" it doesn't matter
// what the succ state is, hence the state for the combination of the
// two blocks is "always panics". In case 2, because there is a path
// to return that avoids the panic in succ, the state for the
// combination of the two statements is "may return".
func blockCombine(pred, succ pstate) pstate {
switch succ {
case psTop:
return pred
case psMayReturn:
if pred == psCallsPanic {
return psCallsPanic
}
return psMayReturn
case psNoInfo:
return pred
case psCallsPanic:
if pred == psMayReturn {
return psMayReturn
}
return psCallsPanic
}
panic("should never execute")
}

// branchCombine combines two states at a control flow branch point where
// either p1 or p2 executes (as in an "if" statement).
func branchCombine(p1, p2 pstate) pstate {
if p1 == psCallsPanic && p2 == psCallsPanic {
return psCallsPanic
}
if p1 == psMayReturn || p2 == psMayReturn {
return psMayReturn
}
return psNoInfo
}

// stateForList walks through a list of statements and computes the
// state/diposition for the entire list as a whole.
func (ffa *funcFlagsAnalyzer) stateForList(list ir.Nodes) pstate {
st := psTop
for i := range list {
n := list[i]
psi := ffa.getstate(n)
if debugTrace&debugTraceFuncFlags != 0 {
fmt.Fprintf(os.Stderr, "=-= %v: stateForList n=%s ps=%s\n",
ir.Line(n), n.Op().String(), psi.String())
}
st = blockCombine(st, psi)
}
if st == psTop {
st = psNoInfo
}
return st
}

func isMainMain(fn *ir.Func) bool {
s := fn.Sym()
return (s.Pkg.Name == "main" && s.Name == "main")
}

func isWellKnownFunc(s *types.Sym, pkg, name string) bool {
return s.Pkg.Path == pkg && s.Name == name
}

// isExitCall reports TRUE if the node itself is an unconditional
// call to os.Exit(), a panic, or a function that does likewise.
func isExitCall(n ir.Node) bool {
if n.Op() != ir.OCALLFUNC {
return false
}
cx := n.(*ir.CallExpr)
name := ir.StaticCalleeName(cx.X)
if name == nil {
return false
}
s := name.Sym()
if isWellKnownFunc(s, "os", "Exit") ||
isWellKnownFunc(s, "runtime", "throw") {
return true
}
// FIXME: consult results of flags computation for
// previously analyzed Go functions, including props
// read from export data for functions in other packages.
return false
}

// pessimize is called to record the fact that we saw something in the
// function that renders it entirely impossible to analyze.
func (ffa *funcFlagsAnalyzer) pessimize() {
ffa.noInfo = true
}

// shouldVisit reports TRUE if this is an interesting node from the
// perspective of computing function flags. NB: due to the fact that
// ir.CallExpr implements the Stmt interface, we wind up visiting
// a lot of nodes that we don't really need to, but these can
// simply be screened out as part of the visit.
func shouldVisit(n ir.Node) bool {
_, isStmt := n.(ir.Stmt)
return n.Op() != ir.ODCL &&
(isStmt || n.Op() == ir.OCALLFUNC || n.Op() == ir.OPANIC)
}

// nodeVisitPost helps implement the propAnalyzer interface; when
// called on a given node, it decides the disposition of that node
// based on the state(s) of the node's children.
func (ffa *funcFlagsAnalyzer) nodeVisitPost(n ir.Node) {
if debugTrace&debugTraceFuncFlags != 0 {
fmt.Fprintf(os.Stderr, "=+= nodevis %v %s should=%v\n",
ir.Line(n), n.Op().String(), shouldVisit(n))
}
if !shouldVisit(n) {
// invoke soft set, since node may be shared (e.g. ONAME)
ffa.setstateSoft(n, psNoInfo)
return
}
var st pstate
switch n.Op() {
case ir.OCALLFUNC:
if isExitCall(n) {
st = psCallsPanic
}
case ir.OPANIC:
st = psCallsPanic
case ir.ORETURN:
st = psMayReturn
case ir.OBREAK, ir.OCONTINUE:
// FIXME: this handling of break/continue is sub-optimal; we
// have them as "mayReturn" in order to help with this case:
//
// for {
// if q() { break }
// panic(...)
// }
//
// where the effect of the 'break' is to cause the subsequent
// panic to be skipped. One possible improvement would be to
// track whether the currently enclosing loop is a "for {" or
// a for/range with condition, then use mayReturn only for the
// former. Note also that "break X" or "continue X" is treated
// the same as "goto", since we don't have a good way to track
// the target of the branch.
st = psMayReturn
n := n.(*ir.BranchStmt)
if n.Label != nil {
ffa.pessimize()
}
case ir.OBLOCK:
n := n.(*ir.BlockStmt)
st = ffa.stateForList(n.List)
case ir.OCASE:
if ccst, ok := n.(*ir.CaseClause); ok {
st = ffa.stateForList(ccst.Body)
} else if ccst, ok := n.(*ir.CommClause); ok {
st = ffa.stateForList(ccst.Body)
} else {
panic("unexpected")
}
case ir.OIF:
n := n.(*ir.IfStmt)
st = branchCombine(ffa.stateForList(n.Body), ffa.stateForList(n.Else))
case ir.OFOR:
// Treat for { XXX } like a block.
// Treat for <cond> { XXX } like an if statement with no else.
n := n.(*ir.ForStmt)
bst := ffa.stateForList(n.Body)
if n.Cond == nil {
st = bst
} else {
if bst == psMayReturn {
st = psMayReturn
}
}
case ir.ORANGE:
// Treat for range { XXX } like an if statement with no else.
n := n.(*ir.RangeStmt)
if ffa.stateForList(n.Body) == psMayReturn {
st = psMayReturn
}
case ir.OGOTO:
// punt if we see even one goto. if we built a control
// flow graph we could do more, but this is just a tree walk.
ffa.pessimize()
case ir.OSELECT:
// process selects for "may return" but not "always panics",
// the latter case seems very improbable.
n := n.(*ir.SelectStmt)
if len(n.Cases) != 0 {
st = psTop
for _, c := range n.Cases {
st = branchCombine(ffa.stateForList(c.Body), st)
}
}
case ir.OSWITCH:
n := n.(*ir.SwitchStmt)
if len(n.Cases) != 0 {
st = psTop
for _, c := range n.Cases {
st = branchCombine(ffa.stateForList(c.Body), st)
}
}

st, fall := psTop, psNoInfo
for i := len(n.Cases) - 1; i >= 0; i-- {
cas := n.Cases[i]
cst := ffa.stateForList(cas.Body)
endsInFallthrough := false
if len(cas.Body) != 0 {
endsInFallthrough = cas.Body[0].Op() == ir.OFALL
}
if endsInFallthrough {
cst = blockCombine(cst, fall)
}
st = branchCombine(st, cst)
fall = cst
}
case ir.OFALL:
// Not important.
case ir.ODCLFUNC, ir.ORECOVER, ir.OAS, ir.OAS2, ir.OAS2FUNC, ir.OASOP,
ir.OPRINTN, ir.OPRINT, ir.OLABEL, ir.OCALLINTER, ir.ODEFER,
ir.OSEND, ir.ORECV, ir.OSELRECV2, ir.OGO, ir.OAPPEND, ir.OAS2DOTTYPE,
ir.OAS2MAPR, ir.OGETG, ir.ODELETE, ir.OINLMARK, ir.OAS2RECV,
ir.OMIN, ir.OMAX, ir.OMAKE, ir.ORECOVERFP, ir.OGETCALLERSP:
// these should all be benign/uninteresting
case ir.OTAILCALL, ir.OJUMPTABLE, ir.OTYPESW:
// don't expect to see these at all.
base.Fatalf("unexpected op %s in func %s",
n.Op().String(), ir.FuncName(ffa.fn))
default:
base.Fatalf("%v: unhandled op %s in func %v",
ir.Line(n), n.Op().String(), ir.FuncName(ffa.fn))
}
if debugTrace&debugTraceFuncFlags != 0 {
fmt.Fprintf(os.Stderr, "=-= %v: visit n=%s returns %s\n",
ir.Line(n), n.Op().String(), st.String())
}
ffa.setstate(n, st)
}

func (ffa *funcFlagsAnalyzer) nodeVisitPre(n ir.Node) {
}

0 comments on commit e844d72

Please sign in to comment.