]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/compile/internal/escape: propagate constants to interface conversions to avoid...
authorthepudds <thepudds1460@gmail.com>
Wed, 12 Feb 2025 23:55:04 +0000 (18:55 -0500)
committerGopher Robot <gobot@golang.org>
Wed, 21 May 2025 19:02:43 +0000 (12:02 -0700)
Currently, the integer value in the following interface conversion gets
heap allocated:

   v := 1000
   fmt.Println(v)

In contrast, this conversion does not currently cause the integer value
to be heap allocated:

   fmt.Println(1000)

The second example is able to avoid heap allocation because of an
optimization in walk (by Josh in #18704 and related issues) that
recognizes a literal is being used. In the first example, that
optimization is currently thwarted by the literal getting assigned
to a local variable prior to use in the interface conversion.

This CL propagates constants to interface conversions like
in the first example to avoid heap allocations, instead using
a read-only global. The net effect is roughly turning the first example
into the second.

One place this comes up in practice currently is with logging or
debug prints. For example, if we have something like:

   func conditionalDebugf(format string, args ...interface{}) {
    if debugEnabled {
    fmt.Fprintf(io.Discard, format, args...)
    }
   }

Prior to this CL, this integer is heap allocated, even when the
debugEnabled flag is false, and even when the compiler
inlines conditionalDebugf:

   v := 1000
   conditionalDebugf("hello %d", v)

With this CL, the integer here is no longer heap allocated, even when
the debugEnabled flag is enabled, because the compiler can now see that
it can use a read-only global.

See the writeup in #71359 for more details.

CL 649076 (earlier in our stack) added most of the tests
along with debug diagnostics in convert.go to make it easier
to test this change.

Updates #71359
Updates #62653
Updates #53465
Updates #8618

Change-Id: I19a51e74b36576ebb0b9cf599267cbd2bd847ce4
Reviewed-on: https://go-review.googlesource.com/c/go/+/649079
Auto-Submit: Keith Randall <khr@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Keith Randall <khr@google.com>
src/cmd/compile/internal/escape/escape.go
src/fmt/fmt_test.go
src/log/slog/logger_test.go
test/escape5.go
test/escape_iface.go
test/escape_iface_data.go
test/fixedbugs/issue12006.go
test/fixedbugs/issue30898.go

index 43fe0b8af54750ea55d4e0d8e5d861e868faa519..06dee7ec41bf9204d9acef9aa0461b6d8e991b2e 100644 (file)
@@ -531,8 +531,11 @@ func (b *batch) rewriteWithLiterals(n ir.Node, fn *ir.Func) {
        if n == nil || fn == nil {
                return
        }
-       if n.Op() != ir.OMAKESLICE {
-               // TODO(thepudds): we handle more cases later in our CL stack.
+       if n.Op() != ir.OMAKESLICE && n.Op() != ir.OCONVIFACE {
+               return
+       }
+       if base.Flag.Cfg.CoverageInfo != nil {
+               // Avoid altering coverage results.
                return
        }
 
@@ -562,6 +565,21 @@ func (b *batch) rewriteWithLiterals(n ir.Node, fn *ir.Func) {
                                *r = lit
                        }
                }
+       case ir.OCONVIFACE:
+               // Check if we can replace a non-constant expression in an interface conversion with
+               // a literal to avoid heap allocating the underlying interface value.
+               conv := n.(*ir.ConvExpr)
+               if conv.X.Op() != ir.OLITERAL && !conv.X.Type().IsInterface() {
+                       v := ro.StaticValue(conv.X)
+                       if v != nil && v.Op() == ir.OLITERAL && ir.ValidTypeForConst(conv.X.Type(), v.Val()) {
+                               if base.Debug.EscapeDebug >= 3 {
+                                       base.WarnfAt(n.Pos(), "rewriting OCONVIFACE value from %v (%v) to %v (%v)", conv.X, conv.X.Type(), v, v.Type())
+                               }
+                               v := v.(*ir.BasicLit)
+                               conv.X = ir.NewBasicLit(conv.X.Pos(), conv.X.Type(), v.Val())
+                               typecheck.Expr(conv)
+                       }
+               }
        }
 }
 
index 82daf6277178145c96e81c51c33b0bf9b2876615..a896b8fe24088ea9e2489149feb3c8df46e44ca0 100644 (file)
@@ -1495,11 +1495,15 @@ var mallocTest = []struct {
        {1, `Sprintf("%x %x")`, func() { _ = Sprintf("%x %x", 7, 112) }},
        {1, `Sprintf("%g")`, func() { _ = Sprintf("%g", float32(3.14159)) }},
        {0, `Fprintf(buf, "%s")`, func() { mallocBuf.Reset(); Fprintf(&mallocBuf, "%s", "hello") }},
+       {0, `Fprintf(buf, "%s")`, func() { mallocBuf.Reset(); s := "hello"; Fprintf(&mallocBuf, "%s", s) }},
+       {1, `Fprintf(buf, "%s")`, func() { mallocBuf.Reset(); s := "hello"; Fprintf(&mallocBuf, "%s", noliteral(s)) }},
        {0, `Fprintf(buf, "%x")`, func() { mallocBuf.Reset(); Fprintf(&mallocBuf, "%x", 7) }},
        {0, `Fprintf(buf, "%x")`, func() { mallocBuf.Reset(); Fprintf(&mallocBuf, "%x", 1<<16) }},
-       {1, `Fprintf(buf, "%x")`, func() { mallocBuf.Reset(); i := 1 << 16; Fprintf(&mallocBuf, "%x", i) }}, // not constant
+       {0, `Fprintf(buf, "%x")`, func() { mallocBuf.Reset(); i := 1 << 16; Fprintf(&mallocBuf, "%x", i) }},
+       {1, `Fprintf(buf, "%x")`, func() { mallocBuf.Reset(); i := 1 << 16; Fprintf(&mallocBuf, "%x", noliteral(i)) }},
        {4, `Fprintf(buf, "%v")`, func() { mallocBuf.Reset(); s := []int{1, 2}; Fprintf(&mallocBuf, "%v", s) }},
        {1, `Fprintf(buf, "%v")`, func() { mallocBuf.Reset(); type P struct{ x, y int }; Fprintf(&mallocBuf, "%v", P{1, 2}) }},
+       {1, `Fprintf(buf, "%v")`, func() { mallocBuf.Reset(); type P struct{ x, y int }; Fprintf(&mallocBuf, "%v", noliteral(P{1, 2})) }},
        {2, `Fprintf(buf, "%80000s")`, func() { mallocBuf.Reset(); Fprintf(&mallocBuf, "%80000s", "hello") }}, // large buffer (>64KB)
        // If the interface value doesn't need to allocate, amortized allocation overhead should be zero.
        {0, `Fprintf(buf, "%x %x %x")`, func() {
@@ -1519,8 +1523,8 @@ func TestCountMallocs(t *testing.T) {
        }
        for _, mt := range mallocTest {
                mallocs := testing.AllocsPerRun(100, mt.fn)
-               if got, max := mallocs, float64(mt.count); got > max {
-                       t.Errorf("%s: got %v allocs, want <=%v", mt.desc, got, max)
+               if got, max := mallocs, float64(mt.count); got != max {
+                       t.Errorf("%s: got %v allocs, want %v", mt.desc, got, max)
                }
        }
 }
@@ -2010,3 +2014,10 @@ func TestAppendln(t *testing.T) {
                t.Fatalf("Appendln allocated a new slice")
        }
 }
+
+// noliteral prevents escape analysis from recognizing a literal value.
+//
+//go:noinline
+func noliteral[T any](t T) T {
+       return t
+}
index 558aecaf6e1a41f543e5d8fc8429a7f0f2883221..63595504fe4fa2d4c0f16d4939e61e65673cf2e8 100644 (file)
@@ -349,7 +349,7 @@ func TestAlloc(t *testing.T) {
        t.Run("2 pairs", func(t *testing.T) {
                s := "abc"
                i := 2000
-               wantAllocs(t, 2, func() {
+               wantAllocs(t, 0, func() {
                        dl.Info("hello",
                                "n", i,
                                "s", s,
@@ -360,7 +360,7 @@ func TestAlloc(t *testing.T) {
                l := New(DiscardHandler)
                s := "abc"
                i := 2000
-               wantAllocs(t, 2, func() {
+               wantAllocs(t, 0, func() {
                        l.Log(ctx, LevelInfo, "hello",
                                "n", i,
                                "s", s,
@@ -384,7 +384,7 @@ func TestAlloc(t *testing.T) {
                s := "abc"
                i := 2000
                d := time.Second
-               wantAllocs(t, 10, func() {
+               wantAllocs(t, 1, func() {
                        dl.Info("hello",
                                "n", i, "s", s, "d", d,
                                "n", i, "s", s, "d", d,
index 133d973ba5d5f8290d7f2746f6a2c28918350a79..2ed8b5789ddd7b24d28ba5aadcd8fb8f9aa00645 100644 (file)
@@ -252,7 +252,7 @@ func f29000(_ int, x interface{}) { // ERROR "leaking param: x"
 
 func g29000() {
        x := 1
-       f29000(2, x) // ERROR "x escapes to heap"
+       f29000(2, x) // ERROR "1 escapes to heap"
 }
 
 // Issue 28369: taking an address of a parameter and converting it into a uintptr causes an
index d822cca2f8485fa6b263cbb2db1599c9fcd4e0e1..78b5209a629f3a69419abf93f83f20fc1d94d6b1 100644 (file)
@@ -228,8 +228,8 @@ func dotTypeEscape2() { // #13805, #15796
                j := 0
                var v int
                var ok bool
-               var x interface{} = i // ERROR "i does not escape"
-               var y interface{} = j // ERROR "j does not escape"
+               var x interface{} = i // ERROR "0 does not escape"
+               var y interface{} = j // ERROR "0 does not escape"
 
                *(&v) = x.(int)
                *(&v), *(&ok) = y.(int)
@@ -238,8 +238,8 @@ func dotTypeEscape2() { // #13805, #15796
                i := 0
                j := 0
                var ok bool
-               var x interface{} = i // ERROR "i does not escape"
-               var y interface{} = j // ERROR "j does not escape"
+               var x interface{} = i // ERROR "0 does not escape"
+               var y interface{} = j // ERROR "0 does not escape"
 
                sink = x.(int)         // ERROR "x.\(int\) escapes to heap"
                sink, *(&ok) = y.(int) // ERROR "autotmp_.* escapes to heap"
index 556be2067c6ecba816fd6c266a68358f0ab987db..46814f3a9fdf744506ae4e885f5b231b14813662 100644 (file)
@@ -17,7 +17,7 @@ func string1() {
 
 func string2() {
        v := "abc"
-       sink = v
+       sink = v // ERROR "using global for interface value"
 }
 
 func string3() {
@@ -26,7 +26,7 @@ func string3() {
 
 func string4() {
        v := ""
-       sink = v
+       sink = v // ERROR "using global for interface value"
 }
 
 func string5() {
@@ -36,8 +36,8 @@ func string5() {
 
 func string6() {
        var a any
-       v := "abc" // ERROR "using stack temporary for interface value"
-       a = v
+       v := "abc"
+       a = v // ERROR "using global for interface value"
        _ = a
 }
 
@@ -49,7 +49,7 @@ func string7(v string) {
 func string8() {
        v0 := "abc"
        v := v0
-       string7(v)
+       string7(v) // ERROR "using global for interface value"
 }
 
 func string9() {
@@ -58,7 +58,7 @@ func string9() {
        f := func() {
                string7(v)
        }
-       f()
+       f() // ERROR "using global for interface value"
 }
 
 func string10() {
@@ -70,14 +70,14 @@ func string10() {
                }
                f2()
        }
-       f()
+       f() // ERROR "using global for interface value"
 }
 
 func string11() {
        v0 := "abc"
        v := v0
        defer func() {
-               string7(v)
+               string7(v) // ERROR "using global for interface value"
        }()
 }
 
@@ -87,7 +87,7 @@ func integer1() {
 
 func integer2() {
        v := 42
-       sink = v
+       sink = v // ERROR "using global for interface value"
 }
 
 func integer3() {
@@ -96,7 +96,7 @@ func integer3() {
 
 func integer4a() {
        v := 0
-       sink = v
+       sink = v // ERROR "using global for interface value"
 }
 
 func integer4b() {
@@ -111,8 +111,8 @@ func integer5() {
 
 func integer6() {
        var a any
-       v := 42 // ERROR "using stack temporary for interface value"
-       a = v
+       v := 42
+       a = v // ERROR "using global for interface value"
        _ = a
 }
 
@@ -140,24 +140,22 @@ func named1b() {
 
 func named2a() {
        v := MyInt(0)
-       sink = v
+       sink = v // ERROR "using global for interface value"
 }
 
 func named2b() {
        v := MyInt(42)
-       escapes(v)
+       escapes(v) // ERROR "using global for interface value"
 }
 
-// Unfortunate: we currently require matching types, which we could relax.
 func named2c() {
        v := 42
-       sink = MyInt(v)
+       sink = MyInt(v) // ERROR "using global for interface value"
 }
 
-// Unfortunate: we currently require matching types, which we could relax.
 func named2d() {
        v := 42
-       escapes(MyInt(v))
+       escapes(MyInt(v)) // ERROR "using global for interface value"
 }
 func named3a() {
        sink = MyInt(42) // ERROR "using global for interface value"
@@ -169,22 +167,22 @@ func named3b() {
 
 func named4a() {
        v := MyInt(0)
-       sink = v
+       sink = v // ERROR "using global for interface value"
 }
 
 func named4b() {
        v := MyInt(0)
-       escapes(v)
+       escapes(v) // ERROR "using global for interface value"
 }
 
 func named4c() {
        v := 0
-       sink = MyInt(v)
+       sink = MyInt(v) // ERROR "using global for interface value"
 }
 
 func named4d() {
        v := 0
-       escapes(MyInt(v))
+       escapes(MyInt(v)) // ERROR "using global for interface value"
 }
 
 func named5() {
@@ -194,8 +192,8 @@ func named5() {
 
 func named6() {
        var a any
-       v := MyInt(42) // ERROR "using stack temporary for interface value"
-       a = v
+       v := MyInt(42)
+       a = v // ERROR "using global for interface value"
        _ = a
 }
 
@@ -255,3 +253,45 @@ func emptyStruct2() {
 func emptyStruct3(v struct{}) { // ERROR "using global for zero-sized interface value"
        sink = v
 }
+
+// Some light emulation of conditional debug printing (such as in #53465).
+
+func Printf(format string, args ...any) {
+       for _, arg := range args {
+               sink = arg
+       }
+}
+
+var enabled = true
+
+func debugf(format string, args ...interface{}) {
+       if enabled {
+               Printf(format, args...)
+       }
+}
+
+//go:noinline
+func debugf2(format string, args ...interface{}) {
+       if enabled {
+               Printf(format, args...)
+       }
+}
+
+func f1() {
+       v := 1000
+       debugf("hello %d", v) // ERROR "using global for interface value"
+}
+
+func f2() {
+       v := 1000
+       debugf2("hello %d", v) // ERROR "using global for interface value"
+}
+
+//go:noinline
+func f3(i int) {
+       debugf("hello %d", i)
+}
+
+func f4() {
+       f3(1000)
+}
index 045ed043bbf0637925be9023d87a6a509fba4a8c..94ff52442cdfdffffdc6586bac008cb18987a2b1 100644 (file)
@@ -84,7 +84,7 @@ func TFooI() {
        a := int32(1) // ERROR "moved to heap: a"
        b := "cat"
        c := &a
-       FooI(a, b, c) // ERROR "a escapes to heap" "b escapes to heap" "... argument does not escape"
+       FooI(a, b, c) // ERROR "a escapes to heap" ".cat. escapes to heap" "... argument does not escape"
 }
 
 func FooJ(args ...interface{}) *int32 { // ERROR "leaking param: args to result ~r0 level=1"
@@ -108,14 +108,14 @@ func TFooJ1() {
        a := int32(1)
        b := "cat"
        c := &a
-       FooJ(a, b, c) // ERROR "a does not escape" "b does not escape" "... argument does not escape"
+       FooJ(a, b, c) // ERROR "a does not escape" ".cat. does not escape" "... argument does not escape"
 }
 
 func TFooJ2() {
        a := int32(1) // ERROR "moved to heap: a"
        b := "cat"
        c := &a
-       isink = FooJ(a, b, c) // ERROR "a escapes to heap" "b escapes to heap" "... argument does not escape"
+       isink = FooJ(a, b, c) // ERROR "a escapes to heap" ".cat. escapes to heap" "... argument does not escape"
 }
 
 type fakeSlice struct {
@@ -144,7 +144,7 @@ func TFooK2() {
        a := int32(1) // ERROR "moved to heap: a"
        b := "cat"
        c := &a
-       fs := fakeSlice{3, &[4]interface{}{a, b, c, nil}} // ERROR "a escapes to heap" "b escapes to heap" "&\[4\]interface {}{...} does not escape"
+       fs := fakeSlice{3, &[4]interface{}{a, b, c, nil}} // ERROR "a escapes to heap" ".cat. escapes to heap" "&\[4\]interface {}{...} does not escape"
        isink = FooK(fs)
 }
 
@@ -169,6 +169,6 @@ func TFooL2() {
        a := int32(1) // ERROR "moved to heap: a"
        b := "cat"
        c := &a
-       s := []interface{}{a, b, c} // ERROR "a escapes to heap" "b escapes to heap" "\[\]interface {}{...} does not escape"
+       s := []interface{}{a, b, c} // ERROR "a escapes to heap" ".cat. escapes to heap" "\[\]interface {}{...} does not escape"
        isink = FooL(s)
 }
index c7f6f2d3712b3cf507cbc8d1338a4604d99e5c47..38358949e23799bb11ce0081fb6349dc6729ef5e 100644 (file)
@@ -15,5 +15,5 @@ func debugf(format string, args ...interface{}) { // ERROR "can inline debugf" "
 
 func bar() { // ERROR "can inline bar"
        value := 10
-       debugf("value is %d", value) // ERROR "inlining call to debugf" "value does not escape" "\.\.\. argument does not escape"
+       debugf("value is %d", value) // ERROR "inlining call to debugf" "10 does not escape" "\.\.\. argument does not escape"
 }