empyreanx / pico_headers

Single-header, cross-platform libraries for game development
272 stars 23 forks source link

3 little design questions #13

Open TheWagi opened 2 months ago

TheWagi commented 2 months ago

Hello,

I've recently discovered your project through https://github.com/abeimler/ecs_benchmark while researching different solutions and implementation ideas for entity component systems. My remarks or questions are only about the ECS.

I've rewritten (renamed, reorganised) completely pico_ecs.h and its corresponding Test and Benchmark to accustom myself with it and also to match my own conventions, and I'm developing it in C++ on and for Windows, my compiler is x64 MSVC v19.29 with VS 16.11, so I've sadly not much code to share or commit. So far, I kept it as much C as possible, the only C++ features used are

I reached a point where the next modifications planned are going to diverge from the original logic. Therefore, I thought it would be a good time to ask questions and or debate on some design choices, with hopefully some enlightenments on both parts.

I'm basing myself on the last available version at the time of writing, which is commit 433f25f

1. Why are you exclusively using a prepopulated entity identifier pool ?

Entity identifiers are simple unsigned integer values that are used as indices. You are only counting down when creating a new entity. The pool is useful when some entities are deleted and new ones are created, so that those new entities re-use identifiers and therefore keep the identifiers within the bounds of arrays as they used as indices. However, in most use cases this concerns only a portion of the entities, usually a lot exist from start to finish. Wouldn't it be best to split them in two groups ? This reduces the memory because the prepopulated pool of identifiers would be smaller and improve the creating and deletion speed by a bit of others entities.

2. Why are you using stacks ?

The three variables that are of type ecs_stack_t are entity_pool, destroy_queue, and remove_queue All could be of type ecs_array_t with the only logic modification being to count up instead of down for entity_pool. Both destroy_queue, and remove_queue are iterated in memory order in ecs_flush_destroyed and ecs_flush_removed which is not what how a stack is meant to work, that is: first-in last-out, last-in first-out, to my knowledge. I suggest to use ecs_array_t instead, it reduces the amount of code, as the type ecs_stack_t and it's associated functions are not required.

3. Why does ecs_entity_t have a ready variable ?

There is no other assignment than at creation in ecs_create. When you create an entity, you generate its identifier and set it ready. Then it is only referred in if statements in

entities contains all ecs_entity_t, it is only iterated in ecs_reset and ecs_free both of which should occur in permitting state, that is not during updates, most likely only when the game starts, changes scene, and terminates. ready is generating padding because the structure is aligned to it's highest alignment variable. Let's say I have PICO_ECS_MAX_COMPONENTS = 135 and entity_count = 500000, in 64bit. entities takes 16M bytes of which 12M bytes are comp_bits, 500K bytes are ready, and 3.5M bytes are padding caused by ready. This is 21.8% of padding memory, which of course varies with the value of PICO_ECS_MAX_COMPONENTS.

The three solutions I see are

My results

Those are three of all the changes I made to increase the performance by 40 to 130% depending on what is benchmarked and still have some planned. I'm also planning on improving the tests and benchmarks.

I'm really interested in your takes, what I missed and got wrong.

Thank you

Wagi

empyreanx commented 2 months ago

Thank you for your very thoughtful comment, I'll do my best to answer your questions. Feel free to respond with more!

1. Why are you exclusively using a prepopulated entity identifier pool ?

The pool is given an initial size and grows when necessary, I'm not clear on how splitting them into two groups would help. Please elaborate on this concept and how it could be implemented.

2. Why are you using stacks ?

The data structures ecs_array_t and ecs_stack_t grew out of different use cases. They could easily be combined. A stack can be used to create a pool, which would require some sort of ecs_array_pop function.

3. Why does ecs_entity_t have a ready variable ?

The ready flag is mostly used for debugging, however there are several other uses, which you indicated. For various reasons, I'm not happy with any of the alternatives you presented. I think I'll try a structure of arrays (SoA) like approach and see if that helps.

You should track issue #12 as I'm working on some bug fixes with another fellow.

empyreanx commented 2 months ago

I suppose if the entity id pool is initialized so that ids are popped off in order, this could reduce upfront memory usage.

empyreanx commented 2 months ago

Some updates in my thinking:

  1. I think I see what you have in mind regarding the entity id pool. Your idea seems sensible and implementing it doesn't seem all that daunting.
  2. I'll consolidate ecs_array_t and ecs_stack_t, but that is a low priority
  3. I plan to try splitting comp_bits and the ready variables into separate arrays. Unless I'm mistaken, this should eliminate the need for padding. I am, however, worried about the cache. I think it is unlikely to create performance problems in release mode, but I'll create a benchmark to be sure. If this doesn't pan out, I'll try storing the ready variable in the MSB of the comp_bits as you suggested.

I'll start working on these upgrades soon, but I have some bugs to squash at the moment. I should be able to get to these within a week or so.