blob: 0a43135420b3993c4ca58b91c8af53e2fdf7e2ed [file] [log] [blame]
// 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
// 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
}
// 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).
callerNode := p.WeightedCG.IRNodes[name]
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)
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
})
}