Help us shape the future of Go! Take the 2026 Go Developer Survey

Source file src/cmd/compile/internal/inline/interleaved/interleaved.go

     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 interleaved implements the interleaved devirtualization and
     6  // inlining pass.
     7  package interleaved
     8  
     9  import (
    10  	"cmd/compile/internal/base"
    11  	"cmd/compile/internal/devirtualize"
    12  	"cmd/compile/internal/inline"
    13  	"cmd/compile/internal/inline/inlheur"
    14  	"cmd/compile/internal/ir"
    15  	"cmd/compile/internal/pgoir"
    16  	"cmd/compile/internal/typecheck"
    17  	"fmt"
    18  )
    19  
    20  // DevirtualizeAndInlinePackage interleaves devirtualization and inlining on
    21  // all functions within pkg.
    22  func DevirtualizeAndInlinePackage(pkg *ir.Package, profile *pgoir.Profile) {
    23  	if base.Flag.W > 1 {
    24  		for _, fn := range typecheck.Target.Funcs {
    25  			s := fmt.Sprintf("\nbefore devirtualize-and-inline %v", fn.Sym())
    26  			ir.DumpList(s, fn.Body)
    27  		}
    28  	}
    29  
    30  	if profile != nil && base.Debug.PGODevirtualize > 0 {
    31  		// TODO(mdempsky): Integrate into DevirtualizeAndInlineFunc below.
    32  		ir.VisitFuncsBottomUp(typecheck.Target.Funcs, func(list []*ir.Func, recursive bool) {
    33  			for _, fn := range list {
    34  				devirtualize.ProfileGuided(fn, profile)
    35  			}
    36  		})
    37  	}
    38  
    39  	if base.Flag.LowerL != 0 {
    40  		inlheur.SetupScoreAdjustments()
    41  	}
    42  
    43  	var inlProfile *pgoir.Profile // copy of profile for inlining
    44  	if base.Debug.PGOInline != 0 {
    45  		inlProfile = profile
    46  	}
    47  
    48  	// First compute inlinability of all functions in the package.
    49  	inline.CanInlineFuncs(pkg.Funcs, inlProfile)
    50  
    51  	inlState := make(map[*ir.Func]*inlClosureState)
    52  	calleeUseCounts := make(map[*ir.Func]int)
    53  
    54  	var state devirtualize.State
    55  
    56  	// Pre-process all the functions, adding parentheses around call sites and starting their "inl state".
    57  	for _, fn := range typecheck.Target.Funcs {
    58  		bigCaller := base.Flag.LowerL != 0 && inline.IsBigFunc(fn)
    59  		if bigCaller && base.Flag.LowerM > 1 {
    60  			fmt.Printf("%v: function %v considered 'big'; reducing max cost of inlinees\n", ir.Line(fn), fn)
    61  		}
    62  
    63  		s := &inlClosureState{bigCaller: bigCaller, profile: profile, fn: fn, callSites: make(map[*ir.ParenExpr]bool), useCounts: calleeUseCounts}
    64  		s.parenthesize()
    65  		inlState[fn] = s
    66  
    67  		// Do a first pass at counting call sites.
    68  		for i := range s.parens {
    69  			s.resolve(&state, i)
    70  		}
    71  	}
    72  
    73  	ir.VisitFuncsBottomUp(typecheck.Target.Funcs, func(list []*ir.Func, recursive bool) {
    74  
    75  		anyInlineHeuristics := false
    76  
    77  		// inline heuristics, placed here because they have static state and that's what seems to work.
    78  		for _, fn := range list {
    79  			if base.Flag.LowerL != 0 {
    80  				if inlheur.Enabled() && !fn.Wrapper() {
    81  					inlheur.ScoreCalls(fn)
    82  					anyInlineHeuristics = true
    83  				}
    84  				if base.Debug.DumpInlFuncProps != "" && !fn.Wrapper() {
    85  					inlheur.DumpFuncProps(fn, base.Debug.DumpInlFuncProps)
    86  				}
    87  			}
    88  		}
    89  
    90  		if anyInlineHeuristics {
    91  			defer inlheur.ScoreCallsCleanup()
    92  		}
    93  
    94  		// Iterate to a fixed point over all the functions.
    95  		done := false
    96  		for !done {
    97  			done = true
    98  			for _, fn := range list {
    99  				s := inlState[fn]
   100  
   101  				ir.WithPos(fn, func() {
   102  					l1 := len(s.parens)
   103  					l0 := 0
   104  
   105  					// Batch iterations so that newly discovered call sites are
   106  					// resolved in a batch before inlining attempts.
   107  					// Do this to avoid discovering new closure calls 1 at a time
   108  					// which might cause first call to be seen as a single (high-budget)
   109  					// call before the second is observed.
   110  					for {
   111  						for i := l0; i < l1; i++ { // can't use "range parens" here
   112  							paren := s.parens[i]
   113  							if origCall, inlinedCall := s.edit(&state, i); inlinedCall != nil {
   114  								// Update AST and recursively mark nodes.
   115  								paren.X = inlinedCall
   116  								ir.EditChildren(inlinedCall, s.mark) // mark may append to parens
   117  								state.InlinedCall(s.fn, origCall, inlinedCall)
   118  								done = false
   119  							}
   120  						}
   121  						l0, l1 = l1, len(s.parens)
   122  						if l0 == l1 {
   123  							break
   124  						}
   125  						for i := l0; i < l1; i++ {
   126  							s.resolve(&state, i)
   127  						}
   128  
   129  					}
   130  
   131  				}) // WithFunc
   132  
   133  			}
   134  		}
   135  	})
   136  
   137  	if base.Flag.LowerL != 0 {
   138  		if base.Debug.DumpInlFuncProps != "" {
   139  			inlheur.DumpFuncProps(nil, base.Debug.DumpInlFuncProps)
   140  		}
   141  		if inlheur.Enabled() {
   142  			inline.PostProcessCallSites(inlProfile)
   143  			inlheur.TearDown()
   144  		}
   145  	}
   146  
   147  	// remove parentheses
   148  	for _, fn := range typecheck.Target.Funcs {
   149  		inlState[fn].unparenthesize()
   150  	}
   151  
   152  }
   153  
   154  // DevirtualizeAndInlineFunc interleaves devirtualization and inlining
   155  // on a single function.
   156  func DevirtualizeAndInlineFunc(fn *ir.Func, profile *pgoir.Profile) {
   157  	ir.WithPos(fn, func() {
   158  		if base.Flag.LowerL != 0 {
   159  			if inlheur.Enabled() && !fn.Wrapper() {
   160  				inlheur.ScoreCalls(fn)
   161  				defer inlheur.ScoreCallsCleanup()
   162  			}
   163  			if base.Debug.DumpInlFuncProps != "" && !fn.Wrapper() {
   164  				inlheur.DumpFuncProps(fn, base.Debug.DumpInlFuncProps)
   165  			}
   166  		}
   167  
   168  		bigCaller := base.Flag.LowerL != 0 && inline.IsBigFunc(fn)
   169  		if bigCaller && base.Flag.LowerM > 1 {
   170  			fmt.Printf("%v: function %v considered 'big'; reducing max cost of inlinees\n", ir.Line(fn), fn)
   171  		}
   172  
   173  		s := &inlClosureState{bigCaller: bigCaller, profile: profile, fn: fn, callSites: make(map[*ir.ParenExpr]bool), useCounts: make(map[*ir.Func]int)}
   174  		s.parenthesize()
   175  		s.fixpoint()
   176  		s.unparenthesize()
   177  	})
   178  }
   179  
   180  type callSite struct {
   181  	fn         *ir.Func
   182  	whichParen int
   183  }
   184  
   185  type inlClosureState struct {
   186  	fn        *ir.Func
   187  	profile   *pgoir.Profile
   188  	callSites map[*ir.ParenExpr]bool // callSites[p] == "p appears in parens" (do not append again)
   189  	resolved  []*ir.Func             // for each call in parens, the resolved target of the call
   190  	useCounts map[*ir.Func]int       // shared among all InlClosureStates
   191  	parens    []*ir.ParenExpr
   192  	bigCaller bool
   193  }
   194  
   195  // resolve attempts to resolve a call to a potentially inlineable callee
   196  // and updates use counts on the callees.  Returns the call site count
   197  // for that callee.
   198  func (s *inlClosureState) resolve(state *devirtualize.State, i int) (*ir.Func, int) {
   199  	p := s.parens[i]
   200  	if i < len(s.resolved) {
   201  		if callee := s.resolved[i]; callee != nil {
   202  			return callee, s.useCounts[callee]
   203  		}
   204  	}
   205  	n := p.X
   206  	call, ok := n.(*ir.CallExpr)
   207  	if !ok { // previously inlined
   208  		return nil, -1
   209  	}
   210  	devirtualize.StaticCall(s.fn, state, call)
   211  	if callee := inline.InlineCallTarget(s.fn, call, s.profile); callee != nil {
   212  		for len(s.resolved) <= i {
   213  			s.resolved = append(s.resolved, nil)
   214  		}
   215  		s.resolved[i] = callee
   216  		c := s.useCounts[callee] + 1
   217  		s.useCounts[callee] = c
   218  		return callee, c
   219  	}
   220  	return nil, 0
   221  }
   222  
   223  func (s *inlClosureState) edit(state *devirtualize.State, i int) (*ir.CallExpr, *ir.InlinedCallExpr) {
   224  	n := s.parens[i].X
   225  	call, ok := n.(*ir.CallExpr)
   226  	if !ok {
   227  		return nil, nil
   228  	}
   229  	// This is redundant with earlier calls to
   230  	// resolve, but because things can change it
   231  	// must be re-checked.
   232  	callee, count := s.resolve(state, i)
   233  	if count <= 0 {
   234  		return nil, nil
   235  	}
   236  	if inlCall := inline.TryInlineCall(s.fn, call, s.bigCaller, s.profile, count == 1 && callee.ClosureParent != nil); inlCall != nil {
   237  		return call, inlCall
   238  	}
   239  	return nil, nil
   240  }
   241  
   242  // Mark inserts parentheses, and is called repeatedly.
   243  // These inserted parentheses mark the call sites where
   244  // inlining will be attempted.
   245  func (s *inlClosureState) mark(n ir.Node) ir.Node {
   246  	// Consider the expression "f(g())". We want to be able to replace
   247  	// "g()" in-place with its inlined representation. But if we first
   248  	// replace "f(...)" with its inlined representation, then "g()" will
   249  	// instead appear somewhere within this new AST.
   250  	//
   251  	// To mitigate this, each matched node n is wrapped in a ParenExpr,
   252  	// so we can reliably replace n in-place by assigning ParenExpr.X.
   253  	// It's safe to use ParenExpr here, because typecheck already
   254  	// removed them all.
   255  
   256  	p, _ := n.(*ir.ParenExpr)
   257  	if p != nil && s.callSites[p] {
   258  		return n // already visited n.X before wrapping
   259  	}
   260  
   261  	if p != nil {
   262  		n = p.X // in this case p was copied in from a (marked) inlined function, this is a new unvisited node.
   263  	}
   264  
   265  	ok := match(n)
   266  
   267  	// can't wrap TailCall's child into ParenExpr
   268  	if t, ok := n.(*ir.TailCallStmt); ok {
   269  		ir.EditChildren(t.Call, s.mark)
   270  	} else {
   271  		ir.EditChildren(n, s.mark)
   272  	}
   273  
   274  	if ok {
   275  		if p == nil {
   276  			p = ir.NewParenExpr(n.Pos(), n)
   277  			p.SetType(n.Type())
   278  			p.SetTypecheck(n.Typecheck())
   279  			s.callSites[p] = true
   280  		}
   281  
   282  		s.parens = append(s.parens, p)
   283  		n = p
   284  	} else if p != nil {
   285  		n = p // didn't change anything, restore n
   286  	}
   287  	return n
   288  }
   289  
   290  // parenthesize applies s.mark to all the nodes within
   291  // s.fn to mark calls and simplify rewriting them in place.
   292  func (s *inlClosureState) parenthesize() {
   293  	ir.EditChildren(s.fn, s.mark)
   294  }
   295  
   296  func (s *inlClosureState) unparenthesize() {
   297  	if s == nil {
   298  		return
   299  	}
   300  	if len(s.parens) == 0 {
   301  		return // short circuit
   302  	}
   303  
   304  	var unparen func(ir.Node) ir.Node
   305  	unparen = func(n ir.Node) ir.Node {
   306  		if paren, ok := n.(*ir.ParenExpr); ok {
   307  			n = paren.X
   308  		}
   309  		ir.EditChildren(n, unparen)
   310  		return n
   311  	}
   312  	ir.EditChildren(s.fn, unparen)
   313  }
   314  
   315  // fixpoint repeatedly edits a function until it stabilizes, returning
   316  // whether anything changed in any of the fixpoint iterations.
   317  //
   318  // It applies s.edit(n) to each node n within the parentheses in s.parens.
   319  // If s.edit(n) returns nil, no change is made. Otherwise, the result
   320  // replaces n in fn's body, and fixpoint iterates at least once more.
   321  //
   322  // After an iteration where all edit calls return nil, fixpoint
   323  // returns.
   324  func (s *inlClosureState) fixpoint() bool {
   325  	changed := false
   326  	var state devirtualize.State
   327  	ir.WithPos(s.fn, func() {
   328  		done := false
   329  		for !done {
   330  			done = true
   331  			for i := 0; i < len(s.parens); i++ { // can't use "range parens" here
   332  				paren := s.parens[i]
   333  				if origCall, inlinedCall := s.edit(&state, i); inlinedCall != nil {
   334  					// Update AST and recursively mark nodes.
   335  					paren.X = inlinedCall
   336  					ir.EditChildren(inlinedCall, s.mark) // mark may append to parens
   337  					state.InlinedCall(s.fn, origCall, inlinedCall)
   338  					done = false
   339  					changed = true
   340  				}
   341  			}
   342  		}
   343  	})
   344  	return changed
   345  }
   346  
   347  func match(n ir.Node) bool {
   348  	switch n := n.(type) {
   349  	case *ir.CallExpr:
   350  		return true
   351  	case *ir.TailCallStmt:
   352  		n.Call.NoInline = true // can't inline yet
   353  	}
   354  	return false
   355  }
   356  

View as plain text