mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
Change-Id: Id991ec0826a4e2857f00330b4b7ff2b71907b789 Reviewed-on: https://go-review.googlesource.com/c/go/+/606615 Auto-Submit: Ian Lance Taylor <iant@google.com> Reviewed-by: Robert Griesemer <gri@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Ian Lance Taylor <iant@google.com> Auto-Submit: Robert Griesemer <gri@google.com> Commit-Queue: Ian Lance Taylor <iant@google.com>
828 lines
25 KiB
Go
828 lines
25 KiB
Go
// 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 devirtualize
|
|
|
|
import (
|
|
"cmd/compile/internal/base"
|
|
"cmd/compile/internal/inline"
|
|
"cmd/compile/internal/ir"
|
|
"cmd/compile/internal/logopt"
|
|
"cmd/compile/internal/pgoir"
|
|
"cmd/compile/internal/typecheck"
|
|
"cmd/compile/internal/types"
|
|
"cmd/internal/obj"
|
|
"cmd/internal/src"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// CallStat summarizes a single call site.
|
|
//
|
|
// This is used only for debug logging.
|
|
type CallStat struct {
|
|
Pkg string // base.Ctxt.Pkgpath
|
|
Pos string // file:line:col of call.
|
|
|
|
Caller string // Linker symbol name of calling function.
|
|
|
|
// Direct or indirect call.
|
|
Direct bool
|
|
|
|
// For indirect calls, interface call or other indirect function call.
|
|
Interface bool
|
|
|
|
// Total edge weight from this call site.
|
|
Weight int64
|
|
|
|
// Hottest callee from this call site, regardless of type
|
|
// compatibility.
|
|
Hottest string
|
|
HottestWeight int64
|
|
|
|
// Devirtualized callee if != "".
|
|
//
|
|
// Note that this may be different than Hottest because we apply
|
|
// type-check restrictions, which helps distinguish multiple calls on
|
|
// the same line.
|
|
Devirtualized string
|
|
DevirtualizedWeight int64
|
|
}
|
|
|
|
// ProfileGuided performs call devirtualization of indirect calls based on
|
|
// profile information.
|
|
//
|
|
// Specifically, it performs conditional devirtualization of interface calls or
|
|
// function value calls for the hottest callee.
|
|
//
|
|
// That is, for interface calls it performs a transformation like:
|
|
//
|
|
// type Iface interface {
|
|
// Foo()
|
|
// }
|
|
//
|
|
// type Concrete struct{}
|
|
//
|
|
// func (Concrete) Foo() {}
|
|
//
|
|
// func foo(i Iface) {
|
|
// i.Foo()
|
|
// }
|
|
//
|
|
// to:
|
|
//
|
|
// func foo(i Iface) {
|
|
// if c, ok := i.(Concrete); ok {
|
|
// c.Foo()
|
|
// } else {
|
|
// i.Foo()
|
|
// }
|
|
// }
|
|
//
|
|
// For function value calls it performs a transformation like:
|
|
//
|
|
// func Concrete() {}
|
|
//
|
|
// func foo(fn func()) {
|
|
// fn()
|
|
// }
|
|
//
|
|
// to:
|
|
//
|
|
// func foo(fn func()) {
|
|
// if internal/abi.FuncPCABIInternal(fn) == internal/abi.FuncPCABIInternal(Concrete) {
|
|
// Concrete()
|
|
// } else {
|
|
// fn()
|
|
// }
|
|
// }
|
|
//
|
|
// The primary benefit of this transformation is enabling inlining of the
|
|
// direct call.
|
|
func ProfileGuided(fn *ir.Func, p *pgoir.Profile) {
|
|
ir.CurFunc = fn
|
|
|
|
name := ir.LinkFuncName(fn)
|
|
|
|
var jsonW *json.Encoder
|
|
if base.Debug.PGODebug >= 3 {
|
|
jsonW = json.NewEncoder(os.Stdout)
|
|
}
|
|
|
|
var edit func(n ir.Node) ir.Node
|
|
edit = func(n ir.Node) ir.Node {
|
|
if n == nil {
|
|
return n
|
|
}
|
|
|
|
ir.EditChildren(n, edit)
|
|
|
|
call, ok := n.(*ir.CallExpr)
|
|
if !ok {
|
|
return n
|
|
}
|
|
|
|
var stat *CallStat
|
|
if base.Debug.PGODebug >= 3 {
|
|
// Statistics about every single call. Handy for external data analysis.
|
|
//
|
|
// TODO(prattmic): Log via logopt?
|
|
stat = constructCallStat(p, fn, name, call)
|
|
if stat != nil {
|
|
defer func() {
|
|
jsonW.Encode(&stat)
|
|
}()
|
|
}
|
|
}
|
|
|
|
op := call.Op()
|
|
if op != ir.OCALLFUNC && op != ir.OCALLINTER {
|
|
return n
|
|
}
|
|
|
|
if base.Debug.PGODebug >= 2 {
|
|
fmt.Printf("%v: PGO devirtualize considering call %v\n", ir.Line(call), call)
|
|
}
|
|
|
|
if call.GoDefer {
|
|
if base.Debug.PGODebug >= 2 {
|
|
fmt.Printf("%v: can't PGO devirtualize go/defer call %v\n", ir.Line(call), call)
|
|
}
|
|
return n
|
|
}
|
|
|
|
var newNode ir.Node
|
|
var callee *ir.Func
|
|
var weight int64
|
|
switch op {
|
|
case ir.OCALLFUNC:
|
|
newNode, callee, weight = maybeDevirtualizeFunctionCall(p, fn, call)
|
|
case ir.OCALLINTER:
|
|
newNode, callee, weight = maybeDevirtualizeInterfaceCall(p, fn, call)
|
|
default:
|
|
panic("unreachable")
|
|
}
|
|
|
|
if newNode == nil {
|
|
return n
|
|
}
|
|
|
|
if stat != nil {
|
|
stat.Devirtualized = ir.LinkFuncName(callee)
|
|
stat.DevirtualizedWeight = weight
|
|
}
|
|
|
|
return newNode
|
|
}
|
|
|
|
ir.EditChildren(fn, edit)
|
|
}
|
|
|
|
// Devirtualize interface call if possible and eligible. Returns the new
|
|
// ir.Node if call was devirtualized, and if so also the callee and weight of
|
|
// the devirtualized edge.
|
|
func maybeDevirtualizeInterfaceCall(p *pgoir.Profile, fn *ir.Func, call *ir.CallExpr) (ir.Node, *ir.Func, int64) {
|
|
if base.Debug.PGODevirtualize < 1 {
|
|
return nil, nil, 0
|
|
}
|
|
|
|
// Bail if we do not have a hot callee.
|
|
callee, weight := findHotConcreteInterfaceCallee(p, fn, call)
|
|
if callee == nil {
|
|
return nil, nil, 0
|
|
}
|
|
// Bail if we do not have a Type node for the hot callee.
|
|
ctyp := methodRecvType(callee)
|
|
if ctyp == nil {
|
|
return nil, nil, 0
|
|
}
|
|
// Bail if we know for sure it won't inline.
|
|
if !shouldPGODevirt(callee) {
|
|
return nil, nil, 0
|
|
}
|
|
// Bail if de-selected by PGO Hash.
|
|
if !base.PGOHash.MatchPosWithInfo(call.Pos(), "devirt", nil) {
|
|
return nil, nil, 0
|
|
}
|
|
|
|
return rewriteInterfaceCall(call, fn, callee, ctyp), callee, weight
|
|
}
|
|
|
|
// Devirtualize an indirect function call if possible and eligible. Returns the new
|
|
// ir.Node if call was devirtualized, and if so also the callee and weight of
|
|
// the devirtualized edge.
|
|
func maybeDevirtualizeFunctionCall(p *pgoir.Profile, fn *ir.Func, call *ir.CallExpr) (ir.Node, *ir.Func, int64) {
|
|
if base.Debug.PGODevirtualize < 2 {
|
|
return nil, nil, 0
|
|
}
|
|
|
|
// Bail if this is a direct call; no devirtualization necessary.
|
|
callee := pgoir.DirectCallee(call.Fun)
|
|
if callee != nil {
|
|
return nil, nil, 0
|
|
}
|
|
|
|
// Bail if we do not have a hot callee.
|
|
callee, weight := findHotConcreteFunctionCallee(p, fn, call)
|
|
if callee == nil {
|
|
return nil, nil, 0
|
|
}
|
|
|
|
// TODO(go.dev/issue/61577): Closures need the closure context passed
|
|
// via the context register. That requires extra plumbing that we
|
|
// haven't done yet.
|
|
if callee.OClosure != nil {
|
|
if base.Debug.PGODebug >= 3 {
|
|
fmt.Printf("callee %s is a closure, skipping\n", ir.FuncName(callee))
|
|
}
|
|
return nil, nil, 0
|
|
}
|
|
// runtime.memhash_varlen does not look like a closure, but it uses
|
|
// runtime.getclosureptr to access data encoded by callers, which are
|
|
// generated by cmd/compile/internal/reflectdata.genhash.
|
|
if callee.Sym().Pkg.Path == "runtime" && callee.Sym().Name == "memhash_varlen" {
|
|
if base.Debug.PGODebug >= 3 {
|
|
fmt.Printf("callee %s is a closure (runtime.memhash_varlen), skipping\n", ir.FuncName(callee))
|
|
}
|
|
return nil, nil, 0
|
|
}
|
|
// TODO(prattmic): We don't properly handle methods as callees in two
|
|
// different dimensions:
|
|
//
|
|
// 1. Method expressions. e.g.,
|
|
//
|
|
// var fn func(*os.File, []byte) (int, error) = (*os.File).Read
|
|
//
|
|
// In this case, typ will report *os.File as the receiver while
|
|
// ctyp reports it as the first argument. types.Identical ignores
|
|
// receiver parameters, so it treats these as different, even though
|
|
// they are still call compatible.
|
|
//
|
|
// 2. Method values. e.g.,
|
|
//
|
|
// var f *os.File
|
|
// var fn func([]byte) (int, error) = f.Read
|
|
//
|
|
// types.Identical will treat these as compatible (since receiver
|
|
// parameters are ignored). However, in this case, we do not call
|
|
// (*os.File).Read directly. Instead, f is stored in closure context
|
|
// and we call the wrapper (*os.File).Read-fm. However, runtime/pprof
|
|
// hides wrappers from profiles, making it appear that there is a call
|
|
// directly to the method. We could recognize this pattern return the
|
|
// wrapper rather than the method.
|
|
//
|
|
// N.B. perf profiles will report wrapper symbols directly, so
|
|
// ideally we should support direct wrapper references as well.
|
|
if callee.Type().Recv() != nil {
|
|
if base.Debug.PGODebug >= 3 {
|
|
fmt.Printf("callee %s is a method, skipping\n", ir.FuncName(callee))
|
|
}
|
|
return nil, nil, 0
|
|
}
|
|
|
|
// Bail if we know for sure it won't inline.
|
|
if !shouldPGODevirt(callee) {
|
|
return nil, nil, 0
|
|
}
|
|
// Bail if de-selected by PGO Hash.
|
|
if !base.PGOHash.MatchPosWithInfo(call.Pos(), "devirt", nil) {
|
|
return nil, nil, 0
|
|
}
|
|
|
|
return rewriteFunctionCall(call, fn, callee), callee, weight
|
|
}
|
|
|
|
// shouldPGODevirt checks if we should perform PGO devirtualization to the
|
|
// target function.
|
|
//
|
|
// PGO devirtualization is most valuable when the callee is inlined, so if it
|
|
// won't inline we can skip devirtualizing.
|
|
func shouldPGODevirt(fn *ir.Func) bool {
|
|
var reason string
|
|
if base.Flag.LowerM > 1 || logopt.Enabled() {
|
|
defer func() {
|
|
if reason != "" {
|
|
if base.Flag.LowerM > 1 {
|
|
fmt.Printf("%v: should not PGO devirtualize %v: %s\n", ir.Line(fn), ir.FuncName(fn), reason)
|
|
}
|
|
if logopt.Enabled() {
|
|
logopt.LogOpt(fn.Pos(), ": should not PGO devirtualize function", "pgoir-devirtualize", ir.FuncName(fn), reason)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
reason = inline.InlineImpossible(fn)
|
|
if reason != "" {
|
|
return false
|
|
}
|
|
|
|
// TODO(prattmic): checking only InlineImpossible is very conservative,
|
|
// primarily excluding only functions with pragmas. We probably want to
|
|
// move in either direction. Either:
|
|
//
|
|
// 1. Don't even bother to check InlineImpossible, as it affects so few
|
|
// functions.
|
|
//
|
|
// 2. Or consider the function body (notably cost) to better determine
|
|
// if the function will actually inline.
|
|
|
|
return true
|
|
}
|
|
|
|
// constructCallStat builds an initial CallStat describing this call, for
|
|
// logging. If the call is devirtualized, the devirtualization fields should be
|
|
// updated.
|
|
func constructCallStat(p *pgoir.Profile, fn *ir.Func, name string, call *ir.CallExpr) *CallStat {
|
|
switch call.Op() {
|
|
case ir.OCALLFUNC, ir.OCALLINTER, ir.OCALLMETH:
|
|
default:
|
|
// We don't care about logging builtin functions.
|
|
return nil
|
|
}
|
|
|
|
stat := CallStat{
|
|
Pkg: base.Ctxt.Pkgpath,
|
|
Pos: ir.Line(call),
|
|
Caller: name,
|
|
}
|
|
|
|
offset := pgoir.NodeLineOffset(call, fn)
|
|
|
|
hotter := func(e *pgoir.IREdge) bool {
|
|
if stat.Hottest == "" {
|
|
return true
|
|
}
|
|
if e.Weight != stat.HottestWeight {
|
|
return e.Weight > stat.HottestWeight
|
|
}
|
|
// If weight is the same, arbitrarily sort lexicographally, as
|
|
// findHotConcreteCallee does.
|
|
return e.Dst.Name() < stat.Hottest
|
|
}
|
|
|
|
callerNode := p.WeightedCG.IRNodes[name]
|
|
if callerNode == nil {
|
|
return nil
|
|
}
|
|
|
|
// Sum of all edges from this callsite, regardless of callee.
|
|
// For direct calls, this should be the same as the single edge
|
|
// weight (except for multiple calls on one line, which we
|
|
// can't distinguish).
|
|
for _, edge := range callerNode.OutEdges {
|
|
if edge.CallSiteOffset != offset {
|
|
continue
|
|
}
|
|
stat.Weight += edge.Weight
|
|
if hotter(edge) {
|
|
stat.HottestWeight = edge.Weight
|
|
stat.Hottest = edge.Dst.Name()
|
|
}
|
|
}
|
|
|
|
switch call.Op() {
|
|
case ir.OCALLFUNC:
|
|
stat.Interface = false
|
|
|
|
callee := pgoir.DirectCallee(call.Fun)
|
|
if callee != nil {
|
|
stat.Direct = true
|
|
if stat.Hottest == "" {
|
|
stat.Hottest = ir.LinkFuncName(callee)
|
|
}
|
|
} else {
|
|
stat.Direct = false
|
|
}
|
|
case ir.OCALLINTER:
|
|
stat.Direct = false
|
|
stat.Interface = true
|
|
case ir.OCALLMETH:
|
|
base.FatalfAt(call.Pos(), "OCALLMETH missed by typecheck")
|
|
}
|
|
|
|
return &stat
|
|
}
|
|
|
|
// copyInputs copies the inputs to a call: the receiver (for interface calls)
|
|
// or function value (for function value calls) and the arguments. These
|
|
// expressions are evaluated once and assigned to temporaries.
|
|
//
|
|
// The assignment statement is added to init and the copied receiver/fn
|
|
// expression and copied arguments expressions are returned.
|
|
func copyInputs(curfn *ir.Func, pos src.XPos, recvOrFn ir.Node, args []ir.Node, init *ir.Nodes) (ir.Node, []ir.Node) {
|
|
// Evaluate receiver/fn and argument expressions. The receiver/fn is
|
|
// used twice but we don't want to cause side effects twice. The
|
|
// arguments are used in two different calls and we can't trivially
|
|
// copy them.
|
|
//
|
|
// recvOrFn must be first in the assignment list as its side effects
|
|
// must be ordered before argument side effects.
|
|
var lhs, rhs []ir.Node
|
|
newRecvOrFn := typecheck.TempAt(pos, curfn, recvOrFn.Type())
|
|
lhs = append(lhs, newRecvOrFn)
|
|
rhs = append(rhs, recvOrFn)
|
|
|
|
for _, arg := range args {
|
|
argvar := typecheck.TempAt(pos, curfn, arg.Type())
|
|
|
|
lhs = append(lhs, argvar)
|
|
rhs = append(rhs, arg)
|
|
}
|
|
|
|
asList := ir.NewAssignListStmt(pos, ir.OAS2, lhs, rhs)
|
|
init.Append(typecheck.Stmt(asList))
|
|
|
|
return newRecvOrFn, lhs[1:]
|
|
}
|
|
|
|
// retTemps returns a slice of temporaries to be used for storing result values from call.
|
|
func retTemps(curfn *ir.Func, pos src.XPos, call *ir.CallExpr) []ir.Node {
|
|
sig := call.Fun.Type()
|
|
var retvars []ir.Node
|
|
for _, ret := range sig.Results() {
|
|
retvars = append(retvars, typecheck.TempAt(pos, curfn, ret.Type))
|
|
}
|
|
return retvars
|
|
}
|
|
|
|
// condCall returns an ir.InlinedCallExpr that performs a call to thenCall if
|
|
// cond is true and elseCall if cond is false. The return variables of the
|
|
// InlinedCallExpr evaluate to the return values from the call.
|
|
func condCall(curfn *ir.Func, pos src.XPos, cond ir.Node, thenCall, elseCall *ir.CallExpr, init ir.Nodes) *ir.InlinedCallExpr {
|
|
// Doesn't matter whether we use thenCall or elseCall, they must have
|
|
// the same return types.
|
|
retvars := retTemps(curfn, pos, thenCall)
|
|
|
|
var thenBlock, elseBlock ir.Nodes
|
|
if len(retvars) == 0 {
|
|
thenBlock.Append(thenCall)
|
|
elseBlock.Append(elseCall)
|
|
} else {
|
|
// Copy slice so edits in one location don't affect another.
|
|
thenRet := append([]ir.Node(nil), retvars...)
|
|
thenAsList := ir.NewAssignListStmt(pos, ir.OAS2, thenRet, []ir.Node{thenCall})
|
|
thenBlock.Append(typecheck.Stmt(thenAsList))
|
|
|
|
elseRet := append([]ir.Node(nil), retvars...)
|
|
elseAsList := ir.NewAssignListStmt(pos, ir.OAS2, elseRet, []ir.Node{elseCall})
|
|
elseBlock.Append(typecheck.Stmt(elseAsList))
|
|
}
|
|
|
|
nif := ir.NewIfStmt(pos, cond, thenBlock, elseBlock)
|
|
nif.SetInit(init)
|
|
nif.Likely = true
|
|
|
|
body := []ir.Node{typecheck.Stmt(nif)}
|
|
|
|
// This isn't really an inlined call of course, but InlinedCallExpr
|
|
// makes handling reassignment of return values easier.
|
|
res := ir.NewInlinedCallExpr(pos, body, retvars)
|
|
res.SetType(thenCall.Type())
|
|
res.SetTypecheck(1)
|
|
return res
|
|
}
|
|
|
|
// rewriteInterfaceCall devirtualizes the given interface call using a direct
|
|
// method call to concretetyp.
|
|
func rewriteInterfaceCall(call *ir.CallExpr, curfn, callee *ir.Func, concretetyp *types.Type) ir.Node {
|
|
if base.Flag.LowerM != 0 {
|
|
fmt.Printf("%v: PGO devirtualizing interface call %v to %v\n", ir.Line(call), call.Fun, callee)
|
|
}
|
|
|
|
// We generate an OINCALL of:
|
|
//
|
|
// var recv Iface
|
|
//
|
|
// var arg1 A1
|
|
// var argN AN
|
|
//
|
|
// var ret1 R1
|
|
// var retN RN
|
|
//
|
|
// recv, arg1, argN = recv expr, arg1 expr, argN expr
|
|
//
|
|
// t, ok := recv.(Concrete)
|
|
// if ok {
|
|
// ret1, retN = t.Method(arg1, ... argN)
|
|
// } else {
|
|
// ret1, retN = recv.Method(arg1, ... argN)
|
|
// }
|
|
//
|
|
// OINCALL retvars: ret1, ... retN
|
|
//
|
|
// This isn't really an inlined call of course, but InlinedCallExpr
|
|
// makes handling reassignment of return values easier.
|
|
//
|
|
// TODO(prattmic): This increases the size of the AST in the caller,
|
|
// making it less like to inline. We may want to compensate for this
|
|
// somehow.
|
|
|
|
sel := call.Fun.(*ir.SelectorExpr)
|
|
method := sel.Sel
|
|
pos := call.Pos()
|
|
init := ir.TakeInit(call)
|
|
|
|
recv, args := copyInputs(curfn, pos, sel.X, call.Args.Take(), &init)
|
|
|
|
// Copy slice so edits in one location don't affect another.
|
|
argvars := append([]ir.Node(nil), args...)
|
|
call.Args = argvars
|
|
|
|
tmpnode := typecheck.TempAt(base.Pos, curfn, concretetyp)
|
|
tmpok := typecheck.TempAt(base.Pos, curfn, types.Types[types.TBOOL])
|
|
|
|
assert := ir.NewTypeAssertExpr(pos, recv, concretetyp)
|
|
|
|
assertAsList := ir.NewAssignListStmt(pos, ir.OAS2, []ir.Node{tmpnode, tmpok}, []ir.Node{typecheck.Expr(assert)})
|
|
init.Append(typecheck.Stmt(assertAsList))
|
|
|
|
concreteCallee := typecheck.XDotMethod(pos, tmpnode, method, true)
|
|
// Copy slice so edits in one location don't affect another.
|
|
argvars = append([]ir.Node(nil), argvars...)
|
|
concreteCall := typecheck.Call(pos, concreteCallee, argvars, call.IsDDD).(*ir.CallExpr)
|
|
|
|
res := condCall(curfn, pos, tmpok, concreteCall, call, init)
|
|
|
|
if base.Debug.PGODebug >= 3 {
|
|
fmt.Printf("PGO devirtualizing interface call to %+v. After: %+v\n", concretetyp, res)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
// rewriteFunctionCall devirtualizes the given OCALLFUNC using a direct
|
|
// function call to callee.
|
|
func rewriteFunctionCall(call *ir.CallExpr, curfn, callee *ir.Func) ir.Node {
|
|
if base.Flag.LowerM != 0 {
|
|
fmt.Printf("%v: PGO devirtualizing function call %v to %v\n", ir.Line(call), call.Fun, callee)
|
|
}
|
|
|
|
// We generate an OINCALL of:
|
|
//
|
|
// var fn FuncType
|
|
//
|
|
// var arg1 A1
|
|
// var argN AN
|
|
//
|
|
// var ret1 R1
|
|
// var retN RN
|
|
//
|
|
// fn, arg1, argN = fn expr, arg1 expr, argN expr
|
|
//
|
|
// fnPC := internal/abi.FuncPCABIInternal(fn)
|
|
// concretePC := internal/abi.FuncPCABIInternal(concrete)
|
|
//
|
|
// if fnPC == concretePC {
|
|
// ret1, retN = concrete(arg1, ... argN) // Same closure context passed (TODO)
|
|
// } else {
|
|
// ret1, retN = fn(arg1, ... argN)
|
|
// }
|
|
//
|
|
// OINCALL retvars: ret1, ... retN
|
|
//
|
|
// This isn't really an inlined call of course, but InlinedCallExpr
|
|
// makes handling reassignment of return values easier.
|
|
|
|
pos := call.Pos()
|
|
init := ir.TakeInit(call)
|
|
|
|
fn, args := copyInputs(curfn, pos, call.Fun, call.Args.Take(), &init)
|
|
|
|
// Copy slice so edits in one location don't affect another.
|
|
argvars := append([]ir.Node(nil), args...)
|
|
call.Args = argvars
|
|
|
|
// FuncPCABIInternal takes an interface{}, emulate that. This is needed
|
|
// for to ensure we get the MAKEFACE we need for SSA.
|
|
fnIface := typecheck.Expr(ir.NewConvExpr(pos, ir.OCONV, types.Types[types.TINTER], fn))
|
|
calleeIface := typecheck.Expr(ir.NewConvExpr(pos, ir.OCONV, types.Types[types.TINTER], callee.Nname))
|
|
|
|
fnPC := ir.FuncPC(pos, fnIface, obj.ABIInternal)
|
|
concretePC := ir.FuncPC(pos, calleeIface, obj.ABIInternal)
|
|
|
|
pcEq := typecheck.Expr(ir.NewBinaryExpr(base.Pos, ir.OEQ, fnPC, concretePC))
|
|
|
|
// TODO(go.dev/issue/61577): Handle callees that a closures and need a
|
|
// copy of the closure context from call. For now, we skip callees that
|
|
// are closures in maybeDevirtualizeFunctionCall.
|
|
if callee.OClosure != nil {
|
|
base.Fatalf("Callee is a closure: %+v", callee)
|
|
}
|
|
|
|
// Copy slice so edits in one location don't affect another.
|
|
argvars = append([]ir.Node(nil), argvars...)
|
|
concreteCall := typecheck.Call(pos, callee.Nname, argvars, call.IsDDD).(*ir.CallExpr)
|
|
|
|
res := condCall(curfn, pos, pcEq, concreteCall, call, init)
|
|
|
|
if base.Debug.PGODebug >= 3 {
|
|
fmt.Printf("PGO devirtualizing function call to %+v. After: %+v\n", ir.FuncName(callee), res)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
// methodRecvType returns the type containing method fn. Returns nil if fn
|
|
// is not a method.
|
|
func methodRecvType(fn *ir.Func) *types.Type {
|
|
recv := fn.Nname.Type().Recv()
|
|
if recv == nil {
|
|
return nil
|
|
}
|
|
return recv.Type
|
|
}
|
|
|
|
// interfaceCallRecvTypeAndMethod returns the type and the method of the interface
|
|
// used in an interface call.
|
|
func interfaceCallRecvTypeAndMethod(call *ir.CallExpr) (*types.Type, *types.Sym) {
|
|
if call.Op() != ir.OCALLINTER {
|
|
base.Fatalf("Call isn't OCALLINTER: %+v", call)
|
|
}
|
|
|
|
sel, ok := call.Fun.(*ir.SelectorExpr)
|
|
if !ok {
|
|
base.Fatalf("OCALLINTER doesn't contain SelectorExpr: %+v", call)
|
|
}
|
|
|
|
return sel.X.Type(), sel.Sel
|
|
}
|
|
|
|
// findHotConcreteCallee returns the *ir.Func of the hottest callee of a call,
|
|
// if available, and its edge weight. extraFn can perform additional
|
|
// applicability checks on each candidate edge. If extraFn returns false,
|
|
// candidate will not be considered a valid callee candidate.
|
|
func findHotConcreteCallee(p *pgoir.Profile, caller *ir.Func, call *ir.CallExpr, extraFn func(callerName string, callOffset int, candidate *pgoir.IREdge) bool) (*ir.Func, int64) {
|
|
callerName := ir.LinkFuncName(caller)
|
|
callerNode := p.WeightedCG.IRNodes[callerName]
|
|
callOffset := pgoir.NodeLineOffset(call, caller)
|
|
|
|
if callerNode == nil {
|
|
return nil, 0
|
|
}
|
|
|
|
var hottest *pgoir.IREdge
|
|
|
|
// Returns true if e is hotter than hottest.
|
|
//
|
|
// Naively this is just e.Weight > hottest.Weight, but because OutEdges
|
|
// has arbitrary iteration order, we need to apply additional sort
|
|
// criteria when e.Weight == hottest.Weight to ensure we have stable
|
|
// selection.
|
|
hotter := func(e *pgoir.IREdge) bool {
|
|
if hottest == nil {
|
|
return true
|
|
}
|
|
if e.Weight != hottest.Weight {
|
|
return e.Weight > hottest.Weight
|
|
}
|
|
|
|
// Now e.Weight == hottest.Weight, we must select on other
|
|
// criteria.
|
|
|
|
// If only one edge has IR, prefer that one.
|
|
if (hottest.Dst.AST == nil) != (e.Dst.AST == nil) {
|
|
if e.Dst.AST != nil {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Arbitrary, but the callee names will always differ. Select
|
|
// the lexicographically first callee.
|
|
return e.Dst.Name() < hottest.Dst.Name()
|
|
}
|
|
|
|
for _, e := range callerNode.OutEdges {
|
|
if e.CallSiteOffset != callOffset {
|
|
continue
|
|
}
|
|
|
|
if !hotter(e) {
|
|
// TODO(prattmic): consider total caller weight? i.e.,
|
|
// if the hottest callee is only 10% of the weight,
|
|
// maybe don't devirtualize? Similarly, if this is call
|
|
// is globally very cold, there is not much value in
|
|
// devirtualizing.
|
|
if base.Debug.PGODebug >= 2 {
|
|
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)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if e.Dst.AST == nil {
|
|
// Destination isn't visible from this package
|
|
// compilation.
|
|
//
|
|
// We must assume it implements the interface.
|
|
//
|
|
// We still record this as the hottest callee so far
|
|
// because we only want to return the #1 hottest
|
|
// callee. If we skip this then we'd return the #2
|
|
// hottest callee.
|
|
if base.Debug.PGODebug >= 2 {
|
|
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)
|
|
}
|
|
hottest = e
|
|
continue
|
|
}
|
|
|
|
if extraFn != nil && !extraFn(callerName, callOffset, e) {
|
|
continue
|
|
}
|
|
|
|
if base.Debug.PGODebug >= 2 {
|
|
fmt.Printf("%v: edge %s:%d -> %s (weight %d): hottest so far\n", ir.Line(call), callerName, callOffset, e.Dst.Name(), e.Weight)
|
|
}
|
|
hottest = e
|
|
}
|
|
|
|
if hottest == nil {
|
|
if base.Debug.PGODebug >= 2 {
|
|
fmt.Printf("%v: call %s:%d: no hot callee\n", ir.Line(call), callerName, callOffset)
|
|
}
|
|
return nil, 0
|
|
}
|
|
|
|
if base.Debug.PGODebug >= 2 {
|
|
fmt.Printf("%v: call %s:%d: hottest callee %s (weight %d)\n", ir.Line(call), callerName, callOffset, hottest.Dst.Name(), hottest.Weight)
|
|
}
|
|
return hottest.Dst.AST, hottest.Weight
|
|
}
|
|
|
|
// findHotConcreteInterfaceCallee returns the *ir.Func of the hottest callee of an
|
|
// interface call, if available, and its edge weight.
|
|
func findHotConcreteInterfaceCallee(p *pgoir.Profile, caller *ir.Func, call *ir.CallExpr) (*ir.Func, int64) {
|
|
inter, method := interfaceCallRecvTypeAndMethod(call)
|
|
|
|
return findHotConcreteCallee(p, caller, call, func(callerName string, callOffset int, e *pgoir.IREdge) bool {
|
|
ctyp := methodRecvType(e.Dst.AST)
|
|
if ctyp == nil {
|
|
// Not a method.
|
|
// TODO(prattmic): Support non-interface indirect calls.
|
|
if base.Debug.PGODebug >= 2 {
|
|
fmt.Printf("%v: edge %s:%d -> %s (weight %d): callee not a method\n", ir.Line(call), callerName, callOffset, e.Dst.Name(), e.Weight)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// If ctyp doesn't implement inter it is most likely from a
|
|
// different call on the same line
|
|
if !typecheck.Implements(ctyp, inter) {
|
|
// TODO(prattmic): this is overly strict. Consider if
|
|
// ctyp is a partial implementation of an interface
|
|
// that gets embedded in types that complete the
|
|
// interface. It would still be OK to devirtualize a
|
|
// call to this method.
|
|
//
|
|
// What we'd need to do is check that the function
|
|
// pointer in the itab matches the method we want,
|
|
// rather than doing a full type assertion.
|
|
if base.Debug.PGODebug >= 2 {
|
|
why := typecheck.ImplementsExplain(ctyp, inter)
|
|
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)
|
|
}
|
|
return false
|
|
}
|
|
|
|
// If the method name is different it is most likely from a
|
|
// different call on the same line
|
|
if !strings.HasSuffix(e.Dst.Name(), "."+method.Name) {
|
|
if base.Debug.PGODebug >= 2 {
|
|
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)
|
|
}
|
|
return false
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
|
|
// findHotConcreteFunctionCallee returns the *ir.Func of the hottest callee of an
|
|
// indirect function call, if available, and its edge weight.
|
|
func findHotConcreteFunctionCallee(p *pgoir.Profile, caller *ir.Func, call *ir.CallExpr) (*ir.Func, int64) {
|
|
typ := call.Fun.Type().Underlying()
|
|
|
|
return findHotConcreteCallee(p, caller, call, func(callerName string, callOffset int, e *pgoir.IREdge) bool {
|
|
ctyp := e.Dst.AST.Type().Underlying()
|
|
|
|
// If ctyp doesn't match typ it is most likely from a different
|
|
// call on the same line.
|
|
//
|
|
// Note that we are comparing underlying types, as different
|
|
// defined types are OK. e.g., a call to a value of type
|
|
// net/http.HandlerFunc can be devirtualized to a function with
|
|
// the same underlying type.
|
|
if !types.Identical(typ, ctyp) {
|
|
if base.Debug.PGODebug >= 2 {
|
|
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)
|
|
}
|
|
return false
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|