Skip to content

Use Component Column ids in tables to skip component maps in queries #19063

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

ElliottjPierce
Copy link
Contributor

@ElliottjPierce ElliottjPierce commented May 4, 2025

Objective

In preparation for components as entities, we will need to switch ImmutableSparseSet<ComponentId, T> to HashMap<ComponentId, T>. This will be a pretty massive regression for those lookups. This PR aims to improve performance (both for components as entities and just for now) by skipping the hottest such component map: Table::columns.

Solution

I tried for longer than I'd like to admit to add per-table/storage caches to QueryState. (It kinda works, but justifying safety is tricky.)

Then I had an idea, and this is something that I think extends to other component maps too! Right now we have an id into something dense (table) and then from there we map a component id to a column. But what if instead we started with an id into something sparse (new table component meta) and then from there we index a dense table id to a column? This is kinda like swapping the rows and columns of a abstract table. That's the general idea here.

We can extend this in a similar way to component maps in archetypes.

Note that we could take this a step further and physically move the columns from the Table to the new TablesComponentMeta. That has some conveniences: A sparse set is just a one column table now (effectively), and we can save another indexing op when setting the table in a query. But it also has the big downside that moving between tables would need an extra indexing op for each component. IDK if that would be worth it, so I left it for now, instead creating column ids.

Testing

CI

Future Work

Currently, creating a query fetch still needs the component map. If we expand init_state and get_state's signatures to include all world metadata, we could put the index of the component metadata directly in the query state, skipping the map entirely.

Also, right now it is still possible to get a component's data from a table directly (which does go through a component map). We should explore removing this extra data and sending everything though the new (faster) way. (And caching the column id where possible!)

Benches

Benchmarks show roughly even with main over all. Most benches fluctuate between ±5 or 6 % compared to main. Some benches take 2.x as long as main. Some things, like change detection see 50% improvements. In general, performance is 20%-50% improved over "normal" query iteration (due to 1 less indexing op) but is much worse over specific gets (ex: Query::get and friends) by 100%-100% (due to one more indexing op).

Note that world_query_get/50000_entities_table_wide is up 500%, a major outlier. This is because it can not cache the column id, it actually doesn't even go through a query. So the extra indexing op is responsible for the 500% regression. If it were just a query get (which has some caching now) this would only be a 33% regression. (Which should be improved with more caching and would be way worse with a hashmap.)

Interpreting these benches , I take away a few things: 1) There are significant performance wins to be had for caching table columns in more places, even without sparse component ids. 2) If adding one indexing op can cause 500% regression, imaging what a hashmap would do! Caching this is a must. 3) More specifically, we should look into expanding signatures of Query::init_state and friends to remove the current hashmap lookup in init_fetch.

Benches differing by 20%
group                                                                                                    main                                   pr19063
-----                                                                                                    ----                                   -------
all_added_detection/50000_entities_ecs::change_detection::Table                                          1.50     44.5±0.45µs        ? ?/sec    1.00     29.7±0.27µs        ? ?/sec
all_added_detection/5000_entities_ecs::change_detection::Table                                           1.47      4.5±0.05µs        ? ?/sec    1.00      3.0±0.03µs        ? ?/sec
all_changed_detection/50000_entities_ecs::change_detection::Table                                        1.50     44.5±0.45µs        ? ?/sec    1.00     29.7±0.46µs        ? ?/sec
all_changed_detection/5000_entities_ecs::change_detection::Table                                         1.43      4.5±0.05µs        ? ?/sec    1.00      3.1±0.25µs        ? ?/sec
event_propagation/four_event_types                                                                       1.00    584.3±8.59µs        ? ?/sec    1.30    761.0±9.92µs        ? ?/sec
event_propagation/single_event_type_no_listeners                                                         1.00    247.0±2.22µs        ? ?/sec    1.70   420.0±46.15µs        ? ?/sec
events_send/size_16_events_100                                                                           1.00    123.2±3.55ns        ? ?/sec    1.21   148.7±27.05ns        ? ?/sec
events_send/size_4_events_100                                                                            1.00     81.3±0.99ns        ? ?/sec    2.06    167.1±1.54ns        ? ?/sec
few_changed_detection/50000_entities_ecs::change_detection::Table                                        1.22     57.2±0.76µs        ? ?/sec    1.00     47.0±0.62µs        ? ?/sec
few_changed_detection/5000_entities_ecs::change_detection::Sparse                                        1.00      3.9±0.39µs        ? ?/sec    1.35      5.3±0.60µs        ? ?/sec
few_changed_detection/5000_entities_ecs::change_detection::Table                                         1.27      5.1±0.25µs        ? ?/sec    1.00      4.0±0.15µs        ? ?/sec
iter_fragmented/base                                                                                     1.26    347.6±7.12ns        ? ?/sec    1.00    275.8±5.96ns        ? ?/sec
iter_fragmented/foreach_wide                                                                             1.00      2.6±0.04µs        ? ?/sec    1.37      3.6±0.05µs        ? ?/sec
iter_fragmented_sparse/foreach                                                                           1.00      5.7±0.05ns        ? ?/sec    1.39      7.9±0.27ns        ? ?/sec
iter_fragmented_sparse/foreach_wide                                                                      1.00     37.1±2.14ns        ? ?/sec    2.11     78.4±0.99ns        ? ?/sec
iter_fragmented_sparse/wide                                                                              1.00     40.2±0.34ns        ? ?/sec    1.71     68.6±0.67ns        ? ?/sec
iter_simple/foreach_wide                                                                                 1.00     16.9±0.14µs        ? ?/sec    2.82     47.8±0.32µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10000_entities_ecs::change_detection::Table    1.54   614.2±14.55µs        ? ?/sec    1.00   397.8±26.30µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_1000_entities_ecs::change_detection::Table     1.52     63.9±5.28µs        ? ?/sec    1.00     41.9±1.65µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_100_entities_ecs::change_detection::Table      1.58      7.4±0.21µs        ? ?/sec    1.00      4.7±0.36µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10_entities_ecs::change_detection::Table       1.38   824.8±10.56ns        ? ?/sec    1.00   595.8±11.85ns        ? ?/sec
multiple_archetypes_none_changed_detection/20_archetypes_10000_entities_ecs::change_detection::Table     1.61    120.3±1.84µs        ? ?/sec    1.00     74.9±1.39µs        ? ?/sec
multiple_archetypes_none_changed_detection/20_archetypes_1000_entities_ecs::change_detection::Table      1.54     12.3±0.29µs        ? ?/sec    1.00      8.0±0.25µs        ? ?/sec
multiple_archetypes_none_changed_detection/20_archetypes_100_entities_ecs::change_detection::Table       1.70  1418.0±15.57ns        ? ?/sec    1.00   834.5±84.47ns        ? ?/sec
multiple_archetypes_none_changed_detection/20_archetypes_10_entities_ecs::change_detection::Table        1.31    168.8±8.94ns        ? ?/sec    1.00   128.8±12.35ns        ? ?/sec
multiple_archetypes_none_changed_detection/5_archetypes_10000_entities_ecs::change_detection::Table      1.62     29.8±0.30µs        ? ?/sec    1.00     18.5±0.30µs        ? ?/sec
multiple_archetypes_none_changed_detection/5_archetypes_1000_entities_ecs::change_detection::Table       1.54      3.0±0.04µs        ? ?/sec    1.00  1971.3±66.64ns        ? ?/sec
multiple_archetypes_none_changed_detection/5_archetypes_100_entities_ecs::change_detection::Table        1.59    358.2±7.57ns        ? ?/sec    1.00   225.2±17.34ns        ? ?/sec
none_changed_detection/50000_entities_ecs::change_detection::Table                                       1.68     29.7±0.39µs        ? ?/sec    1.00     17.7±0.47µs        ? ?/sec
none_changed_detection/5000_entities_ecs::change_detection::Table                                        1.62      3.0±0.03µs        ? ?/sec    1.00  1849.6±55.64ns        ? ?/sec
query_get/50000_entities_table                                                                           1.00    198.6±1.24µs        ? ?/sec    1.33    264.8±4.04µs        ? ?/sec
query_get_many_10/50000_calls_table                                                                      1.00  1405.4±58.51µs        ? ?/sec    1.50      2.1±0.12ms        ? ?/sec
query_get_many_2/50000_calls_table                                                                       1.00    240.7±0.84µs        ? ?/sec    1.91   460.1±10.01µs        ? ?/sec
query_get_many_5/50000_calls_table                                                                       1.00    626.3±4.86µs        ? ?/sec    1.76  1101.7±61.30µs        ? ?/sec
world_query_get/50000_entities_table                                                                     1.00    125.0±0.21µs        ? ?/sec    1.66    208.0±3.80µs        ? ?/sec
world_query_get/50000_entities_table_wide                                                                1.00    124.9±0.31µs        ? ?/sec    5.20    649.2±7.31µs        ? ?/sec

@ElliottjPierce ElliottjPierce added A-ECS Entities, components, systems, and events C-Performance A change motivated by improving speed, memory usage or compile times S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Performance A change motivated by improving speed, memory usage or compile times S-Needs-Review Needs reviewer attention (from anyone!) to move forward
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant