cmd/compile/internal/devirtualize: do not select a zero-weight edge as the hottest one

When both a direct call and an interface call appear on the same line,
PGO devirtualization may make a suboptimal decision. In some cases,
the directly called function becomes a candidate for devirtualization
if no other relevant outgoing edges with non-zero weight exist for the
caller's IRNode in the WeightedCG. The edge to this candidate is
considered the hottest. Despite having zero weight, this edge still
causes the interface call to be devirtualized.

This CL prevents devirtualization when the weight of the hottest edge
is 0.

Fixes #72092

Change-Id: I06c0c5e080398d86f832e09244aceaa4aeb98721
Reviewed-on: https://go-review.googlesource.com/c/go/+/655475
Reviewed-by: Keith Randall <khr@golang.org>
Reviewed-by: Keith Randall <khr@google.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Julia Lapenko 2025-03-06 17:54:17 +03:00 committed by Keith Randall
parent 116b82354c
commit 13b1261175
4 changed files with 112 additions and 11 deletions

View File

@ -741,7 +741,7 @@ func findHotConcreteCallee(p *pgoir.Profile, caller *ir.Func, call *ir.CallExpr,
hottest = e
}
if hottest == nil {
if hottest == nil || hottest.Weight == 0 {
if base.Debug.PGODebug >= 2 {
fmt.Printf("%v: call %s:%d: no hot callee\n", ir.Line(call), callerName, callOffset)
}

View File

@ -23,7 +23,7 @@ const profFileName = "devirt.pprof"
const preProfFileName = "devirt.pprof.node_map"
// testPGODevirtualize tests that specific PGO devirtualize rewrites are performed.
func testPGODevirtualize(t *testing.T, dir string, want []devirtualization, pgoProfileName string) {
func testPGODevirtualize(t *testing.T, dir string, want, nowant []devirtualization, pgoProfileName string) {
testenv.MustHaveGoRun(t)
t.Parallel()
@ -69,8 +69,10 @@ go 1.21
}
got := make(map[devirtualization]struct{})
gotNoHot := make(map[devirtualization]struct{})
devirtualizedLine := regexp.MustCompile(`(.*): PGO devirtualizing \w+ call .* to (.*)`)
noHotLine := regexp.MustCompile(`(.*): call .*: no hot callee`)
scanner := bufio.NewScanner(pr)
for scanner.Scan() {
@ -78,15 +80,21 @@ go 1.21
t.Logf("child: %s", line)
m := devirtualizedLine.FindStringSubmatch(line)
if m == nil {
if m != nil {
d := devirtualization{
pos: m[1],
callee: m[2],
}
got[d] = struct{}{}
continue
}
d := devirtualization{
pos: m[1],
callee: m[2],
m = noHotLine.FindStringSubmatch(line)
if m != nil {
d := devirtualization{
pos: m[1],
}
gotNoHot[d] = struct{}{}
}
got[d] = struct{}{}
}
if err := cmd.Wait(); err != nil {
t.Fatalf("error running go test: %v", err)
@ -104,6 +112,11 @@ go 1.21
}
t.Errorf("devirtualization %v missing; got %v", w, got)
}
for _, nw := range nowant {
if _, ok := gotNoHot[nw]; !ok {
t.Errorf("unwanted devirtualization %v; got %v", nw, got)
}
}
// Run test with PGO to ensure the assertions are still true.
cmd = testenv.CleanCmdEnv(testenv.Command(t, out))
@ -174,8 +187,18 @@ func TestPGODevirtualize(t *testing.T) {
// callee: "mult.MultClosure.func1",
//},
}
nowant := []devirtualization{
// ExerciseIfaceZeroWeight
{
pos: "./devirt.go:256:29",
},
// ExerciseIndirCallZeroWeight
{
pos: "./devirt.go:282:37",
},
}
testPGODevirtualize(t, dir, want, profFileName)
testPGODevirtualize(t, dir, want, nowant, profFileName)
}
// TestPGOPreprocessDevirtualize tests that specific functions are devirtualized when PGO
@ -237,8 +260,18 @@ func TestPGOPreprocessDevirtualize(t *testing.T) {
// callee: "mult.MultClosure.func1",
//},
}
nowant := []devirtualization{
// ExerciseIfaceZeroWeight
{
pos: "./devirt.go:256:29",
},
// ExerciseIndirCallZeroWeight
{
pos: "./devirt.go:282:37",
},
}
testPGODevirtualize(t, dir, want, preProfFileName)
testPGODevirtualize(t, dir, want, nowant, preProfFileName)
}
// Regression test for https://go.dev/issue/65615. If a target function changes
@ -303,8 +336,18 @@ func TestLookupFuncGeneric(t *testing.T) {
// callee: "mult.MultClosure.func1",
//},
}
nowant := []devirtualization{
// ExerciseIfaceZeroWeight
{
pos: "./devirt.go:256:29",
},
// ExerciseIndirCallZeroWeight
{
pos: "./devirt.go:282:37",
},
}
testPGODevirtualize(t, dir, want, profFileName)
testPGODevirtualize(t, dir, want, nowant, profFileName)
}
var multFnRe = regexp.MustCompile(`func MultFn\(a, b int64\) int64`)

View File

@ -250,3 +250,45 @@ func ExerciseFuncClosure(iter int, a1, a2 AddFunc, m1, m2 mult.MultFunc) int {
}
return val
}
//go:noinline
func IfaceZeroWeight(a *Add, b Adder) bool {
return a.Add(1, 2) == b.Add(3, 4) // unwanted devirtualization
}
// ExerciseIfaceZeroWeight never calls IfaceZeroWeight, so the callee
// is not expected to appear in the profile.
//
//go:noinline
func ExerciseIfaceZeroWeight() {
if false {
a := &Add{}
b := &Sub{}
// Unreachable call
IfaceZeroWeight(a, b)
}
}
func DirectCall() bool {
return true
}
func IndirectCall() bool {
return false
}
//go:noinline
func IndirCallZeroWeight(indirectCall func() bool) bool {
return DirectCall() && indirectCall() // unwanted devirtualization
}
// ExerciseIndirCallZeroWeight never calls IndirCallZeroWeight, so the
// callee is not expected to appear in the profile.
//
//go:noinline
func ExerciseIndirCallZeroWeight() {
if false {
// Unreachable call
IndirCallZeroWeight(IndirectCall)
}
}

View File

@ -71,3 +71,19 @@ func TestDevirtFuncClosure(t *testing.T) {
t.Errorf("ExerciseFuncClosure(10) got %d want 1176", v)
}
}
func BenchmarkDevirtIfaceZeroWeight(t *testing.B) {
ExerciseIfaceZeroWeight()
}
func TestDevirtIfaceZeroWeight(t *testing.T) {
ExerciseIfaceZeroWeight()
}
func BenchmarkDevirtIndirCallZeroWeight(t *testing.B) {
ExerciseIndirCallZeroWeight()
}
func TestDevirtIndirCallZeroWeight(t *testing.T) {
ExerciseIndirCallZeroWeight()
}