]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/compile/internal/walk: convert composite literals to interfaces without allocating
authorthepudds <thepudds1460@gmail.com>
Fri, 14 Feb 2025 04:49:50 +0000 (23:49 -0500)
committerDavid Chase <drchase@google.com>
Wed, 21 May 2025 19:23:26 +0000 (12:23 -0700)
Today, this interface conversion causes the struct literal
to be heap allocated:

    var sink any

    func example1() {
        sink = S{1, 1}
    }

For basic literals like integers that are directly used in
an interface conversion that would otherwise allocate, the compiler
is able to use read-only global storage (see #18704).

This CL extends that to struct and array literals as well by creating
read-only global storage that is able to represent for example S{1, 1},
and then using a pointer to that storage in the interface
when the interface conversion happens.

A more challenging example is:

    func example2() {
        v := S{1, 1}
        sink = v
    }

In this case, the struct literal is not directly part of the
interface conversion, but is instead assigned to a local variable.

To still avoid heap allocation in cases like this, in walk we
construct a cache that maps from expressions used in interface
conversions to earlier expressions that can be used to represent the
same value (via ir.ReassignOracle.StaticValue). This is somewhat
analogous to how we avoided heap allocation for basic literals in
CL 649077 earlier in our stack, though here we also need to do a
little more work to create the read-only global.

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

See the writeup in #71359 for details.

Fixes #71359
Fixes #71323
Updates #62653
Updates #53465
Updates #8618

Change-Id: I8924f0c69ff738ea33439bd6af7b4066af493b90
Reviewed-on: https://go-review.googlesource.com/c/go/+/649555
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Keith Randall <khr@golang.org>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Keith Randall <khr@google.com>
src/cmd/compile/internal/walk/order.go
src/cmd/compile/internal/walk/walk.go
src/fmt/fmt_test.go
test/codegen/zerosize.go
test/escape_iface_data.go
test/live.go
test/live_regabi.go

index 8967b7dbba4840833b3f6043914a542d640e22ba..af3bfcbac631cede8f6dc19af45eff79a7b9669d 100644 (file)
@@ -226,7 +226,8 @@ func (o *orderState) addrTemp(n ir.Node) ir.Node {
        // for the implicit conversion of "foo" to any, and we can't handle
        // the relocations in that temp.
        if n.Op() == ir.ONIL || (n.Op() == ir.OLITERAL && !base.Ctxt.IsFIPS()) {
-               // TODO: expand this to all static composite literal nodes?
+               // This is a basic literal or nil that we can store
+               // directly in the read-only data section.
                n = typecheck.DefaultLit(n, nil)
                types.CalcSize(n.Type())
                vstat := readonlystaticname(n.Type())
@@ -239,6 +240,21 @@ func (o *orderState) addrTemp(n ir.Node) ir.Node {
                return vstat
        }
 
+       // Check now for a composite literal to possibly store
+       // in the read-only data section.
+       v := staticValue(n)
+       if v == nil {
+               v = n
+       }
+       if (v.Op() == ir.OSTRUCTLIT || v.Op() == ir.OARRAYLIT) && isStaticCompositeLiteral(v) && !base.Ctxt.IsFIPS() {
+               // v can be directly represented in the read-only data section.
+               lit := v.(*ir.CompLitExpr)
+               vstat := readonlystaticname(lit.Type())
+               fixedlit(inInitFunction, initKindStatic, lit, vstat, nil) // nil init
+               vstat = typecheck.Expr(vstat).(*ir.Name)
+               return vstat
+       }
+
        // Prevent taking the address of an SSA-able local variable (#63332).
        //
        // TODO(mdempsky): Note that OuterValue unwraps OCONVNOPs, but
@@ -337,8 +353,8 @@ func (o *orderState) mapKeyTemp(outerPos src.XPos, t *types.Type, n ir.Node) ir.
 //
 // Note that this code does not handle the case:
 //
-//      s := string(k)
-//      x = m[s]
+//     s := string(k)
+//     x = m[s]
 //
 // Cases like this are handled during SSA, search for slicebytetostring
 // in ../ssa/_gen/generic.rules.
index 25e03359fd69be512442228dd002d64d0bfcce04..2fa51f1280936252afa7427b7a021ed981254c0d 100644 (file)
@@ -24,6 +24,13 @@ const tmpstringbufsize = 32
 
 func Walk(fn *ir.Func) {
        ir.CurFunc = fn
+
+       // Set and then clear a package-level cache of static values for this fn.
+       // (At some point, it might be worthwhile to have a walkState structure
+       // that gets passed everywhere where things like this can go.)
+       staticValues = findStaticValues(fn)
+       defer func() { staticValues = nil }()
+
        errorsBefore := base.Errors()
        order(fn)
        if base.Errors() > errorsBefore {
@@ -422,3 +429,43 @@ func ifaceData(pos src.XPos, n ir.Node, t *types.Type) ir.Node {
        ind.SetBounded(true)
        return ind
 }
+
+// staticValue returns the earliest expression it can find that always
+// evaluates to n, with similar semantics to [ir.StaticValue].
+//
+// It only returns results for the ir.CurFunc being processed in [Walk],
+// including its closures, and uses a cache to reduce duplicative work.
+// It can return n or nil if it does not find an earlier expression.
+//
+// The current use case is reducing OCONVIFACE allocations, and hence
+// staticValue is currently only useful when given an *ir.ConvExpr.X as n.
+func staticValue(n ir.Node) ir.Node {
+       if staticValues == nil {
+               base.Fatalf("staticValues is nil. staticValue called outside of walk.Walk?")
+       }
+       return staticValues[n]
+}
+
+// staticValues is a cache of static values for use by staticValue.
+var staticValues map[ir.Node]ir.Node
+
+// findStaticValues returns a map of static values for fn.
+func findStaticValues(fn *ir.Func) map[ir.Node]ir.Node {
+       // We can't use an ir.ReassignOracle or ir.StaticValue in the
+       // middle of walk because they don't currently handle
+       // transformed assignments (e.g., will complain about 'RHS == nil').
+       // So we instead build this map to use in walk.
+       ro := &ir.ReassignOracle{}
+       ro.Init(fn)
+       m := make(map[ir.Node]ir.Node)
+       ir.Visit(fn, func(n ir.Node) {
+               if n.Op() == ir.OCONVIFACE {
+                       x := n.(*ir.ConvExpr).X
+                       v := ro.StaticValue(x)
+                       if v != nil && v != x {
+                               m[x] = v
+                       }
+               }
+       })
+       return m
+}
index a896b8fe24088ea9e2489149feb3c8df46e44ca0..86e458ae6481fbf68176f875e54b48595e35b3ca 100644 (file)
@@ -1502,7 +1502,7 @@ var mallocTest = []struct {
        {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}) }},
+       {0, `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.
index ecf3305461757866021dee4522662511e64d8a05..86c48194008cf2c0ba00ccc9440786d338ff6627 100644 (file)
@@ -18,8 +18,27 @@ func zeroSize() {
        g(&s, 1, 2, 3, 4, 5)
 
        // amd64:`LEAQ\tcommand-line-arguments\..*\+55\(SP\)`
+       c <- noliteral(struct{}{})
+}
+
+// Like zeroSize, but without hiding the zero-sized struct.
+func zeroSize2() {
+       c := make(chan struct{})
+       // amd64:`MOVQ\t\$0, command-line-arguments\.s\+48\(SP\)`
+       var s *int
+       // force s to be a stack object, also use some (fixed) stack space
+       g(&s, 1, 2, 3, 4, 5)
+
+       // amd64:`LEAQ\tcommand-line-arguments\..*stmp_\d+\(SB\)`
        c <- struct{}{}
 }
 
 //go:noinline
 func g(**int, int, int, int, int, int) {}
+
+// noliteral prevents the compiler from recognizing a literal value.
+//
+//go:noinline
+func noliteral[T any](t T) T {
+       return t
+}
index 46814f3a9fdf744506ae4e885f5b231b14813662..fd993fb892a1a648ba4a153f34552a895b95f211 100644 (file)
@@ -208,32 +208,32 @@ func named7b(v MyInt) {
 type S struct{ a, b int64 }
 
 func struct1() {
-       sink = S{1, 1}
+       sink = S{1, 1} // ERROR "using global for interface value"
 }
 
 func struct2() {
        v := S{1, 1}
-       sink = v
+       sink = v // ERROR "using global for interface value"
 }
 
 func struct3() {
-       sink = S{}
+       sink = S{} // ERROR "using global for interface value"
 }
 
 func struct4() {
        v := S{}
-       sink = v
+       sink = v // ERROR "using global for interface value"
 }
 
 func struct5() {
-       var a any = S{1, 1} // ERROR "using stack temporary for interface value"
+       var a any = S{1, 1} // ERROR "using global for interface value"
        _ = a
 }
 
 func struct6() {
        var a any
        v := S{1, 1}
-       a = v // ERROR "using stack temporary for interface value"
+       a = v // ERROR "using global for interface value"
        _ = a
 }
 
index c0b0fcd274a0cac048b28a1470d6073216dbdca0..46e5e3e757cd60578c22c46e3ae100ce302b981a 100644 (file)
@@ -337,23 +337,47 @@ func f20() {
        ch <- byteptr()
 }
 
-func f21() {
+func f21(x, y string) { // ERROR "live at entry to f21: x y"
        // key temporary for mapaccess using array literal key.
        var z *byte
        if b {
-               z = m2[[2]string{"x", "y"}] // ERROR "stack object .autotmp_[0-9]+ \[2\]string$"
+               z = m2[[2]string{x, y}] // ERROR "stack object .autotmp_[0-9]+ \[2\]string$"
        }
        z = m2[[2]string{"x", "y"}]
        z = m2[[2]string{"x", "y"}]
        printbytepointer(z)
 }
 
-func f23() {
+func f21b() {
+       // key temporary for mapaccess using array literal key.
+       var z *byte
+       if b {
+               z = m2[[2]string{"x", "y"}]
+       }
+       z = m2[[2]string{"x", "y"}]
+       z = m2[[2]string{"x", "y"}]
+       printbytepointer(z)
+}
+
+func f23(x, y string) { // ERROR "live at entry to f23: x y"
+       // key temporary for two-result map access using array literal key.
+       var z *byte
+       var ok bool
+       if b {
+               z, ok = m2[[2]string{x, y}] // ERROR "stack object .autotmp_[0-9]+ \[2\]string$"
+       }
+       z, ok = m2[[2]string{"x", "y"}]
+       z, ok = m2[[2]string{"x", "y"}]
+       printbytepointer(z)
+       print(ok)
+}
+
+func f23b() {
        // key temporary for two-result map access using array literal key.
        var z *byte
        var ok bool
        if b {
-               z, ok = m2[[2]string{"x", "y"}] // ERROR "stack object .autotmp_[0-9]+ \[2\]string$"
+               z, ok = m2[[2]string{"x", "y"}]
        }
        z, ok = m2[[2]string{"x", "y"}]
        z, ok = m2[[2]string{"x", "y"}]
@@ -361,11 +385,21 @@ func f23() {
        print(ok)
 }
 
-func f24() {
+func f24(x, y string) { // ERROR "live at entry to f24: x y"
+       // key temporary for map access using array literal key.
+       // value temporary too.
+       if b {
+               m2[[2]string{x, y}] = nil // ERROR "stack object .autotmp_[0-9]+ \[2\]string$"
+       }
+       m2[[2]string{"x", "y"}] = nil
+       m2[[2]string{"x", "y"}] = nil
+}
+
+func f24b() {
        // key temporary for map access using array literal key.
        // value temporary too.
        if b {
-               m2[[2]string{"x", "y"}] = nil // ERROR "stack object .autotmp_[0-9]+ \[2\]string$"
+               m2[[2]string{"x", "y"}] = nil
        }
        m2[[2]string{"x", "y"}] = nil
        m2[[2]string{"x", "y"}] = nil
index 35f874ecc365f679bf0723920026eedba498c977..ddb4caed1a53e69be00926b192c51a3ebaf8e885 100644 (file)
@@ -335,23 +335,34 @@ func f20() {
        ch <- byteptr()
 }
 
-func f21() {
+func f21(x, y string) { // ERROR "live at entry to f21: x y"
        // key temporary for mapaccess using array literal key.
        var z *byte
        if b {
-               z = m2[[2]string{"x", "y"}] // ERROR "stack object .autotmp_[0-9]+ \[2\]string$"
+               z = m2[[2]string{x, y}] // ERROR "stack object .autotmp_[0-9]+ \[2\]string$"
        }
        z = m2[[2]string{"x", "y"}]
        z = m2[[2]string{"x", "y"}]
        printbytepointer(z)
 }
 
-func f23() {
+func f21b() {
+       // key temporary for mapaccess using array literal key.
+       var z *byte
+       if b {
+               z = m2[[2]string{"x", "y"}]
+       }
+       z = m2[[2]string{"x", "y"}]
+       z = m2[[2]string{"x", "y"}]
+       printbytepointer(z)
+}
+
+func f23(x, y string) { // ERROR "live at entry to f23: x y"
        // key temporary for two-result map access using array literal key.
        var z *byte
        var ok bool
        if b {
-               z, ok = m2[[2]string{"x", "y"}] // ERROR "stack object .autotmp_[0-9]+ \[2\]string$"
+               z, ok = m2[[2]string{x, y}] // ERROR "stack object .autotmp_[0-9]+ \[2\]string$"
        }
        z, ok = m2[[2]string{"x", "y"}]
        z, ok = m2[[2]string{"x", "y"}]
@@ -359,11 +370,34 @@ func f23() {
        print(ok)
 }
 
-func f24() {
+func f23b() {
+       // key temporary for two-result map access using array literal key.
+       var z *byte
+       var ok bool
+       if b {
+               z, ok = m2[[2]string{"x", "y"}]
+       }
+       z, ok = m2[[2]string{"x", "y"}]
+       z, ok = m2[[2]string{"x", "y"}]
+       printbytepointer(z)
+       print(ok)
+}
+
+func f24(x, y string) { // ERROR "live at entry to f24: x y"
+       // key temporary for map access using array lit3ral key.
+       // value temporary too.
+       if b {
+               m2[[2]string{x, y}] = nil // ERROR "stack object .autotmp_[0-9]+ \[2\]string$"
+       }
+       m2[[2]string{"x", "y"}] = nil
+       m2[[2]string{"x", "y"}] = nil
+}
+
+func f24b() {
        // key temporary for map access using array literal key.
        // value temporary too.
        if b {
-               m2[[2]string{"x", "y"}] = nil // ERROR "stack object .autotmp_[0-9]+ \[2\]string$"
+               m2[[2]string{"x", "y"}] = nil
        }
        m2[[2]string{"x", "y"}] = nil
        m2[[2]string{"x", "y"}] = nil