blob: 2aaa68d1b7ace5b121dae16e714124b985796eec [file] [log] [blame]
// 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/ir"
"fmt"
"go/constant"
"go/token"
"os"
)
// resultsAnalyzer stores state information for the process of
// computing flags/properties for the return values of a specific Go
// function, as part of inline heuristics synthesis.
type resultsAnalyzer struct {
fname string
props []ResultPropBits
values []resultVal
inlineMaxBudget int
*nameFinder
}
// resultVal captures information about a specific result returned from
// the function we're analyzing; we are interested in cases where
// the func always returns the same constant, or always returns
// the same function, etc. This container stores info on a the specific
// scenarios we're looking for.
type resultVal struct {
cval constant.Value
fn *ir.Name
fnClo bool
top bool
derived bool // see deriveReturnFlagsFromCallee below
}
// addResultsAnalyzer creates a new resultsAnalyzer helper object for
// the function fn, appends it to the analyzers list, and returns the
// new list. If the function in question doesn't have any returns (or
// any interesting returns) then the analyzer list is left as is, and
// the result flags in "fp" are updated accordingly.
func addResultsAnalyzer(fn *ir.Func, analyzers []propAnalyzer, fp *FuncProps, inlineMaxBudget int, nf *nameFinder) []propAnalyzer {
ra, props := makeResultsAnalyzer(fn, inlineMaxBudget, nf)
if ra != nil {
analyzers = append(analyzers, ra)
} else {
fp.ResultFlags = props
}
return analyzers
}
// makeResultsAnalyzer creates a new helper object to analyze results
// in function fn. If the function doesn't have any interesting
// results, a nil helper is returned along with a set of default
// result flags for the func.
func makeResultsAnalyzer(fn *ir.Func, inlineMaxBudget int, nf *nameFinder) (*resultsAnalyzer, []ResultPropBits) {
results := fn.Type().Results()
if len(results) == 0 {
return nil, nil
}
props := make([]ResultPropBits, len(results))
if fn.Inl == nil {
return nil, props
}
vals := make([]resultVal, len(results))
interestingToAnalyze := false
for i := range results {
rt := results[i].Type
if !rt.IsScalar() && !rt.HasNil() {
// existing properties not applicable here (for things
// like structs, arrays, slices, etc).
continue
}
// set the "top" flag (as in "top element of data flow lattice")
// meaning "we have no info yet, but we might later on".
vals[i].top = true
interestingToAnalyze = true
}
if !interestingToAnalyze {
return nil, props
}
ra := &resultsAnalyzer{
props: props,
values: vals,
inlineMaxBudget: inlineMaxBudget,
nameFinder: nf,
}
return ra, nil
}
// setResults transfers the calculated result properties for this
// function to 'funcProps'.
func (ra *resultsAnalyzer) setResults(funcProps *FuncProps) {
// Promote ResultAlwaysSameFunc to ResultAlwaysSameInlinableFunc
for i := range ra.values {
if ra.props[i] == ResultAlwaysSameFunc && !ra.values[i].derived {
f := ra.values[i].fn.Func
// HACK: in order to allow for call site score
// adjustments, we used a relaxed inline budget in
// determining inlinability. For the check below, however,
// we want to know is whether the func in question is
// likely to be inlined, as opposed to whether it might
// possibly be inlined if all the right score adjustments
// happened, so do a simple check based on the cost.
if f.Inl != nil && f.Inl.Cost <= int32(ra.inlineMaxBudget) {
ra.props[i] = ResultAlwaysSameInlinableFunc
}
}
}
funcProps.ResultFlags = ra.props
}
func (ra *resultsAnalyzer) pessimize() {
for i := range ra.props {
ra.props[i] = ResultNoInfo
}
}
func (ra *resultsAnalyzer) nodeVisitPre(n ir.Node) {
}
func (ra *resultsAnalyzer) nodeVisitPost(n ir.Node) {
if len(ra.values) == 0 {
return
}
if n.Op() != ir.ORETURN {
return
}
if debugTrace&debugTraceResults != 0 {
fmt.Fprintf(os.Stderr, "=+= returns nodevis %v %s\n",
ir.Line(n), n.Op().String())
}
// No support currently for named results, so if we see an empty
// "return" stmt, be conservative.
rs := n.(*ir.ReturnStmt)
if len(rs.Results) != len(ra.values) {
ra.pessimize()
return
}
for i, r := range rs.Results {
ra.analyzeResult(i, r)
}
}
// analyzeResult examines the expression 'n' being returned as the
// 'ii'th argument in some return statement to see whether has
// interesting characteristics (for example, returns a constant), then
// applies a dataflow "meet" operation to combine this result with any
// previous result (for the given return slot) that we've already
// processed.
func (ra *resultsAnalyzer) analyzeResult(ii int, n ir.Node) {
isAllocMem := ra.isAllocatedMem(n)
isConcConvItf := ra.isConcreteConvIface(n)
constVal := ra.constValue(n)
isConst := (constVal != nil)
isNil := ra.isNil(n)
rfunc := ra.funcName(n)
isFunc := (rfunc != nil)
isClo := (rfunc != nil && rfunc.Func.OClosure != nil)
curp := ra.props[ii]
dprops, isDerivedFromCall := ra.deriveReturnFlagsFromCallee(n)
newp := ResultNoInfo
var newcval constant.Value
var newfunc *ir.Name
if debugTrace&debugTraceResults != 0 {
fmt.Fprintf(os.Stderr, "=-= %v: analyzeResult n=%s ismem=%v isconcconv=%v isconst=%v isnil=%v isfunc=%v isclo=%v\n", ir.Line(n), n.Op().String(), isAllocMem, isConcConvItf, isConst, isNil, isFunc, isClo)
}
if ra.values[ii].top {
ra.values[ii].top = false
// this is the first return we've seen; record
// whatever properties it has.
switch {
case isAllocMem:
newp = ResultIsAllocatedMem
case isConcConvItf:
newp = ResultIsConcreteTypeConvertedToInterface
case isFunc:
newp = ResultAlwaysSameFunc
newfunc = rfunc
case isConst:
newp = ResultAlwaysSameConstant
newcval = constVal
case isNil:
newp = ResultAlwaysSameConstant
newcval = nil
case isDerivedFromCall:
newp = dprops
ra.values[ii].derived = true
}
} else {
if !ra.values[ii].derived {
// this is not the first return we've seen; apply
// what amounts of a "meet" operator to combine
// the properties we see here with what we saw on
// the previous returns.
switch curp {
case ResultIsAllocatedMem:
if isAllocMem {
newp = ResultIsAllocatedMem
}
case ResultIsConcreteTypeConvertedToInterface:
if isConcConvItf {
newp = ResultIsConcreteTypeConvertedToInterface
}
case ResultAlwaysSameConstant:
if isNil && ra.values[ii].cval == nil {
newp = ResultAlwaysSameConstant
newcval = nil
} else if isConst && constant.Compare(constVal, token.EQL, ra.values[ii].cval) {
newp = ResultAlwaysSameConstant
newcval = constVal
}
case ResultAlwaysSameFunc:
if isFunc && isSameFuncName(rfunc, ra.values[ii].fn) {
newp = ResultAlwaysSameFunc
newfunc = rfunc
}
}
}
}
ra.values[ii].fn = newfunc
ra.values[ii].fnClo = isClo
ra.values[ii].cval = newcval
ra.props[ii] = newp
if debugTrace&debugTraceResults != 0 {
fmt.Fprintf(os.Stderr, "=-= %v: analyzeResult newp=%s\n",
ir.Line(n), newp)
}
}
// deriveReturnFlagsFromCallee tries to set properties for a given
// return result where we're returning call expression; return value
// is a return property value and a boolean indicating whether the
// prop is valid. Examples:
//
// func foo() int { return bar() }
// func bar() int { return 42 }
// func blix() int { return 43 }
// func two(y int) int {
// if y < 0 { return bar() } else { return blix() }
// }
//
// Since "foo" always returns the result of a call to "bar", we can
// set foo's return property to that of bar. In the case of "two", however,
// even though each return path returns a constant, we don't know
// whether the constants are identical, hence we need to be conservative.
func (ra *resultsAnalyzer) deriveReturnFlagsFromCallee(n ir.Node) (ResultPropBits, bool) {
if n.Op() != ir.OCALLFUNC {
return 0, false
}
ce := n.(*ir.CallExpr)
if ce.Fun.Op() != ir.ONAME {
return 0, false
}
called := ir.StaticValue(ce.Fun)
if called.Op() != ir.ONAME {
return 0, false
}
cname := ra.funcName(called)
if cname == nil {
return 0, false
}
calleeProps := propsForFunc(cname.Func)
if calleeProps == nil {
return 0, false
}
if len(calleeProps.ResultFlags) != 1 {
return 0, false
}
return calleeProps.ResultFlags[0], true
}