-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
reflect: add reflect.Value.Grow #48000
Comments
As noted in #32424 (comment) an alternative could be to define |
That seems like a defect in the escape analysis. Shouldn't the |
@bcmills: Maybe? The slice header allocation occurs in I imagine that in order for the compiler to optimize the allocation of |
Why does AFAICT it just needs the escape analysis to be parametric: “ |
I can see roughly how what you describe might work.
Is this a decision made at compile time or run time? |
I'm assuming it would be made at compile time, although in principle it could be made at run time (at the cost of some branch-predictor resources). |
Is there an issue for such an optimization? I recall #18822, which is somewhat related, but about escape analysis of input argument (rather than return arguments), and also seems to suggest a compile-based approach whether the function is compiled in two ways, one that assumes the argument escapes, and one where it doesn't. In general, an optimization of this nature sounds complex, and historically (and understandably) such changes take a long time to land (if ever). I'm certain that it will provide wide benefit to most Go code. On the other hand, an API change as suggested here can provide benefit within a single Go release at the cost of newly added API surface. |
I just found out about https://blog.filippo.io/efficient-go-apis-with-the-inliner/, it might be worth seeing if we can use that technique in this situation. |
Writing x.Append(y) would be strange, since append(x, y) does not do what you would hope. |
I'm a little confused; that sentiment seems to go against the recently accepted |
What about: // v must be a slice, and v.CanSet() must return true
func (v Value) MakeSlice(len, cap int) I believe this would address the remaining issues that prevent writing extra-allocation-free (de)serialization code. Code that knows the length could allocate and decode the slice in-place (for example, to a struct field), instead of boxing the header with Code that doesn't know the length could append to a buffer of |
@matt0xFF: Interesting idea. It's not clear to me whether // Grow extends the capacity of the current slice by at least n elements.
// The resulting capacity may exceed the current capacity plus n.
// The value must be a settable slice.
func (v Value) Grow(n int) |
@dsnet: The next step is presumably setting the values via I don't know if Other than that I like it better, it solves the same issue without being as redundant with the current API. |
It looks like // Resize sets the slice length to len. If len is greater than the capacity, the existing
// elements will be copied to a new underlying array. Any new trailing elements will be
// set to the zero value of the element type. It panics if v is not a settable slice.
func (v Value) Resize(len int) In terms of the slice capacity, it could behave like this (of course without all the extra allocations): if needed := len-v.Len(); needed > 0 {
emptySlice := reflect.MakeSlice(v.Type(), needed, needed)
newSlice := reflect.Append(v, emptySlice)
v.Set(newSlice)
} Looking at the current implementation, newSlice := reflect.MakeSlice(sliceV.Type(), count, count)
sliceV.Set(newSlice) with sliceV.Resize(count) Which would be equivalent and avoid boxing the slice header. For decoders that don't know the length, this code: for moreObjects {
newValue := decoder.decodeValue()
appendResult := reflect.Append(sliceV, newValue)
sliceV.Set(appendResult)
} could be replaced with: for count := 1; moreObjects; count++ {
sliceV.Resize(count)
newElem := sliceV.Index(count-1)
decoder.decodeToValue(newElem)
} which would avoid boxing (the slice header and value each time through the loop) in that path too. |
I'm not sure I understand, my proposed |
I don't see .Grow in this issue except in that comment. Did I miss something? Is the proposal Append or Grow? |
Either. I'm primarily interested in an allocation-free way to do the equivalent of: v.Set(reflect.Append(v, e)) With v.Append(e) With n := v.Len()
if n == v.Cap() {
v.Grow(1)
}
v.SetLen(n+1)
v.Index(n).Set(e) I have preference for |
This proposal has been added to the active column of the proposals project |
The problem with type MyStruct struct {
samples []float32
} With for count := 1; moreObjects; count++ {
sliceV.Resize(count)
newElem := sliceV.Index(count-1)
decoder.decodeToValue(newElem)
} Which could decode non-pointer types in-place, without extra allocating or copying. That should be true across numbers/structs/arrays. A more minor issue with All these API variations seem simple enough, so I think it would make sense to eliminate as many performance issues as possible so there's no need for additional methods in the future. |
It sounds like there are some good reasons to do Grow instead of Append. |
Just to mention a specific example before it gets baked in: the only two usages of l := v.Len()
v.Set(reflect.Append(v, reflect.Zero(v.Type().Elem())))
decodeTo(v.Index(l)) This could be converted to: decodeTo(v.Extend(1)) or l := v.Len()
v.Resize(l+1)
decodeTo(v.Index(l)) or l := v.Len()
if l+1 > v.Cap() {
v.Grow(1)
}
v.SetLen(l+1)
decodeTo(v.Index(l)) For its intended usage, |
It seems like you would write:
This seems OK, and it will match slices.Grow eventually. |
Based on the discussion above, this proposal seems like a likely accept. |
I think I understand now, the The only remaining issue I can think of is if |
Given that |
@dsnet I agree that not modifying the slice probably meets user expectations better (and it's already possible to avoid that allocation with a small amount of unsafe code). In any event it should probably copy whatever |
While unsubmitted, the draft implementation of |
We definitely want to make slices.Grow and this Grow match as much as possible. We can work that out in review. |
No change in consensus, so accepted. 🎉 |
Change https://go.dev/cl/389635 mentions this issue: |
The Grow method is like the proposed slices.Grow function in that it ensures that the slice has enough capacity to append n elements without allocating. The implementation of Grow is a thin wrapper over runtime.growslice. This also changes Append and AppendSlice to use growslice under the hood. Fixes golang#48000 Change-Id: I992a58584a2ff1448c1c2bc0877fe76073609111 Reviewed-on: https://go-review.googlesource.com/c/go/+/389635 Run-TryBot: Joseph Tsai <joetsai@digital-static.net> Reviewed-by: Keith Randall <khr@golang.org> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Keith Randall <khr@google.com> Reviewed-by: Ian Lance Taylor <iant@google.com>
The current
reflect.Append
function unfortunately always allocates even if the underlying slice has sufficient capacity because it must allocate a slice header on the heap (since it returns areflect.Value
with the resized slice).I propose the addition a new method on
reflect.Value
:that is semantically equivalent to:
Such a method can avoid the allocation for a slice header because it never gets exposed to the application code. The implementation can update the underlying slice header in place.
Performance benefits aside, there is a readability benefit as well:
For consistency, we would also add
reflect.Value.AppendSlice
.Prevalence
Using the public module proxy, there are ~12k usages of
reflect.Append
orreflect.AppendSlice
:v.Set(reflect.Append(v, ...))
. All of these can be switched tov.Append(...)
.v = reflect.Append(v, ...)
. It is unclear whether v is addressable or whether the use case cared about the originalv
being updated. Manual inspection showed that some of these could switch tov.Append(...)
.Thus, up to 82% of all
reflect.Append
usages could benefit fromreflect.Value.Append
.The text was updated successfully, but these errors were encountered: