Skip to content

Commit e844d72

Browse files
committed
cmd/compile/internal/inline: no-return flag analysis for inline heuristics
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 <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 5cdb132 commit e844d72

File tree

6 files changed

+703
-110
lines changed

6 files changed

+703
-110
lines changed

src/cmd/compile/internal/inline/inlheur/analyze.go

+39-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,24 @@ import (
1818

1919
const (
2020
debugTraceFuncs = 1 << iota
21+
debugTraceFuncFlags
2122
)
2223

24+
// propAnalyzer interface is used for defining one or more analyzer
25+
// helper objects, each tasked with computing some specific subset of
26+
// the properties we're interested in. The assumption is that
27+
// properties are independent, so each new analyzer that implements
28+
// this interface can operate entirely on its own. For a given analyzer
29+
// there will be a sequence of calls to nodeVisitPre and nodeVisitPost
30+
// as the nodes within a function are visited, then a followup call to
31+
// setResults so that the analyzer can transfer its results into the
32+
// final properties object.
33+
type propAnalyzer interface {
34+
nodeVisitPre(n ir.Node)
35+
nodeVisitPost(n ir.Node)
36+
setResults(fp *FuncProps)
37+
}
38+
2339
// fnInlHeur contains inline heuristics state information about
2440
// a specific Go function being analyzed/considered by the inliner.
2541
type fnInlHeur struct {
@@ -37,8 +53,29 @@ func computeFuncProps(fn *ir.Func) *FuncProps {
3753
fmt.Fprintf(os.Stderr, "=-= starting analysis of func %v:\n%+v\n",
3854
fn.Sym().Name, fn)
3955
}
40-
// implementation stubbed out for now
41-
return &FuncProps{}
56+
ffa := makeFuncFlagsAnalyzer(fn)
57+
analyzers := []propAnalyzer{ffa}
58+
fp := new(FuncProps)
59+
runAnalyzersOnFunction(fn, analyzers)
60+
for _, a := range analyzers {
61+
a.setResults(fp)
62+
}
63+
return fp
64+
}
65+
66+
func runAnalyzersOnFunction(fn *ir.Func, analyzers []propAnalyzer) {
67+
var doNode func(ir.Node) bool
68+
doNode = func(n ir.Node) bool {
69+
for _, a := range analyzers {
70+
a.nodeVisitPre(n)
71+
}
72+
ir.DoChildren(n, doNode)
73+
for _, a := range analyzers {
74+
a.nodeVisitPost(n)
75+
}
76+
return false
77+
}
78+
doNode(fn)
4279
}
4380

4481
func fnFileLine(fn *ir.Func) (string, uint) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
// Copyright 2023 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+
package inlheur
6+
7+
import (
8+
"cmd/compile/internal/base"
9+
"cmd/compile/internal/ir"
10+
"cmd/compile/internal/types"
11+
"fmt"
12+
"os"
13+
)
14+
15+
// funcFlagsAnalyzer computes the "Flags" value for the FuncProps
16+
// object we're computing. The main item of interest here is "nstate",
17+
// which stores the disposition of a given ir Node with respect to the
18+
// flags/properties we're trying to compute.
19+
type funcFlagsAnalyzer struct {
20+
fn *ir.Func
21+
nstate map[ir.Node]pstate
22+
noInfo bool // set if we see something inscrutable/un-analyzable
23+
}
24+
25+
// pstate keeps track of the disposition of a given node and its
26+
// children with respect to panic/exit calls.
27+
type pstate int
28+
29+
const (
30+
psNoInfo pstate = iota // nothing interesting about this node
31+
psCallsPanic // node causes call to panic or os.Exit
32+
psMayReturn // executing node may trigger a "return" stmt
33+
psTop // dataflow lattice "top" element
34+
)
35+
36+
func makeFuncFlagsAnalyzer(fn *ir.Func) *funcFlagsAnalyzer {
37+
return &funcFlagsAnalyzer{
38+
fn: fn,
39+
nstate: make(map[ir.Node]pstate),
40+
}
41+
}
42+
43+
// setResults transfers func flag results to 'fp'.
44+
func (ffa *funcFlagsAnalyzer) setResults(fp *FuncProps) {
45+
var rv FuncPropBits
46+
if !ffa.noInfo && ffa.stateForList(ffa.fn.Body) == psCallsPanic {
47+
rv = FuncPropNeverReturns
48+
}
49+
// This is slightly hacky and not at all required, but include a
50+
// special case for main.main, which often ends in a call to
51+
// os.Exit. People who write code like this (very common I
52+
// imagine)
53+
//
54+
// func main() {
55+
// rc = perform()
56+
// ...
57+
// foo()
58+
// os.Exit(rc)
59+
// }
60+
//
61+
// will be constantly surprised when foo() is inlined in many
62+
// other spots in the program but not in main().
63+
if isMainMain(ffa.fn) {
64+
rv &^= FuncPropNeverReturns
65+
}
66+
fp.Flags = rv
67+
}
68+
69+
func (ffa *funcFlagsAnalyzer) getstate(n ir.Node) pstate {
70+
val, ok := ffa.nstate[n]
71+
if !ok {
72+
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)
73+
}
74+
return val
75+
}
76+
77+
func (ffa *funcFlagsAnalyzer) setstate(n ir.Node, st pstate) {
78+
if _, ok := ffa.nstate[n]; ok {
79+
base.Fatalf("funcFlagsAnalyzer: fn %q internal error, existing setting for node:\n%+v\n", ffa.fn.Sym().Name, n)
80+
} else {
81+
ffa.nstate[n] = st
82+
}
83+
}
84+
85+
func (ffa *funcFlagsAnalyzer) setstateSoft(n ir.Node, st pstate) {
86+
ffa.nstate[n] = st
87+
}
88+
89+
// blockCombine merges together states as part of a linear sequence of
90+
// statements, where 'pred' and 'succ' are analysis results for a pair
91+
// of consecutive statements. Examples:
92+
//
93+
// case 1: case 2:
94+
// panic("foo") if q { return x } <-pred
95+
// return x panic("boo") <-succ
96+
//
97+
// In case 1, since the pred state is "always panic" it doesn't matter
98+
// what the succ state is, hence the state for the combination of the
99+
// two blocks is "always panics". In case 2, because there is a path
100+
// to return that avoids the panic in succ, the state for the
101+
// combination of the two statements is "may return".
102+
func blockCombine(pred, succ pstate) pstate {
103+
switch succ {
104+
case psTop:
105+
return pred
106+
case psMayReturn:
107+
if pred == psCallsPanic {
108+
return psCallsPanic
109+
}
110+
return psMayReturn
111+
case psNoInfo:
112+
return pred
113+
case psCallsPanic:
114+
if pred == psMayReturn {
115+
return psMayReturn
116+
}
117+
return psCallsPanic
118+
}
119+
panic("should never execute")
120+
}
121+
122+
// branchCombine combines two states at a control flow branch point where
123+
// either p1 or p2 executes (as in an "if" statement).
124+
func branchCombine(p1, p2 pstate) pstate {
125+
if p1 == psCallsPanic && p2 == psCallsPanic {
126+
return psCallsPanic
127+
}
128+
if p1 == psMayReturn || p2 == psMayReturn {
129+
return psMayReturn
130+
}
131+
return psNoInfo
132+
}
133+
134+
// stateForList walks through a list of statements and computes the
135+
// state/diposition for the entire list as a whole.
136+
func (ffa *funcFlagsAnalyzer) stateForList(list ir.Nodes) pstate {
137+
st := psTop
138+
for i := range list {
139+
n := list[i]
140+
psi := ffa.getstate(n)
141+
if debugTrace&debugTraceFuncFlags != 0 {
142+
fmt.Fprintf(os.Stderr, "=-= %v: stateForList n=%s ps=%s\n",
143+
ir.Line(n), n.Op().String(), psi.String())
144+
}
145+
st = blockCombine(st, psi)
146+
}
147+
if st == psTop {
148+
st = psNoInfo
149+
}
150+
return st
151+
}
152+
153+
func isMainMain(fn *ir.Func) bool {
154+
s := fn.Sym()
155+
return (s.Pkg.Name == "main" && s.Name == "main")
156+
}
157+
158+
func isWellKnownFunc(s *types.Sym, pkg, name string) bool {
159+
return s.Pkg.Path == pkg && s.Name == name
160+
}
161+
162+
// isExitCall reports TRUE if the node itself is an unconditional
163+
// call to os.Exit(), a panic, or a function that does likewise.
164+
func isExitCall(n ir.Node) bool {
165+
if n.Op() != ir.OCALLFUNC {
166+
return false
167+
}
168+
cx := n.(*ir.CallExpr)
169+
name := ir.StaticCalleeName(cx.X)
170+
if name == nil {
171+
return false
172+
}
173+
s := name.Sym()
174+
if isWellKnownFunc(s, "os", "Exit") ||
175+
isWellKnownFunc(s, "runtime", "throw") {
176+
return true
177+
}
178+
// FIXME: consult results of flags computation for
179+
// previously analyzed Go functions, including props
180+
// read from export data for functions in other packages.
181+
return false
182+
}
183+
184+
// pessimize is called to record the fact that we saw something in the
185+
// function that renders it entirely impossible to analyze.
186+
func (ffa *funcFlagsAnalyzer) pessimize() {
187+
ffa.noInfo = true
188+
}
189+
190+
// shouldVisit reports TRUE if this is an interesting node from the
191+
// perspective of computing function flags. NB: due to the fact that
192+
// ir.CallExpr implements the Stmt interface, we wind up visiting
193+
// a lot of nodes that we don't really need to, but these can
194+
// simply be screened out as part of the visit.
195+
func shouldVisit(n ir.Node) bool {
196+
_, isStmt := n.(ir.Stmt)
197+
return n.Op() != ir.ODCL &&
198+
(isStmt || n.Op() == ir.OCALLFUNC || n.Op() == ir.OPANIC)
199+
}
200+
201+
// nodeVisitPost helps implement the propAnalyzer interface; when
202+
// called on a given node, it decides the disposition of that node
203+
// based on the state(s) of the node's children.
204+
func (ffa *funcFlagsAnalyzer) nodeVisitPost(n ir.Node) {
205+
if debugTrace&debugTraceFuncFlags != 0 {
206+
fmt.Fprintf(os.Stderr, "=+= nodevis %v %s should=%v\n",
207+
ir.Line(n), n.Op().String(), shouldVisit(n))
208+
}
209+
if !shouldVisit(n) {
210+
// invoke soft set, since node may be shared (e.g. ONAME)
211+
ffa.setstateSoft(n, psNoInfo)
212+
return
213+
}
214+
var st pstate
215+
switch n.Op() {
216+
case ir.OCALLFUNC:
217+
if isExitCall(n) {
218+
st = psCallsPanic
219+
}
220+
case ir.OPANIC:
221+
st = psCallsPanic
222+
case ir.ORETURN:
223+
st = psMayReturn
224+
case ir.OBREAK, ir.OCONTINUE:
225+
// FIXME: this handling of break/continue is sub-optimal; we
226+
// have them as "mayReturn" in order to help with this case:
227+
//
228+
// for {
229+
// if q() { break }
230+
// panic(...)
231+
// }
232+
//
233+
// where the effect of the 'break' is to cause the subsequent
234+
// panic to be skipped. One possible improvement would be to
235+
// track whether the currently enclosing loop is a "for {" or
236+
// a for/range with condition, then use mayReturn only for the
237+
// former. Note also that "break X" or "continue X" is treated
238+
// the same as "goto", since we don't have a good way to track
239+
// the target of the branch.
240+
st = psMayReturn
241+
n := n.(*ir.BranchStmt)
242+
if n.Label != nil {
243+
ffa.pessimize()
244+
}
245+
case ir.OBLOCK:
246+
n := n.(*ir.BlockStmt)
247+
st = ffa.stateForList(n.List)
248+
case ir.OCASE:
249+
if ccst, ok := n.(*ir.CaseClause); ok {
250+
st = ffa.stateForList(ccst.Body)
251+
} else if ccst, ok := n.(*ir.CommClause); ok {
252+
st = ffa.stateForList(ccst.Body)
253+
} else {
254+
panic("unexpected")
255+
}
256+
case ir.OIF:
257+
n := n.(*ir.IfStmt)
258+
st = branchCombine(ffa.stateForList(n.Body), ffa.stateForList(n.Else))
259+
case ir.OFOR:
260+
// Treat for { XXX } like a block.
261+
// Treat for <cond> { XXX } like an if statement with no else.
262+
n := n.(*ir.ForStmt)
263+
bst := ffa.stateForList(n.Body)
264+
if n.Cond == nil {
265+
st = bst
266+
} else {
267+
if bst == psMayReturn {
268+
st = psMayReturn
269+
}
270+
}
271+
case ir.ORANGE:
272+
// Treat for range { XXX } like an if statement with no else.
273+
n := n.(*ir.RangeStmt)
274+
if ffa.stateForList(n.Body) == psMayReturn {
275+
st = psMayReturn
276+
}
277+
case ir.OGOTO:
278+
// punt if we see even one goto. if we built a control
279+
// flow graph we could do more, but this is just a tree walk.
280+
ffa.pessimize()
281+
case ir.OSELECT:
282+
// process selects for "may return" but not "always panics",
283+
// the latter case seems very improbable.
284+
n := n.(*ir.SelectStmt)
285+
if len(n.Cases) != 0 {
286+
st = psTop
287+
for _, c := range n.Cases {
288+
st = branchCombine(ffa.stateForList(c.Body), st)
289+
}
290+
}
291+
case ir.OSWITCH:
292+
n := n.(*ir.SwitchStmt)
293+
if len(n.Cases) != 0 {
294+
st = psTop
295+
for _, c := range n.Cases {
296+
st = branchCombine(ffa.stateForList(c.Body), st)
297+
}
298+
}
299+
300+
st, fall := psTop, psNoInfo
301+
for i := len(n.Cases) - 1; i >= 0; i-- {
302+
cas := n.Cases[i]
303+
cst := ffa.stateForList(cas.Body)
304+
endsInFallthrough := false
305+
if len(cas.Body) != 0 {
306+
endsInFallthrough = cas.Body[0].Op() == ir.OFALL
307+
}
308+
if endsInFallthrough {
309+
cst = blockCombine(cst, fall)
310+
}
311+
st = branchCombine(st, cst)
312+
fall = cst
313+
}
314+
case ir.OFALL:
315+
// Not important.
316+
case ir.ODCLFUNC, ir.ORECOVER, ir.OAS, ir.OAS2, ir.OAS2FUNC, ir.OASOP,
317+
ir.OPRINTN, ir.OPRINT, ir.OLABEL, ir.OCALLINTER, ir.ODEFER,
318+
ir.OSEND, ir.ORECV, ir.OSELRECV2, ir.OGO, ir.OAPPEND, ir.OAS2DOTTYPE,
319+
ir.OAS2MAPR, ir.OGETG, ir.ODELETE, ir.OINLMARK, ir.OAS2RECV,
320+
ir.OMIN, ir.OMAX, ir.OMAKE, ir.ORECOVERFP, ir.OGETCALLERSP:
321+
// these should all be benign/uninteresting
322+
case ir.OTAILCALL, ir.OJUMPTABLE, ir.OTYPESW:
323+
// don't expect to see these at all.
324+
base.Fatalf("unexpected op %s in func %s",
325+
n.Op().String(), ir.FuncName(ffa.fn))
326+
default:
327+
base.Fatalf("%v: unhandled op %s in func %v",
328+
ir.Line(n), n.Op().String(), ir.FuncName(ffa.fn))
329+
}
330+
if debugTrace&debugTraceFuncFlags != 0 {
331+
fmt.Fprintf(os.Stderr, "=-= %v: visit n=%s returns %s\n",
332+
ir.Line(n), n.Op().String(), st.String())
333+
}
334+
ffa.setstate(n, st)
335+
}
336+
337+
func (ffa *funcFlagsAnalyzer) nodeVisitPre(n ir.Node) {
338+
}

0 commit comments

Comments
 (0)