cmd/compile/internal/inline: rework use of ir.StaticValue

When running the code to compute function properties that feed
inlining heuristics, the existing heuristics implementation makes
fairly extensive use of ir.StaticValue and ir.Reassigned to sharpen
the analysis. These calls turn out to cause a significant compile time
increase, due to the fact that each call can potentially walk every
node in the IR for the function. To help with this problem, switch the
heuristics code over to using the new "batch mode" reassignment helper
added in the previous CL.

Change-Id: Ib15a62416134386e34b7cfa1130a4b413a37b225
Reviewed-on: https://go-review.googlesource.com/c/go/+/537977
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
This commit is contained in:
Than McIntosh 2023-10-27 08:58:16 -04:00
parent 8eecf26e3f
commit bbcd85528c
7 changed files with 287 additions and 185 deletions

View File

@ -98,12 +98,13 @@ func AnalyzeFunc(fn *ir.Func, canInline func(*ir.Func), budgetForFunc func(*ir.F
// inlinable; if it is over the default hairyness limit and it // inlinable; if it is over the default hairyness limit and it
// doesn't have any interesting properties, then we don't want // doesn't have any interesting properties, then we don't want
// the overhead of writing out its inline body. // the overhead of writing out its inline body.
nameFinder := newNameFinder(fn)
for i := len(funcs) - 1; i >= 0; i-- { for i := len(funcs) - 1; i >= 0; i-- {
f := funcs[i] f := funcs[i]
if f.OClosure != nil && !f.InlinabilityChecked() { if f.OClosure != nil && !f.InlinabilityChecked() {
canInline(f) canInline(f)
} }
funcProps := analyzeFunc(f, inlineMaxBudget) funcProps := analyzeFunc(f, inlineMaxBudget, nameFinder)
revisitInlinability(f, funcProps, budgetForFunc) revisitInlinability(f, funcProps, budgetForFunc)
if f.Inl != nil { if f.Inl != nil {
f.Inl.Properties = funcProps.SerializeToString() f.Inl.Properties = funcProps.SerializeToString()
@ -122,11 +123,11 @@ func TearDown() {
scoreCallsCache.csl = nil scoreCallsCache.csl = nil
} }
func analyzeFunc(fn *ir.Func, inlineMaxBudget int) *FuncProps { func analyzeFunc(fn *ir.Func, inlineMaxBudget int, nf *nameFinder) *FuncProps {
if funcInlHeur, ok := fpmap[fn]; ok { if funcInlHeur, ok := fpmap[fn]; ok {
return funcInlHeur.props return funcInlHeur.props
} }
funcProps, fcstab := computeFuncProps(fn, inlineMaxBudget) funcProps, fcstab := computeFuncProps(fn, inlineMaxBudget, nf)
file, line := fnFileLine(fn) file, line := fnFileLine(fn)
entry := fnInlHeur{ entry := fnInlHeur{
fname: fn.Sym().Name, fname: fn.Sym().Name,
@ -163,7 +164,7 @@ func revisitInlinability(fn *ir.Func, funcProps *FuncProps, budgetForFunc func(*
// computeFuncProps examines the Go function 'fn' and computes for it // computeFuncProps examines the Go function 'fn' and computes for it
// a function "properties" object, to be used to drive inlining // a function "properties" object, to be used to drive inlining
// heuristics. See comments on the FuncProps type for more info. // heuristics. See comments on the FuncProps type for more info.
func computeFuncProps(fn *ir.Func, inlineMaxBudget int) (*FuncProps, CallSiteTab) { func computeFuncProps(fn *ir.Func, inlineMaxBudget int, nf *nameFinder) (*FuncProps, CallSiteTab) {
if debugTrace&debugTraceFuncs != 0 { if debugTrace&debugTraceFuncs != 0 {
fmt.Fprintf(os.Stderr, "=-= starting analysis of func %v:\n%+v\n", fmt.Fprintf(os.Stderr, "=-= starting analysis of func %v:\n%+v\n",
fn, fn) fn, fn)
@ -171,13 +172,13 @@ func computeFuncProps(fn *ir.Func, inlineMaxBudget int) (*FuncProps, CallSiteTab
funcProps := new(FuncProps) funcProps := new(FuncProps)
ffa := makeFuncFlagsAnalyzer(fn) ffa := makeFuncFlagsAnalyzer(fn)
analyzers := []propAnalyzer{ffa} analyzers := []propAnalyzer{ffa}
analyzers = addResultsAnalyzer(fn, analyzers, funcProps, inlineMaxBudget) analyzers = addResultsAnalyzer(fn, analyzers, funcProps, inlineMaxBudget, nf)
analyzers = addParamsAnalyzer(fn, analyzers, funcProps) analyzers = addParamsAnalyzer(fn, analyzers, funcProps, nf)
runAnalyzersOnFunction(fn, analyzers) runAnalyzersOnFunction(fn, analyzers)
for _, a := range analyzers { for _, a := range analyzers {
a.setResults(funcProps) a.setResults(funcProps)
} }
cstab := computeCallSiteTable(fn, fn.Body, nil, ffa.panicPathTable(), 0) cstab := computeCallSiteTable(fn, fn.Body, nil, ffa.panicPathTable(), 0, nf)
return funcProps, cstab return funcProps, cstab
} }

View File

@ -14,23 +14,37 @@ import (
) )
type callSiteAnalyzer struct { type callSiteAnalyzer struct {
cstab CallSiteTab
fn *ir.Func fn *ir.Func
*nameFinder
}
type callSiteTableBuilder struct {
fn *ir.Func
*nameFinder
cstab CallSiteTab
ptab map[ir.Node]pstate ptab map[ir.Node]pstate
nstack []ir.Node nstack []ir.Node
loopNest int loopNest int
isInit bool isInit bool
} }
func makeCallSiteAnalyzer(fn *ir.Func, cstab CallSiteTab, ptab map[ir.Node]pstate, loopNestingLevel int) *callSiteAnalyzer { func makeCallSiteAnalyzer(fn *ir.Func) *callSiteAnalyzer {
isInit := fn.IsPackageInit() || strings.HasPrefix(fn.Sym().Name, "init.")
return &callSiteAnalyzer{ return &callSiteAnalyzer{
fn: fn,
nameFinder: newNameFinder(fn),
}
}
func makeCallSiteTableBuilder(fn *ir.Func, cstab CallSiteTab, ptab map[ir.Node]pstate, loopNestingLevel int, nf *nameFinder) *callSiteTableBuilder {
isInit := fn.IsPackageInit() || strings.HasPrefix(fn.Sym().Name, "init.")
return &callSiteTableBuilder{
fn: fn, fn: fn,
cstab: cstab, cstab: cstab,
ptab: ptab, ptab: ptab,
isInit: isInit, isInit: isInit,
loopNest: loopNestingLevel, loopNest: loopNestingLevel,
nstack: []ir.Node{fn}, nstack: []ir.Node{fn},
nameFinder: nf,
} }
} }
@ -39,22 +53,22 @@ func makeCallSiteAnalyzer(fn *ir.Func, cstab CallSiteTab, ptab map[ir.Node]pstat
// specific subtree within the AST for a function. The main intended // specific subtree within the AST for a function. The main intended
// use cases are for 'region' to be either A) an entire function body, // use cases are for 'region' to be either A) an entire function body,
// or B) an inlined call expression. // or B) an inlined call expression.
func computeCallSiteTable(fn *ir.Func, region ir.Nodes, cstab CallSiteTab, ptab map[ir.Node]pstate, loopNestingLevel int) CallSiteTab { func computeCallSiteTable(fn *ir.Func, region ir.Nodes, cstab CallSiteTab, ptab map[ir.Node]pstate, loopNestingLevel int, nf *nameFinder) CallSiteTab {
csa := makeCallSiteAnalyzer(fn, cstab, ptab, loopNestingLevel) cstb := makeCallSiteTableBuilder(fn, cstab, ptab, loopNestingLevel, nf)
var doNode func(ir.Node) bool var doNode func(ir.Node) bool
doNode = func(n ir.Node) bool { doNode = func(n ir.Node) bool {
csa.nodeVisitPre(n) cstb.nodeVisitPre(n)
ir.DoChildren(n, doNode) ir.DoChildren(n, doNode)
csa.nodeVisitPost(n) cstb.nodeVisitPost(n)
return false return false
} }
for _, n := range region { for _, n := range region {
doNode(n) doNode(n)
} }
return csa.cstab return cstb.cstab
} }
func (csa *callSiteAnalyzer) flagsForNode(call *ir.CallExpr) CSPropBits { func (cstb *callSiteTableBuilder) flagsForNode(call *ir.CallExpr) CSPropBits {
var r CSPropBits var r CSPropBits
if debugTrace&debugTraceCalls != 0 { if debugTrace&debugTraceCalls != 0 {
@ -63,21 +77,21 @@ func (csa *callSiteAnalyzer) flagsForNode(call *ir.CallExpr) CSPropBits {
} }
// Set a bit if this call is within a loop. // Set a bit if this call is within a loop.
if csa.loopNest > 0 { if cstb.loopNest > 0 {
r |= CallSiteInLoop r |= CallSiteInLoop
} }
// Set a bit if the call is within an init function (either // Set a bit if the call is within an init function (either
// compiler-generated or user-written). // compiler-generated or user-written).
if csa.isInit { if cstb.isInit {
r |= CallSiteInInitFunc r |= CallSiteInInitFunc
} }
// Decide whether to apply the panic path heuristic. Hack: don't // Decide whether to apply the panic path heuristic. Hack: don't
// apply this heuristic in the function "main.main" (mostly just // apply this heuristic in the function "main.main" (mostly just
// to avoid annoying users). // to avoid annoying users).
if !isMainMain(csa.fn) { if !isMainMain(cstb.fn) {
r = csa.determinePanicPathBits(call, r) r = cstb.determinePanicPathBits(call, r)
} }
return r return r
@ -88,15 +102,15 @@ func (csa *callSiteAnalyzer) flagsForNode(call *ir.CallExpr) CSPropBits {
// panic/exit. Do this by walking back up the node stack to see if we // panic/exit. Do this by walking back up the node stack to see if we
// can find either A) an enclosing panic, or B) a statement node that // can find either A) an enclosing panic, or B) a statement node that
// we've determined leads to a panic/exit. // we've determined leads to a panic/exit.
func (csa *callSiteAnalyzer) determinePanicPathBits(call ir.Node, r CSPropBits) CSPropBits { func (cstb *callSiteTableBuilder) determinePanicPathBits(call ir.Node, r CSPropBits) CSPropBits {
csa.nstack = append(csa.nstack, call) cstb.nstack = append(cstb.nstack, call)
defer func() { defer func() {
csa.nstack = csa.nstack[:len(csa.nstack)-1] cstb.nstack = cstb.nstack[:len(cstb.nstack)-1]
}() }()
for ri := range csa.nstack[:len(csa.nstack)-1] { for ri := range cstb.nstack[:len(cstb.nstack)-1] {
i := len(csa.nstack) - ri - 1 i := len(cstb.nstack) - ri - 1
n := csa.nstack[i] n := cstb.nstack[i]
_, isCallExpr := n.(*ir.CallExpr) _, isCallExpr := n.(*ir.CallExpr)
_, isStmt := n.(ir.Stmt) _, isStmt := n.(ir.Stmt)
if isCallExpr { if isCallExpr {
@ -104,7 +118,7 @@ func (csa *callSiteAnalyzer) determinePanicPathBits(call ir.Node, r CSPropBits)
} }
if debugTrace&debugTraceCalls != 0 { if debugTrace&debugTraceCalls != 0 {
ps, inps := csa.ptab[n] ps, inps := cstb.ptab[n]
fmt.Fprintf(os.Stderr, "=-= callpar %d op=%s ps=%s inptab=%v stmt=%v\n", i, n.Op().String(), ps.String(), inps, isStmt) fmt.Fprintf(os.Stderr, "=-= callpar %d op=%s ps=%s inptab=%v stmt=%v\n", i, n.Op().String(), ps.String(), inps, isStmt)
} }
@ -112,7 +126,7 @@ func (csa *callSiteAnalyzer) determinePanicPathBits(call ir.Node, r CSPropBits)
r |= CallSiteOnPanicPath r |= CallSiteOnPanicPath
break break
} }
if v, ok := csa.ptab[n]; ok { if v, ok := cstb.ptab[n]; ok {
if v == psCallsPanic { if v == psCallsPanic {
r |= CallSiteOnPanicPath r |= CallSiteOnPanicPath
break break
@ -126,16 +140,15 @@ func (csa *callSiteAnalyzer) determinePanicPathBits(call ir.Node, r CSPropBits)
} }
// propsForArg returns property bits for a given call argument expression arg. // propsForArg returns property bits for a given call argument expression arg.
func (csa *callSiteAnalyzer) propsForArg(arg ir.Node) ActualExprPropBits { func (cstb *callSiteTableBuilder) propsForArg(arg ir.Node) ActualExprPropBits {
_, islit := isLiteral(arg) if cval := cstb.constValue(arg); cval != nil {
if islit {
return ActualExprConstant return ActualExprConstant
} }
if isConcreteConvIface(arg) { if cstb.isConcreteConvIface(arg) {
return ActualExprIsConcreteConvIface return ActualExprIsConcreteConvIface
} }
fname, isfunc, _ := isFuncName(arg) fname := cstb.funcName(arg)
if isfunc { if fname != nil {
if fn := fname.Func; fn != nil && typecheck.HaveInlineBody(fn) { if fn := fname.Func; fn != nil && typecheck.HaveInlineBody(fn) {
return ActualExprIsInlinableFunc return ActualExprIsInlinableFunc
} }
@ -149,11 +162,11 @@ func (csa *callSiteAnalyzer) propsForArg(arg ir.Node) ActualExprPropBits {
// expression; these will be stored in the CallSite object for a given // expression; these will be stored in the CallSite object for a given
// call and then consulted when scoring. If no arg has any interesting // call and then consulted when scoring. If no arg has any interesting
// properties we try to save some space and return a nil slice. // properties we try to save some space and return a nil slice.
func (csa *callSiteAnalyzer) argPropsForCall(ce *ir.CallExpr) []ActualExprPropBits { func (cstb *callSiteTableBuilder) argPropsForCall(ce *ir.CallExpr) []ActualExprPropBits {
rv := make([]ActualExprPropBits, len(ce.Args)) rv := make([]ActualExprPropBits, len(ce.Args))
somethingInteresting := false somethingInteresting := false
for idx := range ce.Args { for idx := range ce.Args {
argProp := csa.propsForArg(ce.Args[idx]) argProp := cstb.propsForArg(ce.Args[idx])
somethingInteresting = somethingInteresting || (argProp != 0) somethingInteresting = somethingInteresting || (argProp != 0)
rv[idx] = argProp rv[idx] = argProp
} }
@ -163,9 +176,9 @@ func (csa *callSiteAnalyzer) argPropsForCall(ce *ir.CallExpr) []ActualExprPropBi
return rv return rv
} }
func (csa *callSiteAnalyzer) addCallSite(callee *ir.Func, call *ir.CallExpr) { func (cstb *callSiteTableBuilder) addCallSite(callee *ir.Func, call *ir.CallExpr) {
flags := csa.flagsForNode(call) flags := cstb.flagsForNode(call)
argProps := csa.argPropsForCall(call) argProps := cstb.argPropsForCall(call)
if debugTrace&debugTraceCalls != 0 { if debugTrace&debugTraceCalls != 0 {
fmt.Fprintf(os.Stderr, "=-= props %+v for call %v\n", argProps, call) fmt.Fprintf(os.Stderr, "=-= props %+v for call %v\n", argProps, call)
} }
@ -173,12 +186,12 @@ func (csa *callSiteAnalyzer) addCallSite(callee *ir.Func, call *ir.CallExpr) {
cs := &CallSite{ cs := &CallSite{
Call: call, Call: call,
Callee: callee, Callee: callee,
Assign: csa.containingAssignment(call), Assign: cstb.containingAssignment(call),
ArgProps: argProps, ArgProps: argProps,
Flags: flags, Flags: flags,
ID: uint(len(csa.cstab)), ID: uint(len(cstb.cstab)),
} }
if _, ok := csa.cstab[call]; ok { if _, ok := cstb.cstab[call]; ok {
fmt.Fprintf(os.Stderr, "*** cstab duplicate entry at: %s\n", fmt.Fprintf(os.Stderr, "*** cstab duplicate entry at: %s\n",
fmtFullPos(call.Pos())) fmtFullPos(call.Pos()))
fmt.Fprintf(os.Stderr, "*** call: %+v\n", call) fmt.Fprintf(os.Stderr, "*** call: %+v\n", call)
@ -189,38 +202,38 @@ func (csa *callSiteAnalyzer) addCallSite(callee *ir.Func, call *ir.CallExpr) {
// on heuristics. // on heuristics.
cs.Score = int(callee.Inl.Cost) cs.Score = int(callee.Inl.Cost)
if csa.cstab == nil { if cstb.cstab == nil {
csa.cstab = make(CallSiteTab) cstb.cstab = make(CallSiteTab)
} }
csa.cstab[call] = cs cstb.cstab[call] = cs
if debugTrace&debugTraceCalls != 0 { if debugTrace&debugTraceCalls != 0 {
fmt.Fprintf(os.Stderr, "=-= added callsite: caller=%v callee=%v n=%s\n", fmt.Fprintf(os.Stderr, "=-= added callsite: caller=%v callee=%v n=%s\n",
csa.fn, callee, fmtFullPos(call.Pos())) cstb.fn, callee, fmtFullPos(call.Pos()))
} }
} }
func (csa *callSiteAnalyzer) nodeVisitPre(n ir.Node) { func (cstb *callSiteTableBuilder) nodeVisitPre(n ir.Node) {
switch n.Op() { switch n.Op() {
case ir.ORANGE, ir.OFOR: case ir.ORANGE, ir.OFOR:
if !hasTopLevelLoopBodyReturnOrBreak(loopBody(n)) { if !hasTopLevelLoopBodyReturnOrBreak(loopBody(n)) {
csa.loopNest++ cstb.loopNest++
} }
case ir.OCALLFUNC: case ir.OCALLFUNC:
ce := n.(*ir.CallExpr) ce := n.(*ir.CallExpr)
callee := pgo.DirectCallee(ce.Fun) callee := pgo.DirectCallee(ce.Fun)
if callee != nil && callee.Inl != nil { if callee != nil && callee.Inl != nil {
csa.addCallSite(callee, ce) cstb.addCallSite(callee, ce)
} }
} }
csa.nstack = append(csa.nstack, n) cstb.nstack = append(cstb.nstack, n)
} }
func (csa *callSiteAnalyzer) nodeVisitPost(n ir.Node) { func (cstb *callSiteTableBuilder) nodeVisitPost(n ir.Node) {
csa.nstack = csa.nstack[:len(csa.nstack)-1] cstb.nstack = cstb.nstack[:len(cstb.nstack)-1]
switch n.Op() { switch n.Op() {
case ir.ORANGE, ir.OFOR: case ir.ORANGE, ir.OFOR:
if !hasTopLevelLoopBodyReturnOrBreak(loopBody(n)) { if !hasTopLevelLoopBodyReturnOrBreak(loopBody(n)) {
csa.loopNest-- cstb.loopNest--
} }
} }
} }
@ -281,8 +294,8 @@ func hasTopLevelLoopBodyReturnOrBreak(loopBody ir.Nodes) bool {
// call to a pair of auto-temps, then the second one assigning the // call to a pair of auto-temps, then the second one assigning the
// auto-temps to the user-visible vars. This helper will return the // auto-temps to the user-visible vars. This helper will return the
// second (outer) of these two. // second (outer) of these two.
func (csa *callSiteAnalyzer) containingAssignment(n ir.Node) ir.Node { func (cstb *callSiteTableBuilder) containingAssignment(n ir.Node) ir.Node {
parent := csa.nstack[len(csa.nstack)-1] parent := cstb.nstack[len(cstb.nstack)-1]
// assignsOnlyAutoTemps returns TRUE of the specified OAS2FUNC // assignsOnlyAutoTemps returns TRUE of the specified OAS2FUNC
// node assigns only auto-temps. // node assigns only auto-temps.
@ -315,12 +328,12 @@ func (csa *callSiteAnalyzer) containingAssignment(n ir.Node) ir.Node {
// OAS1({x,y},OCONVNOP(OAS2FUNC({auto1,auto2},OCALLFUNC(bar)))) // OAS1({x,y},OCONVNOP(OAS2FUNC({auto1,auto2},OCALLFUNC(bar))))
// //
if assignsOnlyAutoTemps(parent) { if assignsOnlyAutoTemps(parent) {
par2 := csa.nstack[len(csa.nstack)-2] par2 := cstb.nstack[len(cstb.nstack)-2]
if par2.Op() == ir.OAS2 { if par2.Op() == ir.OAS2 {
return par2 return par2
} }
if par2.Op() == ir.OCONVNOP { if par2.Op() == ir.OCONVNOP {
par3 := csa.nstack[len(csa.nstack)-3] par3 := cstb.nstack[len(cstb.nstack)-3]
if par3.Op() == ir.OAS2 { if par3.Op() == ir.OAS2 {
return par3 return par3
} }
@ -378,18 +391,23 @@ func UpdateCallsiteTable(callerfn *ir.Func, n *ir.CallExpr, ic *ir.InlinedCallEx
loopNestLevel = 1 loopNestLevel = 1
} }
ptab := map[ir.Node]pstate{ic: icp} ptab := map[ir.Node]pstate{ic: icp}
icstab := computeCallSiteTable(callerfn, ic.Body, nil, ptab, loopNestLevel) nf := newNameFinder(nil)
icstab := computeCallSiteTable(callerfn, ic.Body, nil, ptab, loopNestLevel, nf)
// Record parent callsite. This is primarily for debug output. // Record parent callsite. This is primarily for debug output.
for _, cs := range icstab { for _, cs := range icstab {
cs.parent = oldcs cs.parent = oldcs
} }
// Score the calls in the inlined body. Note the setting of "doCallResults" // Score the calls in the inlined body. Note the setting of
// to false here: at the moment there isn't any easy way to localize // "doCallResults" to false here: at the moment there isn't any
// or region-ize the work done by "rescoreBasedOnCallResultUses", which // easy way to localize or region-ize the work done by
// currently does a walk over the entire function to look for uses // "rescoreBasedOnCallResultUses", which currently does a walk
// of a given set of results. // over the entire function to look for uses of a given set of
// results. Similarly we're passing nil to makeCallSiteAnalyzer,
// so as to run name finding without the use of static value &
// friends.
csa := makeCallSiteAnalyzer(nil)
const doCallResults = false const doCallResults = false
scoreCallsRegion(callerfn, ic.Body, icstab, doCallResults, ic) csa.scoreCallsRegion(callerfn, ic.Body, icstab, doCallResults, ic)
} }

View File

@ -19,6 +19,7 @@ type paramsAnalyzer struct {
params []*ir.Name params []*ir.Name
top []bool top []bool
*condLevelTracker *condLevelTracker
*nameFinder
} }
// getParams returns an *ir.Name slice containing all params for the // getParams returns an *ir.Name slice containing all params for the
@ -34,8 +35,8 @@ func getParams(fn *ir.Func) []*ir.Name {
// new list. If the function in question doesn't have any interesting // new list. If the function in question doesn't have any interesting
// parameters then the analyzer list is returned unchanged, and the // parameters then the analyzer list is returned unchanged, and the
// params flags in "fp" are updated accordingly. // params flags in "fp" are updated accordingly.
func addParamsAnalyzer(fn *ir.Func, analyzers []propAnalyzer, fp *FuncProps) []propAnalyzer { func addParamsAnalyzer(fn *ir.Func, analyzers []propAnalyzer, fp *FuncProps, nf *nameFinder) []propAnalyzer {
pa, props := makeParamsAnalyzer(fn) pa, props := makeParamsAnalyzer(fn, nf)
if pa != nil { if pa != nil {
analyzers = append(analyzers, pa) analyzers = append(analyzers, pa)
} else { } else {
@ -48,7 +49,7 @@ func addParamsAnalyzer(fn *ir.Func, analyzers []propAnalyzer, fp *FuncProps) []p
// of function fn. If the function doesn't have any interesting // of function fn. If the function doesn't have any interesting
// params, a nil helper is returned along with a set of default param // params, a nil helper is returned along with a set of default param
// flags for the func. // flags for the func.
func makeParamsAnalyzer(fn *ir.Func) (*paramsAnalyzer, []ParamPropBits) { func makeParamsAnalyzer(fn *ir.Func, nf *nameFinder) (*paramsAnalyzer, []ParamPropBits) {
params := getParams(fn) // includes receiver if applicable params := getParams(fn) // includes receiver if applicable
if len(params) == 0 { if len(params) == 0 {
return nil, nil return nil, nil
@ -98,6 +99,7 @@ func makeParamsAnalyzer(fn *ir.Func) (*paramsAnalyzer, []ParamPropBits) {
params: params, params: params,
top: top, top: top,
condLevelTracker: new(condLevelTracker), condLevelTracker: new(condLevelTracker),
nameFinder: nf,
} }
return pa, nil return pa, nil
} }
@ -162,7 +164,7 @@ func (pa *paramsAnalyzer) callCheckParams(ce *ir.CallExpr) {
return return
} }
sel := ce.Fun.(*ir.SelectorExpr) sel := ce.Fun.(*ir.SelectorExpr)
r := ir.StaticValue(sel.X) r := pa.staticValue(sel.X)
if r.Op() != ir.ONAME { if r.Op() != ir.ONAME {
return return
} }
@ -193,8 +195,8 @@ func (pa *paramsAnalyzer) callCheckParams(ce *ir.CallExpr) {
return name == p, false return name == p, false
}) })
} else { } else {
cname, isFunc, _ := isFuncName(called) cname := pa.funcName(called)
if isFunc { if cname != nil {
pa.deriveFlagsFromCallee(ce, cname.Func) pa.deriveFlagsFromCallee(ce, cname.Func)
} }
} }
@ -238,7 +240,7 @@ func (pa *paramsAnalyzer) deriveFlagsFromCallee(ce *ir.CallExpr, callee *ir.Func
} }
// See if one of the caller's parameters is flowing unmodified // See if one of the caller's parameters is flowing unmodified
// into this actual expression. // into this actual expression.
r := ir.StaticValue(arg) r := pa.staticValue(arg)
if r.Op() != ir.ONAME { if r.Op() != ir.ONAME {
return return
} }

View File

@ -20,6 +20,7 @@ type resultsAnalyzer struct {
props []ResultPropBits props []ResultPropBits
values []resultVal values []resultVal
inlineMaxBudget int inlineMaxBudget int
*nameFinder
} }
// resultVal captures information about a specific result returned from // resultVal captures information about a specific result returned from
@ -28,7 +29,7 @@ type resultsAnalyzer struct {
// the same function, etc. This container stores info on a the specific // the same function, etc. This container stores info on a the specific
// scenarios we're looking for. // scenarios we're looking for.
type resultVal struct { type resultVal struct {
lit constant.Value cval constant.Value
fn *ir.Name fn *ir.Name
fnClo bool fnClo bool
top bool top bool
@ -40,8 +41,8 @@ type resultVal struct {
// new list. If the function in question doesn't have any returns (or // 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 // any interesting returns) then the analyzer list is left as is, and
// the result flags in "fp" are updated accordingly. // the result flags in "fp" are updated accordingly.
func addResultsAnalyzer(fn *ir.Func, analyzers []propAnalyzer, fp *FuncProps, inlineMaxBudget int) []propAnalyzer { func addResultsAnalyzer(fn *ir.Func, analyzers []propAnalyzer, fp *FuncProps, inlineMaxBudget int, nf *nameFinder) []propAnalyzer {
ra, props := makeResultsAnalyzer(fn, inlineMaxBudget) ra, props := makeResultsAnalyzer(fn, inlineMaxBudget, nf)
if ra != nil { if ra != nil {
analyzers = append(analyzers, ra) analyzers = append(analyzers, ra)
} else { } else {
@ -54,7 +55,7 @@ func addResultsAnalyzer(fn *ir.Func, analyzers []propAnalyzer, fp *FuncProps, in
// in function fn. If the function doesn't have any interesting // in function fn. If the function doesn't have any interesting
// results, a nil helper is returned along with a set of default // results, a nil helper is returned along with a set of default
// result flags for the func. // result flags for the func.
func makeResultsAnalyzer(fn *ir.Func, inlineMaxBudget int) (*resultsAnalyzer, []ResultPropBits) { func makeResultsAnalyzer(fn *ir.Func, inlineMaxBudget int, nf *nameFinder) (*resultsAnalyzer, []ResultPropBits) {
results := fn.Type().Results() results := fn.Type().Results()
if len(results) == 0 { if len(results) == 0 {
return nil, nil return nil, nil
@ -84,6 +85,7 @@ func makeResultsAnalyzer(fn *ir.Func, inlineMaxBudget int) (*resultsAnalyzer, []
props: props, props: props,
values: vals, values: vals,
inlineMaxBudget: inlineMaxBudget, inlineMaxBudget: inlineMaxBudget,
nameFinder: nf,
} }
return ra, nil return ra, nil
} }
@ -143,29 +145,6 @@ func (ra *resultsAnalyzer) nodeVisitPost(n ir.Node) {
} }
} }
// isFuncName returns the *ir.Name for the func or method
// corresponding to node 'n', along with a boolean indicating success,
// and another boolean indicating whether the func is closure.
func isFuncName(n ir.Node) (*ir.Name, bool, bool) {
sv := ir.StaticValue(n)
if sv.Op() == ir.ONAME {
name := sv.(*ir.Name)
if name.Sym() != nil && name.Class == ir.PFUNC {
return name, true, false
}
}
if sv.Op() == ir.OCLOSURE {
cloex := sv.(*ir.ClosureExpr)
return cloex.Func.Nname, true, true
}
if sv.Op() == ir.OMETHEXPR {
if mn := ir.MethodExprName(sv); mn != nil {
return mn, true, false
}
}
return nil, false, false
}
// analyzeResult examines the expression 'n' being returned as the // analyzeResult examines the expression 'n' being returned as the
// 'ii'th argument in some return statement to see whether has // 'ii'th argument in some return statement to see whether has
// interesting characteristics (for example, returns a constant), then // interesting characteristics (for example, returns a constant), then
@ -173,18 +152,22 @@ func isFuncName(n ir.Node) (*ir.Name, bool, bool) {
// previous result (for the given return slot) that we've already // previous result (for the given return slot) that we've already
// processed. // processed.
func (ra *resultsAnalyzer) analyzeResult(ii int, n ir.Node) { func (ra *resultsAnalyzer) analyzeResult(ii int, n ir.Node) {
isAllocMem := isAllocatedMem(n) isAllocMem := ra.isAllocatedMem(n)
isConcConvItf := isConcreteConvIface(n) isConcConvItf := ra.isConcreteConvIface(n)
lit, isConst := isLiteral(n) constVal := ra.constValue(n)
rfunc, isFunc, isClo := isFuncName(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] curp := ra.props[ii]
dprops, isDerivedFromCall := deriveReturnFlagsFromCallee(n) dprops, isDerivedFromCall := ra.deriveReturnFlagsFromCallee(n)
newp := ResultNoInfo newp := ResultNoInfo
var newlit constant.Value var newcval constant.Value
var newfunc *ir.Name var newfunc *ir.Name
if debugTrace&debugTraceResults != 0 { if debugTrace&debugTraceResults != 0 {
fmt.Fprintf(os.Stderr, "=-= %v: analyzeResult n=%s ismem=%v isconcconv=%v isconst=%v isfunc=%v isclo=%v\n", ir.Line(n), n.Op().String(), isAllocMem, isConcConvItf, isConst, isFunc, isClo) 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 { if ra.values[ii].top {
@ -201,7 +184,10 @@ func (ra *resultsAnalyzer) analyzeResult(ii int, n ir.Node) {
newfunc = rfunc newfunc = rfunc
case isConst: case isConst:
newp = ResultAlwaysSameConstant newp = ResultAlwaysSameConstant
newlit = lit newcval = constVal
case isNil:
newp = ResultAlwaysSameConstant
newcval = nil
case isDerivedFromCall: case isDerivedFromCall:
newp = dprops newp = dprops
ra.values[ii].derived = true ra.values[ii].derived = true
@ -214,17 +200,20 @@ func (ra *resultsAnalyzer) analyzeResult(ii int, n ir.Node) {
// the previous returns. // the previous returns.
switch curp { switch curp {
case ResultIsAllocatedMem: case ResultIsAllocatedMem:
if isAllocatedMem(n) { if isAllocMem {
newp = ResultIsAllocatedMem newp = ResultIsAllocatedMem
} }
case ResultIsConcreteTypeConvertedToInterface: case ResultIsConcreteTypeConvertedToInterface:
if isConcreteConvIface(n) { if isConcConvItf {
newp = ResultIsConcreteTypeConvertedToInterface newp = ResultIsConcreteTypeConvertedToInterface
} }
case ResultAlwaysSameConstant: case ResultAlwaysSameConstant:
if isConst && isSameLiteral(lit, ra.values[ii].lit) { if isNil && ra.values[ii].cval == nil {
newp = ResultAlwaysSameConstant newp = ResultAlwaysSameConstant
newlit = lit newcval = nil
} else if isConst && constant.Compare(constVal, token.EQL, ra.values[ii].cval) {
newp = ResultAlwaysSameConstant
newcval = constVal
} }
case ResultAlwaysSameFunc: case ResultAlwaysSameFunc:
if isFunc && isSameFuncName(rfunc, ra.values[ii].fn) { if isFunc && isSameFuncName(rfunc, ra.values[ii].fn) {
@ -236,7 +225,7 @@ func (ra *resultsAnalyzer) analyzeResult(ii int, n ir.Node) {
} }
ra.values[ii].fn = newfunc ra.values[ii].fn = newfunc
ra.values[ii].fnClo = isClo ra.values[ii].fnClo = isClo
ra.values[ii].lit = newlit ra.values[ii].cval = newcval
ra.props[ii] = newp ra.props[ii] = newp
if debugTrace&debugTraceResults != 0 { if debugTrace&debugTraceResults != 0 {
@ -245,15 +234,6 @@ func (ra *resultsAnalyzer) analyzeResult(ii int, n ir.Node) {
} }
} }
func isAllocatedMem(n ir.Node) bool {
sv := ir.StaticValue(n)
switch sv.Op() {
case ir.OMAKESLICE, ir.ONEW, ir.OPTRLIT, ir.OSLICELIT:
return true
}
return false
}
// deriveReturnFlagsFromCallee tries to set properties for a given // deriveReturnFlagsFromCallee tries to set properties for a given
// return result where we're returning call expression; return value // return result where we're returning call expression; return value
// is a return property value and a boolean indicating whether the // is a return property value and a boolean indicating whether the
@ -270,7 +250,7 @@ func isAllocatedMem(n ir.Node) bool {
// set foo's return property to that of bar. In the case of "two", however, // 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 // even though each return path returns a constant, we don't know
// whether the constants are identical, hence we need to be conservative. // whether the constants are identical, hence we need to be conservative.
func deriveReturnFlagsFromCallee(n ir.Node) (ResultPropBits, bool) { func (ra *resultsAnalyzer) deriveReturnFlagsFromCallee(n ir.Node) (ResultPropBits, bool) {
if n.Op() != ir.OCALLFUNC { if n.Op() != ir.OCALLFUNC {
return 0, false return 0, false
} }
@ -282,8 +262,8 @@ func deriveReturnFlagsFromCallee(n ir.Node) (ResultPropBits, bool) {
if called.Op() != ir.ONAME { if called.Op() != ir.ONAME {
return 0, false return 0, false
} }
cname, isFunc, _ := isFuncName(called) cname := ra.funcName(called)
if !isFunc { if cname == nil {
return 0, false return 0, false
} }
calleeProps := propsForFunc(cname.Func) calleeProps := propsForFunc(cname.Func)
@ -295,41 +275,3 @@ func deriveReturnFlagsFromCallee(n ir.Node) (ResultPropBits, bool) {
} }
return calleeProps.ResultFlags[0], true return calleeProps.ResultFlags[0], true
} }
func isLiteral(n ir.Node) (constant.Value, bool) {
sv := ir.StaticValue(n)
switch sv.Op() {
case ir.ONIL:
return nil, true
case ir.OLITERAL:
return sv.Val(), true
}
return nil, false
}
// isSameLiteral checks to see if 'v1' and 'v2' correspond to the same
// literal value, or if they are both nil.
func isSameLiteral(v1, v2 constant.Value) bool {
if v1 == nil && v2 == nil {
return true
}
if v1 == nil || v2 == nil {
return false
}
return constant.Compare(v1, token.EQL, v2)
}
func isConcreteConvIface(n ir.Node) bool {
sv := ir.StaticValue(n)
if sv.Op() != ir.OCONVIFACE {
return false
}
return !sv.(*ir.ConvExpr).X.Type().IsInterface()
}
func isSameFuncName(v1, v2 *ir.Name) bool {
// NB: there are a few corner cases where pointer equality
// doesn't work here, but this should be good enough for
// our purposes here.
return v1 == v2
}

View File

@ -0,0 +1,129 @@
// 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"
"go/constant"
)
// nameFinder provides a set of "isXXX" query methods for clients to
// ask whether a given AST node corresponds to a function, a constant
// value, and so on. These methods use an underlying ir.ReassignOracle
// to return more precise results in cases where an "interesting"
// value is assigned to a singly-defined local temp. Example:
//
// const q = 101
// fq := func() int { return q }
// copyOfConstant := q
// copyOfFunc := f
// interestingCall(copyOfConstant, copyOfFunc)
//
// A name finder query method invoked on the arguments being passed to
// "interestingCall" will be able detect that 'copyOfConstant' always
// evaluates to a constant (even though it is in fact a PAUTO local
// variable). A given nameFinder can also operate without using
// ir.ReassignOracle (in cases where it is not practical to look
// at the entire function); in such cases queries will still work
// for explicit constant values and functions.
type nameFinder struct {
ro *ir.ReassignOracle
}
// newNameFinder returns a new nameFinder object with a reassignment
// oracle initialized based on the function fn, or if fn is nil,
// without an underlying ReassignOracle.
func newNameFinder(fn *ir.Func) *nameFinder {
var ro *ir.ReassignOracle
if fn != nil {
ro = &ir.ReassignOracle{}
ro.Init(fn)
}
return &nameFinder{ro: ro}
}
// funcName returns the *ir.Name for the func or method
// corresponding to node 'n', or nil if n can't be proven
// to contain a function value.
func (nf *nameFinder) funcName(n ir.Node) *ir.Name {
sv := n
if nf.ro != nil {
sv = nf.ro.StaticValue(n)
}
if name := ir.StaticCalleeName(sv); name != nil {
return name
}
return nil
}
// isAllocatedMem returns true if node n corresponds to a memory
// allocation expression (make, new, or equivalent).
func (nf *nameFinder) isAllocatedMem(n ir.Node) bool {
sv := n
if nf.ro != nil {
sv = nf.ro.StaticValue(n)
}
switch sv.Op() {
case ir.OMAKESLICE, ir.ONEW, ir.OPTRLIT, ir.OSLICELIT:
return true
}
return false
}
// constValue returns the underlying constant.Value for an AST node n
// if n is itself a constant value/expr, or if n is a singly assigned
// local containing constant expr/value (or nil not constant).
func (nf *nameFinder) constValue(n ir.Node) constant.Value {
sv := n
if nf.ro != nil {
sv = nf.ro.StaticValue(n)
}
if sv.Op() == ir.OLITERAL {
return sv.Val()
}
return nil
}
// isNil returns whether n is nil (or singly
// assigned local containing nil).
func (nf *nameFinder) isNil(n ir.Node) bool {
sv := n
if nf.ro != nil {
sv = nf.ro.StaticValue(n)
}
return sv.Op() == ir.ONIL
}
func (nf *nameFinder) staticValue(n ir.Node) ir.Node {
if nf.ro == nil {
return n
}
return nf.ro.StaticValue(n)
}
func (nf *nameFinder) reassigned(n *ir.Name) bool {
if nf.ro == nil {
return true
}
return nf.ro.Reassigned(n)
}
func (nf *nameFinder) isConcreteConvIface(n ir.Node) bool {
sv := n
if nf.ro != nil {
sv = nf.ro.StaticValue(n)
}
if sv.Op() != ir.OCONVIFACE {
return false
}
return !sv.(*ir.ConvExpr).X.Type().IsInterface()
}
func isSameFuncName(v1, v2 *ir.Name) bool {
// NB: there are a few corner cases where pointer equality
// doesn't work here, but this should be good enough for
// our purposes here.
return v1 == v2
}

View File

@ -46,7 +46,7 @@ type resultUseAnalyzer struct {
// rescoreBasedOnCallResultUses examines how call results are used, // rescoreBasedOnCallResultUses examines how call results are used,
// and tries to update the scores of calls based on how their results // and tries to update the scores of calls based on how their results
// are used in the function. // are used in the function.
func rescoreBasedOnCallResultUses(fn *ir.Func, resultNameTab map[*ir.Name]resultPropAndCS, cstab CallSiteTab) { func (csa *callSiteAnalyzer) rescoreBasedOnCallResultUses(fn *ir.Func, resultNameTab map[*ir.Name]resultPropAndCS, cstab CallSiteTab) {
enableDebugTraceIfEnv() enableDebugTraceIfEnv()
rua := &resultUseAnalyzer{ rua := &resultUseAnalyzer{
resultNameTab: resultNameTab, resultNameTab: resultNameTab,
@ -65,7 +65,7 @@ func rescoreBasedOnCallResultUses(fn *ir.Func, resultNameTab map[*ir.Name]result
disableDebugTrace() disableDebugTrace()
} }
func examineCallResults(cs *CallSite, resultNameTab map[*ir.Name]resultPropAndCS) map[*ir.Name]resultPropAndCS { func (csa *callSiteAnalyzer) examineCallResults(cs *CallSite, resultNameTab map[*ir.Name]resultPropAndCS) map[*ir.Name]resultPropAndCS {
if debugTrace&debugTraceScoring != 0 { if debugTrace&debugTraceScoring != 0 {
fmt.Fprintf(os.Stderr, "=-= examining call results for %q\n", fmt.Fprintf(os.Stderr, "=-= examining call results for %q\n",
EncodeCallSiteKey(cs)) EncodeCallSiteKey(cs))
@ -103,7 +103,7 @@ func examineCallResults(cs *CallSite, resultNameTab map[*ir.Name]resultPropAndCS
if rprop&interesting == 0 { if rprop&interesting == 0 {
continue continue
} }
if ir.Reassigned(n) { if csa.nameFinder.reassigned(n) {
continue continue
} }
if resultNameTab == nil { if resultNameTab == nil {

View File

@ -182,13 +182,14 @@ func mustToMay(x scoreAdjustTyp) scoreAdjustTyp {
return 0 return 0
} }
// computeCallSiteScore takes a given call site whose ir node is 'call' and // computeCallSiteScore takes a given call site whose ir node is
// callee function is 'callee' and with previously computed call site // 'call' and callee function is 'callee' and with previously computed
// properties 'csflags', then computes a score for the callsite that // call site properties 'csflags', then computes a score for the
// combines the size cost of the callee with heuristics based on // callsite that combines the size cost of the callee with heuristics
// previously parameter and function properties, then stores the score // based on previously computed argument and function properties,
// and the adjustment mask in the appropriate fields in 'cs' // then stores the score and the adjustment mask in the appropriate
func (cs *CallSite) computeCallSiteScore(calleeProps *FuncProps) { // fields in 'cs'
func (cs *CallSite) computeCallSiteScore(csa *callSiteAnalyzer, calleeProps *FuncProps) {
callee := cs.Callee callee := cs.Callee
csflags := cs.Flags csflags := cs.Flags
call := cs.Call call := cs.Call
@ -438,8 +439,13 @@ type scoreCallsCacheType struct {
// after foo has been analyzed, but it's conceivable that CanInline // after foo has been analyzed, but it's conceivable that CanInline
// might visit bar before foo for this SCC. // might visit bar before foo for this SCC.
func ScoreCalls(fn *ir.Func) { func ScoreCalls(fn *ir.Func) {
if len(fn.Body) == 0 {
return
}
enableDebugTraceIfEnv() enableDebugTraceIfEnv()
nameFinder := newNameFinder(fn)
if debugTrace&debugTraceScoring != 0 { if debugTrace&debugTraceScoring != 0 {
fmt.Fprintf(os.Stderr, "=-= ScoreCalls(%v)\n", ir.FuncName(fn)) fmt.Fprintf(os.Stderr, "=-= ScoreCalls(%v)\n", ir.FuncName(fn))
} }
@ -461,21 +467,25 @@ func ScoreCalls(fn *ir.Func) {
fmt.Fprintf(os.Stderr, "=-= building cstab for non-inl func %s\n", fmt.Fprintf(os.Stderr, "=-= building cstab for non-inl func %s\n",
ir.FuncName(fn)) ir.FuncName(fn))
} }
cstab = computeCallSiteTable(fn, fn.Body, scoreCallsCache.tab, nil, 0) cstab = computeCallSiteTable(fn, fn.Body, scoreCallsCache.tab, nil, 0,
nameFinder)
} }
csa := makeCallSiteAnalyzer(fn)
const doCallResults = true const doCallResults = true
scoreCallsRegion(fn, fn.Body, cstab, doCallResults, nil) csa.scoreCallsRegion(fn, fn.Body, cstab, doCallResults, nil)
disableDebugTrace()
} }
// scoreCallsRegion assigns numeric scores to each of the callsites in // scoreCallsRegion assigns numeric scores to each of the callsites in
// region 'region' within function 'fn'. This can be called on // region 'region' within function 'fn'. This can be called on
// an entire function, or with 'region' set to a chunk of // an entire function, or with 'region' set to a chunk of
// code corresponding to an inlined call. // code corresponding to an inlined call.
func scoreCallsRegion(fn *ir.Func, region ir.Nodes, cstab CallSiteTab, doCallResults bool, ic *ir.InlinedCallExpr) { func (csa *callSiteAnalyzer) scoreCallsRegion(fn *ir.Func, region ir.Nodes, cstab CallSiteTab, doCallResults bool, ic *ir.InlinedCallExpr) {
if debugTrace&debugTraceScoring != 0 { if debugTrace&debugTraceScoring != 0 {
fmt.Fprintf(os.Stderr, "=-= scoreCallsRegion(%v, %s)\n", fmt.Fprintf(os.Stderr, "=-= scoreCallsRegion(%v, %s) len(cstab)=%d\n",
ir.FuncName(fn), region[0].Op().String()) ir.FuncName(fn), region[0].Op().String(), len(cstab))
} }
// Sort callsites to avoid any surprises with non deterministic // Sort callsites to avoid any surprises with non deterministic
@ -510,13 +520,13 @@ func scoreCallsRegion(fn *ir.Func, region ir.Nodes, cstab CallSiteTab, doCallRes
continue continue
} }
} }
cs.computeCallSiteScore(cprops) cs.computeCallSiteScore(csa, cprops)
if doCallResults { if doCallResults {
if debugTrace&debugTraceScoring != 0 { if debugTrace&debugTraceScoring != 0 {
fmt.Fprintf(os.Stderr, "=-= examineCallResults at %s: flags=%d score=%d funcInlHeur=%v deser=%v\n", fmtFullPos(cs.Call.Pos()), cs.Flags, cs.Score, fihcprops, desercprops) fmt.Fprintf(os.Stderr, "=-= examineCallResults at %s: flags=%d score=%d funcInlHeur=%v deser=%v\n", fmtFullPos(cs.Call.Pos()), cs.Flags, cs.Score, fihcprops, desercprops)
} }
resultNameTab = examineCallResults(cs, resultNameTab) resultNameTab = csa.examineCallResults(cs, resultNameTab)
} }
if debugTrace&debugTraceScoring != 0 { if debugTrace&debugTraceScoring != 0 {
@ -525,7 +535,7 @@ func scoreCallsRegion(fn *ir.Func, region ir.Nodes, cstab CallSiteTab, doCallRes
} }
if resultNameTab != nil { if resultNameTab != nil {
rescoreBasedOnCallResultUses(fn, resultNameTab, cstab) csa.rescoreBasedOnCallResultUses(fn, resultNameTab, cstab)
} }
disableDebugTrace() disableDebugTrace()