github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/compile/inline/inlheur/analyze.go (about)

     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  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"path/filepath"
    13  	"sort"
    14  	"strings"
    15  
    16  	"github.com/go-asm/go/buildcfg"
    17  	"github.com/go-asm/go/cmd/compile/base"
    18  	"github.com/go-asm/go/cmd/compile/ir"
    19  	"github.com/go-asm/go/cmd/compile/types"
    20  )
    21  
    22  const (
    23  	debugTraceFuncs = 1 << iota
    24  	debugTraceFuncFlags
    25  	debugTraceResults
    26  	debugTraceParams
    27  	debugTraceExprClassify
    28  	debugTraceCalls
    29  	debugTraceScoring
    30  )
    31  
    32  // propAnalyzer interface is used for defining one or more analyzer
    33  // helper objects, each tasked with computing some specific subset of
    34  // the properties we're interested in. The assumption is that
    35  // properties are independent, so each new analyzer that implements
    36  // this interface can operate entirely on its own. For a given analyzer
    37  // there will be a sequence of calls to nodeVisitPre and nodeVisitPost
    38  // as the nodes within a function are visited, then a followup call to
    39  // setResults so that the analyzer can transfer its results into the
    40  // final properties object.
    41  type propAnalyzer interface {
    42  	nodeVisitPre(n ir.Node)
    43  	nodeVisitPost(n ir.Node)
    44  	setResults(funcProps *FuncProps)
    45  }
    46  
    47  // fnInlHeur contains inline heuristics state information about a
    48  // specific Go function being analyzed/considered by the inliner. Note
    49  // that in addition to constructing a fnInlHeur object by analyzing a
    50  // specific *ir.Func, there is also code in the test harness
    51  // (funcprops_test.go) that builds up fnInlHeur's by reading in and
    52  // parsing a dump. This is the reason why we have file/fname/line
    53  // fields below instead of just an *ir.Func field.
    54  type fnInlHeur struct {
    55  	props *FuncProps
    56  	cstab CallSiteTab
    57  	fname string
    58  	file  string
    59  	line  uint
    60  }
    61  
    62  var fpmap = map[*ir.Func]fnInlHeur{}
    63  
    64  // AnalyzeFunc computes function properties for fn and its contained
    65  // closures, updating the global 'fpmap' table. It is assumed that
    66  // "CanInline" has been run on fn and on the closures that feed
    67  // directly into calls; other closures not directly called will also
    68  // be checked inlinability for inlinability here in case they are
    69  // returned as a result.
    70  func AnalyzeFunc(fn *ir.Func, canInline func(*ir.Func), budgetForFunc func(*ir.Func) int32, inlineMaxBudget int) {
    71  	if fpmap == nil {
    72  		// If fpmap is nil this indicates that the main inliner pass is
    73  		// complete and we're doing inlining of wrappers (no heuristics
    74  		// used here).
    75  		return
    76  	}
    77  	if fn.OClosure != nil {
    78  		// closures will be processed along with their outer enclosing func.
    79  		return
    80  	}
    81  	enableDebugTraceIfEnv()
    82  	if debugTrace&debugTraceFuncs != 0 {
    83  		fmt.Fprintf(os.Stderr, "=-= AnalyzeFunc(%v)\n", fn)
    84  	}
    85  	// Build up a list containing 'fn' and any closures it contains. Along
    86  	// the way, test to see whether each closure is inlinable in case
    87  	// we might be returning it.
    88  	funcs := []*ir.Func{fn}
    89  	ir.VisitFuncAndClosures(fn, func(n ir.Node) {
    90  		if clo, ok := n.(*ir.ClosureExpr); ok {
    91  			funcs = append(funcs, clo.Func)
    92  		}
    93  	})
    94  
    95  	// Analyze the list of functions. We want to visit a given func
    96  	// only after the closures it contains have been processed, so
    97  	// iterate through the list in reverse order. Once a function has
    98  	// been analyzed, revisit the question of whether it should be
    99  	// inlinable; if it is over the default hairyness limit and it
   100  	// doesn't have any interesting properties, then we don't want
   101  	// the overhead of writing out its inline body.
   102  	nameFinder := newNameFinder(fn)
   103  	for i := len(funcs) - 1; i >= 0; i-- {
   104  		f := funcs[i]
   105  		if f.OClosure != nil && !f.InlinabilityChecked() {
   106  			canInline(f)
   107  		}
   108  		funcProps := analyzeFunc(f, inlineMaxBudget, nameFinder)
   109  		revisitInlinability(f, funcProps, budgetForFunc)
   110  		if f.Inl != nil {
   111  			f.Inl.Properties = funcProps.SerializeToString()
   112  		}
   113  	}
   114  	disableDebugTrace()
   115  }
   116  
   117  // TearDown is invoked at the end of the main inlining pass; doing
   118  // function analysis and call site scoring is unlikely to help a lot
   119  // after this point, so nil out fpmap and other globals to reclaim
   120  // storage.
   121  func TearDown() {
   122  	fpmap = nil
   123  	scoreCallsCache.tab = nil
   124  	scoreCallsCache.csl = nil
   125  }
   126  
   127  func analyzeFunc(fn *ir.Func, inlineMaxBudget int, nf *nameFinder) *FuncProps {
   128  	if funcInlHeur, ok := fpmap[fn]; ok {
   129  		return funcInlHeur.props
   130  	}
   131  	funcProps, fcstab := computeFuncProps(fn, inlineMaxBudget, nf)
   132  	file, line := fnFileLine(fn)
   133  	entry := fnInlHeur{
   134  		fname: fn.Sym().Name,
   135  		file:  file,
   136  		line:  line,
   137  		props: funcProps,
   138  		cstab: fcstab,
   139  	}
   140  	fn.SetNeverReturns(entry.props.Flags&FuncPropNeverReturns != 0)
   141  	fpmap[fn] = entry
   142  	if fn.Inl != nil && fn.Inl.Properties == "" {
   143  		fn.Inl.Properties = entry.props.SerializeToString()
   144  	}
   145  	return funcProps
   146  }
   147  
   148  // revisitInlinability revisits the question of whether to continue to
   149  // treat function 'fn' as an inline candidate based on the set of
   150  // properties we've computed for it. If (for example) it has an
   151  // initial size score of 150 and no interesting properties to speak
   152  // of, then there isn't really any point to moving ahead with it as an
   153  // inline candidate.
   154  func revisitInlinability(fn *ir.Func, funcProps *FuncProps, budgetForFunc func(*ir.Func) int32) {
   155  	if fn.Inl == nil {
   156  		return
   157  	}
   158  	maxAdj := int32(LargestNegativeScoreAdjustment(fn, funcProps))
   159  	budget := budgetForFunc(fn)
   160  	if fn.Inl.Cost+maxAdj > budget {
   161  		fn.Inl = nil
   162  	}
   163  }
   164  
   165  // computeFuncProps examines the Go function 'fn' and computes for it
   166  // a function "properties" object, to be used to drive inlining
   167  // heuristics. See comments on the FuncProps type for more info.
   168  func computeFuncProps(fn *ir.Func, inlineMaxBudget int, nf *nameFinder) (*FuncProps, CallSiteTab) {
   169  	if debugTrace&debugTraceFuncs != 0 {
   170  		fmt.Fprintf(os.Stderr, "=-= starting analysis of func %v:\n%+v\n",
   171  			fn, fn)
   172  	}
   173  	funcProps := new(FuncProps)
   174  	ffa := makeFuncFlagsAnalyzer(fn)
   175  	analyzers := []propAnalyzer{ffa}
   176  	analyzers = addResultsAnalyzer(fn, analyzers, funcProps, inlineMaxBudget, nf)
   177  	analyzers = addParamsAnalyzer(fn, analyzers, funcProps, nf)
   178  	runAnalyzersOnFunction(fn, analyzers)
   179  	for _, a := range analyzers {
   180  		a.setResults(funcProps)
   181  	}
   182  	cstab := computeCallSiteTable(fn, fn.Body, nil, ffa.panicPathTable(), 0, nf)
   183  	return funcProps, cstab
   184  }
   185  
   186  func runAnalyzersOnFunction(fn *ir.Func, analyzers []propAnalyzer) {
   187  	var doNode func(ir.Node) bool
   188  	doNode = func(n ir.Node) bool {
   189  		for _, a := range analyzers {
   190  			a.nodeVisitPre(n)
   191  		}
   192  		ir.DoChildren(n, doNode)
   193  		for _, a := range analyzers {
   194  			a.nodeVisitPost(n)
   195  		}
   196  		return false
   197  	}
   198  	doNode(fn)
   199  }
   200  
   201  func propsForFunc(fn *ir.Func) *FuncProps {
   202  	if funcInlHeur, ok := fpmap[fn]; ok {
   203  		return funcInlHeur.props
   204  	} else if fn.Inl != nil && fn.Inl.Properties != "" {
   205  		// FIXME: considering adding some sort of cache or table
   206  		// for deserialized properties of imported functions.
   207  		return DeserializeFromString(fn.Inl.Properties)
   208  	}
   209  	return nil
   210  }
   211  
   212  func fnFileLine(fn *ir.Func) (string, uint) {
   213  	p := base.Ctxt.InnermostPos(fn.Pos())
   214  	return filepath.Base(p.Filename()), p.Line()
   215  }
   216  
   217  func Enabled() bool {
   218  	return buildcfg.Experiment.NewInliner || UnitTesting()
   219  }
   220  
   221  func UnitTesting() bool {
   222  	return base.Debug.DumpInlFuncProps != "" ||
   223  		base.Debug.DumpInlCallSiteScores != 0
   224  }
   225  
   226  // DumpFuncProps computes and caches function properties for the func
   227  // 'fn', writing out a description of the previously computed set of
   228  // properties to the file given in 'dumpfile'. Used for the
   229  // "-d=dumpinlfuncprops=..." command line flag, intended for use
   230  // primarily in unit testing.
   231  func DumpFuncProps(fn *ir.Func, dumpfile string) {
   232  	if fn != nil {
   233  		if fn.OClosure != nil {
   234  			// closures will be processed along with their outer enclosing func.
   235  			return
   236  		}
   237  		captureFuncDumpEntry(fn)
   238  		ir.VisitFuncAndClosures(fn, func(n ir.Node) {
   239  			if clo, ok := n.(*ir.ClosureExpr); ok {
   240  				captureFuncDumpEntry(clo.Func)
   241  			}
   242  		})
   243  	} else {
   244  		emitDumpToFile(dumpfile)
   245  	}
   246  }
   247  
   248  // emitDumpToFile writes out the buffer function property dump entries
   249  // to a file, for unit testing. Dump entries need to be sorted by
   250  // definition line, and due to generics we need to account for the
   251  // possibility that several ir.Func's will have the same def line.
   252  func emitDumpToFile(dumpfile string) {
   253  	mode := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
   254  	if dumpfile[0] == '+' {
   255  		dumpfile = dumpfile[1:]
   256  		mode = os.O_WRONLY | os.O_APPEND | os.O_CREATE
   257  	}
   258  	if dumpfile[0] == '%' {
   259  		dumpfile = dumpfile[1:]
   260  		d, b := filepath.Dir(dumpfile), filepath.Base(dumpfile)
   261  		ptag := strings.ReplaceAll(types.LocalPkg.Path, "/", ":")
   262  		dumpfile = d + "/" + ptag + "." + b
   263  	}
   264  	outf, err := os.OpenFile(dumpfile, mode, 0644)
   265  	if err != nil {
   266  		base.Fatalf("opening function props dump file %q: %v\n", dumpfile, err)
   267  	}
   268  	defer outf.Close()
   269  	dumpFilePreamble(outf)
   270  
   271  	atline := map[uint]uint{}
   272  	sl := make([]fnInlHeur, 0, len(dumpBuffer))
   273  	for _, e := range dumpBuffer {
   274  		sl = append(sl, e)
   275  		atline[e.line] = atline[e.line] + 1
   276  	}
   277  	sl = sortFnInlHeurSlice(sl)
   278  
   279  	prevline := uint(0)
   280  	for _, entry := range sl {
   281  		idx := uint(0)
   282  		if prevline == entry.line {
   283  			idx++
   284  		}
   285  		prevline = entry.line
   286  		atl := atline[entry.line]
   287  		if err := dumpFnPreamble(outf, &entry, nil, idx, atl); err != nil {
   288  			base.Fatalf("function props dump: %v\n", err)
   289  		}
   290  	}
   291  	dumpBuffer = nil
   292  }
   293  
   294  // captureFuncDumpEntry grabs the function properties object for 'fn'
   295  // and enqueues it for later dumping. Used for the
   296  // "-d=dumpinlfuncprops=..." command line flag, intended for use
   297  // primarily in unit testing.
   298  func captureFuncDumpEntry(fn *ir.Func) {
   299  	// avoid capturing compiler-generated equality funcs.
   300  	if strings.HasPrefix(fn.Sym().Name, ".eq.") {
   301  		return
   302  	}
   303  	funcInlHeur, ok := fpmap[fn]
   304  	if !ok {
   305  		// Missing entry is expected for functions that are too large
   306  		// to inline. We still want to write out call site scores in
   307  		// this case however.
   308  		funcInlHeur = fnInlHeur{cstab: callSiteTab}
   309  	}
   310  	if dumpBuffer == nil {
   311  		dumpBuffer = make(map[*ir.Func]fnInlHeur)
   312  	}
   313  	if _, ok := dumpBuffer[fn]; ok {
   314  		return
   315  	}
   316  	if debugTrace&debugTraceFuncs != 0 {
   317  		fmt.Fprintf(os.Stderr, "=-= capturing dump for %v:\n", fn)
   318  	}
   319  	dumpBuffer[fn] = funcInlHeur
   320  }
   321  
   322  // dumpFilePreamble writes out a file-level preamble for a given
   323  // Go function as part of a function properties dump.
   324  func dumpFilePreamble(w io.Writer) {
   325  	fmt.Fprintf(w, "// DO NOT EDIT (use 'go test -v -update-expected' instead.)\n")
   326  	fmt.Fprintf(w, "// See github.com/go-asm/go/cmd/compile/inline/inlheur/testdata/props/README.txt\n")
   327  	fmt.Fprintf(w, "// for more information on the format of this file.\n")
   328  	fmt.Fprintf(w, "// %s\n", preambleDelimiter)
   329  }
   330  
   331  // dumpFnPreamble writes out a function-level preamble for a given
   332  // Go function as part of a function properties dump. See the
   333  // README.txt file in testdata/props for more on the format of
   334  // this preamble.
   335  func dumpFnPreamble(w io.Writer, funcInlHeur *fnInlHeur, ecst encodedCallSiteTab, idx, atl uint) error {
   336  	fmt.Fprintf(w, "// %s %s %d %d %d\n",
   337  		funcInlHeur.file, funcInlHeur.fname, funcInlHeur.line, idx, atl)
   338  	// emit props as comments, followed by delimiter
   339  	fmt.Fprintf(w, "%s// %s\n", funcInlHeur.props.ToString("// "), comDelimiter)
   340  	data, err := json.Marshal(funcInlHeur.props)
   341  	if err != nil {
   342  		return fmt.Errorf("marshall error %v\n", err)
   343  	}
   344  	fmt.Fprintf(w, "// %s\n", string(data))
   345  	dumpCallSiteComments(w, funcInlHeur.cstab, ecst)
   346  	fmt.Fprintf(w, "// %s\n", fnDelimiter)
   347  	return nil
   348  }
   349  
   350  // sortFnInlHeurSlice sorts a slice of fnInlHeur based on
   351  // the starting line of the function definition, then by name.
   352  func sortFnInlHeurSlice(sl []fnInlHeur) []fnInlHeur {
   353  	sort.SliceStable(sl, func(i, j int) bool {
   354  		if sl[i].line != sl[j].line {
   355  			return sl[i].line < sl[j].line
   356  		}
   357  		return sl[i].fname < sl[j].fname
   358  	})
   359  	return sl
   360  }
   361  
   362  // delimiters written to various preambles to make parsing of
   363  // dumps easier.
   364  const preambleDelimiter = "<endfilepreamble>"
   365  const fnDelimiter = "<endfuncpreamble>"
   366  const comDelimiter = "<endpropsdump>"
   367  const csDelimiter = "<endcallsites>"
   368  
   369  // dumpBuffer stores up function properties dumps when
   370  // "-d=dumpinlfuncprops=..." is in effect.
   371  var dumpBuffer map[*ir.Func]fnInlHeur