From: thepudds Date: Fri, 14 Feb 2025 04:49:50 +0000 (-0500) Subject: cmd/compile/internal/walk: convert composite literals to interfaces without allocating X-Git-Tag: go1.25rc1~127 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=f4de2ecffb9c107e6058c1ebb30c68de1157f5c6;p=gostls13.git cmd/compile/internal/walk: convert composite literals to interfaces without allocating 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 Reviewed-by: Keith Randall Reviewed-by: David Chase Reviewed-by: Keith Randall --- diff --git a/src/cmd/compile/internal/walk/order.go b/src/cmd/compile/internal/walk/order.go index 8967b7dbba..af3bfcbac6 100644 --- a/src/cmd/compile/internal/walk/order.go +++ b/src/cmd/compile/internal/walk/order.go @@ -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. diff --git a/src/cmd/compile/internal/walk/walk.go b/src/cmd/compile/internal/walk/walk.go index 25e03359fd..2fa51f1280 100644 --- a/src/cmd/compile/internal/walk/walk.go +++ b/src/cmd/compile/internal/walk/walk.go @@ -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 +} diff --git a/src/fmt/fmt_test.go b/src/fmt/fmt_test.go index a896b8fe24..86e458ae64 100644 --- a/src/fmt/fmt_test.go +++ b/src/fmt/fmt_test.go @@ -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. diff --git a/test/codegen/zerosize.go b/test/codegen/zerosize.go index ecf3305461..86c4819400 100644 --- a/test/codegen/zerosize.go +++ b/test/codegen/zerosize.go @@ -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 +} diff --git a/test/escape_iface_data.go b/test/escape_iface_data.go index 46814f3a9f..fd993fb892 100644 --- a/test/escape_iface_data.go +++ b/test/escape_iface_data.go @@ -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 } diff --git a/test/live.go b/test/live.go index c0b0fcd274..46e5e3e757 100644 --- a/test/live.go +++ b/test/live.go @@ -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 diff --git a/test/live_regabi.go b/test/live_regabi.go index 35f874ecc3..ddb4caed1a 100644 --- a/test/live_regabi.go +++ b/test/live_regabi.go @@ -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