Skip to content

Commit da0235a

Browse files
committed
[bevy_<util/ecs>] Better SparseArray performance
Problem: - bevy_ecs::SparseArray uses an `Option<T>` internally to represent is the value is valid or not. - This causes extra branching as well as extra memory overhead due to Option's discriminatior. Solution: - Implement a `PackedOption` class which uses a value of `T` to represent the `None` state. - Have SparseArray use `PackedOption` internally over `Option`.
1 parent 2390bee commit da0235a

File tree

5 files changed

+351
-35
lines changed

5 files changed

+351
-35
lines changed

crates/bevy_ecs/src/archetype.rs

+64-11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::{
44
entity::{Entity, EntityLocation},
55
storage::{Column, SparseArray, SparseSet, SparseSetIndex, TableId},
66
};
7+
use bevy_utils::{NoneValue, PackedOption};
78
use std::{
89
borrow::Cow,
910
collections::HashMap,
@@ -25,6 +26,11 @@ impl ArchetypeId {
2526
ArchetypeId(0)
2627
}
2728

29+
#[inline]
30+
pub const fn invalid() -> ArchetypeId {
31+
ArchetypeId(usize::MAX)
32+
}
33+
2834
#[inline]
2935
pub const fn resource() -> ArchetypeId {
3036
ArchetypeId(1)
@@ -46,11 +52,30 @@ pub struct AddBundle {
4652
pub bundle_status: Vec<ComponentStatus>,
4753
}
4854

55+
impl NoneValue for ArchetypeId {
56+
const NONE_VALUE: Self = ArchetypeId::invalid();
57+
58+
fn is_none_value(&self) -> bool {
59+
*self == Self::NONE_VALUE
60+
}
61+
}
62+
63+
impl NoneValue for AddBundle {
64+
const NONE_VALUE: Self = Self {
65+
archetype_id: ArchetypeId::NONE_VALUE,
66+
bundle_status: Vec::new(),
67+
};
68+
69+
fn is_none_value(&self) -> bool {
70+
self.archetype_id.is_none_value()
71+
}
72+
}
73+
4974
#[derive(Default)]
5075
pub struct Edges {
5176
pub add_bundle: SparseArray<BundleId, AddBundle>,
52-
pub remove_bundle: SparseArray<BundleId, Option<ArchetypeId>>,
53-
pub remove_bundle_intersection: SparseArray<BundleId, Option<ArchetypeId>>,
77+
pub remove_bundle: SparseArray<BundleId, ArchetypeId>,
78+
pub remove_bundle_intersection: SparseArray<BundleId, ArchetypeId>,
5479
}
5580

5681
impl Edges {
@@ -76,28 +101,32 @@ impl Edges {
76101
}
77102

78103
#[inline]
79-
pub fn get_remove_bundle(&self, bundle_id: BundleId) -> Option<Option<ArchetypeId>> {
80-
self.remove_bundle.get(bundle_id).cloned()
104+
pub fn get_remove_bundle(&self, bundle_id: BundleId) -> PackedOption<ArchetypeId> {
105+
self.remove_bundle.get(bundle_id).cloned().into()
81106
}
82107

83108
#[inline]
84-
pub fn set_remove_bundle(&mut self, bundle_id: BundleId, archetype_id: Option<ArchetypeId>) {
109+
pub fn set_remove_bundle(
110+
&mut self,
111+
bundle_id: BundleId,
112+
archetype_id: PackedOption<ArchetypeId>,
113+
) {
85114
self.remove_bundle.insert(bundle_id, archetype_id);
86115
}
87116

88117
#[inline]
89-
pub fn get_remove_bundle_intersection(
90-
&self,
91-
bundle_id: BundleId,
92-
) -> Option<Option<ArchetypeId>> {
93-
self.remove_bundle_intersection.get(bundle_id).cloned()
118+
pub fn get_remove_bundle_intersection(&self, bundle_id: BundleId) -> PackedOption<ArchetypeId> {
119+
self.remove_bundle_intersection
120+
.get(bundle_id)
121+
.copied()
122+
.into()
94123
}
95124

96125
#[inline]
97126
pub fn set_remove_bundle_intersection(
98127
&mut self,
99128
bundle_id: BundleId,
100-
archetype_id: Option<ArchetypeId>,
129+
archetype_id: PackedOption<ArchetypeId>,
101130
) {
102131
self.remove_bundle_intersection
103132
.insert(bundle_id, archetype_id);
@@ -119,6 +148,17 @@ pub(crate) struct ArchetypeComponentInfo {
119148
pub(crate) archetype_component_id: ArchetypeComponentId,
120149
}
121150

151+
impl NoneValue for ArchetypeComponentInfo {
152+
const NONE_VALUE: Self = Self {
153+
storage_type: StorageType::Table,
154+
archetype_component_id: ArchetypeComponentId::NONE_VALUE,
155+
};
156+
157+
fn is_none_value(&self) -> bool {
158+
self.archetype_component_id.is_none_value()
159+
}
160+
}
161+
122162
pub struct Archetype {
123163
id: ArchetypeId,
124164
entities: Vec<Entity>,
@@ -337,6 +377,11 @@ pub struct ArchetypeIdentity {
337377
pub struct ArchetypeComponentId(usize);
338378

339379
impl ArchetypeComponentId {
380+
#[inline]
381+
pub const fn invalid() -> Self {
382+
Self(usize::MAX)
383+
}
384+
340385
#[inline]
341386
pub const fn new(index: usize) -> Self {
342387
Self(index)
@@ -359,6 +404,14 @@ impl SparseSetIndex for ArchetypeComponentId {
359404
}
360405
}
361406

407+
impl NoneValue for ArchetypeComponentId {
408+
const NONE_VALUE: Self = Self::invalid();
409+
410+
fn is_none_value(&self) -> bool {
411+
*self == Self::invalid()
412+
}
413+
}
414+
362415
pub struct Archetypes {
363416
pub(crate) archetypes: Vec<Archetype>,
364417
pub(crate) archetype_component_count: usize,

crates/bevy_ecs/src/storage/sparse_set.rs

+39-17
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,27 @@ use crate::{
33
entity::Entity,
44
storage::BlobVec,
55
};
6+
use bevy_utils::{NoneValue, PackedOption};
67
use std::{cell::UnsafeCell, marker::PhantomData};
78

8-
#[derive(Debug)]
99
pub struct SparseArray<I, V = I> {
10-
values: Vec<Option<V>>,
10+
values: Vec<PackedOption<V>>,
1111
marker: PhantomData<I>,
1212
}
1313

14-
impl<I: SparseSetIndex, V> Default for SparseArray<I, V> {
14+
impl<I, V> std::fmt::Debug for SparseArray<I, V>
15+
where
16+
V: NoneValue + std::fmt::Debug,
17+
{
18+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19+
f.debug_struct("SparseArray")
20+
.field("values", &self.values)
21+
.field("marker", &self.marker)
22+
.finish()
23+
}
24+
}
25+
26+
impl<I: SparseSetIndex, V: NoneValue> Default for SparseArray<I, V> {
1527
fn default() -> Self {
1628
Self::new()
1729
}
@@ -27,7 +39,7 @@ impl<I, V> SparseArray<I, V> {
2739
}
2840
}
2941

30-
impl<I: SparseSetIndex, V> SparseArray<I, V> {
42+
impl<I: SparseSetIndex, V: NoneValue> SparseArray<I, V> {
3143
pub fn with_capacity(capacity: usize) -> Self {
3244
Self {
3345
values: Vec::with_capacity(capacity),
@@ -36,39 +48,48 @@ impl<I: SparseSetIndex, V> SparseArray<I, V> {
3648
}
3749

3850
#[inline]
39-
pub fn insert(&mut self, index: I, value: V) {
51+
pub fn insert<T: Into<PackedOption<V>>>(&mut self, index: I, value: T) {
4052
let index = index.sparse_set_index();
4153
if index >= self.values.len() {
42-
self.values.resize_with(index + 1, || None);
54+
self.values.resize_with(index + 1, || None.into());
4355
}
44-
self.values[index] = Some(value);
56+
self.values[index] = value.into();
4557
}
4658

4759
#[inline]
4860
pub fn contains(&self, index: I) -> bool {
4961
let index = index.sparse_set_index();
50-
self.values.get(index).map(|v| v.is_some()).unwrap_or(false)
62+
self.values
63+
.get(index)
64+
.map(PackedOption::is_some)
65+
.unwrap_or(false)
5166
}
5267

5368
#[inline]
5469
pub fn get(&self, index: I) -> Option<&V> {
5570
let index = index.sparse_set_index();
56-
self.values.get(index).map(|v| v.as_ref()).unwrap_or(None)
71+
self.values
72+
.get(index)
73+
.map(PackedOption::as_ref)
74+
.unwrap_or(None)
5775
}
5876

5977
#[inline]
6078
pub fn get_mut(&mut self, index: I) -> Option<&mut V> {
6179
let index = index.sparse_set_index();
6280
self.values
6381
.get_mut(index)
64-
.map(|v| v.as_mut())
82+
.map(PackedOption::as_mut)
6583
.unwrap_or(None)
6684
}
6785

6886
#[inline]
69-
pub fn remove(&mut self, index: I) -> Option<V> {
87+
pub fn remove(&mut self, index: I) -> PackedOption<V> {
7088
let index = index.sparse_set_index();
71-
self.values.get_mut(index).and_then(|value| value.take())
89+
self.values
90+
.get_mut(index)
91+
.map(PackedOption::take)
92+
.unwrap_or_default()
7293
}
7394

7495
#[inline]
@@ -77,9 +98,9 @@ impl<I: SparseSetIndex, V> SparseArray<I, V> {
7798
if index < self.values.len() {
7899
return self.values[index].get_or_insert_with(func);
79100
}
80-
self.values.resize_with(index + 1, || None);
101+
self.values.resize_with(index + 1, || None.into());
81102
let value = &mut self.values[index];
82-
*value = Some(func());
103+
*value = func().into();
83104
value.as_mut().unwrap()
84105
}
85106
}
@@ -179,7 +200,7 @@ impl ComponentSparseSet {
179200
/// it exists). It is the caller's responsibility to drop the returned ptr (if Some is
180201
/// returned).
181202
pub fn remove_and_forget(&mut self, entity: Entity) -> Option<*mut u8> {
182-
self.sparse.remove(entity).map(|dense_index| {
203+
self.sparse.remove(entity).expand().map(|dense_index| {
183204
// SAFE: unique access to ticks
184205
unsafe {
185206
(*self.ticks.get()).swap_remove(dense_index);
@@ -197,7 +218,7 @@ impl ComponentSparseSet {
197218
}
198219

199220
pub fn remove(&mut self, entity: Entity) -> bool {
200-
if let Some(dense_index) = self.sparse.remove(entity) {
221+
if let Some(dense_index) = self.sparse.remove(entity).expand() {
201222
self.ticks.get_mut().swap_remove(dense_index);
202223
self.entities.swap_remove(dense_index);
203224
let is_last = dense_index == self.dense.len() - 1;
@@ -233,6 +254,7 @@ impl<I: SparseSetIndex, V> Default for SparseSet<I, V> {
233254
Self::new()
234255
}
235256
}
257+
236258
impl<I, V> SparseSet<I, V> {
237259
pub const fn new() -> Self {
238260
Self {
@@ -338,7 +360,7 @@ impl<I: SparseSetIndex, V> SparseSet<I, V> {
338360
}
339361

340362
pub fn remove(&mut self, index: I) -> Option<V> {
341-
self.sparse.remove(index).map(|dense_index| {
363+
self.sparse.remove(index).expand().map(|dense_index| {
342364
let is_last = dense_index == self.dense.len() - 1;
343365
let value = self.dense.swap_remove(dense_index);
344366
self.indices.swap_remove(dense_index);

crates/bevy_ecs/src/world/entity_ref.rs

+10-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::{
66
storage::{SparseSet, Storages},
77
world::{Mut, World},
88
};
9+
use bevy_utils::PackedOption;
910
use std::any::TypeId;
1011

1112
pub struct EntityRef<'w> {
@@ -309,7 +310,8 @@ impl<'w> EntityMut<'w> {
309310
old_location.archetype_id,
310311
bundle_info,
311312
false,
312-
)?
313+
)
314+
.expand()?
313315
};
314316

315317
if new_archetype_id == old_location.archetype_id {
@@ -767,7 +769,7 @@ unsafe fn remove_bundle_from_archetype(
767769
archetype_id: ArchetypeId,
768770
bundle_info: &BundleInfo,
769771
intersection: bool,
770-
) -> Option<ArchetypeId> {
772+
) -> PackedOption<ArchetypeId> {
771773
// check the archetype graph to see if the Bundle has been removed from this archetype in the
772774
// past
773775
let remove_bundle_result = {
@@ -780,9 +782,10 @@ unsafe fn remove_bundle_from_archetype(
780782
current_archetype.edges().get_remove_bundle(bundle_info.id)
781783
}
782784
};
783-
let result = if let Some(result) = remove_bundle_result {
785+
786+
let result = if remove_bundle_result.is_some() {
784787
// this Bundle removal result is cached. just return that!
785-
result
788+
remove_bundle_result
786789
} else {
787790
let mut next_table_components;
788791
let mut next_sparse_set_components;
@@ -805,8 +808,8 @@ unsafe fn remove_bundle_from_archetype(
805808
// graph
806809
current_archetype
807810
.edges_mut()
808-
.set_remove_bundle(bundle_info.id, None);
809-
return None;
811+
.set_remove_bundle(bundle_info.id, None.into());
812+
return None.into();
810813
}
811814
}
812815

@@ -837,7 +840,7 @@ unsafe fn remove_bundle_from_archetype(
837840
next_table_components,
838841
next_sparse_set_components,
839842
);
840-
Some(new_archetype_id)
843+
new_archetype_id.into()
841844
};
842845
let current_archetype = &mut archetypes[archetype_id];
843846
// cache the result in an edge

crates/bevy_utils/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
mod enum_variant_meta;
2+
mod packed_option;
3+
24
pub use enum_variant_meta::*;
5+
pub use packed_option::*;
36

47
pub use ahash::AHasher;
58
pub use instant::{Duration, Instant};

0 commit comments

Comments
 (0)