diff options
Diffstat (limited to 'src/cmd/compile/internal/inline/inlheur/analyze.go')
-rw-r--r-- | src/cmd/compile/internal/inline/inlheur/analyze.go | 370 |
1 files changed, 370 insertions, 0 deletions
diff --git a/src/cmd/compile/internal/inline/inlheur/analyze.go b/src/cmd/compile/internal/inline/inlheur/analyze.go new file mode 100644 index 0000000..a1b6f35 --- /dev/null +++ b/src/cmd/compile/internal/inline/inlheur/analyze.go @@ -0,0 +1,370 @@ +// 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" + "encoding/json" + "fmt" + "internal/buildcfg" + "io" + "os" + "path/filepath" + "sort" + "strings" +) + +const ( + debugTraceFuncs = 1 << iota + debugTraceFuncFlags + debugTraceResults + debugTraceParams + debugTraceExprClassify + debugTraceCalls + debugTraceScoring +) + +// 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(funcProps *FuncProps) +} + +// fnInlHeur contains inline heuristics state information about a +// specific Go function being analyzed/considered by the inliner. Note +// that in addition to constructing a fnInlHeur object by analyzing a +// specific *ir.Func, there is also code in the test harness +// (funcprops_test.go) that builds up fnInlHeur's by reading in and +// parsing a dump. This is the reason why we have file/fname/line +// fields below instead of just an *ir.Func field. +type fnInlHeur struct { + props *FuncProps + cstab CallSiteTab + fname string + file string + line uint +} + +var fpmap = map[*ir.Func]fnInlHeur{} + +// AnalyzeFunc computes function properties for fn and its contained +// closures, updating the global 'fpmap' table. It is assumed that +// "CanInline" has been run on fn and on the closures that feed +// directly into calls; other closures not directly called will also +// be checked inlinability for inlinability here in case they are +// returned as a result. +func AnalyzeFunc(fn *ir.Func, canInline func(*ir.Func), budgetForFunc func(*ir.Func) int32, inlineMaxBudget int) { + if fpmap == nil { + // If fpmap is nil this indicates that the main inliner pass is + // complete and we're doing inlining of wrappers (no heuristics + // used here). + return + } + if fn.OClosure != nil { + // closures will be processed along with their outer enclosing func. + return + } + enableDebugTraceIfEnv() + if debugTrace&debugTraceFuncs != 0 { + fmt.Fprintf(os.Stderr, "=-= AnalyzeFunc(%v)\n", fn) + } + // Build up a list containing 'fn' and any closures it contains. Along + // the way, test to see whether each closure is inlinable in case + // we might be returning it. + funcs := []*ir.Func{fn} + ir.VisitFuncAndClosures(fn, func(n ir.Node) { + if clo, ok := n.(*ir.ClosureExpr); ok { + funcs = append(funcs, clo.Func) + } + }) + + // Analyze the list of functions. We want to visit a given func + // only after the closures it contains have been processed, so + // iterate through the list in reverse order. Once a function has + // been analyzed, revisit the question of whether it should be + // inlinable; if it is over the default hairyness limit and it + // doesn't have any interesting properties, then we don't want + // the overhead of writing out its inline body. + nameFinder := newNameFinder(fn) + for i := len(funcs) - 1; i >= 0; i-- { + f := funcs[i] + if f.OClosure != nil && !f.InlinabilityChecked() { + canInline(f) + } + funcProps := analyzeFunc(f, inlineMaxBudget, nameFinder) + revisitInlinability(f, funcProps, budgetForFunc) + if f.Inl != nil { + f.Inl.Properties = funcProps.SerializeToString() + } + } + disableDebugTrace() +} + +// TearDown is invoked at the end of the main inlining pass; doing +// function analysis and call site scoring is unlikely to help a lot +// after this point, so nil out fpmap and other globals to reclaim +// storage. +func TearDown() { + fpmap = nil + scoreCallsCache.tab = nil + scoreCallsCache.csl = nil +} + +func analyzeFunc(fn *ir.Func, inlineMaxBudget int, nf *nameFinder) *FuncProps { + if funcInlHeur, ok := fpmap[fn]; ok { + return funcInlHeur.props + } + funcProps, fcstab := computeFuncProps(fn, inlineMaxBudget, nf) + file, line := fnFileLine(fn) + entry := fnInlHeur{ + fname: fn.Sym().Name, + file: file, + line: line, + props: funcProps, + cstab: fcstab, + } + fn.SetNeverReturns(entry.props.Flags&FuncPropNeverReturns != 0) + fpmap[fn] = entry + if fn.Inl != nil && fn.Inl.Properties == "" { + fn.Inl.Properties = entry.props.SerializeToString() + } + return funcProps +} + +// revisitInlinability revisits the question of whether to continue to +// treat function 'fn' as an inline candidate based on the set of +// properties we've computed for it. If (for example) it has an +// initial size score of 150 and no interesting properties to speak +// of, then there isn't really any point to moving ahead with it as an +// inline candidate. +func revisitInlinability(fn *ir.Func, funcProps *FuncProps, budgetForFunc func(*ir.Func) int32) { + if fn.Inl == nil { + return + } + maxAdj := int32(LargestNegativeScoreAdjustment(fn, funcProps)) + budget := budgetForFunc(fn) + if fn.Inl.Cost+maxAdj > budget { + fn.Inl = nil + } +} + +// computeFuncProps examines the Go function 'fn' and computes for it +// a function "properties" object, to be used to drive inlining +// heuristics. See comments on the FuncProps type for more info. +func computeFuncProps(fn *ir.Func, inlineMaxBudget int, nf *nameFinder) (*FuncProps, CallSiteTab) { + if debugTrace&debugTraceFuncs != 0 { + fmt.Fprintf(os.Stderr, "=-= starting analysis of func %v:\n%+v\n", + fn, fn) + } + funcProps := new(FuncProps) + ffa := makeFuncFlagsAnalyzer(fn) + analyzers := []propAnalyzer{ffa} + analyzers = addResultsAnalyzer(fn, analyzers, funcProps, inlineMaxBudget, nf) + analyzers = addParamsAnalyzer(fn, analyzers, funcProps, nf) + runAnalyzersOnFunction(fn, analyzers) + for _, a := range analyzers { + a.setResults(funcProps) + } + cstab := computeCallSiteTable(fn, fn.Body, nil, ffa.panicPathTable(), 0, nf) + return funcProps, cstab +} + +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 propsForFunc(fn *ir.Func) *FuncProps { + if funcInlHeur, ok := fpmap[fn]; ok { + return funcInlHeur.props + } else if fn.Inl != nil && fn.Inl.Properties != "" { + // FIXME: considering adding some sort of cache or table + // for deserialized properties of imported functions. + return DeserializeFromString(fn.Inl.Properties) + } + return nil +} + +func fnFileLine(fn *ir.Func) (string, uint) { + p := base.Ctxt.InnermostPos(fn.Pos()) + return filepath.Base(p.Filename()), p.Line() +} + +func Enabled() bool { + return buildcfg.Experiment.NewInliner || UnitTesting() +} + +func UnitTesting() bool { + return base.Debug.DumpInlFuncProps != "" || + base.Debug.DumpInlCallSiteScores != 0 +} + +// DumpFuncProps computes and caches function properties for the func +// 'fn', writing out a description of the previously computed set of +// properties to the file given in 'dumpfile'. Used for the +// "-d=dumpinlfuncprops=..." command line flag, intended for use +// primarily in unit testing. +func DumpFuncProps(fn *ir.Func, dumpfile string) { + if fn != nil { + if fn.OClosure != nil { + // closures will be processed along with their outer enclosing func. + return + } + captureFuncDumpEntry(fn) + ir.VisitFuncAndClosures(fn, func(n ir.Node) { + if clo, ok := n.(*ir.ClosureExpr); ok { + captureFuncDumpEntry(clo.Func) + } + }) + } else { + emitDumpToFile(dumpfile) + } +} + +// emitDumpToFile writes out the buffer function property dump entries +// to a file, for unit testing. Dump entries need to be sorted by +// definition line, and due to generics we need to account for the +// possibility that several ir.Func's will have the same def line. +func emitDumpToFile(dumpfile string) { + mode := os.O_WRONLY | os.O_CREATE | os.O_TRUNC + if dumpfile[0] == '+' { + dumpfile = dumpfile[1:] + mode = os.O_WRONLY | os.O_APPEND | os.O_CREATE + } + if dumpfile[0] == '%' { + dumpfile = dumpfile[1:] + d, b := filepath.Dir(dumpfile), filepath.Base(dumpfile) + ptag := strings.ReplaceAll(types.LocalPkg.Path, "/", ":") + dumpfile = d + "/" + ptag + "." + b + } + outf, err := os.OpenFile(dumpfile, mode, 0644) + if err != nil { + base.Fatalf("opening function props dump file %q: %v\n", dumpfile, err) + } + defer outf.Close() + dumpFilePreamble(outf) + + atline := map[uint]uint{} + sl := make([]fnInlHeur, 0, len(dumpBuffer)) + for _, e := range dumpBuffer { + sl = append(sl, e) + atline[e.line] = atline[e.line] + 1 + } + sl = sortFnInlHeurSlice(sl) + + prevline := uint(0) + for _, entry := range sl { + idx := uint(0) + if prevline == entry.line { + idx++ + } + prevline = entry.line + atl := atline[entry.line] + if err := dumpFnPreamble(outf, &entry, nil, idx, atl); err != nil { + base.Fatalf("function props dump: %v\n", err) + } + } + dumpBuffer = nil +} + +// captureFuncDumpEntry grabs the function properties object for 'fn' +// and enqueues it for later dumping. Used for the +// "-d=dumpinlfuncprops=..." command line flag, intended for use +// primarily in unit testing. +func captureFuncDumpEntry(fn *ir.Func) { + // avoid capturing compiler-generated equality funcs. + if strings.HasPrefix(fn.Sym().Name, ".eq.") { + return + } + funcInlHeur, ok := fpmap[fn] + if !ok { + // Missing entry is expected for functions that are too large + // to inline. We still want to write out call site scores in + // this case however. + funcInlHeur = fnInlHeur{cstab: callSiteTab} + } + if dumpBuffer == nil { + dumpBuffer = make(map[*ir.Func]fnInlHeur) + } + if _, ok := dumpBuffer[fn]; ok { + return + } + if debugTrace&debugTraceFuncs != 0 { + fmt.Fprintf(os.Stderr, "=-= capturing dump for %v:\n", fn) + } + dumpBuffer[fn] = funcInlHeur +} + +// dumpFilePreamble writes out a file-level preamble for a given +// Go function as part of a function properties dump. +func dumpFilePreamble(w io.Writer) { + fmt.Fprintf(w, "// DO NOT EDIT (use 'go test -v -update-expected' instead.)\n") + fmt.Fprintf(w, "// See cmd/compile/internal/inline/inlheur/testdata/props/README.txt\n") + fmt.Fprintf(w, "// for more information on the format of this file.\n") + fmt.Fprintf(w, "// %s\n", preambleDelimiter) +} + +// dumpFnPreamble writes out a function-level preamble for a given +// Go function as part of a function properties dump. See the +// README.txt file in testdata/props for more on the format of +// this preamble. +func dumpFnPreamble(w io.Writer, funcInlHeur *fnInlHeur, ecst encodedCallSiteTab, idx, atl uint) error { + fmt.Fprintf(w, "// %s %s %d %d %d\n", + funcInlHeur.file, funcInlHeur.fname, funcInlHeur.line, idx, atl) + // emit props as comments, followed by delimiter + fmt.Fprintf(w, "%s// %s\n", funcInlHeur.props.ToString("// "), comDelimiter) + data, err := json.Marshal(funcInlHeur.props) + if err != nil { + return fmt.Errorf("marshall error %v\n", err) + } + fmt.Fprintf(w, "// %s\n", string(data)) + dumpCallSiteComments(w, funcInlHeur.cstab, ecst) + fmt.Fprintf(w, "// %s\n", fnDelimiter) + return nil +} + +// sortFnInlHeurSlice sorts a slice of fnInlHeur based on +// the starting line of the function definition, then by name. +func sortFnInlHeurSlice(sl []fnInlHeur) []fnInlHeur { + sort.SliceStable(sl, func(i, j int) bool { + if sl[i].line != sl[j].line { + return sl[i].line < sl[j].line + } + return sl[i].fname < sl[j].fname + }) + return sl +} + +// delimiters written to various preambles to make parsing of +// dumps easier. +const preambleDelimiter = "<endfilepreamble>" +const fnDelimiter = "<endfuncpreamble>" +const comDelimiter = "<endpropsdump>" +const csDelimiter = "<endcallsites>" + +// dumpBuffer stores up function properties dumps when +// "-d=dumpinlfuncprops=..." is in effect. +var dumpBuffer map[*ir.Func]fnInlHeur |