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