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.
8 "cmd/compile/internal/ir"
13 // This file contains code to re-score callsites based on how the
14 // results of the call were used. Example:
19 // case 10: fptr = baz()
25 // The initial scoring pass will assign a score to "bar()" based on
26 // various criteria, however once the first pass of scoring is done,
27 // we look at the flags on the result from bar, and check to see
28 // how those results are used. If bar() always returns the same constant
29 // for its first result, and if the variable receiving that result
30 // isn't redefined, and if that variable feeds into an if/switch
31 // condition, then we will try to adjust the score for "bar" (on the
32 // theory that if we inlined, we can constant fold / deadcode).
34 type resultPropAndCS struct {
39 type resultUseAnalyzer struct {
40 resultNameTab map[*ir.Name]resultPropAndCS
46 // rescoreBasedOnCallResultUses examines how call results are used,
47 // and tries to update the scores of calls based on how their results
48 // are used in the function.
49 func rescoreBasedOnCallResultUses(fn *ir.Func, resultNameTab map[*ir.Name]resultPropAndCS, cstab CallSiteTab) {
50 if os.Getenv("THANM_DEBUG") != "" {
53 enableDebugTraceIfEnv()
54 rua := &resultUseAnalyzer{
55 resultNameTab: resultNameTab,
58 condLevelTracker: new(condLevelTracker),
60 var doNode func(ir.Node) bool
61 doNode = func(n ir.Node) bool {
63 ir.DoChildren(n, doNode)
71 func examineCallResults(cs *CallSite, resultNameTab map[*ir.Name]resultPropAndCS) {
72 if debugTrace&debugTraceScoring != 0 {
73 fmt.Fprintf(os.Stderr, "=-= examining call results for %q\n",
74 EncodeCallSiteKey(cs))
77 // Invoke a helper to pick out the specific ir.Name's the results
78 // from this call are assigned into, e.g. "x, y := fooBar()". If
79 // the call is not part of an assignment statement, or if the
80 // variables in question are not newly defined, then we'll receive
81 // an empty list here.
83 names, autoTemps, props := namesDefined(cs)
88 if debugTrace&debugTraceScoring != 0 {
89 fmt.Fprintf(os.Stderr, "=-= %d names defined\n", len(names))
92 // For each returned value, if the value has interesting
93 // properties (ex: always returns the same constant), and the name
94 // in question is never redefined, then make an entry in the
95 // result table for it.
96 const interesting = (ResultIsConcreteTypeConvertedToInterface |
97 ResultAlwaysSameConstant | ResultAlwaysSameInlinableFunc | ResultAlwaysSameFunc)
98 for idx, n := range names {
99 rprop := props.ResultFlags[idx]
101 if debugTrace&debugTraceScoring != 0 {
102 fmt.Fprintf(os.Stderr, "=-= props for ret %d %q: %s\n",
103 idx, n.Sym().Name, rprop.String())
106 if rprop&interesting == 0 {
109 if ir.Reassigned(n) {
112 if _, ok := resultNameTab[n]; ok {
113 panic("should never happen")
115 entry := resultPropAndCS{
119 resultNameTab[n] = entry
120 if autoTemps[idx] != nil {
121 resultNameTab[autoTemps[idx]] = entry
123 if debugTrace&debugTraceScoring != 0 {
124 fmt.Fprintf(os.Stderr, "=-= add resultNameTab table entry n=%v autotemp=%v props=%s\n", n, autoTemps[idx], rprop.String())
129 // namesDefined returns a list of ir.Name's corresponding to locals
130 // that receive the results from the call at site 'cs', plus the
131 // properties object for the called function. If a given result
132 // isn't cleanly assigned to a newly defined local, the
133 // slot for that result in the returned list will be nil. Example:
135 // call returned name list
138 // z, y := bar() [ nil, nil ]
139 // _, q := baz() [ nil, q ]
141 // In the case of a multi-return call, such as "x, y := foo()",
142 // the pattern we see from the front end will be a call op
143 // assigning to auto-temps, and then an assignment of the auto-temps
144 // to the user-level variables. In such cases we return
145 // first the user-level variable (in the first func result)
146 // and then the auto-temp name in the second result.
147 func namesDefined(cs *CallSite) ([]*ir.Name, []*ir.Name, *FuncProps) {
148 // If this call doesn't feed into an assignment (and of course not
149 // all calls do), then we don't have anything to work with here.
150 if cs.Assign == nil {
153 fih, ok := fpmap[cs.Callee]
155 // TODO: add an assert/panic here.
158 if len(fih.props.ResultFlags) == 0 {
162 // Single return case.
163 if len(fih.props.ResultFlags) == 1 {
164 asgn, ok := cs.Assign.(*ir.AssignStmt)
168 // locate name being assigned
169 aname, ok := asgn.X.(*ir.Name)
173 return []*ir.Name{aname}, []*ir.Name{nil}, fih.props
177 asgn, ok := cs.Assign.(*ir.AssignListStmt)
178 if !ok || !asgn.Def {
181 userVars := make([]*ir.Name, len(fih.props.ResultFlags))
182 autoTemps := make([]*ir.Name, len(fih.props.ResultFlags))
183 for idx, x := range asgn.Lhs {
184 if n, ok := x.(*ir.Name); ok {
187 if r.Op() == ir.OCONVNOP {
188 r = r.(*ir.ConvExpr).X
191 autoTemps[idx] = r.(*ir.Name)
193 if debugTrace&debugTraceScoring != 0 {
194 fmt.Fprintf(os.Stderr, "=-= multi-ret namedef uv=%v at=%v\n",
201 return userVars, autoTemps, fih.props
204 func (rua *resultUseAnalyzer) nodeVisitPost(n ir.Node) {
205 rua.condLevelTracker.post(n)
208 func (rua *resultUseAnalyzer) nodeVisitPre(n ir.Node) {
209 rua.condLevelTracker.pre(n)
212 if debugTrace&debugTraceScoring != 0 {
213 fmt.Fprintf(os.Stderr, "=-= rescore examine iface call %v:\n", n)
215 rua.callTargetCheckResults(n)
217 if debugTrace&debugTraceScoring != 0 {
218 fmt.Fprintf(os.Stderr, "=-= rescore examine call %v:\n", n)
220 rua.callTargetCheckResults(n)
222 ifst := n.(*ir.IfStmt)
223 rua.foldCheckResults(ifst.Cond)
225 swst := n.(*ir.SwitchStmt)
227 rua.foldCheckResults(swst.Tag)
233 // callTargetCheckResults examines a given call to see whether the
234 // callee expression is potentially an inlinable function returned
235 // from a potentially inlinable call. Examples:
237 // Scenario 1: named intermediate
239 // fn1 := foo() conc := bar()
240 // fn1("blah") conc.MyMethod()
242 // Scenario 2: returned func or concrete object feeds directly to call
244 // foo()("blah") bar().MyMethod()
246 // In the second case although at the source level the result of the
247 // direct call feeds right into the method call or indirect call,
248 // we're relying on the front end having inserted an auto-temp to
249 // capture the value.
250 func (rua *resultUseAnalyzer) callTargetCheckResults(call ir.Node) {
251 ce := call.(*ir.CallExpr)
252 rname := rua.getCallResultName(ce)
256 if debugTrace&debugTraceScoring != 0 {
257 fmt.Fprintf(os.Stderr, "=-= staticvalue returns %v:\n",
260 if rname.Class != ir.PAUTO {
265 if debugTrace&debugTraceScoring != 0 {
266 fmt.Fprintf(os.Stderr, "=-= in %s checking %v for cci prop:\n",
267 rua.fn.Sym().Name, rname)
269 if cs := rua.returnHasProp(rname, ResultIsConcreteTypeConvertedToInterface); cs != nil {
270 // FIXME: add cond level support here
271 adj := passConcreteToItfCallAdj
272 cs.Score, cs.ScoreMask = adjustScore(adj, cs.Score, cs.ScoreMask)
273 adj = callResultRescoreAdj
274 cs.Score, cs.ScoreMask = adjustScore(adj, cs.Score, cs.ScoreMask)
277 if debugTrace&debugTraceScoring != 0 {
278 fmt.Fprintf(os.Stderr, "=-= in %s checking %v for samefunc props:\n",
279 rua.fn.Sym().Name, rname)
280 v, ok := rua.resultNameTab[rname]
282 fmt.Fprintf(os.Stderr, "=-= no entry for %v in rt\n", rname)
284 fmt.Fprintf(os.Stderr, "=-= props for %v: %q\n", rname, v.props.String())
287 if cs := rua.returnHasProp(rname, ResultAlwaysSameInlinableFunc); cs != nil {
288 // FIXME: add cond level support here
289 adj := passInlinableFuncToIndCallAdj
290 cs.Score, cs.ScoreMask = adjustScore(adj, cs.Score, cs.ScoreMask)
291 adj = callResultRescoreAdj
292 cs.Score, cs.ScoreMask = adjustScore(adj, cs.Score, cs.ScoreMask)
293 } else if cs := rua.returnHasProp(rname, ResultAlwaysSameFunc); cs != nil {
294 // FIXME: add cond level support here
295 adj := passFuncToIndCallAdj
296 cs.Score, cs.ScoreMask = adjustScore(adj, cs.Score, cs.ScoreMask)
297 adj = callResultRescoreAdj
298 cs.Score, cs.ScoreMask = adjustScore(adj, cs.Score, cs.ScoreMask)
303 // foldCheckResults examines the specified if/switch condition 'cond'
304 // to see if it refers to locals defined by a (potentially inlinable)
305 // function call at call site C, and if so, whether 'cond' contains
306 // only combinations of simple references to all of the names in
307 // 'names' with selected constants + operators. If these criteria are
308 // met, then we adjust the score for call site C to reflect the
309 // fact that inlining will enable deadcode and/or constant propagation.
310 // Note: for this heuristic to kick in, the names in question have to
311 // be all from the same callsite. Examples:
313 // q, r := baz() x, y := foo()
314 // switch q+r { a, b, c := bar()
315 // ... if x && y && a && b && c {
319 // For the call to "baz" above we apply a score adjustment, but not
320 // for the calls to "foo" or "bar".
321 func (rua *resultUseAnalyzer) foldCheckResults(cond ir.Node) {
322 namesUsed := collectNamesUsed(cond)
323 if len(namesUsed) == 0 {
327 for _, n := range namesUsed {
328 rpcs, found := rua.resultNameTab[n]
332 if cs != nil && rpcs.defcs != cs {
336 if rpcs.props&ResultAlwaysSameConstant == 0 {
340 if debugTrace&debugTraceScoring != 0 {
341 nls := func(nl []*ir.Name) string {
343 for _, n := range nl {
344 r += " " + n.Sym().Name
348 fmt.Fprintf(os.Stderr, "=-= calling ShouldFoldIfNameConstant on names={%s} cond=%v\n", nls(namesUsed), cond)
351 if !ShouldFoldIfNameConstant(cond, namesUsed) {
354 // FIXME: add cond level support here
355 adj := passConstToIfAdj
356 cs.Score, cs.ScoreMask = adjustScore(adj, cs.Score, cs.ScoreMask)
357 adj = callResultRescoreAdj
358 cs.Score, cs.ScoreMask = adjustScore(adj, cs.Score, cs.ScoreMask)
361 func collectNamesUsed(expr ir.Node) []*ir.Name {
363 ir.Visit(expr, func(n ir.Node) {
364 if n.Op() != ir.ONAME {
368 if nn.Class != ir.PAUTO {
371 res = append(res, nn)
376 func (rua *resultUseAnalyzer) returnHasProp(name *ir.Name, prop ResultPropBits) *CallSite {
377 v, ok := rua.resultNameTab[name]
381 if v.props&prop == 0 {
387 func (rua *resultUseAnalyzer) getCallResultName(ce *ir.CallExpr) *ir.Name {
389 if sel, ok := ce.Fun.(*ir.SelectorExpr); ok {
392 } else if ctarg, ok := ce.Fun.(*ir.Name); ok {
398 r := ir.StaticValue(callTarg)
399 if debugTrace&debugTraceScoring != 0 {
400 fmt.Fprintf(os.Stderr, "=-= staticname on %v returns %v:\n",
403 if r.Op() == ir.OCALLFUNC {
404 // This corresponds to the "x := foo()" case; here
405 // ir.StaticValue has brought us all the way back to
406 // the call expression itself. We need to back off to
407 // the name defined by the call; do this by looking up
409 ce := r.(*ir.CallExpr)
410 cs, ok := rua.cstab[ce]
414 names, _, _ := namesDefined(cs)
419 } else if r.Op() == ir.ONAME {