Skip to content
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

[Merged by Bors] - Immutable sparse sets for metadata storage #4928

Closed
wants to merge 15 commits into from
Closed
94 changes: 94 additions & 0 deletions crates/bevy_ecs/src/storage/sparse_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,33 @@ impl<I: SparseSetIndex, V> SparseArray<I, V> {
pub fn clear(&mut self) {
self.values.clear();
}

pub fn to_immutable(self) -> ImmutableSparseArray<I, V> {
ImmutableSparseArray {
values: self.values.into_boxed_slice(),
marker: PhantomData,
}
}
}

#[derive(Debug)]
pub struct ImmutableSparseArray<I, V = I> {
james7132 marked this conversation as resolved.
Show resolved Hide resolved
values: Box<[Option<V>]>,
marker: PhantomData<I>,
}

impl<I: SparseSetIndex, V> ImmutableSparseArray<I, V> {
#[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)
}
}

/// A sparse data structure of [Components](crate::component::Component)
Expand Down Expand Up @@ -426,6 +453,73 @@ impl<I: SparseSetIndex, V> SparseSet<I, V> {
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&I, &mut V)> {
self.indices.iter().zip(self.dense.iter_mut())
}

pub fn to_immutable(self) -> ImmutableSparseSet<I, V> {
ImmutableSparseSet {
dense: self.dense.into_boxed_slice(),
indices: self.indices.into_boxed_slice(),
sparse: self.sparse.to_immutable(),
}
}
}

#[derive(Debug)]
pub struct ImmutableSparseSet<I, V: 'static> {
james7132 marked this conversation as resolved.
Show resolved Hide resolved
dense: Box<[V]>,
indices: Box<[I]>,
sparse: ImmutableSparseArray<I, usize>,
}

impl<I: SparseSetIndex, V> ImmutableSparseSet<I, V> {
#[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| {
// SAFE: 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| {
// SAFE: 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<Item = I> + '_ {
self.indices.iter().cloned()
}

pub fn values(&self) -> impl Iterator<Item = &V> {
self.dense.iter()
}

pub fn values_mut(&mut self) -> impl Iterator<Item = &mut V> {
self.dense.iter_mut()
}

pub fn iter(&self) -> impl Iterator<Item = (&I, &V)> {
self.indices.iter().zip(self.dense.iter())
}

pub fn iter_mut(&mut self) -> impl Iterator<Item = (&I, &mut V)> {
self.indices.iter().zip(self.dense.iter_mut())
}
}

pub trait SparseSetIndex: Clone + PartialEq + Eq + Hash {
Expand Down
56 changes: 32 additions & 24 deletions crates/bevy_ecs/src/storage/table.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
component::{ComponentId, ComponentInfo, ComponentTicks, Components},
entity::Entity,
storage::{BlobVec, SparseSet},
storage::{BlobVec, ImmutableSparseSet, SparseSet},
};
use bevy_ptr::{OwningPtr, Ptr, PtrMut};
use bevy_utils::HashMap;
Expand Down Expand Up @@ -186,38 +186,45 @@ impl Column {
}
}

pub struct Table {
pub struct TableBuilder {
james7132 marked this conversation as resolved.
Show resolved Hide resolved
columns: SparseSet<ComponentId, Column>,
entities: Vec<Entity>,
capacity: usize,
}

impl Table {
pub const fn new() -> Table {
impl TableBuilder {
pub fn with_capacity(capacity: usize, column_capacity: usize) -> Self {
james7132 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a chance that we end up realloc-ing our tables on construction in some / all cases, due to the fact that with_capacity isn't guaranteed to allocate exactly capacity, but shrink_to_fit will shrink to capacity?

Copy link
Member Author

@james7132 james7132 Nov 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is, but unless we're constantly making new Tables and Archetypes, the cost IMO is negligible over the lifetime of the app.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The frequency might go up drastically if we start doing "fragmenting relations". In that context the bytes saved might not be worth it. Hard to say what to value more.

I guess if/when we implement fragmenting relations we can let benchmarks decide. But this level of nuance is easy to forget about.

Self {
columns: SparseSet::new(),
entities: Vec::new(),
columns: SparseSet::with_capacity(column_capacity),
capacity,
}
}

pub fn with_capacity(capacity: usize, column_capacity: usize) -> Table {
Self {
columns: SparseSet::with_capacity(column_capacity),
entities: Vec::with_capacity(capacity),
pub fn add_column(&mut self, component_info: &ComponentInfo) {
self.columns.insert(
component_info.id(),
Column::with_capacity(component_info, self.capacity),
);
}

pub fn build(self) -> Table {
Table {
columns: self.columns.to_immutable(),
entities: Vec::with_capacity(self.capacity),
}
}
}

pub struct Table {
james7132 marked this conversation as resolved.
Show resolved Hide resolved
columns: ImmutableSparseSet<ComponentId, Column>,
entities: Vec<Entity>,
}

impl Table {
#[inline]
pub fn entities(&self) -> &[Entity] {
&self.entities
}

pub fn add_column(&mut self, component_info: &ComponentInfo) {
self.columns.insert(
component_info.id(),
Column::with_capacity(component_info, self.entities.capacity()),
);
}

/// Removes the entity at the given row and returns the entity swapped in to replace it (if an
/// entity was swapped in)
///
Expand Down Expand Up @@ -414,7 +421,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(),
Expand Down Expand Up @@ -472,11 +479,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.iter() {
table.add_column(components.get_info_unchecked(*component_id));
}
tables.push(table);
tables.push(table.build());
(component_ids.to_vec(), TableId(tables.len() - 1))
});

Expand Down Expand Up @@ -526,7 +533,7 @@ mod tests {
use crate::component::Component;
use crate::ptr::OwningPtr;
use crate::storage::Storages;
use crate::{component::Components, entity::Entity, storage::Table};
use crate::{component::Components, entity::Entity, storage::TableBuilder};
#[derive(Component)]
struct W<T>(T);

Expand All @@ -536,8 +543,9 @@ mod tests {
let mut storages = Storages::default();
let component_id = components.init_component::<W<usize>>(&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::<Vec<_>>();
for entity in &entities {
// SAFE: we allocate and immediately set data afterwards
Expand Down