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 <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>
This commit is contained in:
thepudds 2025-02-13 23:49:50 -05:00 committed by David Chase
parent ce46c9db86
commit f4de2ecffb
7 changed files with 172 additions and 22 deletions

View 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.

View 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
}

View 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.

View 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
}

View 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
}

View file

@ -337,23 +337,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"}]
@ -361,11 +372,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 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 // 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
}
m2[[2]string{"x", "y"}] = nil
m2[[2]string{"x", "y"}] = nil

View 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