Skip to content
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: optimize v.Set(reflect.Zero(v.Type())) to not allocate #33136

Closed
dsnet opened this issue Jul 16, 2019 · 20 comments
Closed

reflect: optimize v.Set(reflect.Zero(v.Type())) to not allocate #33136

dsnet opened this issue Jul 16, 2019 · 20 comments

Comments

@dsnet
Copy link
Member

dsnet commented Jul 16, 2019

The Value API lacks an efficient way to zero out a value. Currently, one can do v.Set(reflect.Zero(v.Type())). However, this is not efficient since reflect.Zero may need to allocate a large object, and also reflect.Value.Set will use runtime.typedmemmove under the hood instead of the more efficient runtime.typedmemclr.

I propose adding reflect.Value.SetZero method as a more efficient way to set the value to the zero value.

@gopherbot gopherbot added this to the Proposal milestone Jul 16, 2019
@martisch
Copy link
Contributor

I would also like to see this added and want to give some context where this can have a big effect with a use case:
I came across this "inefficiency" also recently when looking into code for a proto sanitizer for log writing that should wipe out sensitive fields and therefore needs to "unset"/"wipe" some proto fields to a zero value. This was using reflecting on structs. Creating zero values was showing up in profiling for a non trivial amount of time and memory allocation. A future new proto API might provide some better method but then that might also be able or need to have an efficient reflect method under the hood.

@dsnet
Copy link
Member Author

dsnet commented Jul 17, 2019

A future new proto API might provide some better method but then that might also be able or need to have an efficient reflect method under the hood.

Heh. Ironically, I filed this issue in trying to optimize the internal protobuf implementation itself and couldn't figure out an efficient to this.

@dsnet dsnet added the NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. label Aug 7, 2019
@gopherbot gopherbot removed the NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. label Aug 16, 2019
@rsc
Copy link
Contributor

rsc commented Aug 20, 2019

Given v.Set(reflect.Zero(v.Type())), the place where the inefficiency happens is if v.Type() is a value larger than a single word, so that the reflect.Value contains a pointer to a zeroed allocation.

I think we could change the representation of reflect.Value to define that if that pointer is nil, it is interpreted as pointing to an appropriate number of zeroed bytes. Then the idiom in question starts being more efficient without any changes to client code, no new API to learn, and so on.

The change seems like it could be done completely invisibly. The Value containing a nil pointer could not be addressable, but the result of reflect.Zero is not addressable anyway, so that wouldn't be a problem.

Should we think about doing that invisible optimization instead of new API?

@ianlancetaylor
Copy link
Contributor

I tried a simple version of that optimization and it did save 75% of the time on this benchmark. So the idea does seem worth pursuing. @dsnet Is this a plausible benchmark for the kinds of code you are concerned about?

(It's not a tiny change to the reflect package, diffstat reports 159 insertions, 35 deletions.)

func BenchmarkZero(b *testing.B) {
	t := TypeOf(struct {a, b, c, d *byte}{})
	v := New(t)
	for i := 0; i < b.N; i++ {
		v.Elem().Set(Zero(t))
	}
}

@dsnet
Copy link
Member Author

dsnet commented Aug 21, 2019

Do you have a CL for this experiment that I can take for a spin?

@ianlancetaylor
Copy link
Contributor

@gopherbot
Copy link

Change https://golang.org/cl/191199 mentions this issue: reflect: treat nil Value ptr as zero value of type

@randall77
Copy link
Contributor

The runtime already has a 1K region of zeros that we can use to be the backing store for the result of reflect.Zero.
The change would then be as simple as:

	if ifaceIndir(t) {
		var p unsafe.Pointer
		if t.size <= maxZero {
			p = unsafe.Pointer(&zeroVal[0])
		} else {
			p = unsafe_New(t)
		}
		return Value{t, p, fl | flagIndir}
	}

// Buffer of zeros. We could share this with runtime.zeroVal with linkname tricks, or keep it separate.
const maxZero = 1024
var zeroVal [maxZero]byte

We could also detect this particular buffer in Set and use typedmemclr instead of typedmemmove.

It would only require allocation in the >1K case, which I think is rare.

@mvdan
Copy link
Member

mvdan commented Aug 29, 2019

This is also a big inefficiency in packages like encoding/json. Please let me know when a CL is ready and I'll be happy to help review and test.

If this can be done without new API, all the better :)

@gopherbot
Copy link

Change https://golang.org/cl/192331 mentions this issue: reflect: use zero buffer to back the Value returned by Zero

@ianlancetaylor
Copy link
Contributor

@dsnet If you want one of these changes to go into 1.14, please do some benchmarking with real code. Thanks.

@rsc rsc added this to Hold in Proposals (old) Dec 4, 2019
@rsc rsc moved this from Hold to Incoming in Proposals (old) Dec 4, 2019
@rsc
Copy link
Contributor

rsc commented Jan 15, 2020

It seems like this proposal is stalled on deciding whether it is possible to make v.Set(reflect.Zero(v.Type())) as efficient (by recognizing it in the library, not the compiler) as the proposed v.SetZero(), to avoid new API surface.

@dsnet, do either of Ian's or Keith's patch (both above) work well enough for you?

@dsnet
Copy link
Member Author

dsnet commented Jan 15, 2020

The use case I originally needed this for is no longer relevant. Perhaps one of the other 👍's could comment?

@randall77
Copy link
Contributor

My CL is simpler but has a performance discontinuity depending on size.
Ian's is more invasive but will handle all sizes.

@rsc rsc moved this from Incoming to Active in Proposals (old) Jan 15, 2020
@mvdan
Copy link
Member

mvdan commented Jan 15, 2020

I briefly mentioned encoding/json last year - in particular, we set reflect values to zero in three places:

$ grep -nE 'Zero.*Type' *.go
decode.go:601:			z := reflect.Zero(v.Type().Elem())
decode.go:708:				mapElem.Set(reflect.Zero(elemType))
decode.go:917:			v.Set(reflect.Zero(v.Type()))

I'm travelling at the moment, so I can't spend the time to test both of the CLs against the current set of decode/unmarshal benchmarks. I can have a look next week, if noone beats me to it. From a quick look, they seem to be, respectively:

  • zeroing remaining array elements (unlikely path)
  • zeroing a map element value before reusing it to decode (hot path when decoding maps)
  • zeroing a value when decoding null into it (depends on the input)

My hunch is that JSON often involves smaller types. If one is decoding huge types, plenty of other pieces in the decoder would start getting slow, so I don't think extra allocations there would matter. So I'd default for Keith's simpler CL, and perhaps archive Ian's in case anyone has a strong need for it in the future.

@rsc rsc changed the title proposal: reflect: add Value.SetZero proposal: reflect: optimize v.Set(reflect.Zero(v.Type())) to not allocate Jan 22, 2020
@rsc
Copy link
Contributor

rsc commented Jan 22, 2020

It sounds like Ian's change is the more complete one and does not have significant performance problems, so we should probably go with that.

Based on the discussion above and the lack of API change, this now seems like a likely accept.

@josharian
Copy link
Contributor

I think it'd be good to fill out Ian's change before choosing it over Keith's to confirm that there are no meaningful performance side-effects from the as-yet-unimplemented runtime changes. That shouldn't change the prognosis for this proposal, just possibly which implementation we should.

@rsc
Copy link
Contributor

rsc commented Feb 5, 2020

No change in consensus so accepted.

(For clarity, that there's no API change here, so if the idea had started at just an optimization, this would not have needed to be in the proposal process.)

@rsc rsc moved this from Likely Accept to Accepted in Proposals (old) Feb 5, 2020
@rsc rsc changed the title proposal: reflect: optimize v.Set(reflect.Zero(v.Type())) to not allocate reflect: optimize v.Set(reflect.Zero(v.Type())) to not allocate Feb 5, 2020
@rsc rsc modified the milestones: Proposal, Go1.15 Feb 5, 2020
@ianlancetaylor ianlancetaylor modified the milestones: Go1.15, Go1.16 May 19, 2020
@gopherbot
Copy link

Change https://golang.org/cl/300992 mentions this issue: [release-branch.go1.16] doc: describe how Zero optimization might change results of incorrect reflect.Value comparison

@gopherbot
Copy link

Change https://golang.org/cl/302269 mentions this issue: doc: describe how Zero optimization might change results of incorrect reflect.Value comparison

gopherbot pushed a commit to golang/website that referenced this issue Mar 17, 2021
… reflect.Value comparison

Update golang/go#33136
Update golang/go#43993

Change-Id: I306a8fd60a4d58cfd338edea4f21690338bf9a0b
Reviewed-on: https://go-review.googlesource.com/c/website/+/302269
Trust: Keith Randall <khr@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
@golang golang locked and limited conversation to collaborators Mar 16, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
No open projects
Development

No branches or pull requests

8 participants