Skip to content

proposal: reflect: add NewAtPtr, ValueAt #41993

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

Closed
yazver opened this issue Oct 15, 2020 · 15 comments
Closed

proposal: reflect: add NewAtPtr, ValueAt #41993

yazver opened this issue Oct 15, 2020 · 15 comments

Comments

@yazver
Copy link

yazver commented Oct 15, 2020

The function reflect.NewAt works very slowly. Because it calls ptrTo to convert type to a pointer type, ptrTo uses sync.Map internally to find a pointer type data. There is no way to pre-create a pointer type and reuse it. Also some cases needs to create a value directly at pointer. The following functions solve this problem.

// NewAtPtr returns a Value representing a pointer to a value of the
// specified pointer type, using p as that pointer.
func NewAtPtr(typ Type, p unsafe.Pointer) Value {
	if typ == nil {
		panic("reflect: NewAtPtr(nil)")
	}
	fl := flag(Ptr)
	t := typ.(*rtype)
	if t.Kind() != Ptr {
		panic("non-pointer type")
	}
	return Value{t, p, fl}
}

// At returns a Value representing a value of the
// specified type, using p as that pointer.
func ValueAt(typ Type, p unsafe.Pointer) Value {
	if typ == nil {
		panic("reflect: At(nil)")
	}
	if p == nil {
		return Value{}
	}
	t := typ.(*rtype)
	fl := flagIndir | flagAddr
	fl |= flag(t.Kind())
	return Value{t, p, fl}
}

Code: https://github.com/yazver/go/blob/reflect-newatptr/src/reflect/value.go#L2406-L2433
Tests: https://github.com/yazver/go/blob/reflect-newatptr/src/reflect/all_test.go#L7358-L7393.

Benchmark tests for two cases:

  1. Create indirect Value from unsafe.Pointer (ValueAt 10 times faster then NewAt);
  2. Create Value of a pointer to Value from unsafe.Pointer (NewAtPtr 10 times faster then NewAt);
Running tool: go test -benchmem -run=^$ github.com/yazver/go-test/reflect -bench ^(BenchmarkNewAt|BenchmarkNewAtPtr|BenchmarkValueAt|BenchmarkNewAtAddr|BenchmarkNewAtPtrAddr|BenchmarkValueAtAddr)$

goos: darwin
goarch: amd64
pkg: github.com/yazver/go-test/reflect
BenchmarkNewAt-12           	124112732	         9.98 ns/op	       0 B/op	       0 allocs/op
BenchmarkNewAtPtr-12        	321658622	         3.48 ns/op	       0 B/op	       0 allocs/op
BenchmarkValueAt-12         	1000000000	         0.759 ns/op	       0 B/op	       0 allocs/op
BenchmarkNewAtAddr-12       	178886904	         6.83 ns/op	       0 B/op	       0 allocs/op
BenchmarkNewAtPtrAddr-12    	1000000000	         0.698 ns/op	       0 B/op	       0 allocs/op
BenchmarkValueAtAddr-12     	128124268	         9.77 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	github.com/yazver/go-test/reflect	9.813s

Sources of tests:

func BenchmarkNewAt(b *testing.B) {
	v := reflect.ValueOf(new(int))
	t := v.Type().Elem()
	ptr := unsafe.Pointer(v.Pointer())
	for i := 0; i < b.N; i++ {
		_ = reflect.NewAt(t, ptr).Elem()
	}
}

func BenchmarkNewAtPtr(b *testing.B) {
	v := reflect.ValueOf(new(int))
	t := v.Type()
	ptr := unsafe.Pointer(v.Pointer())
	for i := 0; i < b.N; i++ {
		_ = reflect.NewAtPtr(t, ptr).Elem()
	}
}

func BenchmarkValueAt(b *testing.B) {
	v := reflect.ValueOf(new(int))
	t := v.Type().Elem()
	ptr := unsafe.Pointer(v.Pointer())
	for i := 0; i < b.N; i++ {
		_ = reflect.ValueAt(t, ptr)
	}
}

func BenchmarkNewAtAddr(b *testing.B) {
	v := reflect.ValueOf(new(int))
	t := v.Type().Elem()
	ptr := unsafe.Pointer(v.Pointer())
	for i := 0; i < b.N; i++ {
		_ = reflect.NewAt(t, ptr)
	}
}

func BenchmarkNewAtPtrAddr(b *testing.B) {
	v := reflect.ValueOf(new(int))
	t := v.Type()
	ptr := unsafe.Pointer(v.Pointer())
	for i := 0; i < b.N; i++ {
		_ = reflect.NewAtPtr(t, ptr)
	}
}

func BenchmarkValueAtAddr(b *testing.B) {
	v := reflect.ValueOf(new(int))
	ptr := unsafe.Pointer(v.Pointer())
	t := v.Type().Elem()
	for i := 0; i < b.N; i++ {
		_ = reflect.ValueAt(t, ptr).Addr()
	}
}
@gopherbot gopherbot added this to the Proposal milestone Oct 15, 2020
@seankhliao
Copy link
Member

the double loop in the benchmark looks weird, also it violates the pointer safety rules

@yazver
Copy link
Author

yazver commented Oct 15, 2020

the double loop in the benchmark looks weird, also it violates the pointer safety rules

Accepted it and fixed.

@ianlancetaylor ianlancetaylor changed the title proposal: add reflect.NewAtPtr, reflect.ValueAt proposal: reflect: add NewAtPtr, ValueAt Oct 15, 2020
@ianlancetaylor
Copy link
Member

What kind of code would use these new functions?

@yazver
Copy link
Author

yazver commented Oct 16, 2020

What kind of code would use these new functions?

For example, It allows more fast access to the struct fields by pre-cached offsets.

src := reflect.ValueAt(srcFieldType, unsafe.Pointer(uintptr(srcPtr)+srcOffset))
dest := reflect.ValueAt(destFieldType, unsafe.Pointer(uintptr(destPtr)+destOffset))
dest.Set(src)

@mvdan
Copy link
Member

mvdan commented Oct 16, 2020

Have you attempted to make the current APIs faster?

Also, if you find reflect to be too slow for your use case in general, you could always give up some safety and use more unsafe directly instead of reflect. You appear to be using unsafe pointer arithmetic already.

@yazver
Copy link
Author

yazver commented Oct 16, 2020

Have you attempted to make the current APIs faster?

I don't see way to do it. NewAt uses ptrTo and it is very heavy internally.

Also, if you find reflect to be too slow for your use case in general, you could always give up some safety and use more unsafe directly instead of reflect. You appear to be using unsafe pointer arithmetic already.

Go does't have a function that allows to copy arbitrary count of bytes. If some memory copy function were there, it would most likely solve the problem. And one way, that i know, to copy the variable of unknown type is to use Set(), that is required the indirect Value and which can only be obtained in the following way:

src := reflect.NewAt(srcType, srcPtr).Elem() // slow
dest := reflect.NewAt(destType, destPtr).Elem() // slow
dest.Set(src)

@beoran
Copy link

beoran commented Oct 17, 2020

With the copy function and a few unsafe casts to array of byte, you can actually copy an arbitrary amount of bytes.

@rsc
Copy link
Contributor

rsc commented Oct 21, 2020

The only difference between this API and the existing API is which type gets passed in.
NewAt(typ, p) == NewAtPtr(PtrTo(typ), p).
And the issue is that PtrTo sometimes requires a map lookup.
But that map lookup is only needed for types that are not themselves already in the binary.
If type T gets reflect information in the binary, then we also write out *T in the binary.
You'd only see this if you constructed T itself at runtime, or if you were using **T or something like that.
What is the context in which this occurs?

If T is constructed at runtime, the data is writable, so we could fill in the *T link once and reuse it instead of using the map.
(That's not true for a *T built into the binary - there we can't write the link to **T.)

So it seems like in whatever important case we have, we could make PtrTo faster instead of adding new API here.

But would that address the case where you are seeing this?
(That is, again, what is the context in which this occurs?)

@gopherbot
Copy link
Contributor

Change https://golang.org/cl/264237 mentions this issue: reflect: set ptrToThis when asked for pointer to dynamic type

@ianlancetaylor
Copy link
Member

ianlancetaylor commented Oct 21, 2020

I was curious how hard this would be, so I wrote https://golang.org/cl/264237 to record pointer types in the ptrToThis field of newly created dynamic types. It would be interesting to know what effect this has on the benchmarks here.

@ianlancetaylor
Copy link
Member

Actually that code is slower. Never mind.

@rsc
Copy link
Contributor

rsc commented Nov 4, 2020

Maybe we should re-expand ptrToThis to be a full pointer. I don't know how much this specific field would expand the reflect data.

But the more important question is again, what is the context in which this slow execution occurs? /cc @yazver

@rsc
Copy link
Contributor

rsc commented Nov 11, 2020

Does anyone want to describe a time when the speed of NewAt was too slow?
We think we know how to make it faster but it would be nice to understand what we would be making faster, if anything, to justify the added complexity and slight size overhead.

@rsc
Copy link
Contributor

rsc commented Nov 18, 2020

We think we know how to optimize NewAt if it is too slow. But we need to see a use case to understand the tradeoff there.

On the new API topic (this issue), based on the discussion above this seems like a likely decline.

@rsc
Copy link
Contributor

rsc commented Dec 2, 2020

No change in consensus, so declined.

@rsc rsc closed this as completed Dec 2, 2020
@golang golang locked and limited conversation to collaborators Dec 2, 2021
@rsc rsc moved this to Declined in Proposals Aug 10, 2022
@rsc rsc added this to Proposals Aug 10, 2022
@rsc rsc removed this from Proposals Oct 19, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

7 participants