Replies: 1 comment 1 reply
-
I think the animation use-case is super important here since, so others are aware, it's been proposed to have reflection power animation. And this seems like it could help improve performance by a lot. I'll be honest, I'm not much of a low level programmer so I don't fully understand the tradeoffs and limitations. How do we access the offset information? And how do we know that the offset is correct? For example, are there any concerns around padding, You also mentioned pointers being the caveat to the pure-offset pattern. How would we want to handle those for things like nested fields (e.g. like, offset -> pointer -> offset)? And are there other types that might be an issue like maps or linked lists? Lastly, do you have any pseudo code of the basic architecture of what you're thinking? It might help |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Time to unveil my grand vision for the next level of perfection for reflection.
GetPath
evolutionIn the past, bevy only supported
&str
as path as argument to theGetPath
trait methods.Then, we introduced
ParsedPath
, this allowed pre-parsing paths in order to not have to parse a&str
each time the field is accessed.Then, we introduced
ReflectPath
, a trait to abstract over the path used inGetPath
trait methods.What is the way forward? What's next?
Remove heap allocation
ParsedPath
is aBox<[(Access, usize)]>
. It requires heap allocation. But in truth, most paths created in bevy are single field accessors.We could convert the
Box<[(Access, usize)]>
into aSmallVec<[Access; 1]>
(IMO theusize
is a heavy cost we absolutely do not need to pay), but then, what if someone wants aParsedPath
that works mostly well with 2+ elements?Instead, we generalize
ParsedPath
over the storage medium. This way, we let users decide what kind of optimization they want.Offset-based accessors
But there is even better. We got rid of one level of indirection by using a stack-allocated array. But there is still a few indirections involved.
A problem of
ParsedPath
(and the&str
-basedGetPath
) is that it calls a method on a trait object (on&dyn Reflect
to be precise). It is not slow, but it's still the overhead of a table lookup, and a mandatory function call, even if the function body is tinny.Take an animation system, it might do several hundreds — if not thousands — of field accesses per frames. Now suddenly this tinny overhead is a massive headache!
But we don't need to call the trait object method. We could just… you know… build the pointer. Basically, we take the address of the parent object, we add the offset of the field to it, and we have the address of the field.
Even better: a field of a field of a field is their offsets added together! We don't even need an array to represent path access.
Well, there are caveats. Pointers, for one. The address of the value of a
Box<T>
is not an offset from the struct containing it, it is the value of the field within the struct (ok, it's a bit complicated to explain at this point. If you are confused, the Kernighan and Ritchie "C programming language" is a good starting point)Another caveat: With the trait object approach, we are always guaranteed to have the right accessors for our types. If we store an offset (a mere
usize
), we "erase" the type, we've no idea what type we should be accessing and what type the field we are accessing is. This is pretty bad, using an accessor with the wrong type is like usingtransmute
willy-nilly!There are a few ways to make sure we use accessors with the correct types:
PhantomData<(Src, Trgt)>
field, allowing to encode the association in the type systemunsafe
s, but swearing you know what you are doing, you know?TypeId
s (Source and Target), check at runtime we are accessing the correct types.In truth. each approach has advantages and tradeoffs. Depending on the use-case, one is better than the other, and vis-versa. In fact ¿Por qué not los tres? We have
ReflectPath
, if they all implementReflectPath
, they are drop-in replacement for each other.Pretty neat eh?
The path to the new path
In fact, at one point, I had a fork of bevy with this idea partially implemented, but I must have mislaid it.
Beta Was this translation helpful? Give feedback.
All reactions