mirror of
https://github.com/golang/go.git
synced 2025-10-19 11:03:18 +00:00
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:
parent
ce46c9db86
commit
f4de2ecffb
7 changed files with 172 additions and 22 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
46
test/live.go
46
test/live.go
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue