Skip to content

Commit 0520947

Browse files
committed
reduce tricky unsafety and simplify table structure (#2221)
I've noticed that we are overusing interior mutability of the Table data, where in many cases we already own a unique reference to it. That prompted a slight refactor aiming to reduce number of safety constraints that must be manually upheld. Now the majority of those are just about avoiding bound checking, which is relatively easy to prove right. Another aspect is reducing the complexity of Table struct. Notably, we don't ever use archetypes stored there, so this whole thing goes away. Capacity and grow amount were mostly superficial, as we are already using Vecs inside anyway, so I've got rid of those too. Now the overall table capacity is being driven by the internal entity Vec capacity. This has a side effect of automatically implementing exponential growth pattern for BitVecs reallocations inside Table, which to my measurements slightly improves performance in tests that are heavy on inserts. YMMV, but I hope that those tests were at least remotely correct.
1 parent 5cccba5 commit 0520947

File tree

5 files changed

+70
-91
lines changed

5 files changed

+70
-91
lines changed

crates/bevy_ecs/src/bundle.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ impl BundleInfo {
130130
&self,
131131
sparse_sets: &mut SparseSets,
132132
entity: Entity,
133-
table: &Table,
133+
table: &mut Table,
134134
table_row: usize,
135135
bundle_status: &[ComponentStatus],
136136
bundle: T,
@@ -145,8 +145,8 @@ impl BundleInfo {
145145
let component_status = bundle_status.get_unchecked(bundle_component);
146146
match self.storage_types[bundle_component] {
147147
StorageType::Table => {
148-
let column = table.get_column(component_id).unwrap();
149-
column.set_unchecked(table_row, component_ptr);
148+
let column = table.get_column_mut(component_id).unwrap();
149+
column.set_data_unchecked(table_row, component_ptr);
150150
let column_status = column.get_ticks_unchecked_mut(table_row);
151151
match component_status {
152152
ComponentStatus::Added => {

crates/bevy_ecs/src/storage/blob_vec.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ impl BlobVec {
3535
item_layout,
3636
drop,
3737
};
38-
blob_vec.reserve(capacity);
38+
blob_vec.reserve_exact(capacity);
3939
blob_vec
4040
}
4141
}
@@ -55,14 +55,14 @@ impl BlobVec {
5555
self.capacity
5656
}
5757

58-
pub fn reserve(&mut self, amount: usize) {
58+
pub fn reserve_exact(&mut self, additional: usize) {
5959
let available_space = self.capacity - self.len;
60-
if available_space < amount {
61-
self.grow(amount - available_space);
60+
if available_space < additional {
61+
self.grow_exact(additional - available_space);
6262
}
6363
}
6464

65-
fn grow(&mut self, increment: usize) {
65+
fn grow_exact(&mut self, increment: usize) {
6666
debug_assert!(self.item_layout.size() != 0);
6767

6868
let new_capacity = self.capacity + increment;
@@ -102,7 +102,7 @@ impl BlobVec {
102102
/// the newly allocated space must be immediately populated with a valid value
103103
#[inline]
104104
pub unsafe fn push_uninit(&mut self) -> usize {
105-
self.reserve(1);
105+
self.reserve_exact(1);
106106
let index = self.len;
107107
self.len += 1;
108108
index

crates/bevy_ecs/src/storage/table.rs

Lines changed: 55 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use crate::{
2-
archetype::ArchetypeId,
32
component::{ComponentId, ComponentInfo, ComponentTicks, Components},
43
entity::Entity,
54
storage::{BlobVec, SparseSet},
@@ -48,12 +47,24 @@ impl Column {
4847
}
4948
}
5049

50+
#[inline]
51+
fn ticks_mut(&mut self) -> &mut Vec<ComponentTicks> {
52+
self.ticks.get_mut()
53+
}
54+
55+
/// # Safety
56+
/// Assumes data has already been allocated for the given row.
57+
#[inline]
58+
pub unsafe fn set_unchecked(&mut self, row: usize, data: *mut u8, ticks: ComponentTicks) {
59+
self.set_data_unchecked(row, data);
60+
*self.ticks_mut().get_unchecked_mut(row) = ticks;
61+
}
62+
5163
/// # Safety
52-
/// Assumes data has already been allocated for the given row/column.
53-
/// Allows aliased mutable accesses to the data at the given `row`. Caller must ensure that this
54-
/// does not happen.
64+
/// Assumes data has already been allocated for the given row.
5565
#[inline]
56-
pub unsafe fn set_unchecked(&self, row: usize, data: *mut u8) {
66+
pub unsafe fn set_data_unchecked(&mut self, row: usize, data: *mut u8) {
67+
debug_assert!(row < self.len());
5768
self.data.set_unchecked(row, data);
5869
}
5970

@@ -68,20 +79,19 @@ impl Column {
6879
}
6980

7081
/// # Safety
71-
/// Assumes data has already been allocated for the given row/column.
72-
/// Allows aliased mutable accesses to the row's [ComponentTicks].
73-
/// Caller must ensure that this does not happen.
82+
/// Assumes data has already been allocated for the given row.
7483
#[inline]
75-
#[allow(clippy::mut_from_ref)]
76-
pub unsafe fn get_ticks_unchecked_mut(&self, row: usize) -> &mut ComponentTicks {
84+
pub unsafe fn get_ticks_unchecked_mut(&mut self, row: usize) -> &mut ComponentTicks {
7785
debug_assert!(row < self.len());
78-
(*self.ticks.get()).get_unchecked_mut(row)
86+
self.ticks_mut().get_unchecked_mut(row)
7987
}
8088

89+
/// # Safety
90+
/// index must be in-bounds
8191
#[inline]
8292
pub(crate) unsafe fn swap_remove_unchecked(&mut self, row: usize) {
8393
self.data.swap_remove_and_drop_unchecked(row);
84-
(*self.ticks.get()).swap_remove(row);
94+
self.ticks_mut().swap_remove(row);
8595
}
8696

8797
#[inline]
@@ -90,26 +100,22 @@ impl Column {
90100
row: usize,
91101
) -> (*mut u8, ComponentTicks) {
92102
let data = self.data.swap_remove_and_forget_unchecked(row);
93-
let ticks = (*self.ticks.get()).swap_remove(row);
103+
let ticks = self.ticks_mut().swap_remove(row);
94104
(data, ticks)
95105
}
96106

97-
/// # Safety
98-
/// allocated value must be immediately set at the returned row
99-
pub(crate) unsafe fn push_uninit(&mut self) -> usize {
107+
// # Safety
108+
// - ptr must point to valid data of this column's component type
109+
pub(crate) unsafe fn push(&mut self, ptr: *mut u8, ticks: ComponentTicks) {
100110
let row = self.data.push_uninit();
101-
(*self.ticks.get()).push(ComponentTicks::new(0));
102-
row
111+
self.data.set_unchecked(row, ptr);
112+
self.ticks_mut().push(ticks);
103113
}
104114

105115
#[inline]
106-
pub(crate) fn reserve(&mut self, additional: usize) {
107-
self.data.reserve(additional);
108-
// SAFE: unique access to self
109-
unsafe {
110-
let ticks = &mut (*self.ticks.get());
111-
ticks.reserve(additional);
112-
}
116+
pub(crate) fn reserve_exact(&mut self, additional: usize) {
117+
self.data.reserve_exact(additional);
118+
self.ticks_mut().reserve_exact(additional);
113119
}
114120

115121
/// # Safety
@@ -144,8 +150,7 @@ impl Column {
144150

145151
#[inline]
146152
pub(crate) fn check_change_ticks(&mut self, change_tick: u32) {
147-
let ticks = unsafe { (*self.ticks.get()).iter_mut() };
148-
for component_ticks in ticks {
153+
for component_ticks in self.ticks_mut() {
149154
component_ticks.check_ticks(change_tick);
150155
}
151156
}
@@ -154,29 +159,20 @@ impl Column {
154159
pub struct Table {
155160
columns: SparseSet<ComponentId, Column>,
156161
entities: Vec<Entity>,
157-
archetypes: Vec<ArchetypeId>,
158-
grow_amount: usize,
159-
capacity: usize,
160162
}
161163

162164
impl Table {
163-
pub const fn new(grow_amount: usize) -> Table {
165+
pub const fn new() -> Table {
164166
Self {
165167
columns: SparseSet::new(),
166168
entities: Vec::new(),
167-
archetypes: Vec::new(),
168-
grow_amount,
169-
capacity: 0,
170169
}
171170
}
172171

173-
pub fn with_capacity(capacity: usize, column_capacity: usize, grow_amount: usize) -> Table {
172+
pub fn with_capacity(capacity: usize, column_capacity: usize) -> Table {
174173
Self {
175174
columns: SparseSet::with_capacity(column_capacity),
176175
entities: Vec::with_capacity(capacity),
177-
archetypes: Vec::new(),
178-
grow_amount,
179-
capacity,
180176
}
181177
}
182178

@@ -185,14 +181,10 @@ impl Table {
185181
&self.entities
186182
}
187183

188-
pub fn add_archetype(&mut self, archetype_id: ArchetypeId) {
189-
self.archetypes.push(archetype_id);
190-
}
191-
192184
pub fn add_column(&mut self, component_info: &ComponentInfo) {
193185
self.columns.insert(
194186
component_info.id(),
195-
Column::with_capacity(component_info, self.capacity()),
187+
Column::with_capacity(component_info, self.entities.capacity()),
196188
)
197189
}
198190

@@ -232,8 +224,7 @@ impl Table {
232224
for column in self.columns.values_mut() {
233225
let (data, ticks) = column.swap_remove_and_forget_unchecked(row);
234226
if let Some(new_column) = new_table.get_column_mut(column.component_id) {
235-
new_column.set_unchecked(new_row, data);
236-
*new_column.get_ticks_unchecked_mut(new_row) = ticks;
227+
new_column.set_unchecked(new_row, data, ticks);
237228
}
238229
}
239230
TableMoveResult {
@@ -263,8 +254,7 @@ impl Table {
263254
for column in self.columns.values_mut() {
264255
if let Some(new_column) = new_table.get_column_mut(column.component_id) {
265256
let (data, ticks) = column.swap_remove_and_forget_unchecked(row);
266-
new_column.set_unchecked(new_row, data);
267-
*new_column.get_ticks_unchecked_mut(new_row) = ticks;
257+
new_column.set_unchecked(new_row, data, ticks);
268258
} else {
269259
column.swap_remove_unchecked(row);
270260
}
@@ -296,8 +286,7 @@ impl Table {
296286
for column in self.columns.values_mut() {
297287
let new_column = new_table.get_column_mut(column.component_id).unwrap();
298288
let (data, ticks) = column.swap_remove_and_forget_unchecked(row);
299-
new_column.set_unchecked(new_row, data);
300-
*new_column.get_ticks_unchecked_mut(new_row) = ticks;
289+
new_column.set_unchecked(new_row, data, ticks);
301290
}
302291
TableMoveResult {
303292
new_row,
@@ -324,20 +313,16 @@ impl Table {
324313
self.columns.contains(component_id)
325314
}
326315

327-
pub fn reserve(&mut self, amount: usize) {
328-
let available_space = self.capacity - self.len();
329-
if available_space < amount {
330-
let min_capacity = self.len() + amount;
331-
// normally we would check if min_capacity is 0 for the below calculation, but amount >
332-
// available_space and available_space > 0, so min_capacity > 1
333-
let new_capacity =
334-
((min_capacity + self.grow_amount - 1) / self.grow_amount) * self.grow_amount;
335-
let reserve_amount = new_capacity - self.len();
316+
pub fn reserve(&mut self, additional: usize) {
317+
if self.entities.capacity() - self.entities.len() < additional {
318+
self.entities.reserve(additional);
319+
320+
// use entities vector capacity as driving capacity for all related allocations
321+
let new_capacity = self.entities.capacity();
322+
336323
for column in self.columns.values_mut() {
337-
column.reserve(reserve_amount);
324+
column.reserve_exact(new_capacity - column.len());
338325
}
339-
self.entities.reserve(reserve_amount);
340-
self.capacity = new_capacity;
341326
}
342327
}
343328

@@ -358,7 +343,7 @@ impl Table {
358343

359344
#[inline]
360345
pub fn capacity(&self) -> usize {
361-
self.capacity
346+
self.entities.capacity()
362347
}
363348

364349
#[inline]
@@ -389,7 +374,7 @@ pub struct Tables {
389374

390375
impl Default for Tables {
391376
fn default() -> Self {
392-
let empty_table = Table::with_capacity(0, 0, 64);
377+
let empty_table = Table::with_capacity(0, 0);
393378
Tables {
394379
tables: vec![empty_table],
395380
table_ids: HashMap::default(),
@@ -446,7 +431,7 @@ impl Tables {
446431
let hash = hasher.finish();
447432
let tables = &mut self.tables;
448433
*self.table_ids.entry(hash).or_insert_with(move || {
449-
let mut table = Table::with_capacity(0, component_ids.len(), 64);
434+
let mut table = Table::with_capacity(0, component_ids.len());
450435
for component_id in component_ids.iter() {
451436
table.add_column(components.get_info_unchecked(*component_id));
452437
}
@@ -496,18 +481,19 @@ mod tests {
496481
let type_info = TypeInfo::of::<usize>();
497482
let component_id = components.get_or_insert_with(type_info.type_id(), || type_info);
498483
let columns = &[component_id];
499-
let mut table = Table::with_capacity(0, columns.len(), 64);
484+
let mut table = Table::with_capacity(0, columns.len());
500485
table.add_column(components.get_info(component_id).unwrap());
501486
let entities = (0..200).map(Entity::new).collect::<Vec<_>>();
502-
for (row, entity) in entities.iter().cloned().enumerate() {
487+
for entity in entities.iter() {
488+
// SAFE: we allocate and immediately set data afterwards
503489
unsafe {
504-
table.allocate(entity);
490+
let row = table.allocate(*entity);
505491
let mut value = row;
506492
let value_ptr = ((&mut value) as *mut usize).cast::<u8>();
507493
table
508-
.get_column(component_id)
494+
.get_column_mut(component_id)
509495
.unwrap()
510-
.set_unchecked(row, value_ptr);
496+
.set_data_unchecked(row, value_ptr);
511497
};
512498
}
513499

crates/bevy_ecs/src/world/entity_ref.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ impl<'w> EntityMut<'w> {
275275
};
276276
self.location = new_location;
277277

278-
let table = &storages.tables[archetype.table_id()];
278+
let table = &mut storages.tables[archetype.table_id()];
279279
let table_row = archetype.entity_table_row(new_location.index);
280280
// SAFE: table row is valid
281281
unsafe {

crates/bevy_ecs/src/world/mod.rs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -718,12 +718,10 @@ impl World {
718718
let column = unique_components
719719
.get_mut(component_id)
720720
.unwrap_or_else(|| panic!("resource does not exist: {}", std::any::type_name::<T>()));
721-
// SAFE: new location is immediately written to below
722-
let row = unsafe { column.push_uninit() };
723-
// SAFE: row was just allocated above
724-
unsafe { column.set_unchecked(row, ptr) };
725-
// SAFE: row was just allocated above
726-
unsafe { *column.get_ticks_unchecked_mut(row) = ticks };
721+
unsafe {
722+
// SAFE: pointer is of type T
723+
column.push(ptr, ticks);
724+
}
727725
result
728726
}
729727

@@ -787,13 +785,8 @@ impl World {
787785
if column.is_empty() {
788786
// SAFE: column is of type T and has been allocated above
789787
let data = (&mut value as *mut T).cast::<u8>();
790-
// SAFE: new location is immediately written to below
791-
let row = column.push_uninit();
792-
// SAFE: index was just allocated above
793-
column.set_unchecked(row, data);
794788
std::mem::forget(value);
795-
// SAFE: index was just allocated above
796-
*column.get_ticks_unchecked_mut(row) = ComponentTicks::new(change_tick);
789+
column.push(data, ComponentTicks::new(change_tick));
797790
} else {
798791
// SAFE: column is of type T and has already been allocated
799792
*column.get_unchecked(0).cast::<T>() = value;

0 commit comments

Comments
 (0)