From 1b22aebc4ed4c0f7068d20601286f8dc18aab966 Mon Sep 17 00:00:00 2001 From: James Liu Date: Tue, 15 Nov 2022 22:21:19 +0000 Subject: [PATCH] Immutable sparse sets for metadata storage (#4928) # Objective Make core types in ECS smaller. The column sparse set in Tables is never updated after creation. ## Solution Create `ImmutableSparseSet` which removes the capacity fields in the backing vec's and the APIs for inserting or removing elements. Drops the size of the sparse set by 3 usizes (24 bytes on 64-bit systems) ## Followup ~~After #4809, Archetype's component SparseSet should be replaced with it.~~ This has been done. --- ## Changelog Removed: `Table::component_capacity` ## Migration Guide `Table::component_capacity()` has been removed as Tables do not support adding/removing columns after construction. Co-authored-by: Carter Anderson --- crates/bevy_ecs/src/archetype.rs | 8 +- crates/bevy_ecs/src/storage/sparse_set.rs | 161 ++++++++++++++-------- crates/bevy_ecs/src/storage/table.rs | 81 +++++++---- 3 files changed, 167 insertions(+), 83 deletions(-) diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index f72ac28784a47..b6bcc154cd561 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -5,7 +5,7 @@ use crate::{ bundle::BundleId, component::{ComponentId, StorageType}, entity::{Entity, EntityLocation}, - storage::{SparseArray, SparseSet, SparseSetIndex, TableId}, + storage::{ImmutableSparseSet, SparseArray, SparseSet, SparseSetIndex, TableId}, }; use std::{ collections::HashMap, @@ -182,7 +182,7 @@ pub struct Archetype { table_id: TableId, edges: Edges, entities: Vec, - components: SparseSet, + components: ImmutableSparseSet, } impl Archetype { @@ -217,8 +217,8 @@ impl Archetype { Self { id, table_id, - components, - entities: Default::default(), + entities: Vec::new(), + components: components.into_immutable(), edges: Default::default(), } } diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index f746708fa6c39..b935911ed2989 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -14,6 +14,14 @@ pub(crate) struct SparseArray { marker: PhantomData, } +/// A space-optimized version of [`SparseArray`] that cannot be changed +/// after construction. +#[derive(Debug)] +pub(crate) struct ImmutableSparseArray { + values: Box<[Option]>, + marker: PhantomData, +} + impl Default for SparseArray { fn default() -> Self { Self::new() @@ -30,6 +38,27 @@ impl SparseArray { } } +macro_rules! impl_sparse_array { + ($ty:ident) => { + impl $ty { + #[inline] + pub fn contains(&self, index: I) -> bool { + let index = index.sparse_set_index(); + self.values.get(index).map(|v| v.is_some()).unwrap_or(false) + } + + #[inline] + pub fn get(&self, index: I) -> Option<&V> { + let index = index.sparse_set_index(); + self.values.get(index).map(|v| v.as_ref()).unwrap_or(None) + } + } + }; +} + +impl_sparse_array!(SparseArray); +impl_sparse_array!(ImmutableSparseArray); + impl SparseArray { #[inline] pub fn insert(&mut self, index: I, value: V) { @@ -40,18 +69,6 @@ impl SparseArray { self.values[index] = Some(value); } - #[inline] - pub fn contains(&self, index: I) -> bool { - let index = index.sparse_set_index(); - self.values.get(index).map(|v| v.is_some()).unwrap_or(false) - } - - #[inline] - pub fn get(&self, index: I) -> Option<&V> { - let index = index.sparse_set_index(); - self.values.get(index).map(|v| v.as_ref()).unwrap_or(None) - } - #[inline] pub fn get_mut(&mut self, index: I) -> Option<&mut V> { let index = index.sparse_set_index(); @@ -70,6 +87,13 @@ impl SparseArray { pub fn clear(&mut self) { self.values.clear(); } + + pub(crate) fn into_immutable(self) -> ImmutableSparseArray { + ImmutableSparseArray { + values: self.values.into_boxed_slice(), + marker: PhantomData, + } + } } /// A sparse data structure of [Components](crate::component::Component) @@ -249,11 +273,75 @@ pub struct SparseSet { sparse: SparseArray, } +/// A space-optimized version of [`SparseSet`] that cannot be changed +/// after construction. +#[derive(Debug)] +pub(crate) struct ImmutableSparseSet { + dense: Box<[V]>, + indices: Box<[I]>, + sparse: ImmutableSparseArray, +} + +macro_rules! impl_sparse_set { + ($ty:ident) => { + impl $ty { + #[inline] + pub fn len(&self) -> usize { + self.dense.len() + } + + #[inline] + pub fn contains(&self, index: I) -> bool { + self.sparse.contains(index) + } + + pub fn get(&self, index: I) -> Option<&V> { + self.sparse.get(index).map(|dense_index| { + // SAFETY: if the sparse index points to something in the dense vec, it exists + unsafe { self.dense.get_unchecked(*dense_index) } + }) + } + + pub fn get_mut(&mut self, index: I) -> Option<&mut V> { + let dense = &mut self.dense; + self.sparse.get(index).map(move |dense_index| { + // SAFETY: if the sparse index points to something in the dense vec, it exists + unsafe { dense.get_unchecked_mut(*dense_index) } + }) + } + + pub fn indices(&self) -> impl Iterator + '_ { + self.indices.iter().cloned() + } + + pub fn values(&self) -> impl Iterator { + self.dense.iter() + } + + pub fn values_mut(&mut self) -> impl Iterator { + self.dense.iter_mut() + } + + pub fn iter(&self) -> impl Iterator { + self.indices.iter().zip(self.dense.iter()) + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.indices.iter().zip(self.dense.iter_mut()) + } + } + }; +} + +impl_sparse_set!(SparseSet); +impl_sparse_set!(ImmutableSparseSet); + impl Default for SparseSet { fn default() -> Self { Self::new() } } + impl SparseSet { pub const fn new() -> Self { Self { @@ -306,36 +394,11 @@ impl SparseSet { } } - #[inline] - pub fn len(&self) -> usize { - self.dense.len() - } - #[inline] pub fn is_empty(&self) -> bool { self.dense.len() == 0 } - #[inline] - pub fn contains(&self, index: I) -> bool { - self.sparse.contains(index) - } - - pub fn get(&self, index: I) -> Option<&V> { - self.sparse.get(index).map(|dense_index| { - // SAFETY: if the sparse index points to something in the dense vec, it exists - unsafe { self.dense.get_unchecked(*dense_index) } - }) - } - - pub fn get_mut(&mut self, index: I) -> Option<&mut V> { - let dense = &mut self.dense; - self.sparse.get(index).map(move |dense_index| { - // SAFETY: if the sparse index points to something in the dense vec, it exists - unsafe { dense.get_unchecked_mut(*dense_index) } - }) - } - pub fn remove(&mut self, index: I) -> Option { self.sparse.remove(index).map(|dense_index| { let is_last = dense_index == self.dense.len() - 1; @@ -349,24 +412,12 @@ impl SparseSet { }) } - pub fn indices(&self) -> impl Iterator + '_ { - self.indices.iter().cloned() - } - - pub fn values(&self) -> impl Iterator { - self.dense.iter() - } - - pub fn values_mut(&mut self) -> impl Iterator { - self.dense.iter_mut() - } - - pub fn iter(&self) -> impl Iterator { - self.indices.iter().zip(self.dense.iter()) - } - - pub fn iter_mut(&mut self) -> impl Iterator { - self.indices.iter().zip(self.dense.iter_mut()) + pub(crate) fn into_immutable(self) -> ImmutableSparseSet { + ImmutableSparseSet { + dense: self.dense.into_boxed_slice(), + indices: self.indices.into_boxed_slice(), + sparse: self.sparse.into_immutable(), + } } } diff --git a/crates/bevy_ecs/src/storage/table.rs b/crates/bevy_ecs/src/storage/table.rs index adb684469a6d6..aa7bb32e535ec 100644 --- a/crates/bevy_ecs/src/storage/table.rs +++ b/crates/bevy_ecs/src/storage/table.rs @@ -2,7 +2,7 @@ use crate::{ component::{ComponentId, ComponentInfo, ComponentTicks, Components}, entity::Entity, query::DebugCheckedUnwrap, - storage::{blob_vec::BlobVec, SparseSet}, + storage::{blob_vec::BlobVec, ImmutableSparseSet, SparseSet}, }; use bevy_ptr::{OwningPtr, Ptr, PtrMut}; use bevy_utils::HashMap; @@ -262,31 +262,68 @@ impl Column { } } -pub struct Table { +/// A builder type for constructing [`Table`]s. +/// +/// - Use [`with_capacity`] to initialize the builder. +/// - Repeatedly call [`add_column`] to add columns for components. +/// - Finalize with [`build`] to get the constructed [`Table`]. +/// +/// [`with_capacity`]: Self::with_capacity +/// [`add_column`]: Self::add_column +/// [`build`]: Self::build +pub(crate) struct TableBuilder { columns: SparseSet, - entities: Vec, + capacity: usize, } -impl Table { - pub(crate) fn with_capacity(capacity: usize, column_capacity: usize) -> Table { +impl TableBuilder { + /// Creates a blank [`Table`], allocating space for `column_capacity` columns + /// with the capacity to hold `capacity` entities worth of components each. + pub fn with_capacity(capacity: usize, column_capacity: usize) -> Self { Self { columns: SparseSet::with_capacity(column_capacity), - entities: Vec::with_capacity(capacity), + capacity, } } - #[inline] - pub fn entities(&self) -> &[Entity] { - &self.entities - } - - pub(crate) fn add_column(&mut self, component_info: &ComponentInfo) { + pub fn add_column(&mut self, component_info: &ComponentInfo) { self.columns.insert( component_info.id(), - Column::with_capacity(component_info, self.entities.capacity()), + Column::with_capacity(component_info, self.capacity), ); } + pub fn build(self) -> Table { + Table { + columns: self.columns.into_immutable(), + entities: Vec::with_capacity(self.capacity), + } + } +} + +/// A column-oriented [structure-of-arrays] based storage for [`Component`]s of entities +/// in a [`World`]. +/// +/// Conceptually, a `Table` can be thought of as an `HashMap`, where +/// each `Column` is a type-erased `Vec`. Each row corresponds to a single entity +/// (i.e. index 3 in Column A and index 3 in Column B point to different components on the same +/// entity). Fetching components from a table involves fetching the associated column for a +/// component type (via it's [`ComponentId`]), then fetching the entity's row within that column. +/// +/// [structure-of-arrays]: https://en.wikipedia.org/wiki/AoS_and_SoA#Structure_of_arrays +/// [`Component`]: crate::component::Component +/// [`World`]: crate::world::World +pub struct Table { + columns: ImmutableSparseSet, + entities: Vec, +} + +impl Table { + #[inline] + pub fn entities(&self) -> &[Entity] { + &self.entities + } + /// Removes the entity at the given row and returns the entity swapped in to replace it (if an /// entity was swapped in) /// @@ -457,11 +494,6 @@ impl Table { self.entities.capacity() } - #[inline] - pub fn component_capacity(&self) -> usize { - self.columns.capacity() - } - #[inline] pub fn is_empty(&self) -> bool { self.entities.is_empty() @@ -495,7 +527,7 @@ pub struct Tables { impl Default for Tables { fn default() -> Self { - let empty_table = Table::with_capacity(0, 0); + let empty_table = TableBuilder::with_capacity(0, 0).build(); Tables { tables: vec![empty_table], table_ids: HashMap::default(), @@ -548,11 +580,11 @@ impl Tables { .raw_entry_mut() .from_key(component_ids) .or_insert_with(|| { - let mut table = Table::with_capacity(0, component_ids.len()); + let mut table = TableBuilder::with_capacity(0, component_ids.len()); for component_id in component_ids { table.add_column(components.get_info_unchecked(*component_id)); } - tables.push(table); + tables.push(table.build()); (component_ids.to_vec(), TableId(tables.len() - 1)) }); @@ -601,7 +633,7 @@ mod tests { use crate::{ component::{ComponentTicks, Components}, entity::Entity, - storage::Table, + storage::TableBuilder, }; #[derive(Component)] struct W(T); @@ -612,8 +644,9 @@ mod tests { let mut storages = Storages::default(); let component_id = components.init_component::>(&mut storages); let columns = &[component_id]; - let mut table = Table::with_capacity(0, columns.len()); - table.add_column(components.get_info(component_id).unwrap()); + let mut builder = TableBuilder::with_capacity(0, columns.len()); + builder.add_column(components.get_info(component_id).unwrap()); + let mut table = builder.build(); let entities = (0..200).map(Entity::from_raw).collect::>(); for entity in &entities { // SAFETY: we allocate and immediately set data afterwards