-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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 Value.Caller #49340
Comments
Going forward would it make sense to write your package using generics? |
I don't think so. This is doing deep sorting on struct fields (admittedly this is not a high priority package). If the field is a slice, I check the inner type for the method. With generics, even if I know the method has a |
If we add CallInto or something like it, does reflect.Call go down to zero allocations? |
This proposal has been added to the active column of the proposals project |
This seems like a duplicate of #43732.
According to my comment #43732 (comment), we can get it down to zero allocations. While there is a run time improvement of 50%, it wasn't a great as I had hoped. |
Interesting, yes I think this is a duplicate of #43732. Does the CL linked take care of methods? I don't see a change in I also tried removing the allocation locally -- I got a similar sort of speedup: not as much as I hoped, but the allocation drop is somewhat nice regardless. |
I didn't do anything special for method functions. I'll take a look later if something is needed there. |
Indeed, this is a duplicate of #43732, but this one is in the active column, so I'll close that one. |
It sounds like people basically agree about the signature (even in #43732): it takes two []Value and returns nothing. |
What should we do about Call vs CallSlice? We probably don't want to add two new functions. |
The naming problem could be avoided by introducing a //Caller is a low-level type used to make
//zero-allocation function calls.
type Caller struct {} I suspect that adding this type has an additional advantage, it leaves the door open to a echo := reflect.ValueOf(func(in string) string { return in }).Caller() or echo := reflect.NewCaller(reflect.ValueOf(func(in string) string { return in })) then results := make([]reflect.Value, 1)
args := []reflect.Value{
reflect.ValueOf("hello"),
}
for i := 0; i < 100; i++ {
echo.Call(args, results) //more efficient
}
It can then be considered that
In my results := []reflect.Value{new(string)} //all results must be pre-allocated.
args := []reflect.Value{reflect.ValueOf("hello")}
for i := 0; i < 100; i++ {
echo.Call(args, results) //zero-allocations.
} |
Adding the ability to specify the output slice removes allocations for non-method function calls. For methods, there still is an allocation to create a method pointer. Since |
It sounds like we are having trouble getting to an API that will actually guarantee zero allocations and that is simple enough to be a reasonable API change. Does anyone see how to thread this needle? |
It still sounds like we don't have a final API to decide about, |
@twmb |
I think Also, // Call calls the function v with the input arguments in. For example, if len(in)
// == 3, v.Call(in) represents the Go call v(in[0], in[1], in[2]). It returns the
// output results as Values. As in Go, each input argument must be assignable to
// the type of the function's corresponding input parameter. If v is a variadic
// function, Call creates the variadic slice parameter itself, copying in the
// corresponding values.
//
// The returned slice is reused across repeated invocations to call. If you wish
// to keep the argument slice itself, it must be copied.
func (c reflect.Caller) Call(args []reflect.Value) []reflect.Value In this scheme, the returned argument slice can be allocated once on the first call, and reused across future invocations. Alternatively, this type could have a second method. In summary for new APIs, func NewCaller(interface{}) reflect.Caller
func (v reflect.Value) Caller() reflect.Caller
type reflect.Caller struct { ... }
func (c reflect.Caller) Call(args []reflect.Value) []reflect.Value // reuses return slice across repeated invocations
func (c reflect.Caller) CallWithReturns(args, returns []reflect.Value) I believe it should be possible to have zero allocation calls with this helper type. |
Is Caller just a cache for allocated things? If so, maybe it shouldn't embed the receiver? That is, maybe it should be var caller reflect.Caller because then you can do caller.Call(otherV, otherArgs) Does anyone want to try implementing this? |
Sounds like we are waiting on someone to implement this to understand whether it satisfies the performance goals for the proposal. |
Placed on hold. |
Based on the discussion above, this proposal seems like a likely accept. |
No change in consensus, so accepted. 🎉 |
Overall, the API LGTM. One minor change is that perhaps we should simply expose the type of the function rather than provide the Thus, just: func (*Caller) Type() Type |
I have run into an issue with reusing the output values. Initially I had used the following to reuse where possible:
found here This was to not break the current behaviour of call, but as it turns out, this allows for the following in the
This cases some serious weirdness, and it seems I have not way to detect this case, as the only flag set is This could be resolved by checking It is unclear to me how to proceed or if in fact I have made a mistake somewhere. |
Change https://go.dev/cl/482375 mentions this issue: |
@nrwiersma What is the status of your work on this issue? I see that we now have https://go.dev/cl/482375 to implement this. |
@ianlancetaylor I ran into a road block which I don't know how to resolve without changing the behaviour of I saw the implementation, and it seems it suffers the same issue I ran into, allowing
|
Sorry, I'm not sure I entirely understand what you mean. Can you give a short example using |
Sure. This is from a test I expected to fail, and it does, kind of.
The actual error you get is The issue here is that |
It output in my mac m1:
I think this is reasonable. |
@nrwiersma I see, thanks for the example. I agree that the behavior is not ideal but I think it's OK. It seems understandable. And it doesn't seem like a likely mistake. |
Change https://go.dev/cl/483255 mentions this issue: |
In looking at the concrete CL, it seems that there is still a If the Moreover: once we promise that So I think it is important that the |
That seems too subtle to me: making a small change to a program that was previously allocating new values could cause it to silently switch to overwriting existing values (or vice-versa), the resulting aliasing bugs seem likely to be difficult to diagnose. Perhaps instead the That method could look like: // MakeOut returns a slice of Values suitable for passing as the out argument
// to Call or CallSlice.
func (*Caller) MakeOut() []Value |
This would mean that |
Why is that a problem? Even in https://go.dev/cl/483255/6 a new
That doesn't follow. It still seems useful to pass in the |
Fair enough. If this is the new direction, I don't have the time to completely restart this PR. In that case I would rather close it and let someone else have a go. |
@bcmills Note that the Personally I think the current behavior of the output slice is correct: it gets automatically initialized for you, and if you keep passing the same output slice, it doesn't do any additional allocations. Yes, if you change the output slice in some way it may not behave as you expect. Don't do that. Or, we could panic if the element in the output slice is both initialized and the wrong type. |
reflect.Call
allocates a[]reflect.Value
slice of lengthnout
for the number of return arguments from the function. This is fine for a one-off call of a function, but when repeatedly invoking a function,reflect.Call
allocations can quickly add up.As an example, I have a helper package that sorts arbitrary types, and if a type has a
func (t T) Less(other T) bool
(i.e.,Name == "Less", NumIn() == 1, NumOut() == 1, In(0) == T, Out(0) == bool
), the package calls that function for sorting. This is done within sort.Slice, meaningLess
is called as many times assort.Slice
needs to compare. For an ordered slice of 1000 elements, eachsort.Slice
allocates 25735 times.The only four allocations in this benchmark are (tip @ 2cf85b1)
70% of the allocations are from allocating the output argument slice. If possible, I could provide the input slice and reuse it for all
Call
s, which would effectively eliminate this alloc.I looked into line 862 and into the runtime, I'm not sure how to get rid of that allocation: the code takes the address of an unsafe pointer, and that allocates; if possible it would be quite nice to get rid of this alloc as well but this would likely require deeper wiring into the runtime (the point here is to have a method pointer). Line 609 has a TODO that could eliminate the allocation. runtime/proc.go:1866 looks to be unrelated.
reflect.Value.call
can be:If out is non-nil, then it is used rather than allocating a new slice. If the input
out
is too small, the code will panic. Essentially, line 565 turns into:The text was updated successfully, but these errors were encountered: