Skip to content

reflect: allow stack allocation of underlying value for reflect.Value.Interface #71349

@thepudds

Description

@thepudds

Go version

tip

What did you do?

Run this simple test (playground link):

func TestAllocsReflectInterface(t *testing.T) {
	allocs := testing.AllocsPerRun(100, func() {
		v := reflect.ValueOf(Point{x: 1, y: 1})
		_ = v.Interface() // Causes the Point value to escape to heap.
	})
	if allocs != 0 {
		t.Errorf("allocs = %d, want 0", int(allocs))
	}
}

What did you see happen?

    prog_test.go:18: allocs = 1, want 0
--- FAIL: TestAllocsReflectInterface (0.00s)

What did you expect to see?

Ideally, zero heap allocations in this test, and ideally reflect.Value.Interface would allow the underlying value to not always be heap allocated. This affects the fmt package and can affect other users of reflect like serialization libraries. This issue is a spin out from #8618 (comment).

There are currently two reasons the underlying value escapes to the heap:

  1. reflect.packEface causes reflect.Value.Interface to escape its underlying value:
 parameter v leaks to <heap> for packEface with derefs=0:
   flow: <heap> ← v:
     from v.ptr (dot) at .\value.go:145:13
     from e.word = v.ptr (assign) at ./value.go:145:10
  1. Within reflect.Value.Interface and its helpers, method values cause conditional allocation of a methodValue struct, which also causes underlying values of other types to also escape:
 parameter v leaks to <heap> for valueInterface with derefs=0:
   flow: {heap} ← v:
     from makeMethodValue("Interface", v) (call parameter) at ./value.go:1492:22

I sent https://go.dev/cl/528535 to hopefully address the first reason in reflect.packEface.

For addressing the second reason above and handling method values, I sent:

  • https://go.dev/cl/530095, which changes the runtime so that it manually creates the stack map
    for reflect.methodValueCall so that a stack-based methodValue can be tracked, including for example to properly update pointers during a stack copy operation.
  • https://go.dev/cl/530097, which updates reflect.Value.Interface to stack allocate a methodValue if needed and pass a pointer to it down the stack. This is done while side-stepping other reasons the *methodValue might escape, while keeping reflect.Value.Interface within the inlining budget so that a caller of reflect.Value.Interface can stack allocate a methodValue (which then ends up avoiding the need to always store all underlying values in the heap, regardless of whether or not they are a method value).
  • https://go.dev/cl/530096, which updates the compiler's escape analysis to recognize more self-assignment patterns. (This was originally used to help squeak past the inlining budget without escaping, but 530096 might end up not being needed for this issue).

Together, these changes allow the test to pass without heap allocations.


I am hopefully returning to this shortly, and filing this issue to re-summarize the approach and to help with tracking & discussion. (Previously, a short overview was in #8618 (comment), in addition to more details in the individual CLs).

Metadata

Metadata

Assignees

Labels

BugReportIssues describing a possible bug in the Go implementation.NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.Performancecompiler/runtimeIssues related to the Go compiler and/or runtime.

Type

No type

Projects

Status

Todo

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions