diff options
| author | Michael Pratt <mpratt@google.com> | 2023-10-12 16:01:34 -0400 |
|---|---|---|
| committer | Michael Pratt <mpratt@google.com> | 2023-11-13 18:17:47 +0000 |
| commit | fb6ff1e4caaece9be61c45518ffb51081e892a73 (patch) | |
| tree | 85b672308e7601987dfd2d3ed418c937016f4b8b /src/cmd/compile/internal/test/testdata | |
| parent | 0c66ae5c27706d4fe5c43fc71f92b52052b24497 (diff) | |
| download | go-fb6ff1e4caaece9be61c45518ffb51081e892a73.tar.xz | |
cmd/compile: initial function value devirtualization
Today, PGO-based devirtualization only applies to interface calls. This
CL extends initial support to function values (i.e., function/closure
pointers passed as arguments or stored in a struct).
This CL is a minimal implementation with several limitations.
* Export data lookup of function value callees not implemented
(equivalent of CL 497175; done in CL 540258).
* Callees must be standard static functions. Callees that are closures
(requiring closure context) are not supported.
For #61577.
Change-Id: I7d328859035249e176294cd0d9885b2d08c853f6
Reviewed-on: https://go-review.googlesource.com/c/go/+/539699
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Diffstat (limited to 'src/cmd/compile/internal/test/testdata')
| -rw-r--r-- | src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt.go | 214 | ||||
| -rw-r--r-- | src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt.pprof | bin | 890 -> 1411 bytes | |||
| -rw-r--r-- | src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt_test.go | 48 | ||||
| -rw-r--r-- | src/cmd/compile/internal/test/testdata/pgo/devirtualize/mult.pkg/mult.go | 28 |
4 files changed, 282 insertions, 8 deletions
diff --git a/src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt.go b/src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt.go index 4748e19e10..63de3d3c3f 100644 --- a/src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt.go +++ b/src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt.go @@ -16,7 +16,11 @@ package devirt // // Dots in the last package path component are escaped in symbol names. Use one // to ensure the escaping doesn't break lookup. -import "example.com/pgo/devirtualize/mult.pkg" +import ( + "fmt" + + "example.com/pgo/devirtualize/mult.pkg" +) var sink int @@ -42,15 +46,46 @@ func (Sub) Add(a, b int) int { return a - b } -// Exercise calls mostly a1 and m1. +// ExerciseIface calls mostly a1 and m1. // //go:noinline -func Exercise(iter int, a1, a2 Adder, m1, m2 mult.Multiplier) { +func ExerciseIface(iter int, a1, a2 Adder, m1, m2 mult.Multiplier) int { + // The call below must evaluate selectA() to determine the receiver to + // use. This should happen exactly once per iteration. Assert that is + // the case to ensure the IR manipulation does not result in over- or + // under-evaluation. + selectI := 0 + selectA := func(gotI int) Adder { + if gotI != selectI { + panic(fmt.Sprintf("selectA not called once per iteration; got i %d want %d", gotI, selectI)) + } + selectI++ + + if gotI%10 == 0 { + return a2 + } + return a1 + } + oneI := 0 + one := func(gotI int) int { + if gotI != oneI { + panic(fmt.Sprintf("one not called once per iteration; got i %d want %d", gotI, oneI)) + } + oneI++ + + // The function value must be evaluated before arguments, so + // selectI must have been incremented already. + if selectI != oneI { + panic(fmt.Sprintf("selectA not called before not called before one; got i %d want %d", selectI, oneI)) + } + + return 1 + } + + val := 0 for i := 0; i < iter; i++ { - a := a1 m := m1 if i%10 == 0 { - a = a2 m = m2 } @@ -63,6 +98,173 @@ func Exercise(iter int, a1, a2 Adder, m1, m2 mult.Multiplier) { // If they were not mutually exclusive (for example, two Add // calls), then we could not definitively select the correct // callee. - sink += m.Multiply(42, a.Add(1, 2)) + val += m.Multiply(42, selectA(i).Add(one(i), 2)) + } + return val +} + +type AddFunc func(int, int) int + +func AddFn(a, b int) int { + for i := 0; i < 1000; i++ { + sink++ + } + return a + b +} + +func SubFn(a, b int) int { + for i := 0; i < 1000; i++ { + sink++ + } + return a - b +} + +// ExerciseFuncConcrete calls mostly a1 and m1. +// +//go:noinline +func ExerciseFuncConcrete(iter int, a1, a2 AddFunc, m1, m2 mult.MultFunc) int { + // The call below must evaluate selectA() to determine the function to + // call. This should happen exactly once per iteration. Assert that is + // the case to ensure the IR manipulation does not result in over- or + // under-evaluation. + selectI := 0 + selectA := func(gotI int) AddFunc { + if gotI != selectI { + panic(fmt.Sprintf("selectA not called once per iteration; got i %d want %d", gotI, selectI)) + } + selectI++ + + if gotI%10 == 0 { + return a2 + } + return a1 + } + oneI := 0 + one := func(gotI int) int { + if gotI != oneI { + panic(fmt.Sprintf("one not called once per iteration; got i %d want %d", gotI, oneI)) + } + oneI++ + + // The function value must be evaluated before arguments, so + // selectI must have been incremented already. + if selectI != oneI { + panic(fmt.Sprintf("selectA not called before not called before one; got i %d want %d", selectI, oneI)) + } + + return 1 + } + + val := 0 + for i := 0; i < iter; i++ { + m := m1 + if i%10 == 0 { + m = m2 + } + + // N.B. Profiles only distinguish calls on a per-line level, + // making the two calls ambiguous. However because the + // function types are mutually exclusive, devirtualization can + // still select the correct callee for each. + // + // If they were not mutually exclusive (for example, two + // AddFunc calls), then we could not definitively select the + // correct callee. + // + // TODO(prattmic): Export data lookup for function value + // callees not implemented, meaning the type is unavailable. + //sink += int(m(42, int64(a(1, 2)))) + + v := selectA(i)(one(i), 2) + val += int(m(42, int64(v))) + } + return val +} + +// ExerciseFuncField calls mostly a1 and m1. +// +// This is a simplified version of ExerciseFuncConcrete, but accessing the +// function values via a struct field. +// +//go:noinline +func ExerciseFuncField(iter int, a1, a2 AddFunc, m1, m2 mult.MultFunc) int { + ops := struct { + a AddFunc + m mult.MultFunc + }{} + + val := 0 + for i := 0; i < iter; i++ { + ops.a = a1 + ops.m = m1 + if i%10 == 0 { + ops.a = a2 + ops.m = m2 + } + + // N.B. Profiles only distinguish calls on a per-line level, + // making the two calls ambiguous. However because the + // function types are mutually exclusive, devirtualization can + // still select the correct callee for each. + // + // If they were not mutually exclusive (for example, two + // AddFunc calls), then we could not definitively select the + // correct callee. + // + // TODO(prattmic): Export data lookup for function value + // callees not implemented, meaning the type is unavailable. + //sink += int(ops.m(42, int64(ops.a(1, 2)))) + + v := ops.a(1, 2) + val += int(ops.m(42, int64(v))) + } + return val +} + +//go:noinline +func AddClosure() AddFunc { + // Implicit closure by capturing the receiver. + var a Add + return a.Add +} + +//go:noinline +func SubClosure() AddFunc { + var s Sub + return s.Add +} + +// ExerciseFuncClosure calls mostly a1 and m1. +// +// This is a simplified version of ExerciseFuncConcrete, but we need two +// distinct call sites to test two different types of function values. +// +//go:noinline +func ExerciseFuncClosure(iter int, a1, a2 AddFunc, m1, m2 mult.MultFunc) int { + val := 0 + for i := 0; i < iter; i++ { + a := a1 + m := m1 + if i%10 == 0 { + a = a2 + m = m2 + } + + // N.B. Profiles only distinguish calls on a per-line level, + // making the two calls ambiguous. However because the + // function types are mutually exclusive, devirtualization can + // still select the correct callee for each. + // + // If they were not mutually exclusive (for example, two + // AddFunc calls), then we could not definitively select the + // correct callee. + // + // TODO(prattmic): Export data lookup for function value + // callees not implemented, meaning the type is unavailable. + //sink += int(m(42, int64(a(1, 2)))) + + v := a(1, 2) + val += int(m(42, int64(v))) } + return val } diff --git a/src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt.pprof b/src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt.pprof Binary files differindex 87e7b62736..de064582ff 100644 --- a/src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt.pprof +++ b/src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt.pprof diff --git a/src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt_test.go b/src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt_test.go index ef637a876b..59b565d77f 100644 --- a/src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt_test.go +++ b/src/cmd/compile/internal/test/testdata/pgo/devirtualize/devirt_test.go @@ -17,7 +17,7 @@ import ( "example.com/pgo/devirtualize/mult.pkg" ) -func BenchmarkDevirt(b *testing.B) { +func BenchmarkDevirtIface(b *testing.B) { var ( a1 Add a2 Sub @@ -25,5 +25,49 @@ func BenchmarkDevirt(b *testing.B) { m2 mult.NegMult ) - Exercise(b.N, a1, a2, m1, m2) + ExerciseIface(b.N, a1, a2, m1, m2) +} + +// Verify that devirtualization doesn't result in calls or side effects applying more than once. +func TestDevirtIface(t *testing.T) { + var ( + a1 Add + a2 Sub + m1 mult.Mult + m2 mult.NegMult + ) + + if v := ExerciseIface(10, a1, a2, m1, m2); v != 1176 { + t.Errorf("ExerciseIface(10) got %d want 1176", v) + } +} + +func BenchmarkDevirtFuncConcrete(b *testing.B) { + ExerciseFuncConcrete(b.N, AddFn, SubFn, mult.MultFn, mult.NegMultFn) +} + +func TestDevirtFuncConcrete(t *testing.T) { + if v := ExerciseFuncConcrete(10, AddFn, SubFn, mult.MultFn, mult.NegMultFn); v != 1176 { + t.Errorf("ExerciseFuncConcrete(10) got %d want 1176", v) + } +} + +func BenchmarkDevirtFuncField(b *testing.B) { + ExerciseFuncField(b.N, AddFn, SubFn, mult.MultFn, mult.NegMultFn) +} + +func TestDevirtFuncField(t *testing.T) { + if v := ExerciseFuncField(10, AddFn, SubFn, mult.MultFn, mult.NegMultFn); v != 1176 { + t.Errorf("ExerciseFuncField(10) got %d want 1176", v) + } +} + +func BenchmarkDevirtFuncClosure(b *testing.B) { + ExerciseFuncClosure(b.N, AddClosure(), SubClosure(), mult.MultClosure(), mult.NegMultClosure()) +} + +func TestDevirtFuncClosure(t *testing.T) { + if v := ExerciseFuncClosure(10, AddClosure(), SubClosure(), mult.MultClosure(), mult.NegMultClosure()); v != 1176 { + t.Errorf("ExerciseFuncClosure(10) got %d want 1176", v) + } } diff --git a/src/cmd/compile/internal/test/testdata/pgo/devirtualize/mult.pkg/mult.go b/src/cmd/compile/internal/test/testdata/pgo/devirtualize/mult.pkg/mult.go index 8a026a52f5..64f405ff9e 100644 --- a/src/cmd/compile/internal/test/testdata/pgo/devirtualize/mult.pkg/mult.go +++ b/src/cmd/compile/internal/test/testdata/pgo/devirtualize/mult.pkg/mult.go @@ -30,3 +30,31 @@ func (NegMult) Multiply(a, b int) int { } return -1 * a * b } + +// N.B. Different types than AddFunc to test intra-line disambiguation. +type MultFunc func(int64, int64) int64 + +func MultFn(a, b int64) int64 { + return a * b +} + +func NegMultFn(a, b int64) int64 { + return -1 * a * b +} + +//go:noinline +func MultClosure() MultFunc { + // Explicit closure to differentiate from AddClosure. + c := 1 + return func(a, b int64) int64 { + return a * b * int64(c) + } +} + +//go:noinline +func NegMultClosure() MultFunc { + c := 1 + return func(a, b int64) int64 { + return -1 * a * b * int64(c) + } +} |
