Post
by albinopapa » November 20th, 2018, 10:20 am
I'm still feeling my way around with allocators with the PMR stuff. I'm kind of looking through the STL as a reference, but every step is an unknown for me.
In Box2D, it uses a single class for allocations ( b2BlockAllocator ) and am not entirely sure if I might be able to use separate instances or am restricted. For instance, would I need to do as the OA did and create a derived PMR in b2World, then pass around the pointer to any class that needs it to instantiate their containers ( custom containers b2Buffer and b2List created by me )? Would it be better to make it a global/singleton? Or, could each custom container create and store it's own resource ( b2BlockMemoryResource ) and creating an allocator from that?
Currently, I have chosen the first route of creating a single instance and passing around a pointer to the resource. My reasoning is possibly having better data locality. Since the allocations are done by size, it's possible that all b2Body objects will allocate from the same pool, but in b2Body there are lists of these objects called b2Fixture. Since they are all the same size, all b2Fixture objects will be allocated from the same pool. Now, if b2Body and b2Fixture are the same size ( or actually near the same size as the blocks are predetermine in mostly multiples of 32 bytes: 200 bytes and 220 bytes would allocate from the same pool ) then there is going to be some issues with locality.
Not really sure this has much affect though since bodies and fixtures are stored at random based on their collision as well. If two bodies collide, then pointers to those bodies are stored in b2Contact, then those bodies fixtures are accessed through either accessor functions or directly through friendship. So locality can get pretty bad and is only reduced by copying all related data for a particular function to locals then stored back once all the calculations are done.
It's crazy to see all the tricks done to keep track of these objects. In most cases, a pointer is copied and an index is stored in that object of where it lives in some data structure.
Aside from the memory and layout issues, I'm struggling with a design decision, I guess redesign decision. Since I'm wanting to use std::variant, I a few options for implementing the interface.
1) Have the public interface incorporate ALL private interfaces and just have the functions do nothing where they do not apply.
2) Keep the public interface consistent with the original Box2D API, but copy all base class data to their derived classes.
Option 1 might be confusing as you might think all functions apply to all sub-types, when in fact they do not.
Option 2 would be less confusing as far as interface, but accessing the sub-types would require the user to also use the visitor pattern.
So as an example, b2Joint is the original base class and there are 11 derived joints.
Each derived joint had a set of overloaded functions, these would be no problem to emulate using simple overload resolution. The issue is these 11 other joints have their own functions, mostly get/set accessors. With option 1, some getters would return garbage ( b2Vec2( 0.f, 0.f ) ) and setters would basically be a no-op. With option 2, I could return the underlying joint variant and leave it up to the user to decide how to handle it. Either way, not very useful.
I'm kind of thinking of a third option though. What if, I also had a base class that allowed for dynamic polymorphism. I could use the std::variant to allow for storing in an std::vector so I'd get the data locality there, and the flexibility of allowing the user to be able to cast the base pointer to the derived type based on some enumerator.
This means a possible boost in overall performance depending on different circumstance, and still relying on current known behavior. Users already have to do this with the current implementation, and surely by now we all know that the biggest problem with dynamic polymorphism isn't the virtual functions but the lack of data locality due to randomly allocated spots in memory.
The only other thing I can think of is returning to the user a pointer to the underlying sub-type through a templated function during creation. This way, returning the underlying sub-type is guaranteed since it was just created. This would work, but it leaves it up to chance that if they destroy the b2Joint object, they also nullify the pointer to the underlying sub-type. I guess this wouldn't be an issue as long as they didn't store it and only used it to set parameters just after creation.
Ultimately, I think I'm leaning toward the combined static/dynamic polymorphism approach, performant, flexible and easy to use.
Now, the hard part is going to be untangling the other parts. Surely there is a way to consolidate all the needed data so that it's more accessible than having to copy around so many pointers and decouple a lot of the code. As I mentioned before, there's a lot of forward declarations and friendships going on.
If you think paging some data from disk into RAM is slow, try paging it into a simian cerebrum over a pair of optical nerves. - gameprogrammingpatterns.com