github.com/bir3/gocompiler@v0.9.2202/src/cmd/compile/internal/devirtualize/pgo.go (about) 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 5 package devirtualize 6 7 import ( 8 "github.com/bir3/gocompiler/src/cmd/compile/internal/base" 9 "github.com/bir3/gocompiler/src/cmd/compile/internal/inline" 10 "github.com/bir3/gocompiler/src/cmd/compile/internal/ir" 11 "github.com/bir3/gocompiler/src/cmd/compile/internal/logopt" 12 "github.com/bir3/gocompiler/src/cmd/compile/internal/pgo" 13 "github.com/bir3/gocompiler/src/cmd/compile/internal/typecheck" 14 "github.com/bir3/gocompiler/src/cmd/compile/internal/types" 15 "github.com/bir3/gocompiler/src/cmd/internal/obj" 16 "github.com/bir3/gocompiler/src/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. 26 type 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. 105 func ProfileGuided(fn *ir.Func, p *pgo.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. 187 func maybeDevirtualizeInterfaceCall(p *pgo.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. 217 func maybeDevirtualizeFunctionCall(p *pgo.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 := pgo.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. 303 func 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", "pgo-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. 339 func constructCallStat(p *pgo.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 := pgo.NodeLineOffset(call, fn) 354 355 hotter := func(e *pgo.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 // Sum of all edges from this callsite, regardless of callee. 368 // For direct calls, this should be the same as the single edge 369 // weight (except for multiple calls on one line, which we 370 // can't distinguish). 371 callerNode := p.WeightedCG.IRNodes[name] 372 for _, edge := range callerNode.OutEdges { 373 if edge.CallSiteOffset != offset { 374 continue 375 } 376 stat.Weight += edge.Weight 377 if hotter(edge) { 378 stat.HottestWeight = edge.Weight 379 stat.Hottest = edge.Dst.Name() 380 } 381 } 382 383 switch call.Op() { 384 case ir.OCALLFUNC: 385 stat.Interface = false 386 387 callee := pgo.DirectCallee(call.Fun) 388 if callee != nil { 389 stat.Direct = true 390 if stat.Hottest == "" { 391 stat.Hottest = ir.LinkFuncName(callee) 392 } 393 } else { 394 stat.Direct = false 395 } 396 case ir.OCALLINTER: 397 stat.Direct = false 398 stat.Interface = true 399 case ir.OCALLMETH: 400 base.FatalfAt(call.Pos(), "OCALLMETH missed by typecheck") 401 } 402 403 return &stat 404 } 405 406 // copyInputs copies the inputs to a call: the receiver (for interface calls) 407 // or function value (for function value calls) and the arguments. These 408 // expressions are evaluated once and assigned to temporaries. 409 // 410 // The assignment statement is added to init and the copied receiver/fn 411 // expression and copied arguments expressions are returned. 412 func copyInputs(curfn *ir.Func, pos src.XPos, recvOrFn ir.Node, args []ir.Node, init *ir.Nodes) (ir.Node, []ir.Node) { 413 // Evaluate receiver/fn and argument expressions. The receiver/fn is 414 // used twice but we don't want to cause side effects twice. The 415 // arguments are used in two different calls and we can't trivially 416 // copy them. 417 // 418 // recvOrFn must be first in the assignment list as its side effects 419 // must be ordered before argument side effects. 420 var lhs, rhs []ir.Node 421 newRecvOrFn := typecheck.TempAt(pos, curfn, recvOrFn.Type()) 422 lhs = append(lhs, newRecvOrFn) 423 rhs = append(rhs, recvOrFn) 424 425 for _, arg := range args { 426 argvar := typecheck.TempAt(pos, curfn, arg.Type()) 427 428 lhs = append(lhs, argvar) 429 rhs = append(rhs, arg) 430 } 431 432 asList := ir.NewAssignListStmt(pos, ir.OAS2, lhs, rhs) 433 init.Append(typecheck.Stmt(asList)) 434 435 return newRecvOrFn, lhs[1:] 436 } 437 438 // retTemps returns a slice of temporaries to be used for storing result values from call. 439 func retTemps(curfn *ir.Func, pos src.XPos, call *ir.CallExpr) []ir.Node { 440 sig := call.Fun.Type() 441 var retvars []ir.Node 442 for _, ret := range sig.Results() { 443 retvars = append(retvars, typecheck.TempAt(pos, curfn, ret.Type)) 444 } 445 return retvars 446 } 447 448 // condCall returns an ir.InlinedCallExpr that performs a call to thenCall if 449 // cond is true and elseCall if cond is false. The return variables of the 450 // InlinedCallExpr evaluate to the return values from the call. 451 func condCall(curfn *ir.Func, pos src.XPos, cond ir.Node, thenCall, elseCall *ir.CallExpr, init ir.Nodes) *ir.InlinedCallExpr { 452 // Doesn't matter whether we use thenCall or elseCall, they must have 453 // the same return types. 454 retvars := retTemps(curfn, pos, thenCall) 455 456 var thenBlock, elseBlock ir.Nodes 457 if len(retvars) == 0 { 458 thenBlock.Append(thenCall) 459 elseBlock.Append(elseCall) 460 } else { 461 // Copy slice so edits in one location don't affect another. 462 thenRet := append([]ir.Node(nil), retvars...) 463 thenAsList := ir.NewAssignListStmt(pos, ir.OAS2, thenRet, []ir.Node{thenCall}) 464 thenBlock.Append(typecheck.Stmt(thenAsList)) 465 466 elseRet := append([]ir.Node(nil), retvars...) 467 elseAsList := ir.NewAssignListStmt(pos, ir.OAS2, elseRet, []ir.Node{elseCall}) 468 elseBlock.Append(typecheck.Stmt(elseAsList)) 469 } 470 471 nif := ir.NewIfStmt(pos, cond, thenBlock, elseBlock) 472 nif.SetInit(init) 473 nif.Likely = true 474 475 body := []ir.Node{typecheck.Stmt(nif)} 476 477 // This isn't really an inlined call of course, but InlinedCallExpr 478 // makes handling reassignment of return values easier. 479 res := ir.NewInlinedCallExpr(pos, body, retvars) 480 res.SetType(thenCall.Type()) 481 res.SetTypecheck(1) 482 return res 483 } 484 485 // rewriteInterfaceCall devirtualizes the given interface call using a direct 486 // method call to concretetyp. 487 func rewriteInterfaceCall(call *ir.CallExpr, curfn, callee *ir.Func, concretetyp *types.Type) ir.Node { 488 if base.Flag.LowerM != 0 { 489 fmt.Printf("%v: PGO devirtualizing interface call %v to %v\n", ir.Line(call), call.Fun, callee) 490 } 491 492 // We generate an OINCALL of: 493 // 494 // var recv Iface 495 // 496 // var arg1 A1 497 // var argN AN 498 // 499 // var ret1 R1 500 // var retN RN 501 // 502 // recv, arg1, argN = recv expr, arg1 expr, argN expr 503 // 504 // t, ok := recv.(Concrete) 505 // if ok { 506 // ret1, retN = t.Method(arg1, ... argN) 507 // } else { 508 // ret1, retN = recv.Method(arg1, ... argN) 509 // } 510 // 511 // OINCALL retvars: ret1, ... retN 512 // 513 // This isn't really an inlined call of course, but InlinedCallExpr 514 // makes handling reassignment of return values easier. 515 // 516 // TODO(prattmic): This increases the size of the AST in the caller, 517 // making it less like to inline. We may want to compensate for this 518 // somehow. 519 520 sel := call.Fun.(*ir.SelectorExpr) 521 method := sel.Sel 522 pos := call.Pos() 523 init := ir.TakeInit(call) 524 525 recv, args := copyInputs(curfn, pos, sel.X, call.Args.Take(), &init) 526 527 // Copy slice so edits in one location don't affect another. 528 argvars := append([]ir.Node(nil), args...) 529 call.Args = argvars 530 531 tmpnode := typecheck.TempAt(base.Pos, curfn, concretetyp) 532 tmpok := typecheck.TempAt(base.Pos, curfn, types.Types[types.TBOOL]) 533 534 assert := ir.NewTypeAssertExpr(pos, recv, concretetyp) 535 536 assertAsList := ir.NewAssignListStmt(pos, ir.OAS2, []ir.Node{tmpnode, tmpok}, []ir.Node{typecheck.Expr(assert)}) 537 init.Append(typecheck.Stmt(assertAsList)) 538 539 concreteCallee := typecheck.XDotMethod(pos, tmpnode, method, true) 540 // Copy slice so edits in one location don't affect another. 541 argvars = append([]ir.Node(nil), argvars...) 542 concreteCall := typecheck.Call(pos, concreteCallee, argvars, call.IsDDD).(*ir.CallExpr) 543 544 res := condCall(curfn, pos, tmpok, concreteCall, call, init) 545 546 if base.Debug.PGODebug >= 3 { 547 fmt.Printf("PGO devirtualizing interface call to %+v. After: %+v\n", concretetyp, res) 548 } 549 550 return res 551 } 552 553 // rewriteFunctionCall devirtualizes the given OCALLFUNC using a direct 554 // function call to callee. 555 func rewriteFunctionCall(call *ir.CallExpr, curfn, callee *ir.Func) ir.Node { 556 if base.Flag.LowerM != 0 { 557 fmt.Printf("%v: PGO devirtualizing function call %v to %v\n", ir.Line(call), call.Fun, callee) 558 } 559 560 // We generate an OINCALL of: 561 // 562 // var fn FuncType 563 // 564 // var arg1 A1 565 // var argN AN 566 // 567 // var ret1 R1 568 // var retN RN 569 // 570 // fn, arg1, argN = fn expr, arg1 expr, argN expr 571 // 572 // fnPC := internal/abi.FuncPCABIInternal(fn) 573 // concretePC := internal/abi.FuncPCABIInternal(concrete) 574 // 575 // if fnPC == concretePC { 576 // ret1, retN = concrete(arg1, ... argN) // Same closure context passed (TODO) 577 // } else { 578 // ret1, retN = fn(arg1, ... argN) 579 // } 580 // 581 // OINCALL retvars: ret1, ... retN 582 // 583 // This isn't really an inlined call of course, but InlinedCallExpr 584 // makes handling reassignment of return values easier. 585 586 pos := call.Pos() 587 init := ir.TakeInit(call) 588 589 fn, args := copyInputs(curfn, pos, call.Fun, call.Args.Take(), &init) 590 591 // Copy slice so edits in one location don't affect another. 592 argvars := append([]ir.Node(nil), args...) 593 call.Args = argvars 594 595 // FuncPCABIInternal takes an interface{}, emulate that. This is needed 596 // for to ensure we get the MAKEFACE we need for SSA. 597 fnIface := typecheck.Expr(ir.NewConvExpr(pos, ir.OCONV, types.Types[types.TINTER], fn)) 598 calleeIface := typecheck.Expr(ir.NewConvExpr(pos, ir.OCONV, types.Types[types.TINTER], callee.Nname)) 599 600 fnPC := ir.FuncPC(pos, fnIface, obj.ABIInternal) 601 concretePC := ir.FuncPC(pos, calleeIface, obj.ABIInternal) 602 603 pcEq := typecheck.Expr(ir.NewBinaryExpr(base.Pos, ir.OEQ, fnPC, concretePC)) 604 605 // TODO(go.dev/issue/61577): Handle callees that a closures and need a 606 // copy of the closure context from call. For now, we skip callees that 607 // are closures in maybeDevirtualizeFunctionCall. 608 if callee.OClosure != nil { 609 base.Fatalf("Callee is a closure: %+v", callee) 610 } 611 612 // Copy slice so edits in one location don't affect another. 613 argvars = append([]ir.Node(nil), argvars...) 614 concreteCall := typecheck.Call(pos, callee.Nname, argvars, call.IsDDD).(*ir.CallExpr) 615 616 res := condCall(curfn, pos, pcEq, concreteCall, call, init) 617 618 if base.Debug.PGODebug >= 3 { 619 fmt.Printf("PGO devirtualizing function call to %+v. After: %+v\n", ir.FuncName(callee), res) 620 } 621 622 return res 623 } 624 625 // methodRecvType returns the type containing method fn. Returns nil if fn 626 // is not a method. 627 func methodRecvType(fn *ir.Func) *types.Type { 628 recv := fn.Nname.Type().Recv() 629 if recv == nil { 630 return nil 631 } 632 return recv.Type 633 } 634 635 // interfaceCallRecvTypeAndMethod returns the type and the method of the interface 636 // used in an interface call. 637 func interfaceCallRecvTypeAndMethod(call *ir.CallExpr) (*types.Type, *types.Sym) { 638 if call.Op() != ir.OCALLINTER { 639 base.Fatalf("Call isn't OCALLINTER: %+v", call) 640 } 641 642 sel, ok := call.Fun.(*ir.SelectorExpr) 643 if !ok { 644 base.Fatalf("OCALLINTER doesn't contain SelectorExpr: %+v", call) 645 } 646 647 return sel.X.Type(), sel.Sel 648 } 649 650 // findHotConcreteCallee returns the *ir.Func of the hottest callee of a call, 651 // if available, and its edge weight. extraFn can perform additional 652 // applicability checks on each candidate edge. If extraFn returns false, 653 // candidate will not be considered a valid callee candidate. 654 func findHotConcreteCallee(p *pgo.Profile, caller *ir.Func, call *ir.CallExpr, extraFn func(callerName string, callOffset int, candidate *pgo.IREdge) bool) (*ir.Func, int64) { 655 callerName := ir.LinkFuncName(caller) 656 callerNode := p.WeightedCG.IRNodes[callerName] 657 callOffset := pgo.NodeLineOffset(call, caller) 658 659 var hottest *pgo.IREdge 660 661 // Returns true if e is hotter than hottest. 662 // 663 // Naively this is just e.Weight > hottest.Weight, but because OutEdges 664 // has arbitrary iteration order, we need to apply additional sort 665 // criteria when e.Weight == hottest.Weight to ensure we have stable 666 // selection. 667 hotter := func(e *pgo.IREdge) bool { 668 if hottest == nil { 669 return true 670 } 671 if e.Weight != hottest.Weight { 672 return e.Weight > hottest.Weight 673 } 674 675 // Now e.Weight == hottest.Weight, we must select on other 676 // criteria. 677 678 // If only one edge has IR, prefer that one. 679 if (hottest.Dst.AST == nil) != (e.Dst.AST == nil) { 680 if e.Dst.AST != nil { 681 return true 682 } 683 return false 684 } 685 686 // Arbitrary, but the callee names will always differ. Select 687 // the lexicographically first callee. 688 return e.Dst.Name() < hottest.Dst.Name() 689 } 690 691 for _, e := range callerNode.OutEdges { 692 if e.CallSiteOffset != callOffset { 693 continue 694 } 695 696 if !hotter(e) { 697 // TODO(prattmic): consider total caller weight? i.e., 698 // if the hottest callee is only 10% of the weight, 699 // maybe don't devirtualize? Similarly, if this is call 700 // is globally very cold, there is not much value in 701 // devirtualizing. 702 if base.Debug.PGODebug >= 2 { 703 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) 704 } 705 continue 706 } 707 708 if e.Dst.AST == nil { 709 // Destination isn't visible from this package 710 // compilation. 711 // 712 // We must assume it implements the interface. 713 // 714 // We still record this as the hottest callee so far 715 // because we only want to return the #1 hottest 716 // callee. If we skip this then we'd return the #2 717 // hottest callee. 718 if base.Debug.PGODebug >= 2 { 719 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) 720 } 721 hottest = e 722 continue 723 } 724 725 if extraFn != nil && !extraFn(callerName, callOffset, e) { 726 continue 727 } 728 729 if base.Debug.PGODebug >= 2 { 730 fmt.Printf("%v: edge %s:%d -> %s (weight %d): hottest so far\n", ir.Line(call), callerName, callOffset, e.Dst.Name(), e.Weight) 731 } 732 hottest = e 733 } 734 735 if hottest == nil { 736 if base.Debug.PGODebug >= 2 { 737 fmt.Printf("%v: call %s:%d: no hot callee\n", ir.Line(call), callerName, callOffset) 738 } 739 return nil, 0 740 } 741 742 if base.Debug.PGODebug >= 2 { 743 fmt.Printf("%v call %s:%d: hottest callee %s (weight %d)\n", ir.Line(call), callerName, callOffset, hottest.Dst.Name(), hottest.Weight) 744 } 745 return hottest.Dst.AST, hottest.Weight 746 } 747 748 // findHotConcreteInterfaceCallee returns the *ir.Func of the hottest callee of an 749 // interface call, if available, and its edge weight. 750 func findHotConcreteInterfaceCallee(p *pgo.Profile, caller *ir.Func, call *ir.CallExpr) (*ir.Func, int64) { 751 inter, method := interfaceCallRecvTypeAndMethod(call) 752 753 return findHotConcreteCallee(p, caller, call, func(callerName string, callOffset int, e *pgo.IREdge) bool { 754 ctyp := methodRecvType(e.Dst.AST) 755 if ctyp == nil { 756 // Not a method. 757 // TODO(prattmic): Support non-interface indirect calls. 758 if base.Debug.PGODebug >= 2 { 759 fmt.Printf("%v: edge %s:%d -> %s (weight %d): callee not a method\n", ir.Line(call), callerName, callOffset, e.Dst.Name(), e.Weight) 760 } 761 return false 762 } 763 764 // If ctyp doesn't implement inter it is most likely from a 765 // different call on the same line 766 if !typecheck.Implements(ctyp, inter) { 767 // TODO(prattmic): this is overly strict. Consider if 768 // ctyp is a partial implementation of an interface 769 // that gets embedded in types that complete the 770 // interface. It would still be OK to devirtualize a 771 // call to this method. 772 // 773 // What we'd need to do is check that the function 774 // pointer in the itab matches the method we want, 775 // rather than doing a full type assertion. 776 if base.Debug.PGODebug >= 2 { 777 why := typecheck.ImplementsExplain(ctyp, inter) 778 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) 779 } 780 return false 781 } 782 783 // If the method name is different it is most likely from a 784 // different call on the same line 785 if !strings.HasSuffix(e.Dst.Name(), "."+method.Name) { 786 if base.Debug.PGODebug >= 2 { 787 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) 788 } 789 return false 790 } 791 792 return true 793 }) 794 } 795 796 // findHotConcreteFunctionCallee returns the *ir.Func of the hottest callee of an 797 // indirect function call, if available, and its edge weight. 798 func findHotConcreteFunctionCallee(p *pgo.Profile, caller *ir.Func, call *ir.CallExpr) (*ir.Func, int64) { 799 typ := call.Fun.Type().Underlying() 800 801 return findHotConcreteCallee(p, caller, call, func(callerName string, callOffset int, e *pgo.IREdge) bool { 802 ctyp := e.Dst.AST.Type().Underlying() 803 804 // If ctyp doesn't match typ it is most likely from a different 805 // call on the same line. 806 // 807 // Note that we are comparing underlying types, as different 808 // defined types are OK. e.g., a call to a value of type 809 // net/http.HandlerFunc can be devirtualized to a function with 810 // the same underlying type. 811 if !types.Identical(typ, ctyp) { 812 if base.Debug.PGODebug >= 2 { 813 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) 814 } 815 return false 816 } 817 818 return true 819 }) 820 }