src/cmd/compile/internal/devirtualize/pgo.go GO 831 lines View on github.com → Search inside
1// Copyright 2023 The Go Authors. All rights reserved.2// Use of this source code is governed by a BSD-style3// license that can be found in the LICENSE file.45package devirtualize67import (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)2223// CallStat summarizes a single call site.24//25// This is used only for debug logging.26type CallStat struct {27	Pkg string // base.Ctxt.Pkgpath28	Pos string // file:line:col of call.2930	Caller string // Linker symbol name of calling function.3132	// Direct or indirect call.33	Direct bool3435	// For indirect calls, interface call or other indirect function call.36	Interface bool3738	// Total edge weight from this call site.39	Weight int644041	// Hottest callee from this call site, regardless of type42	// compatibility.43	Hottest       string44	HottestWeight int644546	// Devirtualized callee if != "".47	//48	// Note that this may be different than Hottest because we apply49	// type-check restrictions, which helps distinguish multiple calls on50	// the same line.51	Devirtualized       string52	DevirtualizedWeight int6453}5455// ProfileGuided performs call devirtualization of indirect calls based on56// profile information.57//58// Specifically, it performs conditional devirtualization of interface calls or59// 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 the104// direct call.105func ProfileGuided(fn *ir.Func, p *pgoir.Profile) {106	ir.CurFunc = fn107108	name := ir.LinkFuncName(fn)109110	var jsonW *json.Encoder111	if base.Debug.PGODebug >= 3 {112		jsonW = json.NewEncoder(os.Stdout)113	}114115	var edit func(n ir.Node) ir.Node116	edit = func(n ir.Node) ir.Node {117		if n == nil {118			return n119		}120121		ir.EditChildren(n, edit)122123		call, ok := n.(*ir.CallExpr)124		if !ok {125			return n126		}127128		var stat *CallStat129		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		}140141		op := call.Op()142		if op != ir.OCALLFUNC && op != ir.OCALLINTER {143			return n144		}145146		if base.Debug.PGODebug >= 2 {147			fmt.Printf("%v: PGO devirtualize considering call %v\n", ir.Line(call), call)148		}149150		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 n155		}156157		var newNode ir.Node158		var callee *ir.Func159		var weight int64160		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		}168169		if newNode == nil {170			return n171		}172173		if stat != nil {174			stat.Devirtualized = ir.LinkFuncName(callee)175			stat.DevirtualizedWeight = weight176		}177178		return newNode179	}180181	ir.EditChildren(fn, edit)182}183184// Devirtualize interface call if possible and eligible. Returns the new185// ir.Node if call was devirtualized, and if so also the callee and weight of186// the devirtualized edge.187func 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, 0190	}191192	// Bail if we do not have a hot callee.193	callee, weight := findHotConcreteInterfaceCallee(p, fn, call)194	if callee == nil {195		return nil, nil, 0196	}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, 0201	}202	// Bail if we know for sure it won't inline.203	if !shouldPGODevirt(callee) {204		return nil, nil, 0205	}206	// Bail if de-selected by PGO Hash.207	if !base.PGOHash.MatchPosWithInfo(call.Pos(), "devirt", nil) {208		return nil, nil, 0209	}210211	return rewriteInterfaceCall(call, fn, callee, ctyp), callee, weight212}213214// Devirtualize an indirect function call if possible and eligible. Returns the new215// ir.Node if call was devirtualized, and if so also the callee and weight of216// the devirtualized edge.217func 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, 0220	}221222	// Bail if this is a direct call; no devirtualization necessary.223	callee := pgoir.DirectCallee(call.Fun)224	if callee != nil {225		return nil, nil, 0226	}227228	// Bail if we do not have a hot callee.229	callee, weight := findHotConcreteFunctionCallee(p, fn, call)230	if callee == nil {231		return nil, nil, 0232	}233234	// TODO(go.dev/issue/61577): Closures need the closure context passed235	// via the context register. That requires extra plumbing that we236	// 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, 0242	}243	// runtime.memhash_varlen does not look like a closure, but it uses244	// internal/runtime/sys.GetClosurePtr to access data encoded by245	// callers, which are generated by246	// cmd/compile/internal/reflectdata.genhash.247	if callee.Sym().Pkg.Path == "runtime" && callee.Sym().Name == "memhash_varlen" {248		if base.Debug.PGODebug >= 3 {249			fmt.Printf("callee %s is a closure (runtime.memhash_varlen), skipping\n", ir.FuncName(callee))250		}251		return nil, nil, 0252	}253	// TODO(prattmic): We don't properly handle methods as callees in two254	// different dimensions:255	//256	// 1. Method expressions. e.g.,257	//258	//      var fn func(*os.File, []byte) (int, error) = (*os.File).Read259	//260	// In this case, typ will report *os.File as the receiver while261	// ctyp reports it as the first argument. types.Identical ignores262	// receiver parameters, so it treats these as different, even though263	// they are still call compatible.264	//265	// 2. Method values. e.g.,266	//267	//      var f *os.File268	//      var fn func([]byte) (int, error) = f.Read269	//270	// types.Identical will treat these as compatible (since receiver271	// parameters are ignored). However, in this case, we do not call272	// (*os.File).Read directly. Instead, f is stored in closure context273	// and we call the wrapper (*os.File).Read-fm. However, runtime/pprof274	// hides wrappers from profiles, making it appear that there is a call275	// directly to the method. We could recognize this pattern return the276	// wrapper rather than the method.277	//278	// N.B. perf profiles will report wrapper symbols directly, so279	// ideally we should support direct wrapper references as well.280	if callee.Type().Recv() != nil {281		if base.Debug.PGODebug >= 3 {282			fmt.Printf("callee %s is a method, skipping\n", ir.FuncName(callee))283		}284		return nil, nil, 0285	}286287	// Bail if we know for sure it won't inline.288	if !shouldPGODevirt(callee) {289		return nil, nil, 0290	}291	// Bail if de-selected by PGO Hash.292	if !base.PGOHash.MatchPosWithInfo(call.Pos(), "devirt", nil) {293		return nil, nil, 0294	}295296	return rewriteFunctionCall(call, fn, callee), callee, weight297}298299// shouldPGODevirt checks if we should perform PGO devirtualization to the300// target function.301//302// PGO devirtualization is most valuable when the callee is inlined, so if it303// won't inline we can skip devirtualizing.304func shouldPGODevirt(fn *ir.Func) bool {305	var reason string306	if base.Flag.LowerM > 1 || logopt.Enabled() {307		defer func() {308			if reason != "" {309				if base.Flag.LowerM > 1 {310					fmt.Printf("%v: should not PGO devirtualize %v: %s\n", ir.Line(fn), ir.FuncName(fn), reason)311				}312				if logopt.Enabled() {313					logopt.LogOpt(fn.Pos(), ": should not PGO devirtualize function", "pgoir-devirtualize", ir.FuncName(fn), reason)314				}315			}316		}()317	}318319	reason = inline.InlineImpossible(fn)320	if reason != "" {321		return false322	}323324	// TODO(prattmic): checking only InlineImpossible is very conservative,325	// primarily excluding only functions with pragmas. We probably want to326	// move in either direction. Either:327	//328	// 1. Don't even bother to check InlineImpossible, as it affects so few329	// functions.330	//331	// 2. Or consider the function body (notably cost) to better determine332	// if the function will actually inline.333334	return true335}336337// constructCallStat builds an initial CallStat describing this call, for338// logging. If the call is devirtualized, the devirtualization fields should be339// updated.340func constructCallStat(p *pgoir.Profile, fn *ir.Func, name string, call *ir.CallExpr) *CallStat {341	switch call.Op() {342	case ir.OCALLFUNC, ir.OCALLINTER, ir.OCALLMETH:343	default:344		// We don't care about logging builtin functions.345		return nil346	}347348	stat := CallStat{349		Pkg:    base.Ctxt.Pkgpath,350		Pos:    ir.Line(call),351		Caller: name,352	}353354	offset := pgoir.NodeLineOffset(call, fn)355356	hotter := func(e *pgoir.IREdge) bool {357		if stat.Hottest == "" {358			return true359		}360		if e.Weight != stat.HottestWeight {361			return e.Weight > stat.HottestWeight362		}363		// If weight is the same, arbitrarily sort lexicographally, as364		// findHotConcreteCallee does.365		return e.Dst.Name() < stat.Hottest366	}367368	callerNode := p.WeightedCG.IRNodes[name]369	if callerNode == nil {370		return nil371	}372373	// Sum of all edges from this callsite, regardless of callee.374	// For direct calls, this should be the same as the single edge375	// weight (except for multiple calls on one line, which we376	// can't distinguish).377	for _, edge := range callerNode.OutEdges {378		if edge.CallSiteOffset != offset {379			continue380		}381		stat.Weight += edge.Weight382		if hotter(edge) {383			stat.HottestWeight = edge.Weight384			stat.Hottest = edge.Dst.Name()385		}386	}387388	switch call.Op() {389	case ir.OCALLFUNC:390		stat.Interface = false391392		callee := pgoir.DirectCallee(call.Fun)393		if callee != nil {394			stat.Direct = true395			if stat.Hottest == "" {396				stat.Hottest = ir.LinkFuncName(callee)397			}398		} else {399			stat.Direct = false400		}401	case ir.OCALLINTER:402		stat.Direct = false403		stat.Interface = true404	case ir.OCALLMETH:405		base.FatalfAt(call.Pos(), "OCALLMETH missed by typecheck")406	}407408	return &stat409}410411// copyInputs copies the inputs to a call: the receiver (for interface calls)412// or function value (for function value calls) and the arguments. These413// expressions are evaluated once and assigned to temporaries.414//415// The assignment statement is added to init and the copied receiver/fn416// expression and copied arguments expressions are returned.417func copyInputs(curfn *ir.Func, pos src.XPos, recvOrFn ir.Node, args []ir.Node, init *ir.Nodes) (ir.Node, []ir.Node) {418	// Evaluate receiver/fn and argument expressions. The receiver/fn is419	// used twice but we don't want to cause side effects twice. The420	// arguments are used in two different calls and we can't trivially421	// copy them.422	//423	// recvOrFn must be first in the assignment list as its side effects424	// must be ordered before argument side effects.425	var lhs, rhs []ir.Node426	newRecvOrFn := typecheck.TempAt(pos, curfn, recvOrFn.Type())427	lhs = append(lhs, newRecvOrFn)428	rhs = append(rhs, recvOrFn)429430	for _, arg := range args {431		argvar := typecheck.TempAt(pos, curfn, arg.Type())432433		lhs = append(lhs, argvar)434		rhs = append(rhs, arg)435	}436437	asList := ir.NewAssignListStmt(pos, ir.OAS2, lhs, rhs)438	init.Append(typecheck.Stmt(asList))439440	return newRecvOrFn, lhs[1:]441}442443// retTemps returns a slice of temporaries to be used for storing result values from call.444func retTemps(curfn *ir.Func, pos src.XPos, call *ir.CallExpr) []ir.Node {445	sig := call.Fun.Type()446	var retvars []ir.Node447	for _, ret := range sig.Results() {448		retvars = append(retvars, typecheck.TempAt(pos, curfn, ret.Type))449	}450	return retvars451}452453// condCall returns an ir.InlinedCallExpr that performs a call to thenCall if454// cond is true and elseCall if cond is false. The return variables of the455// InlinedCallExpr evaluate to the return values from the call.456func condCall(curfn *ir.Func, pos src.XPos, cond ir.Node, thenCall, elseCall *ir.CallExpr, init ir.Nodes) *ir.InlinedCallExpr {457	// Doesn't matter whether we use thenCall or elseCall, they must have458	// the same return types.459	retvars := retTemps(curfn, pos, thenCall)460461	var thenBlock, elseBlock ir.Nodes462	if len(retvars) == 0 {463		thenBlock.Append(thenCall)464		elseBlock.Append(elseCall)465	} else {466		// Copy slice so edits in one location don't affect another.467		thenRet := append([]ir.Node(nil), retvars...)468		thenAsList := ir.NewAssignListStmt(pos, ir.OAS2, thenRet, []ir.Node{thenCall})469		thenBlock.Append(typecheck.Stmt(thenAsList))470471		elseRet := append([]ir.Node(nil), retvars...)472		elseAsList := ir.NewAssignListStmt(pos, ir.OAS2, elseRet, []ir.Node{elseCall})473		elseBlock.Append(typecheck.Stmt(elseAsList))474	}475476	nif := ir.NewIfStmt(pos, cond, thenBlock, elseBlock)477	nif.SetInit(init)478	nif.Likely = true479480	body := []ir.Node{typecheck.Stmt(nif)}481482	// This isn't really an inlined call of course, but InlinedCallExpr483	// makes handling reassignment of return values easier.484	res := ir.NewInlinedCallExpr(pos, body, retvars)485	res.SetType(thenCall.Type())486	res.SetTypecheck(1)487	res.Reshape = thenCall.Reshape488	return res489}490491// rewriteInterfaceCall devirtualizes the given interface call using a direct492// method call to concretetyp.493func rewriteInterfaceCall(call *ir.CallExpr, curfn, callee *ir.Func, concretetyp *types.Type) ir.Node {494	if base.Flag.LowerM != 0 {495		fmt.Printf("%v: PGO devirtualizing interface call %v to %v\n", ir.Line(call), call.Fun, callee)496	}497498	// We generate an OINCALL of:499	//500	// var recv Iface501	//502	// var arg1 A1503	// var argN AN504	//505	// var ret1 R1506	// var retN RN507	//508	// recv, arg1, argN = recv expr, arg1 expr, argN expr509	//510	// t, ok := recv.(Concrete)511	// if ok {512	//   ret1, retN = t.Method(arg1, ... argN)513	// } else {514	//   ret1, retN = recv.Method(arg1, ... argN)515	// }516	//517	// OINCALL retvars: ret1, ... retN518	//519	// This isn't really an inlined call of course, but InlinedCallExpr520	// makes handling reassignment of return values easier.521	//522	// TODO(prattmic): This increases the size of the AST in the caller,523	// making it less like to inline. We may want to compensate for this524	// somehow.525526	sel := call.Fun.(*ir.SelectorExpr)527	method := sel.Sel528	pos := call.Pos()529	init := ir.TakeInit(call)530531	recv, args := copyInputs(curfn, pos, sel.X, call.Args.Take(), &init)532533	// Copy slice so edits in one location don't affect another.534	argvars := append([]ir.Node(nil), args...)535	call.Args = argvars536537	tmpnode := typecheck.TempAt(base.Pos, curfn, concretetyp)538	tmpok := typecheck.TempAt(base.Pos, curfn, types.Types[types.TBOOL])539540	assert := ir.NewTypeAssertExpr(pos, recv, concretetyp)541542	assertAsList := ir.NewAssignListStmt(pos, ir.OAS2, []ir.Node{tmpnode, tmpok}, []ir.Node{typecheck.Expr(assert)})543	init.Append(typecheck.Stmt(assertAsList))544545	concreteCallee := typecheck.XDotMethod(pos, tmpnode, method, true)546	// Copy slice so edits in one location don't affect another.547	argvars = append([]ir.Node(nil), argvars...)548	concreteCall := typecheck.Call(pos, concreteCallee, argvars, call.IsDDD).(*ir.CallExpr)549550	res := condCall(curfn, pos, tmpok, concreteCall, call, init)551552	if base.Debug.PGODebug >= 3 {553		fmt.Printf("PGO devirtualizing interface call to %+v. After: %+v\n", concretetyp, res)554	}555556	return res557}558559// rewriteFunctionCall devirtualizes the given OCALLFUNC using a direct560// function call to callee.561func rewriteFunctionCall(call *ir.CallExpr, curfn, callee *ir.Func) ir.Node {562	if base.Flag.LowerM != 0 {563		fmt.Printf("%v: PGO devirtualizing function call %v to %v\n", ir.Line(call), call.Fun, callee)564	}565566	// We generate an OINCALL of:567	//568	// var fn FuncType569	//570	// var arg1 A1571	// var argN AN572	//573	// var ret1 R1574	// var retN RN575	//576	// fn, arg1, argN = fn expr, arg1 expr, argN expr577	//578	// fnPC := internal/abi.FuncPCABIInternal(fn)579	// concretePC := internal/abi.FuncPCABIInternal(concrete)580	//581	// if fnPC == concretePC {582	//   ret1, retN = concrete(arg1, ... argN) // Same closure context passed (TODO)583	// } else {584	//   ret1, retN = fn(arg1, ... argN)585	// }586	//587	// OINCALL retvars: ret1, ... retN588	//589	// This isn't really an inlined call of course, but InlinedCallExpr590	// makes handling reassignment of return values easier.591592	pos := call.Pos()593	init := ir.TakeInit(call)594595	fn, args := copyInputs(curfn, pos, call.Fun, call.Args.Take(), &init)596597	// Copy slice so edits in one location don't affect another.598	argvars := append([]ir.Node(nil), args...)599	call.Args = argvars600601	// FuncPCABIInternal takes an interface{}, emulate that. This is needed602	// for to ensure we get the MAKEFACE we need for SSA.603	fnIface := typecheck.Expr(ir.NewConvExpr(pos, ir.OCONV, types.Types[types.TINTER], fn))604	calleeIface := typecheck.Expr(ir.NewConvExpr(pos, ir.OCONV, types.Types[types.TINTER], callee.Nname))605606	fnPC := ir.FuncPC(pos, fnIface, obj.ABIInternal)607	concretePC := ir.FuncPC(pos, calleeIface, obj.ABIInternal)608609	pcEq := typecheck.Expr(ir.NewBinaryExpr(base.Pos, ir.OEQ, fnPC, concretePC))610611	// TODO(go.dev/issue/61577): Handle callees that a closures and need a612	// copy of the closure context from call. For now, we skip callees that613	// are closures in maybeDevirtualizeFunctionCall.614	if callee.OClosure != nil {615		base.Fatalf("Callee is a closure: %+v", callee)616	}617618	// Copy slice so edits in one location don't affect another.619	argvars = append([]ir.Node(nil), argvars...)620	concreteCall := typecheck.Call(pos, callee.Nname, argvars, call.IsDDD).(*ir.CallExpr)621622	res := condCall(curfn, pos, pcEq, concreteCall, call, init)623624	if base.Debug.PGODebug >= 3 {625		fmt.Printf("PGO devirtualizing function call to %+v. After: %+v\n", ir.FuncName(callee), res)626	}627628	return res629}630631// methodRecvType returns the type containing method fn. Returns nil if fn632// is not a method.633func methodRecvType(fn *ir.Func) *types.Type {634	recv := fn.Nname.Type().Recv()635	if recv == nil {636		return nil637	}638	return recv.Type639}640641// interfaceCallRecvTypeAndMethod returns the type and the method of the interface642// used in an interface call.643func interfaceCallRecvTypeAndMethod(call *ir.CallExpr) (*types.Type, *types.Sym) {644	if call.Op() != ir.OCALLINTER {645		base.Fatalf("Call isn't OCALLINTER: %+v", call)646	}647648	sel, ok := call.Fun.(*ir.SelectorExpr)649	if !ok {650		base.Fatalf("OCALLINTER doesn't contain SelectorExpr: %+v", call)651	}652653	return sel.X.Type(), sel.Sel654}655656// findHotConcreteCallee returns the *ir.Func of the hottest callee of a call,657// if available, and its edge weight. extraFn can perform additional658// applicability checks on each candidate edge. If extraFn returns false,659// candidate will not be considered a valid callee candidate.660func findHotConcreteCallee(p *pgoir.Profile, caller *ir.Func, call *ir.CallExpr, extraFn func(callerName string, callOffset int, candidate *pgoir.IREdge) bool) (*ir.Func, int64) {661	callerName := ir.LinkFuncName(caller)662	callerNode := p.WeightedCG.IRNodes[callerName]663	callOffset := pgoir.NodeLineOffset(call, caller)664665	if callerNode == nil {666		return nil, 0667	}668669	var hottest *pgoir.IREdge670671	// Returns true if e is hotter than hottest.672	//673	// Naively this is just e.Weight > hottest.Weight, but because OutEdges674	// has arbitrary iteration order, we need to apply additional sort675	// criteria when e.Weight == hottest.Weight to ensure we have stable676	// selection.677	hotter := func(e *pgoir.IREdge) bool {678		if hottest == nil {679			return true680		}681		if e.Weight != hottest.Weight {682			return e.Weight > hottest.Weight683		}684685		// Now e.Weight == hottest.Weight, we must select on other686		// criteria.687688		// If only one edge has IR, prefer that one.689		if (hottest.Dst.AST == nil) != (e.Dst.AST == nil) {690			if e.Dst.AST != nil {691				return true692			}693			return false694		}695696		// Arbitrary, but the callee names will always differ. Select697		// the lexicographically first callee.698		return e.Dst.Name() < hottest.Dst.Name()699	}700701	for _, e := range callerNode.OutEdges {702		if e.CallSiteOffset != callOffset {703			continue704		}705706		if !hotter(e) {707			// TODO(prattmic): consider total caller weight? i.e.,708			// if the hottest callee is only 10% of the weight,709			// maybe don't devirtualize? Similarly, if this is call710			// is globally very cold, there is not much value in711			// devirtualizing.712			if base.Debug.PGODebug >= 2 {713				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)714			}715			continue716		}717718		if e.Dst.AST == nil {719			// Destination isn't visible from this package720			// compilation.721			//722			// We must assume it implements the interface.723			//724			// We still record this as the hottest callee so far725			// because we only want to return the #1 hottest726			// callee. If we skip this then we'd return the #2727			// hottest callee.728			if base.Debug.PGODebug >= 2 {729				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)730			}731			hottest = e732			continue733		}734735		if extraFn != nil && !extraFn(callerName, callOffset, e) {736			continue737		}738739		if base.Debug.PGODebug >= 2 {740			fmt.Printf("%v: edge %s:%d -> %s (weight %d): hottest so far\n", ir.Line(call), callerName, callOffset, e.Dst.Name(), e.Weight)741		}742		hottest = e743	}744745	if hottest == nil || hottest.Weight == 0 {746		if base.Debug.PGODebug >= 2 {747			fmt.Printf("%v: call %s:%d: no hot callee\n", ir.Line(call), callerName, callOffset)748		}749		return nil, 0750	}751752	if base.Debug.PGODebug >= 2 {753		fmt.Printf("%v: call %s:%d: hottest callee %s (weight %d)\n", ir.Line(call), callerName, callOffset, hottest.Dst.Name(), hottest.Weight)754	}755	return hottest.Dst.AST, hottest.Weight756}757758// findHotConcreteInterfaceCallee returns the *ir.Func of the hottest callee of an759// interface call, if available, and its edge weight.760func findHotConcreteInterfaceCallee(p *pgoir.Profile, caller *ir.Func, call *ir.CallExpr) (*ir.Func, int64) {761	inter, method := interfaceCallRecvTypeAndMethod(call)762763	return findHotConcreteCallee(p, caller, call, func(callerName string, callOffset int, e *pgoir.IREdge) bool {764		ctyp := methodRecvType(e.Dst.AST)765		if ctyp == nil {766			// Not a method.767			// TODO(prattmic): Support non-interface indirect calls.768			if base.Debug.PGODebug >= 2 {769				fmt.Printf("%v: edge %s:%d -> %s (weight %d): callee not a method\n", ir.Line(call), callerName, callOffset, e.Dst.Name(), e.Weight)770			}771			return false772		}773774		// If ctyp doesn't implement inter it is most likely from a775		// different call on the same line776		if !typecheck.Implements(ctyp, inter) {777			// TODO(prattmic): this is overly strict. Consider if778			// ctyp is a partial implementation of an interface779			// that gets embedded in types that complete the780			// interface. It would still be OK to devirtualize a781			// call to this method.782			//783			// What we'd need to do is check that the function784			// pointer in the itab matches the method we want,785			// rather than doing a full type assertion.786			if base.Debug.PGODebug >= 2 {787				why := typecheck.ImplementsExplain(ctyp, inter)788				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)789			}790			return false791		}792793		// If the method name is different it is most likely from a794		// different call on the same line795		if !strings.HasSuffix(e.Dst.Name(), "."+method.Name) {796			if base.Debug.PGODebug >= 2 {797				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)798			}799			return false800		}801802		return true803	})804}805806// findHotConcreteFunctionCallee returns the *ir.Func of the hottest callee of an807// indirect function call, if available, and its edge weight.808func findHotConcreteFunctionCallee(p *pgoir.Profile, caller *ir.Func, call *ir.CallExpr) (*ir.Func, int64) {809	typ := call.Fun.Type().Underlying()810811	return findHotConcreteCallee(p, caller, call, func(callerName string, callOffset int, e *pgoir.IREdge) bool {812		ctyp := e.Dst.AST.Type().Underlying()813814		// If ctyp doesn't match typ it is most likely from a different815		// call on the same line.816		//817		// Note that we are comparing underlying types, as different818		// defined types are OK. e.g., a call to a value of type819		// net/http.HandlerFunc can be devirtualized to a function with820		// the same underlying type.821		if !types.Identical(typ, ctyp) {822			if base.Debug.PGODebug >= 2 {823				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)824			}825			return false826		}827828		return true829	})830}

Code quality findings 29

Empty interface; prefer specific types or generics for type safety
empty-interface
// FuncPCABIInternal takes an interface{}, emulate that. This is needed
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("%v: PGO devirtualize considering call %v\n", ir.Line(call), call)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("%v: can't PGO devirtualize go/defer call %v\n", ir.Line(call), call)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("callee %s is a closure, skipping\n", ir.FuncName(callee))
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("callee %s is a closure (runtime.memhash_varlen), skipping\n", ir.FuncName(callee))
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("callee %s is a method, skipping\n", ir.FuncName(callee))
Deeply nested control structures reduce readability; consider extracting to functions or using early returns
info maintainability deep-nesting
if reason != "" {
Deeply nested control structures reduce readability; consider extracting to functions or using early returns
info maintainability deep-nesting
if base.Flag.LowerM > 1 {
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("%v: should not PGO devirtualize %v: %s\n", ir.Line(fn), ir.FuncName(fn), reason)
Deeply nested control structures reduce readability; consider extracting to functions or using early returns
info maintainability deep-nesting
if logopt.Enabled() {
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
lhs = append(lhs, newRecvOrFn)
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
rhs = append(rhs, recvOrFn)
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
lhs = append(lhs, argvar)
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
rhs = append(rhs, arg)
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
retvars = append(retvars, typecheck.TempAt(pos, curfn, ret.Type))
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("%v: PGO devirtualizing interface call %v to %v\n", ir.Line(call), call.Fun, callee)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("PGO devirtualizing interface call to %+v. After: %+v\n", concretetyp, res)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("%v: PGO devirtualizing function call %v to %v\n", ir.Line(call), call.Fun, callee)
Multiple appends without pre-allocation; use make() with capacity when size is known
info performance append-without-prealloc
argvars := append([]ir.Node(nil), args...)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("PGO devirtualizing function call to %+v. After: %+v\n", ir.FuncName(callee), res)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
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)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
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)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("%v: edge %s:%d -> %s (weight %d): hottest so far\n", ir.Line(call), callerName, callOffset, e.Dst.Name(), e.Weight)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("%v: call %s:%d: no hot callee\n", ir.Line(call), callerName, callOffset)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("%v: call %s:%d: hottest callee %s (weight %d)\n", ir.Line(call), callerName, callOffset, hottest.Dst.Name(), hottest.Weight)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
fmt.Printf("%v: edge %s:%d -> %s (weight %d): callee not a method\n", ir.Line(call), callerName, callOffset, e.Dst.Name(), e.Weight)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
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)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
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)
Formatted output to console; prefer structured logging for consistency
info correctness fmt-printf
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)

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.