Skip to content

Commit b257fff

Browse files
notverymoeNatalie Baker
and
Natalie Baker
authored
Change Entity::generation from u32 to NonZeroU32 for niche optimization (#9907)
# Objective - Implements change described in #3022 - Goal is to allow Entity to benefit from niche optimization, especially in the case of Option<Entity> to reduce memory overhead with structures with empty slots ## Discussion - First PR attempt: #3029 - Discord: https://discord.com/channels/691052431525675048/1154573759752183808/1154573764240093224 ## Solution - Change `Entity::generation` from u32 to NonZeroU32 to allow for niche optimization. - The reason for changing generation rather than index is so that the costs are only encountered on Entity free, instead of on Entity alloc - There was some concern with generations being used, due to there being some desire to introduce flags. This was more to do with the original retirement approach, however, in reality even if generations were reduced to 24-bits, we would still have 16 million generations available before wrapping and current ideas indicate that we would be using closer to 4-bits for flags. - Additionally, another concern was the representation of relationships where NonZeroU32 prevents us using the full address space, talking with Joy it seems unlikely to be an issue. The majority of the time these entity references will be low-index entries (ie. `ChildOf`, `Owes`), these will be able to be fast lookups, and the remainder of the range can use slower lookups to map to the address space. - It has the additional benefit of being less visible to most users, since generation is only ever really set through `from_bits` type methods. - `EntityMeta` was changed to match - On free, generation now explicitly wraps: - Originally, generation would panic in debug mode and wrap in release mode due to using regular ops. - The first attempt at this PR changed the behavior to "retire" slots and remove them from use when generations overflowed. This change was controversial, and likely needs a proper RFC/discussion. - Wrapping matches current release behaviour, and should therefore be less controversial. - Wrapping also more easily migrates to the retirement approach, as users likely to exhaust the exorbitant supply of generations will code defensively against aliasing and that defensive code is less likely to break than code assuming that generations don't wrap. - We use some unsafe code here when wrapping generations, to avoid branch on NonZeroU32 construction. It's guaranteed safe due to how we perform wrapping and it results in significantly smaller ASM code. - https://godbolt.org/z/6b6hj8PrM ## Migration - Previous `bevy_scene` serializations have a high likelihood of being broken, as they contain 0th generation entities. ## Current Issues - `Entities::reserve_generations` and `EntityMapper` wrap now, even in debug - although they technically did in release mode already so this probably isn't a huge issue. It just depends if we need to change anything here? --------- Co-authored-by: Natalie Baker <[email protected]>
1 parent dfa1a5e commit b257fff

File tree

5 files changed

+146
-59
lines changed

5 files changed

+146
-59
lines changed

benches/benches/bevy_utils/entity_hash.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ fn make_entity(rng: &mut impl Rng, size: usize) -> Entity {
1313
// -log₂(1-x) gives an exponential distribution with median 1.0
1414
// That lets us get values that are mostly small, but some are quite large
1515
// * For ids, half are in [0, size), half are unboundedly larger.
16-
// * For generations, half are in [0, 2), half are unboundedly larger.
16+
// * For generations, half are in [1, 3), half are unboundedly larger.
1717

1818
let x: f64 = rng.gen();
1919
let id = -(1.0 - x).log2() * (size as f64);
2020
let x: f64 = rng.gen();
21-
let gen = -(1.0 - x).log2() * 2.0;
21+
let gen = 1.0 + -(1.0 - x).log2() * 2.0;
2222

2323
// this is not reliable, but we're internal so a hack is ok
2424
let bits = ((gen as u64) << 32) | (id as u64);

crates/bevy_ecs/src/entity/map_entities.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::{entity::Entity, world::World};
22
use bevy_utils::EntityHashMap;
33

4+
use super::inc_generation_by;
5+
46
/// Operation to map all contained [`Entity`] fields in a type to new values.
57
///
68
/// As entity IDs are valid only for the [`World`] they're sourced from, using [`Entity`]
@@ -69,7 +71,7 @@ impl<'m> EntityMapper<'m> {
6971

7072
// this new entity reference is specifically designed to never represent any living entity
7173
let new = Entity {
72-
generation: self.dead_start.generation + self.generations,
74+
generation: inc_generation_by(self.dead_start.generation, self.generations),
7375
index: self.dead_start.index,
7476
};
7577
self.generations += 1;
@@ -146,7 +148,7 @@ mod tests {
146148
let mut world = World::new();
147149
let mut mapper = EntityMapper::new(&mut map, &mut world);
148150

149-
let mapped_ent = Entity::new(FIRST_IDX, 0);
151+
let mapped_ent = Entity::from_raw(FIRST_IDX);
150152
let dead_ref = mapper.get_or_reserve(mapped_ent);
151153

152154
assert_eq!(
@@ -155,7 +157,7 @@ mod tests {
155157
"should persist the allocated mapping from the previous line"
156158
);
157159
assert_eq!(
158-
mapper.get_or_reserve(Entity::new(SECOND_IDX, 0)).index(),
160+
mapper.get_or_reserve(Entity::from_raw(SECOND_IDX)).index(),
159161
dead_ref.index(),
160162
"should re-use the same index for further dead refs"
161163
);
@@ -173,7 +175,7 @@ mod tests {
173175
let mut world = World::new();
174176

175177
let dead_ref = EntityMapper::world_scope(&mut map, &mut world, |_, mapper| {
176-
mapper.get_or_reserve(Entity::new(0, 0))
178+
mapper.get_or_reserve(Entity::from_raw(0))
177179
});
178180

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

crates/bevy_ecs/src/entity/mod.rs

+113-28
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,15 @@
3737
//! [`EntityWorldMut::remove`]: crate::world::EntityWorldMut::remove
3838
mod map_entities;
3939

40+
use bevy_utils::tracing::warn;
4041
pub use map_entities::*;
4142

4243
use crate::{
4344
archetype::{ArchetypeId, ArchetypeRow},
4445
storage::{SparseSetIndex, TableId, TableRow},
4546
};
4647
use serde::{Deserialize, Serialize};
47-
use std::{convert::TryFrom, fmt, hash::Hash, mem, sync::atomic::Ordering};
48+
use std::{convert::TryFrom, fmt, hash::Hash, mem, num::NonZeroU32, sync::atomic::Ordering};
4849

4950
#[cfg(target_has_atomic = "64")]
5051
use std::sync::atomic::AtomicI64 as AtomicIdCursor;
@@ -124,7 +125,7 @@ pub struct Entity {
124125
// to make this struct equivalent to a u64.
125126
#[cfg(target_endian = "little")]
126127
index: u32,
127-
generation: u32,
128+
generation: NonZeroU32,
128129
#[cfg(target_endian = "big")]
129130
index: u32,
130131
}
@@ -188,8 +189,12 @@ pub(crate) enum AllocAtWithoutReplacement {
188189

189190
impl Entity {
190191
#[cfg(test)]
191-
pub(crate) const fn new(index: u32, generation: u32) -> Entity {
192-
Entity { index, generation }
192+
pub(crate) const fn new(index: u32, generation: u32) -> Result<Entity, &'static str> {
193+
if let Some(generation) = NonZeroU32::new(generation) {
194+
Ok(Entity { index, generation })
195+
} else {
196+
Err("Failed to construct Entity, check that generation > 0")
197+
}
193198
}
194199

195200
/// An entity ID with a placeholder value. This may or may not correspond to an actual entity,
@@ -228,7 +233,7 @@ impl Entity {
228233
/// ```
229234
pub const PLACEHOLDER: Self = Self::from_raw(u32::MAX);
230235

231-
/// Creates a new entity ID with the specified `index` and a generation of 0.
236+
/// Creates a new entity ID with the specified `index` and a generation of 1.
232237
///
233238
/// # Note
234239
///
@@ -244,7 +249,7 @@ impl Entity {
244249
pub const fn from_raw(index: u32) -> Entity {
245250
Entity {
246251
index,
247-
generation: 0,
252+
generation: NonZeroU32::MIN,
248253
}
249254
}
250255

@@ -256,17 +261,41 @@ impl Entity {
256261
/// No particular structure is guaranteed for the returned bits.
257262
#[inline(always)]
258263
pub const fn to_bits(self) -> u64 {
259-
(self.generation as u64) << 32 | self.index as u64
264+
(self.generation.get() as u64) << 32 | self.index as u64
260265
}
261266

262267
/// Reconstruct an `Entity` previously destructured with [`Entity::to_bits`].
263268
///
264269
/// Only useful when applied to results from `to_bits` in the same instance of an application.
265-
#[inline(always)]
270+
///
271+
/// # Panics
272+
///
273+
/// This method will likely panic if given `u64` values that did not come from [`Entity::to_bits`]
274+
#[inline]
266275
pub const fn from_bits(bits: u64) -> Self {
267-
Self {
268-
generation: (bits >> 32) as u32,
269-
index: bits as u32,
276+
// Construct an Identifier initially to extract the kind from.
277+
let id = Self::try_from_bits(bits);
278+
279+
match id {
280+
Ok(entity) => entity,
281+
Err(_) => panic!("Attempted to initialise invalid bits as an entity"),
282+
}
283+
}
284+
285+
/// Reconstruct an `Entity` previously destructured with [`Entity::to_bits`].
286+
///
287+
/// Only useful when applied to results from `to_bits` in the same instance of an application.
288+
///
289+
/// This method is the fallible counterpart to [`Entity::from_bits`].
290+
#[inline(always)]
291+
pub const fn try_from_bits(bits: u64) -> Result<Self, &'static str> {
292+
if let Some(generation) = NonZeroU32::new((bits >> 32) as u32) {
293+
Ok(Self {
294+
generation,
295+
index: bits as u32,
296+
})
297+
} else {
298+
Err("Invalid generation bits")
270299
}
271300
}
272301

@@ -285,7 +314,7 @@ impl Entity {
285314
/// given index has been reused (index, generation) pairs uniquely identify a given Entity.
286315
#[inline]
287316
pub const fn generation(self) -> u32 {
288-
self.generation
317+
self.generation.get()
289318
}
290319
}
291320

@@ -303,8 +332,9 @@ impl<'de> Deserialize<'de> for Entity {
303332
where
304333
D: serde::Deserializer<'de>,
305334
{
335+
use serde::de::Error;
306336
let id: u64 = serde::de::Deserialize::deserialize(deserializer)?;
307-
Ok(Entity::from_bits(id))
337+
Entity::try_from_bits(id).map_err(D::Error::custom)
308338
}
309339
}
310340

@@ -350,7 +380,7 @@ impl<'a> Iterator for ReserveEntitiesIterator<'a> {
350380
})
351381
.or_else(|| {
352382
self.index_range.next().map(|index| Entity {
353-
generation: 0,
383+
generation: NonZeroU32::MIN,
354384
index,
355385
})
356386
})
@@ -499,7 +529,7 @@ impl Entities {
499529
// As `self.free_cursor` goes more and more negative, we return IDs farther
500530
// and farther beyond `meta.len()`.
501531
Entity {
502-
generation: 0,
532+
generation: NonZeroU32::MIN,
503533
index: u32::try_from(self.meta.len() as IdCursor - n).expect("too many entities"),
504534
}
505535
}
@@ -528,7 +558,7 @@ impl Entities {
528558
let index = u32::try_from(self.meta.len()).expect("too many entities");
529559
self.meta.push(EntityMeta::EMPTY);
530560
Entity {
531-
generation: 0,
561+
generation: NonZeroU32::MIN,
532562
index,
533563
}
534564
}
@@ -615,7 +645,15 @@ impl Entities {
615645
if meta.generation != entity.generation {
616646
return None;
617647
}
618-
meta.generation += 1;
648+
649+
meta.generation = inc_generation_by(meta.generation, 1);
650+
651+
if meta.generation == NonZeroU32::MIN {
652+
warn!(
653+
"Entity({}) generation wrapped on Entities::free, aliasing may occur",
654+
entity.index
655+
);
656+
}
619657

620658
let loc = mem::replace(&mut meta.location, EntityMeta::EMPTY.location);
621659

@@ -646,7 +684,7 @@ impl Entities {
646684
// not reallocated since the generation is incremented in `free`
647685
pub fn contains(&self, entity: Entity) -> bool {
648686
self.resolve_from_id(entity.index())
649-
.map_or(false, |e| e.generation() == entity.generation)
687+
.map_or(false, |e| e.generation() == entity.generation())
650688
}
651689

652690
/// Clears all [`Entity`] from the World.
@@ -698,7 +736,7 @@ impl Entities {
698736

699737
let meta = &mut self.meta[index as usize];
700738
if meta.location.archetype_id == ArchetypeId::INVALID {
701-
meta.generation += generations;
739+
meta.generation = inc_generation_by(meta.generation, generations);
702740
true
703741
} else {
704742
false
@@ -722,7 +760,7 @@ impl Entities {
722760
// Returning None handles that case correctly
723761
let num_pending = usize::try_from(-free_cursor).ok()?;
724762
(idu < self.meta.len() + num_pending).then_some(Entity {
725-
generation: 0,
763+
generation: NonZeroU32::MIN,
726764
index,
727765
})
728766
}
@@ -840,15 +878,15 @@ impl Entities {
840878
#[repr(C)]
841879
struct EntityMeta {
842880
/// The current generation of the [`Entity`].
843-
pub generation: u32,
881+
pub generation: NonZeroU32,
844882
/// The current location of the [`Entity`]
845883
pub location: EntityLocation,
846884
}
847885

848886
impl EntityMeta {
849887
/// meta for **pending entity**
850888
const EMPTY: EntityMeta = EntityMeta {
851-
generation: 0,
889+
generation: NonZeroU32::MIN,
852890
location: EntityLocation::INVALID,
853891
};
854892
}
@@ -892,14 +930,61 @@ impl EntityLocation {
892930
};
893931
}
894932

933+
/// Offsets a generation value by the specified amount, wrapping to 1 instead of 0
934+
pub(crate) const fn inc_generation_by(lhs: NonZeroU32, rhs: u32) -> NonZeroU32 {
935+
let (lo, hi) = lhs.get().overflowing_add(rhs);
936+
let ret = lo + hi as u32;
937+
// SAFETY:
938+
// - Adding the overflow flag will offet overflows to start at 1 instead of 0
939+
// - The sum of `NonZeroU32::MAX` + `u32::MAX` + 1 (overflow) == `NonZeroU32::MAX`
940+
// - If the operation doesn't overflow, no offsetting takes place
941+
unsafe { NonZeroU32::new_unchecked(ret) }
942+
}
943+
895944
#[cfg(test)]
896945
mod tests {
897946
use super::*;
898947

948+
#[test]
949+
fn inc_generation_by_is_safe() {
950+
// Correct offsets without overflow
951+
assert_eq!(
952+
inc_generation_by(NonZeroU32::MIN, 1),
953+
NonZeroU32::new(2).unwrap()
954+
);
955+
956+
assert_eq!(
957+
inc_generation_by(NonZeroU32::MIN, 10),
958+
NonZeroU32::new(11).unwrap()
959+
);
960+
961+
// Check that overflows start at 1 correctly
962+
assert_eq!(inc_generation_by(NonZeroU32::MAX, 1), NonZeroU32::MIN);
963+
964+
assert_eq!(
965+
inc_generation_by(NonZeroU32::MAX, 2),
966+
NonZeroU32::new(2).unwrap()
967+
);
968+
969+
// Ensure we can't overflow twice
970+
assert_eq!(
971+
inc_generation_by(NonZeroU32::MAX, u32::MAX),
972+
NonZeroU32::MAX
973+
);
974+
}
975+
976+
#[test]
977+
fn entity_niche_optimization() {
978+
assert_eq!(
979+
std::mem::size_of::<Entity>(),
980+
std::mem::size_of::<Option<Entity>>()
981+
);
982+
}
983+
899984
#[test]
900985
fn entity_bits_roundtrip() {
901986
let e = Entity {
902-
generation: 0xDEADBEEF,
987+
generation: NonZeroU32::new(0xDEADBEEF).unwrap(),
903988
index: 0xBAADF00D,
904989
};
905990
assert_eq!(Entity::from_bits(e.to_bits()), e);
@@ -935,12 +1020,12 @@ mod tests {
9351020
#[test]
9361021
fn entity_const() {
9371022
const C1: Entity = Entity::from_raw(42);
938-
assert_eq!(42, C1.index);
939-
assert_eq!(0, C1.generation);
1023+
assert_eq!(42, C1.index());
1024+
assert_eq!(1, C1.generation());
9401025

9411026
const C2: Entity = Entity::from_bits(0x0000_00ff_0000_00cc);
942-
assert_eq!(0x0000_00cc, C2.index);
943-
assert_eq!(0x0000_00ff, C2.generation);
1027+
assert_eq!(0x0000_00cc, C2.index());
1028+
assert_eq!(0x0000_00ff, C2.generation());
9441029

9451030
const C3: u32 = Entity::from_raw(33).index();
9461031
assert_eq!(33, C3);
@@ -971,7 +1056,7 @@ mod tests {
9711056
// The very next entity allocated should be a further generation on the same index
9721057
let next_entity = entities.alloc();
9731058
assert_eq!(next_entity.index(), entity.index());
974-
assert!(next_entity.generation > entity.generation + GENERATIONS);
1059+
assert!(next_entity.generation() > entity.generation() + GENERATIONS);
9751060
}
9761061

9771062
#[test]

0 commit comments

Comments
 (0)