Source file src/cmd/compile/internal/devirtualize/pgo.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 devirtualize
     6  
     7  import (
     8  	"cmd/compile/internal/base"
     9  	"cmd/compile/internal/inline"
    10  	"cmd/compile/internal/ir"
    11  	"cmd/compile/internal/logopt"
    12  	"cmd/compile/internal/pgoir"
    13  	"cmd/compile/internal/typecheck"
    14  	"cmd/compile/internal/types"
    15  	"cmd/internal/obj"
    16  	"cmd/internal/src"
    17  	"encoding/json"
    18  	"fmt"
    19  	"os"
    20  	"strings"
    21  )
    22  
    23  // CallStat summarizes a single call site.
    24  //
    25  // This is used only for debug logging.
    26  type CallStat struct {
    27  	Pkg string // base.Ctxt.Pkgpath
    28  	Pos string // file:line:col of call.
    29  
    30  	Caller string // Linker symbol name of calling function.
    31  
    32  	// Direct or indirect call.
    33  	Direct bool
    34  
    35  	// For indirect calls, interface call or other indirect function call.
    36  	Interface bool
    37  
    38  	// Total edge weight from this call site.
    39  	Weight int64
    40  
    41  	// Hottest callee from this call site, regardless of type
    42  	// compatibility.
    43  	Hottest       string
    44  	HottestWeight int64
    45  
    46  	// Devirtualized callee if != "".
    47  	//
    48  	// Note that this may be different than Hottest because we apply
    49  	// type-check restrictions, which helps distinguish multiple calls on
    50  	// the same line.
    51  	Devirtualized       string
    52  	DevirtualizedWeight int64
    53  }
    54  
    55  // ProfileGuided performs call devirtualization of indirect calls based on
    56  // profile information.
    57  //
    58  // Specifically, it performs conditional devirtualization of interface calls or
    59  // function value calls for the hottest callee.
    60  //
    61  // That is, for interface calls it performs a transformation like:
    62  //
    63  //	type Iface interface {
    64  //		Foo()
    65  //	}
    66  //
    67  //	type Concrete struct{}
    68  //
    69  //	func (Concrete) Foo() {}
    70  //
    71  //	func foo(i Iface) {
    72  //		i.Foo()
    73  //	}
    74  //
    75  // to:
    76  //
    77  //	func foo(i Iface) {
    78  //		if c, ok := i.(Concrete); ok {
    79  //			c.Foo()
    80  //		} else {
    81  //			i.Foo()
    82  //		}
    83  //	}
    84  //
    85  // For function value calls it performs a transformation like:
    86  //
    87  //	func Concrete() {}
    88  //
    89  //	func foo(fn func()) {
    90  //		fn()
    91  //	}
    92  //
    93  // to:
    94  //
    95  //	func foo(fn func()) {
    96  //		if internal/abi.FuncPCABIInternal(fn) == internal/abi.FuncPCABIInternal(Concrete) {
    97  //			Concrete()
    98  //		} else {
    99  //			fn()
   100  //		}
   101  //	}
   102  //
   103  // The primary benefit of this transformation is enabling inlining of the
   104  // direct call.
   105  func ProfileGuided(fn *ir.Func, p *pgoir.Profile) {
   106  	ir.CurFunc = fn
   107  
   108  	name := ir.LinkFuncName(fn)
   109  
   110  	var jsonW *json.Encoder
   111  	if base.Debug.PGODebug >= 3 {
   112  		jsonW = json.NewEncoder(os.Stdout)
   113  	}
   114  
   115  	var edit func(n ir.Node) ir.Node
   116  	edit = func(n ir.Node) ir.Node {
   117  		if n == nil {
   118  			return n
   119  		}
   120  
   121  		ir.EditChildren(n, edit)
   122  
   123  		call, ok := n.(*ir.CallExpr)
   124  		if !ok {
   125  			return n
   126  		}
   127  
   128  		var stat *CallStat
   129  		if base.Debug.PGODebug >= 3 {
   130  			// Statistics about every single call. Handy for external data analysis.
   131  			//
   132  			// TODO(prattmic): Log via logopt?
   133  			stat = constructCallStat(p, fn, name, call)
   134  			if stat != nil {
   135  				defer func() {
   136  					jsonW.Encode(&stat)
   137  				}()
   138  			}
   139  		}
   140  
   141  		op := call.Op()
   142  		if op != ir.OCALLFUNC && op != ir.OCALLINTER {
   143  			return n
   144  		}
   145  
   146  		if base.Debug.PGODebug >= 2 {
   147  			fmt.Printf("%v: PGO devirtualize considering call %v\n", ir.Line(call), call)
   148  		}
   149  
   150  		if call.GoDefer {
   151  			if base.Debug.PGODebug >= 2 {
   152  				fmt.Printf("%v: can't PGO devirtualize go/defer call %v\n", ir.Line(call), call)
   153  			}
   154  			return n
   155  		}
   156  
   157  		var newNode ir.Node
   158  		var callee *ir.Func
   159  		var weight int64
   160  		switch op {
   161  		case ir.OCALLFUNC:
   162  			newNode, callee, weight = maybeDevirtualizeFunctionCall(p, fn, call)
   163  		case ir.OCALLINTER:
   164  			newNode, callee, weight = maybeDevirtualizeInterfaceCall(p, fn, call)
   165  		default:
   166  			panic("unreachable")
   167  		}
   168  
   169  		if newNode == nil {
   170  			return n
   171  		}
   172  
   173  		if stat != nil {
   174  			stat.Devirtualized = ir.LinkFuncName(callee)
   175  			stat.DevirtualizedWeight = weight
   176  		}
   177  
   178  		return newNode
   179  	}
   180  
   181  	ir.EditChildren(fn, edit)
   182  }
   183  
   184  // Devirtualize interface call if possible and eligible. Returns the new
   185  // ir.Node if call was devirtualized, and if so also the callee and weight of
   186  // the devirtualized edge.
   187  func maybeDevirtualizeInterfaceCall(p *pgoir.Profile, fn *ir.Func, call *ir.CallExpr) (ir.Node, *ir.Func, int64) {
   188  	if base.Debug.PGODevirtualize < 1 {
   189  		return nil, nil, 0
   190  	}
   191  
   192  	// Bail if we do not have a hot callee.
   193  	callee, weight := findHotConcreteInterfaceCallee(p, fn, call)
   194  	if callee == nil {
   195  		return nil, nil, 0
   196  	}
   197  	// Bail if we do not have a Type node for the hot callee.
   198  	ctyp := methodRecvType(callee)
   199  	if ctyp == nil {
   200  		return nil, nil, 0
   201  	}
   202  	// Bail if we know for sure it won't inline.
   203  	if !shouldPGODevirt(callee) {
   204  		return nil, nil, 0
   205  	}
   206  	// Bail if de-selected by PGO Hash.
   207  	if !base.PGOHash.MatchPosWithInfo(call.Pos(), "devirt", nil) {
   208  		return nil, nil, 0
   209  	}
   210  
   211  	return rewriteInterfaceCall(call, fn, callee, ctyp), callee, weight
   212  }
   213  
   214  // Devirtualize an indirect function call if possible and eligible. Returns the new
   215  // ir.Node if call was devirtualized, and if so also the callee and weight of
   216  // the devirtualized edge.
   217  func maybeDevirtualizeFunctionCall(p *pgoir.Profile, fn *ir.Func, call *ir.CallExpr) (ir.Node, *ir.Func, int64) {
   218  	if base.Debug.PGODevirtualize < 2 {
   219  		return nil, nil, 0
   220  	}
   221  
   222  	// Bail if this is a direct call; no devirtualization necessary.
   223  	callee := pgoir.DirectCallee(call.Fun)
   224  	if callee != nil {
   225  		return nil, nil, 0
   226  	}
   227  
   228  	// Bail if we do not have a hot callee.
   229  	callee, weight := findHotConcreteFunctionCallee(p, fn, call)
   230  	if callee == nil {
   231  		return nil, nil, 0
   232  	}
   233  
   234  	// TODO(go.dev/issue/61577): Closures need the closure context passed
   235  	// via the context register. That requires extra plumbing that we
   236  	// haven't done yet.
   237  	if callee.OClosure != nil {
   238  		if base.Debug.PGODebug >= 3 {
   239  			fmt.Printf("callee %s is a closure, skipping\n", ir.FuncName(callee))
   240  		}
   241  		return nil, nil, 0
   242  	}
   243  	// runtime.memhash_varlen does not look like a closure, but it uses
   244  	// runtime.getclosureptr to access data encoded by callers, which are
   245  	// are generated by cmd/compile/internal/reflectdata.genhash.
   246  	if callee.Sym().Pkg.Path == "runtime" && callee.Sym().Name == "memhash_varlen" {
   247  		if base.Debug.PGODebug >= 3 {
   248  			fmt.Printf("callee %s is a closure (runtime.memhash_varlen), skipping\n", ir.FuncName(callee))
   249  		}
   250  		return nil, nil, 0
   251  	}
   252  	// TODO(prattmic): We don't properly handle methods as callees in two
   253  	// different dimensions:
   254  	//
   255  	// 1. Method expressions. e.g.,
   256  	//
   257  	//      var fn func(*os.File, []byte) (int, error) = (*os.File).Read
   258  	//
   259  	// In this case, typ will report *os.File as the receiver while
   260  	// ctyp reports it as the first argument. types.Identical ignores
   261  	// receiver parameters, so it treats these as different, even though
   262  	// they are still call compatible.
   263  	//
   264  	// 2. Method values. e.g.,
   265  	//
   266  	//      var f *os.File
   267  	//      var fn func([]byte) (int, error) = f.Read
   268  	//
   269  	// types.Identical will treat these as compatible (since receiver
   270  	// parameters are ignored). However, in this case, we do not call
   271  	// (*os.File).Read directly. Instead, f is stored in closure context
   272  	// and we call the wrapper (*os.File).Read-fm. However, runtime/pprof
   273  	// hides wrappers from profiles, making it appear that there is a call
   274  	// directly to the method. We could recognize this pattern return the
   275  	// wrapper rather than the method.
   276  	//
   277  	// N.B. perf profiles will report wrapper symbols directly, so
   278  	// ideally we should support direct wrapper references as well.
   279  	if callee.Type().Recv() != nil {
   280  		if base.Debug.PGODebug >= 3 {
   281  			fmt.Printf("callee %s is a method, skipping\n", ir.FuncName(callee))
   282  		}
   283  		return nil, nil, 0
   284  	}
   285  
   286  	// Bail if we know for sure it won't inline.
   287  	if !shouldPGODevirt(callee) {
   288  		return nil, nil, 0
   289  	}
   290  	// Bail if de-selected by PGO Hash.
   291  	if !base.PGOHash.MatchPosWithInfo(call.Pos(), "devirt", nil) {
   292  		return nil, nil, 0
   293  	}
   294  
   295  	return rewriteFunctionCall(call, fn, callee), callee, weight
   296  }
   297  
   298  // shouldPGODevirt checks if we should perform PGO devirtualization to the
   299  // target function.
   300  //
   301  // PGO devirtualization is most valuable when the callee is inlined, so if it
   302  // won't inline we can skip devirtualizing.
   303  func shouldPGODevirt(fn *ir.Func) bool {
   304  	var reason string
   305  	if base.Flag.LowerM > 1 || logopt.Enabled() {
   306  		defer func() {
   307  			if reason != "" {
   308  				if base.Flag.LowerM > 1 {
   309  					fmt.Printf("%v: should not PGO devirtualize %v: %s\n", ir.Line(fn), ir.FuncName(fn), reason)
   310  				}
   311  				if logopt.Enabled() {
   312  					logopt.LogOpt(fn.Pos(), ": should not PGO devirtualize function", "pgoir-devirtualize", ir.FuncName(fn), reason)
   313  				}
   314  			}
   315  		}()
   316  	}
   317  
   318  	reason = inline.InlineImpossible(fn)
   319  	if reason != "" {
   320  		return false
   321  	}
   322  
   323  	// TODO(prattmic): checking only InlineImpossible is very conservative,
   324  	// primarily excluding only functions with pragmas. We probably want to
   325  	// move in either direction. Either:
   326  	//
   327  	// 1. Don't even bother to check InlineImpossible, as it affects so few
   328  	// functions.
   329  	//
   330  	// 2. Or consider the function body (notably cost) to better determine
   331  	// if the function will actually inline.
   332  
   333  	return true
   334  }
   335  
   336  // constructCallStat builds an initial CallStat describing this call, for
   337  // logging. If the call is devirtualized, the devirtualization fields should be
   338  // updated.
   339  func constructCallStat(p *pgoir.Profile, fn *ir.Func, name string, call *ir.CallExpr) *CallStat {
   340  	switch call.Op() {
   341  	case ir.OCALLFUNC, ir.OCALLINTER, ir.OCALLMETH:
   342  	default:
   343  		// We don't care about logging builtin functions.
   344  		return nil
   345  	}
   346  
   347  	stat := CallStat{
   348  		Pkg:    base.Ctxt.Pkgpath,
   349  		Pos:    ir.Line(call),
   350  		Caller: name,
   351  	}
   352  
   353  	offset := pgoir.NodeLineOffset(call, fn)
   354  
   355  	hotter := func(e *pgoir.IREdge) bool {
   356  		if stat.Hottest == "" {
   357  			return true
   358  		}
   359  		if e.Weight != stat.HottestWeight {
   360  			return e.Weight > stat.HottestWeight
   361  		}
   362  		// If weight is the same, arbitrarily sort lexicographally, as
   363  		// findHotConcreteCallee does.
   364  		return e.Dst.Name() < stat.Hottest
   365  	}
   366  
   367  	callerNode := p.WeightedCG.IRNodes[name]
   368  	if callerNode == nil {
   369  		return nil
   370  	}
   371  
   372  	// Sum of all edges from this callsite, regardless of callee.
   373  	// For direct calls, this should be the same as the single edge
   374  	// weight (except for multiple calls on one line, which we
   375  	// can't distinguish).
   376  	for _, edge := range callerNode.OutEdges {
   377  		if edge.CallSiteOffset != offset {
   378  			continue
   379  		}
   380  		stat.Weight += edge.Weight
   381  		if hotter(edge) {
   382  			stat.HottestWeight = edge.Weight
   383  			stat.Hottest = edge.Dst.Name()
   384  		}
   385  	}
   386  
   387  	switch call.Op() {
   388  	case ir.OCALLFUNC:
   389  		stat.Interface = false
   390  
   391  		callee := pgoir.DirectCallee(call.Fun)
   392  		if callee != nil {
   393  			stat.Direct = true
   394  			if stat.Hottest == "" {
   395  				stat.Hottest = ir.LinkFuncName(callee)
   396  			}
   397  		} else {
   398  			stat.Direct = false
   399  		}
   400  	case ir.OCALLINTER:
   401  		stat.Direct = false
   402  		stat.Interface = true
   403  	case ir.OCALLMETH:
   404  		base.FatalfAt(call.Pos(), "OCALLMETH missed by typecheck")
   405  	}
   406  
   407  	return &stat
   408  }
   409  
   410  // copyInputs copies the inputs to a call: the receiver (for interface calls)
   411  // or function value (for function value calls) and the arguments. These
   412  // expressions are evaluated once and assigned to temporaries.
   413  //
   414  // The assignment statement is added to init and the copied receiver/fn
   415  // expression and copied arguments expressions are returned.
   416  func copyInputs(curfn *ir.Func, pos src.XPos, recvOrFn ir.Node, args []ir.Node, init *ir.Nodes) (ir.Node, []ir.Node) {
   417  	// Evaluate receiver/fn and argument expressions. The receiver/fn is
   418  	// used twice but we don't want to cause side effects twice. The
   419  	// arguments are used in two different calls and we can't trivially
   420  	// copy them.
   421  	//
   422  	// recvOrFn must be first in the assignment list as its side effects
   423  	// must be ordered before argument side effects.
   424  	var lhs, rhs []ir.Node
   425  	newRecvOrFn := typecheck.TempAt(pos, curfn, recvOrFn.Type())
   426  	lhs = append(lhs, newRecvOrFn)
   427  	rhs = append(rhs, recvOrFn)
   428  
   429  	for _, arg := range args {
   430  		argvar := typecheck.TempAt(pos, curfn, arg.Type())
   431  
   432  		lhs = append(lhs, argvar)
   433  		rhs = append(rhs, arg)
   434  	}
   435  
   436  	asList := ir.NewAssignListStmt(pos, ir.OAS2, lhs, rhs)
   437  	init.Append(typecheck.Stmt(asList))
   438  
   439  	return newRecvOrFn, lhs[1:]
   440  }
   441  
   442  // retTemps returns a slice of temporaries to be used for storing result values from call.
   443  func retTemps(curfn *ir.Func, pos src.XPos, call *ir.CallExpr) []ir.Node {
   444  	sig := call.Fun.Type()
   445  	var retvars []ir.Node
   446  	for _, ret := range sig.Results() {
   447  		retvars = append(retvars, typecheck.TempAt(pos, curfn, ret.Type))
   448  	}
   449  	return retvars
   450  }
   451  
   452  // condCall returns an ir.InlinedCallExpr that performs a call to thenCall if
   453  // cond is true and elseCall if cond is false. The return variables of the
   454  // InlinedCallExpr evaluate to the return values from the call.
   455  func condCall(curfn *ir.Func, pos src.XPos, cond ir.Node, thenCall, elseCall *ir.CallExpr, init ir.Nodes) *ir.InlinedCallExpr {
   456  	// Doesn't matter whether we use thenCall or elseCall, they must have
   457  	// the same return types.
   458  	retvars := retTemps(curfn, pos, thenCall)
   459  
   460  	var thenBlock, elseBlock ir.Nodes
   461  	if len(retvars) == 0 {
   462  		thenBlock.Append(thenCall)
   463  		elseBlock.Append(elseCall)
   464  	} else {
   465  		// Copy slice so edits in one location don't affect another.
   466  		thenRet := append([]ir.Node(nil), retvars...)
   467  		thenAsList := ir.NewAssignListStmt(pos, ir.OAS2, thenRet, []ir.Node{thenCall})
   468  		thenBlock.Append(typecheck.Stmt(thenAsList))
   469  
   470  		elseRet := append([]ir.Node(nil), retvars...)
   471  		elseAsList := ir.NewAssignListStmt(pos, ir.OAS2, elseRet, []ir.Node{elseCall})
   472  		elseBlock.Append(typecheck.Stmt(elseAsList))
   473  	}
   474  
   475  	nif := ir.NewIfStmt(pos, cond, thenBlock, elseBlock)
   476  	nif.SetInit(init)
   477  	nif.Likely = true
   478  
   479  	body := []ir.Node{typecheck.Stmt(nif)}
   480  
   481  	// This isn't really an inlined call of course, but InlinedCallExpr
   482  	// makes handling reassignment of return values easier.
   483  	res := ir.NewInlinedCallExpr(pos, body, retvars)
   484  	res.SetType(thenCall.Type())
   485  	res.SetTypecheck(1)
   486  	return res
   487  }
   488  
   489  // rewriteInterfaceCall devirtualizes the given interface call using a direct
   490  // method call to concretetyp.
   491  func rewriteInterfaceCall(call *ir.CallExpr, curfn, callee *ir.Func, concretetyp *types.Type) ir.Node {
   492  	if base.Flag.LowerM != 0 {
   493  		fmt.Printf("%v: PGO devirtualizing interface call %v to %v\n", ir.Line(call), call.Fun, callee)
   494  	}
   495  
   496  	// We generate an OINCALL of:
   497  	//
   498  	// var recv Iface
   499  	//
   500  	// var arg1 A1
   501  	// var argN AN
   502  	//
   503  	// var ret1 R1
   504  	// var retN RN
   505  	//
   506  	// recv, arg1, argN = recv expr, arg1 expr, argN expr
   507  	//
   508  	// t, ok := recv.(Concrete)
   509  	// if ok {
   510  	//   ret1, retN = t.Method(arg1, ... argN)
   511  	// } else {
   512  	//   ret1, retN = recv.Method(arg1, ... argN)
   513  	// }
   514  	//
   515  	// OINCALL retvars: ret1, ... retN
   516  	//
   517  	// This isn't really an inlined call of course, but InlinedCallExpr
   518  	// makes handling reassignment of return values easier.
   519  	//
   520  	// TODO(prattmic): This increases the size of the AST in the caller,
   521  	// making it less like to inline. We may want to compensate for this
   522  	// somehow.
   523  
   524  	sel := call.Fun.(*ir.SelectorExpr)
   525  	method := sel.Sel
   526  	pos := call.Pos()
   527  	init := ir.TakeInit(call)
   528  
   529  	recv, args := copyInputs(curfn, pos, sel.X, call.Args.Take(), &init)
   530  
   531  	// Copy slice so edits in one location don't affect another.
   532  	argvars := append([]ir.Node(nil), args...)
   533  	call.Args = argvars
   534  
   535  	tmpnode := typecheck.TempAt(base.Pos, curfn, concretetyp)
   536  	tmpok := typecheck.TempAt(base.Pos, curfn, types.Types[types.TBOOL])
   537  
   538  	assert := ir.NewTypeAssertExpr(pos, recv, concretetyp)
   539  
   540  	assertAsList := ir.NewAssignListStmt(pos, ir.OAS2, []ir.Node{tmpnode, tmpok}, []ir.Node{typecheck.Expr(assert)})
   541  	init.Append(typecheck.Stmt(assertAsList))
   542  
   543  	concreteCallee := typecheck.XDotMethod(pos, tmpnode, method, true)
   544  	// Copy slice so edits in one location don't affect another.
   545  	argvars = append([]ir.Node(nil), argvars...)
   546  	concreteCall := typecheck.Call(pos, concreteCallee, argvars, call.IsDDD).(*ir.CallExpr)
   547  
   548  	res := condCall(curfn, pos, tmpok, concreteCall, call, init)
   549  
   550  	if base.Debug.PGODebug >= 3 {
   551  		fmt.Printf("PGO devirtualizing interface call to %+v. After: %+v\n", concretetyp, res)
   552  	}
   553  
   554  	return res
   555  }
   556  
   557  // rewriteFunctionCall devirtualizes the given OCALLFUNC using a direct
   558  // function call to callee.
   559  func rewriteFunctionCall(call *ir.CallExpr, curfn, callee *ir.Func) ir.Node {
   560  	if base.Flag.LowerM != 0 {
   561  		fmt.Printf("%v: PGO devirtualizing function call %v to %v\n", ir.Line(call), call.Fun, callee)
   562  	}
   563  
   564  	// We generate an OINCALL of:
   565  	//
   566  	// var fn FuncType
   567  	//
   568  	// var arg1 A1
   569  	// var argN AN
   570  	//
   571  	// var ret1 R1
   572  	// var retN RN
   573  	//
   574  	// fn, arg1, argN = fn expr, arg1 expr, argN expr
   575  	//
   576  	// fnPC := internal/abi.FuncPCABIInternal(fn)
   577  	// concretePC := internal/abi.FuncPCABIInternal(concrete)
   578  	//
   579  	// if fnPC == concretePC {
   580  	//   ret1, retN = concrete(arg1, ... argN) // Same closure context passed (TODO)
   581  	// } else {
   582  	//   ret1, retN = fn(arg1, ... argN)
   583  	// }
   584  	//
   585  	// OINCALL retvars: ret1, ... retN
   586  	//
   587  	// This isn't really an inlined call of course, but InlinedCallExpr
   588  	// makes handling reassignment of return values easier.
   589  
   590  	pos := call.Pos()
   591  	init := ir.TakeInit(call)
   592  
   593  	fn, args := copyInputs(curfn, pos, call.Fun, call.Args.Take(), &init)
   594  
   595  	// Copy slice so edits in one location don't affect another.
   596  	argvars := append([]ir.Node(nil), args...)
   597  	call.Args = argvars
   598  
   599  	// FuncPCABIInternal takes an interface{}, emulate that. This is needed
   600  	// for to ensure we get the MAKEFACE we need for SSA.
   601  	fnIface := typecheck.Expr(ir.NewConvExpr(pos, ir.OCONV, types.Types[types.TINTER], fn))
   602  	calleeIface := typecheck.Expr(ir.NewConvExpr(pos, ir.OCONV, types.Types[types.TINTER], callee.Nname))
   603  
   604  	fnPC := ir.FuncPC(pos, fnIface, obj.ABIInternal)
   605  	concretePC := ir.FuncPC(pos, calleeIface, obj.ABIInternal)
   606  
   607  	pcEq := typecheck.Expr(ir.NewBinaryExpr(base.Pos, ir.OEQ, fnPC, concretePC))
   608  
   609  	// TODO(go.dev/issue/61577): Handle callees that a closures and need a
   610  	// copy of the closure context from call. For now, we skip callees that
   611  	// are closures in maybeDevirtualizeFunctionCall.
   612  	if callee.OClosure != nil {
   613  		base.Fatalf("Callee is a closure: %+v", callee)
   614  	}
   615  
   616  	// Copy slice so edits in one location don't affect another.
   617  	argvars = append([]ir.Node(nil), argvars...)
   618  	concreteCall := typecheck.Call(pos, callee.Nname, argvars, call.IsDDD).(*ir.CallExpr)
   619  
   620  	res := condCall(curfn, pos, pcEq, concreteCall, call, init)
   621  
   622  	if base.Debug.PGODebug >= 3 {
   623  		fmt.Printf("PGO devirtualizing function call to %+v. After: %+v\n", ir.FuncName(callee), res)
   624  	}
   625  
   626  	return res
   627  }
   628  
   629  // methodRecvType returns the type containing method fn. Returns nil if fn
   630  // is not a method.
   631  func methodRecvType(fn *ir.Func) *types.Type {
   632  	recv := fn.Nname.Type().Recv()
   633  	if recv == nil {
   634  		return nil
   635  	}
   636  	return recv.Type
   637  }
   638  
   639  // interfaceCallRecvTypeAndMethod returns the type and the method of the interface
   640  // used in an interface call.
   641  func interfaceCallRecvTypeAndMethod(call *ir.CallExpr) (*types.Type, *types.Sym) {
   642  	if call.Op() != ir.OCALLINTER {
   643  		base.Fatalf("Call isn't OCALLINTER: %+v", call)
   644  	}
   645  
   646  	sel, ok := call.Fun.(*ir.SelectorExpr)
   647  	if !ok {
   648  		base.Fatalf("OCALLINTER doesn't contain SelectorExpr: %+v", call)
   649  	}
   650  
   651  	return sel.X.Type(), sel.Sel
   652  }
   653  
   654  // findHotConcreteCallee returns the *ir.Func of the hottest callee of a call,
   655  // if available, and its edge weight. extraFn can perform additional
   656  // applicability checks on each candidate edge. If extraFn returns false,
   657  // candidate will not be considered a valid callee candidate.
   658  func findHotConcreteCallee(p *pgoir.Profile, caller *ir.Func, call *ir.CallExpr, extraFn func(callerName string, callOffset int, candidate *pgoir.IREdge) bool) (*ir.Func, int64) {
   659  	callerName := ir.LinkFuncName(caller)
   660  	callerNode := p.WeightedCG.IRNodes[callerName]
   661  	callOffset := pgoir.NodeLineOffset(call, caller)
   662  
   663  	if callerNode == nil {
   664  		return nil, 0
   665  	}
   666  
   667  	var hottest *pgoir.IREdge
   668  
   669  	// Returns true if e is hotter than hottest.
   670  	//
   671  	// Naively this is just e.Weight > hottest.Weight, but because OutEdges
   672  	// has arbitrary iteration order, we need to apply additional sort
   673  	// criteria when e.Weight == hottest.Weight to ensure we have stable
   674  	// selection.
   675  	hotter := func(e *pgoir.IREdge) bool {
   676  		if hottest == nil {
   677  			return true
   678  		}
   679  		if e.Weight != hottest.Weight {
   680  			return e.Weight > hottest.Weight
   681  		}
   682  
   683  		// Now e.Weight == hottest.Weight, we must select on other
   684  		// criteria.
   685  
   686  		// If only one edge has IR, prefer that one.
   687  		if (hottest.Dst.AST == nil) != (e.Dst.AST == nil) {
   688  			if e.Dst.AST != nil {
   689  				return true
   690  			}
   691  			return false
   692  		}
   693  
   694  		// Arbitrary, but the callee names will always differ. Select
   695  		// the lexicographically first callee.
   696  		return e.Dst.Name() < hottest.Dst.Name()
   697  	}
   698  
   699  	for _, e := range callerNode.OutEdges {
   700  		if e.CallSiteOffset != callOffset {
   701  			continue
   702  		}
   703  
   704  		if !hotter(e) {
   705  			// TODO(prattmic): consider total caller weight? i.e.,
   706  			// if the hottest callee is only 10% of the weight,
   707  			// maybe don't devirtualize? Similarly, if this is call
   708  			// is globally very cold, there is not much value in
   709  			// devirtualizing.
   710  			if base.Debug.PGODebug >= 2 {
   711  				fmt.Printf("%v: edge %s:%d -> %s (weight %d): too cold (hottest %d)\n", ir.Line(call), callerName, callOffset, e.Dst.Name(), e.Weight, hottest.Weight)
   712  			}
   713  			continue
   714  		}
   715  
   716  		if e.Dst.AST == nil {
   717  			// Destination isn't visible from this package
   718  			// compilation.
   719  			//
   720  			// We must assume it implements the interface.
   721  			//
   722  			// We still record this as the hottest callee so far
   723  			// because we only want to return the #1 hottest
   724  			// callee. If we skip this then we'd return the #2
   725  			// hottest callee.
   726  			if base.Debug.PGODebug >= 2 {
   727  				fmt.Printf("%v: edge %s:%d -> %s (weight %d) (missing IR): hottest so far\n", ir.Line(call), callerName, callOffset, e.Dst.Name(), e.Weight)
   728  			}
   729  			hottest = e
   730  			continue
   731  		}
   732  
   733  		if extraFn != nil && !extraFn(callerName, callOffset, e) {
   734  			continue
   735  		}
   736  
   737  		if base.Debug.PGODebug >= 2 {
   738  			fmt.Printf("%v: edge %s:%d -> %s (weight %d): hottest so far\n", ir.Line(call), callerName, callOffset, e.Dst.Name(), e.Weight)
   739  		}
   740  		hottest = e
   741  	}
   742  
   743  	if hottest == nil {
   744  		if base.Debug.PGODebug >= 2 {
   745  			fmt.Printf("%v: call %s:%d: no hot callee\n", ir.Line(call), callerName, callOffset)
   746  		}
   747  		return nil, 0
   748  	}
   749  
   750  	if base.Debug.PGODebug >= 2 {
   751  		fmt.Printf("%v: call %s:%d: hottest callee %s (weight %d)\n", ir.Line(call), callerName, callOffset, hottest.Dst.Name(), hottest.Weight)
   752  	}
   753  	return hottest.Dst.AST, hottest.Weight
   754  }
   755  
   756  // findHotConcreteInterfaceCallee returns the *ir.Func of the hottest callee of an
   757  // interface call, if available, and its edge weight.
   758  func findHotConcreteInterfaceCallee(p *pgoir.Profile, caller *ir.Func, call *ir.CallExpr) (*ir.Func, int64) {
   759  	inter, method := interfaceCallRecvTypeAndMethod(call)
   760  
   761  	return findHotConcreteCallee(p, caller, call, func(callerName string, callOffset int, e *pgoir.IREdge) bool {
   762  		ctyp := methodRecvType(e.Dst.AST)
   763  		if ctyp == nil {
   764  			// Not a method.
   765  			// TODO(prattmic): Support non-interface indirect calls.
   766  			if base.Debug.PGODebug >= 2 {
   767  				fmt.Printf("%v: edge %s:%d -> %s (weight %d): callee not a method\n", ir.Line(call), callerName, callOffset, e.Dst.Name(), e.Weight)
   768  			}
   769  			return false
   770  		}
   771  
   772  		// If ctyp doesn't implement inter it is most likely from a
   773  		// different call on the same line
   774  		if !typecheck.Implements(ctyp, inter) {
   775  			// TODO(prattmic): this is overly strict. Consider if
   776  			// ctyp is a partial implementation of an interface
   777  			// that gets embedded in types that complete the
   778  			// interface. It would still be OK to devirtualize a
   779  			// call to this method.
   780  			//
   781  			// What we'd need to do is check that the function
   782  			// pointer in the itab matches the method we want,
   783  			// rather than doing a full type assertion.
   784  			if base.Debug.PGODebug >= 2 {
   785  				why := typecheck.ImplementsExplain(ctyp, inter)
   786  				fmt.Printf("%v: edge %s:%d -> %s (weight %d): %v doesn't implement %v (%s)\n", ir.Line(call), callerName, callOffset, e.Dst.Name(), e.Weight, ctyp, inter, why)
   787  			}
   788  			return false
   789  		}
   790  
   791  		// If the method name is different it is most likely from a
   792  		// different call on the same line
   793  		if !strings.HasSuffix(e.Dst.Name(), "."+method.Name) {
   794  			if base.Debug.PGODebug >= 2 {
   795  				fmt.Printf("%v: edge %s:%d -> %s (weight %d): callee is a different method\n", ir.Line(call), callerName, callOffset, e.Dst.Name(), e.Weight)
   796  			}
   797  			return false
   798  		}
   799  
   800  		return true
   801  	})
   802  }
   803  
   804  // findHotConcreteFunctionCallee returns the *ir.Func of the hottest callee of an
   805  // indirect function call, if available, and its edge weight.
   806  func findHotConcreteFunctionCallee(p *pgoir.Profile, caller *ir.Func, call *ir.CallExpr) (*ir.Func, int64) {
   807  	typ := call.Fun.Type().Underlying()
   808  
   809  	return findHotConcreteCallee(p, caller, call, func(callerName string, callOffset int, e *pgoir.IREdge) bool {
   810  		ctyp := e.Dst.AST.Type().Underlying()
   811  
   812  		// If ctyp doesn't match typ it is most likely from a different
   813  		// call on the same line.
   814  		//
   815  		// Note that we are comparing underlying types, as different
   816  		// defined types are OK. e.g., a call to a value of type
   817  		// net/http.HandlerFunc can be devirtualized to a function with
   818  		// the same underlying type.
   819  		if !types.Identical(typ, ctyp) {
   820  			if base.Debug.PGODebug >= 2 {
   821  				fmt.Printf("%v: edge %s:%d -> %s (weight %d): %v doesn't match %v\n", ir.Line(call), callerName, callOffset, e.Dst.Name(), e.Weight, ctyp, typ)
   822  			}
   823  			return false
   824  		}
   825  
   826  		return true
   827  	})
   828  }
   829  

View as plain text