Current Path : /usr/local/go/src/cmd/compile/internal/devirtualize/ |
Current File : //usr/local/go/src/cmd/compile/internal/devirtualize/pgo.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/pgo" "cmd/compile/internal/typecheck" "cmd/compile/internal/types" "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 // for the hottest callee. That is, 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() // } // } // // The primary benefit of this transformation is enabling inlining of the // direct call. func ProfileGuided(fn *ir.Func, p *pgo.Profile) { ir.CurFunc = fn name := ir.LinkFuncName(fn) // Can't devirtualize go/defer calls. See comment in Static. goDeferCall := make(map[*ir.CallExpr]bool) 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 } if gds, ok := n.(*ir.GoDeferStmt); ok { if call, ok := gds.Call.(*ir.CallExpr); ok { goDeferCall[call] = true } } 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) }() } } if call.Op() != ir.OCALLINTER { return n } if base.Debug.PGODebug >= 2 { fmt.Printf("%v: PGO devirtualize considering call %v\n", ir.Line(call), call) } if goDeferCall[call] { if base.Debug.PGODebug >= 2 { fmt.Printf("%v: can't PGO devirtualize go/defer call %v\n", ir.Line(call), call) } return n } // Bail if we do not have a hot callee. callee, weight := findHotConcreteCallee(p, fn, call) if callee == nil { return n } // Bail if we do not have a Type node for the hot callee. ctyp := methodRecvType(callee) if ctyp == nil { return n } // Bail if we know for sure it won't inline. if !shouldPGODevirt(callee) { return n } if stat != nil { stat.Devirtualized = ir.LinkFuncName(callee) stat.DevirtualizedWeight = weight } return rewriteCondCall(call, fn, callee, ctyp) } ir.EditChildren(fn, edit) } // 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", "pgo-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 *pgo.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 := pgo.NodeLineOffset(call, fn) // 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 edge.Weight > stat.HottestWeight { stat.HottestWeight = edge.Weight stat.Hottest = edge.Dst.Name() } } switch call.Op() { case ir.OCALLFUNC: stat.Interface = false callee := pgo.DirectCallee(call.X) 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 } // rewriteCondCall devirtualizes the given call using a direct method call to // concretetyp. func rewriteCondCall(call *ir.CallExpr, curfn, callee *ir.Func, concretetyp *types.Type) ir.Node { if base.Flag.LowerM != 0 { fmt.Printf("%v: PGO devirtualizing %v to %v\n", ir.Line(call), call.X, 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. var retvars []ir.Node sig := call.X.Type() for _, ret := range sig.Results().FieldSlice() { retvars = append(retvars, typecheck.Temp(ret.Type)) } sel := call.X.(*ir.SelectorExpr) method := sel.Sel pos := call.Pos() init := ir.TakeInit(call) // Evaluate receiver and argument expressions. The receiver 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. // // recv must be first in the assignment list as its side effects must // be ordered before argument side effects. var lhs, rhs []ir.Node recv := typecheck.Temp(sel.X.Type()) lhs = append(lhs, recv) rhs = append(rhs, sel.X) // Move arguments to assignments prior to the if statement. We cannot // simply copy the args' IR, as some IR constructs cannot be copied, // such as labels (possible in InlinedCall nodes). args := call.Args.Take() for _, arg := range args { argvar := typecheck.Temp(arg.Type()) lhs = append(lhs, argvar) rhs = append(rhs, arg) } asList := ir.NewAssignListStmt(pos, ir.OAS2, lhs, rhs) init.Append(typecheck.Stmt(asList)) // Copy slice so edits in one location don't affect another. argvars := append([]ir.Node(nil), lhs[1:]...) call.Args = argvars tmpnode := typecheck.Temp(concretetyp) tmpok := typecheck.Temp(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.Callee(ir.NewSelectorExpr(pos, ir.OXDOT, tmpnode, method)) // 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) var thenBlock, elseBlock ir.Nodes if len(retvars) == 0 { thenBlock.Append(concreteCall) elseBlock.Append(call) } 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{concreteCall}) thenBlock.Append(typecheck.Stmt(thenAsList)) elseRet := append([]ir.Node(nil), retvars...) elseAsList := ir.NewAssignListStmt(pos, ir.OAS2, elseRet, []ir.Node{call}) elseBlock.Append(typecheck.Stmt(elseAsList)) } cond := ir.NewIfStmt(pos, nil, nil, nil) cond.SetInit(init) cond.Cond = tmpok cond.Body = thenBlock cond.Else = elseBlock cond.Likely = true body := []ir.Node{typecheck.Stmt(cond)} res := ir.NewInlinedCallExpr(pos, body, retvars) res.SetType(call.Type()) res.SetTypecheck(1) if base.Debug.PGODebug >= 3 { fmt.Printf("PGO devirtualizing call to %+v. After: %+v\n", concretetyp, 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.X.(*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 an // indirect call, if available, and its edge weight. func findHotConcreteCallee(p *pgo.Profile, caller *ir.Func, call *ir.CallExpr) (*ir.Func, int64) { callerName := ir.LinkFuncName(caller) callerNode := p.WeightedCG.IRNodes[callerName] callOffset := pgo.NodeLineOffset(call, caller) inter, method := interfaceCallRecvTypeAndMethod(call) var hottest *pgo.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 *pgo.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 hottest.Dst.AST == nil && e.Dst.AST != nil { // Prefer the edge with IR available. return true } // 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 } 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) } continue } // 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) } continue } // 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) } 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 }