-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Store only the IDs needed for Query iteration #12476
Changes from all commits
b35c3d1
edcc766
057adf3
7881113
89c77fd
8e8b82b
190af72
f24f77f
69f267b
5010e6f
ad484ea
caf91fd
c7e5e8e
f77497b
361deca
16fd523
1893268
3cfebae
c0f0835
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,25 @@ use super::{ | |
QuerySingleError, ROQueryItem, | ||
}; | ||
|
||
/// An ID for either a table or an archetype. Used for Query iteration. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know if it would be useful to specify that this is used for optimizing query iteration; in the case where all components are in tables, we can iterate through table_ids directly instead of archetypes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would also maybe add a comment on why this being a |
||
/// | ||
/// Query iteration is exclusively dense (over tables) or archetypal (over archetypes) based on whether | ||
/// both `D::IS_DENSE` and `F::IS_DENSE` are true or not. | ||
/// | ||
/// This is a union instead of an enum as the usage is determined at compile time, as all [`StorageId`]s for | ||
/// a [`QueryState`] will be all [`TableId`]s or all [`ArchetypeId`]s, and not a mixture of both. This | ||
/// removes the need for discriminator to minimize memory usage and branching during iteration, but requires | ||
/// a safety invariant be verified when disambiguating them. | ||
/// | ||
/// # Safety | ||
/// Must be initialized and accessed as a [`TableId`], if both generic parameters to the query are dense. | ||
/// Must be initialized and accessed as an [`ArchetypeId`] otherwise. | ||
#[derive(Clone, Copy)] | ||
pub(super) union StorageId { | ||
pub(super) table_id: TableId, | ||
pub(super) archetype_id: ArchetypeId, | ||
} | ||
|
||
/// Provides scoped access to a [`World`] state according to a given [`QueryData`] and [`QueryFilter`]. | ||
#[repr(C)] | ||
// SAFETY NOTE: | ||
|
@@ -32,10 +51,8 @@ pub struct QueryState<D: QueryData, F: QueryFilter = ()> { | |
pub(crate) matched_tables: FixedBitSet, | ||
pub(crate) matched_archetypes: FixedBitSet, | ||
pub(crate) component_access: FilteredAccess<ComponentId>, | ||
// NOTE: we maintain both a TableId bitset and a vec because iterating the vec is faster | ||
pub(crate) matched_table_ids: Vec<TableId>, | ||
// NOTE: we maintain both a ArchetypeId bitset and a vec because iterating the vec is faster | ||
pub(crate) matched_archetype_ids: Vec<ArchetypeId>, | ||
// NOTE: we maintain both a bitset and a vec because iterating the vec is faster | ||
pub(super) matched_storage_ids: Vec<StorageId>, | ||
pub(crate) fetch_state: D::State, | ||
pub(crate) filter_state: F::State, | ||
#[cfg(feature = "trace")] | ||
|
@@ -46,8 +63,11 @@ impl<D: QueryData, F: QueryFilter> fmt::Debug for QueryState<D, F> { | |
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
f.debug_struct("QueryState") | ||
.field("world_id", &self.world_id) | ||
.field("matched_table_count", &self.matched_table_ids.len()) | ||
.field("matched_archetype_count", &self.matched_archetype_ids.len()) | ||
.field("matched_table_count", &self.matched_tables.count_ones(..)) | ||
.field( | ||
"matched_archetype_count", | ||
&self.matched_archetypes.count_ones(..), | ||
) | ||
.finish_non_exhaustive() | ||
} | ||
} | ||
|
@@ -101,13 +121,13 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> { | |
} | ||
|
||
/// Returns the tables matched by this query. | ||
pub fn matched_tables(&self) -> &[TableId] { | ||
&self.matched_table_ids | ||
pub fn matched_tables(&self) -> impl Iterator<Item = TableId> + '_ { | ||
self.matched_tables.ones().map(TableId::from_usize) | ||
} | ||
|
||
/// Returns the archetypes matched by this query. | ||
pub fn matched_archetypes(&self) -> &[ArchetypeId] { | ||
&self.matched_archetype_ids | ||
pub fn matched_archetypes(&self) -> impl Iterator<Item = ArchetypeId> + '_ { | ||
self.matched_archetypes.ones().map(ArchetypeId::new) | ||
} | ||
} | ||
|
||
|
@@ -158,8 +178,7 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> { | |
Self { | ||
world_id: world.id(), | ||
archetype_generation: ArchetypeGeneration::initial(), | ||
matched_table_ids: Vec::new(), | ||
matched_archetype_ids: Vec::new(), | ||
matched_storage_ids: Vec::new(), | ||
fetch_state, | ||
filter_state, | ||
component_access, | ||
|
@@ -183,8 +202,7 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> { | |
let mut state = Self { | ||
world_id: builder.world().id(), | ||
archetype_generation: ArchetypeGeneration::initial(), | ||
matched_table_ids: Vec::new(), | ||
matched_archetype_ids: Vec::new(), | ||
matched_storage_ids: Vec::new(), | ||
fetch_state, | ||
filter_state, | ||
component_access: builder.access().clone(), | ||
|
@@ -338,12 +356,20 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> { | |
let archetype_index = archetype.id().index(); | ||
if !self.matched_archetypes.contains(archetype_index) { | ||
self.matched_archetypes.grow_and_insert(archetype_index); | ||
self.matched_archetype_ids.push(archetype.id()); | ||
if !D::IS_DENSE || !F::IS_DENSE { | ||
self.matched_storage_ids.push(StorageId { | ||
archetype_id: archetype.id(), | ||
}); | ||
} | ||
} | ||
let table_index = archetype.table_id().as_usize(); | ||
if !self.matched_tables.contains(table_index) { | ||
self.matched_tables.grow_and_insert(table_index); | ||
self.matched_table_ids.push(archetype.table_id()); | ||
if D::IS_DENSE && F::IS_DENSE { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we still also need separate Maybe it's still needed for things like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep exactly, join needs them, and get_unchecked_manual needs specifically matched_archetypes (right now). |
||
self.matched_storage_ids.push(StorageId { | ||
table_id: archetype.table_id(), | ||
}); | ||
} | ||
} | ||
true | ||
} else { | ||
|
@@ -424,8 +450,7 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> { | |
QueryState { | ||
world_id: self.world_id, | ||
archetype_generation: self.archetype_generation, | ||
matched_table_ids: self.matched_table_ids.clone(), | ||
matched_archetype_ids: self.matched_archetype_ids.clone(), | ||
matched_storage_ids: self.matched_storage_ids.clone(), | ||
fetch_state, | ||
filter_state, | ||
component_access: self.component_access.clone(), | ||
|
@@ -515,24 +540,30 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> { | |
} | ||
|
||
// take the intersection of the matched ids | ||
let matched_tables: FixedBitSet = self | ||
.matched_tables | ||
.intersection(&other.matched_tables) | ||
.collect(); | ||
let matched_table_ids: Vec<TableId> = | ||
matched_tables.ones().map(TableId::from_usize).collect(); | ||
let matched_archetypes: FixedBitSet = self | ||
.matched_archetypes | ||
.intersection(&other.matched_archetypes) | ||
.collect(); | ||
let matched_archetype_ids: Vec<ArchetypeId> = | ||
matched_archetypes.ones().map(ArchetypeId::new).collect(); | ||
let mut matched_tables = self.matched_tables.clone(); | ||
let mut matched_archetypes = self.matched_archetypes.clone(); | ||
matched_tables.intersect_with(&other.matched_tables); | ||
matched_archetypes.intersect_with(&other.matched_archetypes); | ||
let matched_storage_ids = if NewD::IS_DENSE && NewF::IS_DENSE { | ||
matched_tables | ||
.ones() | ||
.map(|id| StorageId { | ||
table_id: TableId::from_usize(id), | ||
}) | ||
.collect() | ||
} else { | ||
matched_archetypes | ||
.ones() | ||
.map(|id| StorageId { | ||
archetype_id: ArchetypeId::new(id), | ||
}) | ||
.collect() | ||
}; | ||
|
||
QueryState { | ||
world_id: self.world_id, | ||
archetype_generation: self.archetype_generation, | ||
matched_table_ids, | ||
matched_archetype_ids, | ||
matched_storage_ids, | ||
fetch_state: new_fetch_state, | ||
filter_state: new_filter_state, | ||
component_access: joined_component_access, | ||
|
@@ -1306,12 +1337,15 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> { | |
) { | ||
// NOTE: If you are changing query iteration code, remember to update the following places, where relevant: | ||
// QueryIter, QueryIterationCursor, QueryManyIter, QueryCombinationIter, QueryState::for_each_unchecked_manual, QueryState::par_for_each_unchecked_manual | ||
|
||
bevy_tasks::ComputeTaskPool::get().scope(|scope| { | ||
if D::IS_DENSE && F::IS_DENSE { | ||
// SAFETY: We only access table data that has been registered in `self.archetype_component_access`. | ||
let tables = unsafe { &world.storages().tables }; | ||
for table_id in &self.matched_table_ids { | ||
let table = &tables[*table_id]; | ||
// SAFETY: We only access table data that has been registered in `self.archetype_component_access`. | ||
let tables = unsafe { &world.storages().tables }; | ||
let archetypes = world.archetypes(); | ||
for storage_id in &self.matched_storage_ids { | ||
if D::IS_DENSE && F::IS_DENSE { | ||
let table_id = storage_id.table_id; | ||
let table = &tables[table_id]; | ||
if table.is_empty() { | ||
continue; | ||
} | ||
|
@@ -1320,39 +1354,34 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> { | |
while offset < table.entity_count() { | ||
let mut func = func.clone(); | ||
let len = batch_size.min(table.entity_count() - offset); | ||
let batch = offset..offset + len; | ||
scope.spawn(async move { | ||
#[cfg(feature = "trace")] | ||
let _span = self.par_iter_span.enter(); | ||
let table = &world | ||
.storages() | ||
.tables | ||
.get(*table_id) | ||
.debug_checked_unwrap(); | ||
let batch = offset..offset + len; | ||
let table = | ||
&world.storages().tables.get(table_id).debug_checked_unwrap(); | ||
self.iter_unchecked_manual(world, last_run, this_run) | ||
.for_each_in_table_range(&mut func, table, batch); | ||
}); | ||
offset += batch_size; | ||
} | ||
} | ||
} else { | ||
let archetypes = world.archetypes(); | ||
for archetype_id in &self.matched_archetype_ids { | ||
let mut offset = 0; | ||
let archetype = &archetypes[*archetype_id]; | ||
} else { | ||
let archetype_id = storage_id.archetype_id; | ||
let archetype = &archetypes[archetype_id]; | ||
if archetype.is_empty() { | ||
continue; | ||
} | ||
|
||
let mut offset = 0; | ||
while offset < archetype.len() { | ||
let mut func = func.clone(); | ||
let len = batch_size.min(archetype.len() - offset); | ||
let batch = offset..offset + len; | ||
scope.spawn(async move { | ||
#[cfg(feature = "trace")] | ||
let _span = self.par_iter_span.enter(); | ||
let archetype = | ||
world.archetypes().get(*archetype_id).debug_checked_unwrap(); | ||
let batch = offset..offset + len; | ||
world.archetypes().get(archetype_id).debug_checked_unwrap(); | ||
self.iter_unchecked_manual(world, last_run, this_run) | ||
.for_each_in_archetype_range(&mut func, archetype, batch); | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does that mean that the
table_entities
andarchetype_entities
below could have a similar optiimization asstorage_entities: &'w [StorageEntities]
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what #5085 does, however the benefit may be limited as this struct doesn't really exist at runtime: it's never formally materialized to the stack or the heap under normal use cases and any inlined iteration will decompose the fetches and updates.
Compare this with the Vec in QueryState, which requires both stack and heap space due to being a persisted heap allocated backing for Query. There's real memory savings by using the union.
Doable but I'm not sure if the savings are worth the introduction of more unsafe and readability impact.