Skip to content

Commit 13b1261

Browse files
Julia Lapenkorandall77
Julia Lapenko
authored andcommitted
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 <[email protected]> Reviewed-by: Keith Randall <[email protected]> Reviewed-by: Michael Pratt <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 116b823 commit 13b1261

File tree

4 files changed

+112
-11
lines changed

4 files changed

+112
-11
lines changed

src/cmd/compile/internal/devirtualize/pgo.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,7 @@ func findHotConcreteCallee(p *pgoir.Profile, caller *ir.Func, call *ir.CallExpr,
741741
hottest = e
742742
}
743743

744-
if hottest == nil {
744+
if hottest == nil || hottest.Weight == 0 {
745745
if base.Debug.PGODebug >= 2 {
746746
fmt.Printf("%v: call %s:%d: no hot callee\n", ir.Line(call), callerName, callOffset)
747747
}

src/cmd/compile/internal/test/pgo_devirtualize_test.go

+53-10
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const profFileName = "devirt.pprof"
2323
const preProfFileName = "devirt.pprof.node_map"
2424

2525
// testPGODevirtualize tests that specific PGO devirtualize rewrites are performed.
26-
func testPGODevirtualize(t *testing.T, dir string, want []devirtualization, pgoProfileName string) {
26+
func testPGODevirtualize(t *testing.T, dir string, want, nowant []devirtualization, pgoProfileName string) {
2727
testenv.MustHaveGoRun(t)
2828
t.Parallel()
2929

@@ -69,24 +69,32 @@ go 1.21
6969
}
7070

7171
got := make(map[devirtualization]struct{})
72+
gotNoHot := make(map[devirtualization]struct{})
7273

7374
devirtualizedLine := regexp.MustCompile(`(.*): PGO devirtualizing \w+ call .* to (.*)`)
75+
noHotLine := regexp.MustCompile(`(.*): call .*: no hot callee`)
7476

7577
scanner := bufio.NewScanner(pr)
7678
for scanner.Scan() {
7779
line := scanner.Text()
7880
t.Logf("child: %s", line)
7981

8082
m := devirtualizedLine.FindStringSubmatch(line)
81-
if m == nil {
83+
if m != nil {
84+
d := devirtualization{
85+
pos: m[1],
86+
callee: m[2],
87+
}
88+
got[d] = struct{}{}
8289
continue
8390
}
84-
85-
d := devirtualization{
86-
pos: m[1],
87-
callee: m[2],
91+
m = noHotLine.FindStringSubmatch(line)
92+
if m != nil {
93+
d := devirtualization{
94+
pos: m[1],
95+
}
96+
gotNoHot[d] = struct{}{}
8897
}
89-
got[d] = struct{}{}
9098
}
9199
if err := cmd.Wait(); err != nil {
92100
t.Fatalf("error running go test: %v", err)
@@ -104,6 +112,11 @@ go 1.21
104112
}
105113
t.Errorf("devirtualization %v missing; got %v", w, got)
106114
}
115+
for _, nw := range nowant {
116+
if _, ok := gotNoHot[nw]; !ok {
117+
t.Errorf("unwanted devirtualization %v; got %v", nw, got)
118+
}
119+
}
107120

108121
// Run test with PGO to ensure the assertions are still true.
109122
cmd = testenv.CleanCmdEnv(testenv.Command(t, out))
@@ -174,8 +187,18 @@ func TestPGODevirtualize(t *testing.T) {
174187
// callee: "mult.MultClosure.func1",
175188
//},
176189
}
190+
nowant := []devirtualization{
191+
// ExerciseIfaceZeroWeight
192+
{
193+
pos: "./devirt.go:256:29",
194+
},
195+
// ExerciseIndirCallZeroWeight
196+
{
197+
pos: "./devirt.go:282:37",
198+
},
199+
}
177200

178-
testPGODevirtualize(t, dir, want, profFileName)
201+
testPGODevirtualize(t, dir, want, nowant, profFileName)
179202
}
180203

181204
// TestPGOPreprocessDevirtualize tests that specific functions are devirtualized when PGO
@@ -237,8 +260,18 @@ func TestPGOPreprocessDevirtualize(t *testing.T) {
237260
// callee: "mult.MultClosure.func1",
238261
//},
239262
}
263+
nowant := []devirtualization{
264+
// ExerciseIfaceZeroWeight
265+
{
266+
pos: "./devirt.go:256:29",
267+
},
268+
// ExerciseIndirCallZeroWeight
269+
{
270+
pos: "./devirt.go:282:37",
271+
},
272+
}
240273

241-
testPGODevirtualize(t, dir, want, preProfFileName)
274+
testPGODevirtualize(t, dir, want, nowant, preProfFileName)
242275
}
243276

244277
// Regression test for https://go.dev/issue/65615. If a target function changes
@@ -303,8 +336,18 @@ func TestLookupFuncGeneric(t *testing.T) {
303336
// callee: "mult.MultClosure.func1",
304337
//},
305338
}
339+
nowant := []devirtualization{
340+
// ExerciseIfaceZeroWeight
341+
{
342+
pos: "./devirt.go:256:29",
343+
},
344+
// ExerciseIndirCallZeroWeight
345+
{
346+
pos: "./devirt.go:282:37",
347+
},
348+
}
306349

307-
testPGODevirtualize(t, dir, want, profFileName)
350+
testPGODevirtualize(t, dir, want, nowant, profFileName)
308351
}
309352

310353
var multFnRe = regexp.MustCompile(`func MultFn\(a, b int64\) int64`)

src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt.go

+42
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,45 @@ func ExerciseFuncClosure(iter int, a1, a2 AddFunc, m1, m2 mult.MultFunc) int {
250250
}
251251
return val
252252
}
253+
254+
//go:noinline
255+
func IfaceZeroWeight(a *Add, b Adder) bool {
256+
return a.Add(1, 2) == b.Add(3, 4) // unwanted devirtualization
257+
}
258+
259+
// ExerciseIfaceZeroWeight never calls IfaceZeroWeight, so the callee
260+
// is not expected to appear in the profile.
261+
//
262+
//go:noinline
263+
func ExerciseIfaceZeroWeight() {
264+
if false {
265+
a := &Add{}
266+
b := &Sub{}
267+
// Unreachable call
268+
IfaceZeroWeight(a, b)
269+
}
270+
}
271+
272+
func DirectCall() bool {
273+
return true
274+
}
275+
276+
func IndirectCall() bool {
277+
return false
278+
}
279+
280+
//go:noinline
281+
func IndirCallZeroWeight(indirectCall func() bool) bool {
282+
return DirectCall() && indirectCall() // unwanted devirtualization
283+
}
284+
285+
// ExerciseIndirCallZeroWeight never calls IndirCallZeroWeight, so the
286+
// callee is not expected to appear in the profile.
287+
//
288+
//go:noinline
289+
func ExerciseIndirCallZeroWeight() {
290+
if false {
291+
// Unreachable call
292+
IndirCallZeroWeight(IndirectCall)
293+
}
294+
}

src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt_test.go

+16
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,19 @@ func TestDevirtFuncClosure(t *testing.T) {
7171
t.Errorf("ExerciseFuncClosure(10) got %d want 1176", v)
7272
}
7373
}
74+
75+
func BenchmarkDevirtIfaceZeroWeight(t *testing.B) {
76+
ExerciseIfaceZeroWeight()
77+
}
78+
79+
func TestDevirtIfaceZeroWeight(t *testing.T) {
80+
ExerciseIfaceZeroWeight()
81+
}
82+
83+
func BenchmarkDevirtIndirCallZeroWeight(t *testing.B) {
84+
ExerciseIndirCallZeroWeight()
85+
}
86+
87+
func TestDevirtIndirCallZeroWeight(t *testing.T) {
88+
ExerciseIndirCallZeroWeight()
89+
}

0 commit comments

Comments
 (0)