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