Skip to content

Commit 0b48587

Browse files
ElliottjPierceatlv24bushrat011899
authored
Make entity::index non max (#18704)
# Objective There are two problems this aims to solve. First, `Entity::index` is currently a `u32`. That means there are `u32::MAX + 1` possible entities. Not only is that awkward, but it also make `Entity` allocation more difficult. I discovered this while working on remote entity reservation, but even on main, `Entities` doesn't handle the `u32::MAX + 1` entity very well. It can not be batch reserved because that iterator uses exclusive ranges, which has a maximum upper bound of `u32::MAX - 1`. In other words, having `u32::MAX` as a valid index can be thought of as a bug right now. We either need to make that invalid (this PR), which makes Entity allocation cleaner and makes remote reservation easier (because the length only needs to be u32 instead of u64, which, in atomics is a big deal), or we need to take another pass at `Entities` to make it handle the `u32::MAX` index properly. Second, `TableRow`, `ArchetypeRow` and `EntityIndex` (a type alias for u32) all have `u32` as the underlying type. That means using these as the index type in a `SparseSet` uses 64 bits for the sparse list because it stores `Option<IndexType>`. By using `NonMaxU32` here, we cut the memory of that list in half. To my knowledge, `EntityIndex` is the only thing that would really benefit from this niche. `TableRow` and `ArchetypeRow` I think are not stored in an `Option` in bulk. But if they ever are, this would help. Additionally this ensures `TableRow::INVALID` and `ArchetypeRow::INVALID` never conflict with an actual row, which in a nice bonus. As a related note, if we do components as entities where `ComponentId` becomes `Entity`, the the `SparseSet<ComponentId>` will see a similar memory improvement too. ## Solution Create a new type `EntityRow` that wraps `NonMaxU32`, similar to `TableRow` and `ArchetypeRow`. Change `Entity::index` to this type. ## Downsides `NonMax` is implemented as a `NonZero` with a binary inversion. That means accessing and storing the value takes one more instruction. I don't think that's a big deal, but it's worth mentioning. As a consequence, `to_bits` uses `transmute` to skip the inversion which keeps it a nop. But that also means that ordering has now flipped. In other words, higher indices are considered less than lower indices. I don't think that's a problem, but it's also worth mentioning. ## Alternatives We could keep the index as a u32 type and just document that `u32::MAX` is invalid, modifying `Entities` to ensure it never gets handed out. (But that's not enforced by the type system.) We could still take advantage of the niche here in `ComponentSparseSet`. We'd just need some unsafe manual conversions, which is probably fine, but opens up the possibility for correctness problems later. We could change `Entities` to fully support the `u32::MAX` index. (But that makes `Entities` more complex and potentially slightly slower.) ## Testing - CI - A few tests were changed because they depend on different ordering and `to_bits` values. ## Future Work - It might be worth removing the niche on `Entity::generation` since there is now a different niche. - We could move `Entity::generation` into it's own type too for clarity. - We should change `ComponentSparseSet` to take advantage of the new niche. (This PR doesn't change that yet.) - Consider removing or updating `Identifier`. This is only used for `Entity`, so it might be worth combining since `Entity` is now more unique. --------- Co-authored-by: atlv <email@atlasdostal.com> Co-authored-by: Zachary Harrold <zac@harrold.com.au>
1 parent 9e2bd8a commit 0b48587

File tree

22 files changed

+439
-186
lines changed

22 files changed

+439
-186
lines changed

assets/scenes/load_scene_example.scn.ron

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
),
66
},
77
entities: {
8-
4294967296: (
8+
4294967297: (
99
components: {
1010
"bevy_ecs::name::Name": "joe",
1111
"bevy_transform::components::global_transform::GlobalTransform": ((1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0)),
@@ -23,7 +23,7 @@
2323
),
2424
},
2525
),
26-
4294967297: (
26+
4294967298: (
2727
components: {
2828
"scene::ComponentA": (
2929
x: 3.0,
@@ -32,4 +32,4 @@
3232
},
3333
),
3434
},
35-
)
35+
)

benches/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ bevy_platform = { path = "../crates/bevy_platform", default-features = false, fe
3232
glam = "0.29"
3333
rand = "0.8"
3434
rand_chacha = "0.3"
35+
nonmax = { version = "0.5", default-features = false }
3536

3637
# Make `bevy_render` compile on Linux with x11 windowing. x11 vs. Wayland does not matter here
3738
# because the benches do not actually open any windows.

benches/benches/bevy_ecs/world/entity_hash.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ fn make_entity(rng: &mut impl Rng, size: usize) -> Entity {
1717
let generation = 1.0 + -(1.0 - x).log2() * 2.0;
1818

1919
// this is not reliable, but we're internal so a hack is ok
20-
let bits = ((generation as u64) << 32) | (id as u64);
20+
let id = id as u64 + 1;
21+
let bits = ((generation as u64) << 32) | id;
2122
let e = Entity::from_bits(bits);
22-
assert_eq!(e.index(), id as u32);
23+
assert_eq!(e.index(), !(id as u32));
2324
assert_eq!(e.generation(), generation as u32);
2425
e
2526
}

benches/benches/bevy_ecs/world/world_get.rs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use core::hint::black_box;
2+
use nonmax::NonMaxU32;
23

34
use bevy_ecs::{
45
bundle::{Bundle, NoBundleEffect},
56
component::Component,
6-
entity::Entity,
7+
entity::{Entity, EntityRow},
78
system::{Query, SystemState},
89
world::World,
910
};
@@ -53,7 +54,9 @@ pub fn world_entity(criterion: &mut Criterion) {
5354

5455
bencher.iter(|| {
5556
for i in 0..entity_count {
56-
let entity = Entity::from_raw(i);
57+
let entity =
58+
// SAFETY: Range is exclusive.
59+
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
5760
black_box(world.entity(entity));
5861
}
5962
});
@@ -74,7 +77,9 @@ pub fn world_get(criterion: &mut Criterion) {
7477

7578
bencher.iter(|| {
7679
for i in 0..entity_count {
77-
let entity = Entity::from_raw(i);
80+
let entity =
81+
// SAFETY: Range is exclusive.
82+
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
7883
assert!(world.get::<Table>(entity).is_some());
7984
}
8085
});
@@ -84,7 +89,9 @@ pub fn world_get(criterion: &mut Criterion) {
8489

8590
bencher.iter(|| {
8691
for i in 0..entity_count {
87-
let entity = Entity::from_raw(i);
92+
let entity =
93+
// SAFETY: Range is exclusive.
94+
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
8895
assert!(world.get::<Sparse>(entity).is_some());
8996
}
9097
});
@@ -106,7 +113,9 @@ pub fn world_query_get(criterion: &mut Criterion) {
106113

107114
bencher.iter(|| {
108115
for i in 0..entity_count {
109-
let entity = Entity::from_raw(i);
116+
let entity =
117+
// SAFETY: Range is exclusive.
118+
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
110119
assert!(query.get(&world, entity).is_ok());
111120
}
112121
});
@@ -131,7 +140,9 @@ pub fn world_query_get(criterion: &mut Criterion) {
131140

132141
bencher.iter(|| {
133142
for i in 0..entity_count {
134-
let entity = Entity::from_raw(i);
143+
let entity =
144+
// SAFETY: Range is exclusive.
145+
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
135146
assert!(query.get(&world, entity).is_ok());
136147
}
137148
});
@@ -142,7 +153,9 @@ pub fn world_query_get(criterion: &mut Criterion) {
142153

143154
bencher.iter(|| {
144155
for i in 0..entity_count {
145-
let entity = Entity::from_raw(i);
156+
let entity =
157+
// SAFETY: Range is exclusive.
158+
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
146159
assert!(query.get(&world, entity).is_ok());
147160
}
148161
});
@@ -169,7 +182,10 @@ pub fn world_query_get(criterion: &mut Criterion) {
169182

170183
bencher.iter(|| {
171184
for i in 0..entity_count {
172-
let entity = Entity::from_raw(i);
185+
// SAFETY: Range is exclusive.
186+
let entity = Entity::from_raw(EntityRow::new(unsafe {
187+
NonMaxU32::new_unchecked(i)
188+
}));
173189
assert!(query.get(&world, entity).is_ok());
174190
}
175191
});

crates/bevy_ecs/src/entity/map_entities.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ impl<T: MapEntities, A: smallvec::Array<Item = T>> MapEntities for SmallVec<A> {
171171
/// fn get_mapped(&mut self, entity: Entity) -> Entity {
172172
/// self.map.get(&entity).copied().unwrap_or(entity)
173173
/// }
174-
///
174+
///
175175
/// fn set_mapped(&mut self, source: Entity, target: Entity) {
176176
/// self.map.insert(source, target);
177177
/// }
@@ -228,7 +228,7 @@ impl EntityMapper for SceneEntityMapper<'_> {
228228

229229
// this new entity reference is specifically designed to never represent any living entity
230230
let new = Entity::from_raw_and_generation(
231-
self.dead_start.index(),
231+
self.dead_start.row(),
232232
IdentifierMask::inc_masked_high_by(self.dead_start.generation, self.generations),
233233
);
234234

@@ -331,21 +331,19 @@ impl<'m> SceneEntityMapper<'m> {
331331

332332
#[cfg(test)]
333333
mod tests {
334+
334335
use crate::{
335336
entity::{Entity, EntityHashMap, EntityMapper, SceneEntityMapper},
336337
world::World,
337338
};
338339

339340
#[test]
340341
fn entity_mapper() {
341-
const FIRST_IDX: u32 = 1;
342-
const SECOND_IDX: u32 = 2;
343-
344342
let mut map = EntityHashMap::default();
345343
let mut world = World::new();
346344
let mut mapper = SceneEntityMapper::new(&mut map, &mut world);
347345

348-
let mapped_ent = Entity::from_raw(FIRST_IDX);
346+
let mapped_ent = Entity::from_raw_u32(1).unwrap();
349347
let dead_ref = mapper.get_mapped(mapped_ent);
350348

351349
assert_eq!(
@@ -354,7 +352,7 @@ mod tests {
354352
"should persist the allocated mapping from the previous line"
355353
);
356354
assert_eq!(
357-
mapper.get_mapped(Entity::from_raw(SECOND_IDX)).index(),
355+
mapper.get_mapped(Entity::from_raw_u32(2).unwrap()).index(),
358356
dead_ref.index(),
359357
"should re-use the same index for further dead refs"
360358
);
@@ -372,7 +370,7 @@ mod tests {
372370
let mut world = World::new();
373371

374372
let dead_ref = SceneEntityMapper::world_scope(&mut map, &mut world, |_, mapper| {
375-
mapper.get_mapped(Entity::from_raw(0))
373+
mapper.get_mapped(Entity::from_raw_u32(0).unwrap())
376374
});
377375

378376
// Next allocated entity should be a further generation on the same index

0 commit comments

Comments
 (0)