Add Experimental stores - #160
Conversation
When entities are outside the normal growth region, allocate extra capacity to handle likely ety+1, etc+2 pattern so we don't do two resizes immediately after each other
|
Please ask Claude to use tasty-bench and compare against cached map store as a baseline. (apecs-stm bench does this) |
|
All the benchmarks I added do already compare against |
Concurrently with what?.. Would it mess up deterministic updates required for lockstep multiplayer? 🤔 |
|
The idea would be to reference the store from another IORef which contains something like: data Resizable s = Live s | Resizing (Map Entity (Maybe (Elem s)) swhen it’s I haven’t worked through all the details yet but I’m hoping it should be able to offset the resize latency spiked. |
|
U-versions clearly look very interesting. Absolutely worth for scalar types like Float/Int and tuples of them. |
|
@dpwiz will do. Yeah the boxed versions only really make sense if there isn’t an efficient Vector to store them in or the objects will be shared (or avoiding excessive strictness is important). It’s mainly there because it works for all types without any extra instances; it’s an easy default to try but quite wasteful for memory. |
I was curious whether other storage mechanisms might be useful if the developer has a good idea about the access patterns of each component. The results are somewhat mixed but look promising, particularly where unboxed vectors are used.
If none of this gets merged, I'm fine with that, it was mostly an excuse for some discussion (and my first time using Claude to write code I'd be perfectly capable of writing myself, but faster). Marked draft as I'm sure there's plenty of changes that would be needed and a few cleanup steps needed.
Structures include (names to be bikesheded):
ArrayMap ~= (Vector Bool, Vector c) which grows like a C++ vector when entities are written past the end. Parameterised by the component vector type so you can use unboxed or primitive vectors, which has a noticeable performance impact
SparseMap -= (Vector Int, Vector c, Vector Entity), stores the index that each component is stored at in the first vector so the Vector c is dense, with the last vector doing the reverse mapping which allows for O(1) operations for add and delete. Uses the same growing behaviour. Mostly useful when the component is sparse to improve memory usage. Also parameterised by the vector. The Vector Entity (really Int) allows the explMembers action to just return that vector, but it means the order is essentially random, if that a problem? See https://youtu.be/L4xOCvELWlU for details of the structure.
ChunkMap ~= IntMap (Vector Bool, Vector c), like lots of smallish ArrayMaps, useful when you know a component will be created in contiguous runs that might be sparse. Performance isn’t as good as I’d hoped but thought it was an interesting idea.
At the moment regrowing the arrays can add delays as you’d expect, but I have an idea for how to push that to a thread to be handled concurrently which should be safe via the magic of atomicModifyIORef (but I’ll need to check that safety carefully)
Other changes
Adds benchmarks to try and compare the different structures, as well as refactoring the ecs-bench style benchmark to remove the init overhead from the step benchmark (I got very confused when the results didn't match what the paper showed).
Performance
I've included the outputs from criterion (html and CSV) which I think does a decent job of showing how each store behaves for different access patterns. If there's anything here worth merging, I'll remove those (and the CLAUDE.md, which I included because it had a decent high level summary of the new stores).