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

Change Entity::generation from u32 to NonZeroU32 for niche optimization #9907

4 changes: 2 additions & 2 deletions benches/benches/bevy_utils/entity_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ fn make_entity(rng: &mut impl Rng, size: usize) -> Entity {
// -log₂(1-x) gives an exponential distribution with median 1.0
// That lets us get values that are mostly small, but some are quite large
// * For ids, half are in [0, size), half are unboundedly larger.
// * For generations, half are in [0, 2), half are unboundedly larger.
// * For generations, half are in [1, 3), half are unboundedly larger.

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

// this is not reliable, but we're internal so a hack is ok
let bits = ((gen as u64) << 32) | (id as u64);
Expand Down
10 changes: 6 additions & 4 deletions crates/bevy_ecs/src/entity/map_entities.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::{entity::Entity, world::World};
use bevy_utils::EntityHashMap;

use super::inc_generation_by;

/// Operation to map all contained [`Entity`] fields in a type to new values.
///
/// As entity IDs are valid only for the [`World`] they're sourced from, using [`Entity`]
Expand Down Expand Up @@ -69,7 +71,7 @@ impl<'m> EntityMapper<'m> {

// this new entity reference is specifically designed to never represent any living entity
let new = Entity {
generation: self.dead_start.generation + self.generations,
generation: inc_generation_by(self.dead_start.generation, self.generations),
index: self.dead_start.index,
};
self.generations += 1;
Expand Down Expand Up @@ -146,7 +148,7 @@ mod tests {
let mut world = World::new();
let mut mapper = EntityMapper::new(&mut map, &mut world);

let mapped_ent = Entity::new(FIRST_IDX, 0);
let mapped_ent = Entity::from_raw(FIRST_IDX);
let dead_ref = mapper.get_or_reserve(mapped_ent);

assert_eq!(
Expand All @@ -155,7 +157,7 @@ mod tests {
"should persist the allocated mapping from the previous line"
);
assert_eq!(
mapper.get_or_reserve(Entity::new(SECOND_IDX, 0)).index(),
mapper.get_or_reserve(Entity::from_raw(SECOND_IDX)).index(),
dead_ref.index(),
"should re-use the same index for further dead refs"
);
Expand All @@ -173,7 +175,7 @@ mod tests {
let mut world = World::new();

let dead_ref = EntityMapper::world_scope(&mut map, &mut world, |_, mapper| {
mapper.get_or_reserve(Entity::new(0, 0))
mapper.get_or_reserve(Entity::from_raw(0))
});

// Next allocated entity should be a further generation on the same index
Expand Down
141 changes: 113 additions & 28 deletions crates/bevy_ecs/src/entity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@
//! [`EntityWorldMut::remove`]: crate::world::EntityWorldMut::remove
mod map_entities;

use bevy_utils::tracing::warn;
pub use map_entities::*;

use crate::{
archetype::{ArchetypeId, ArchetypeRow},
storage::{SparseSetIndex, TableId, TableRow},
};
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, fmt, hash::Hash, mem, sync::atomic::Ordering};
use std::{convert::TryFrom, fmt, hash::Hash, mem, num::NonZeroU32, sync::atomic::Ordering};

#[cfg(target_has_atomic = "64")]
use std::sync::atomic::AtomicI64 as AtomicIdCursor;
Expand Down Expand Up @@ -124,7 +125,7 @@ pub struct Entity {
// to make this struct equivalent to a u64.
#[cfg(target_endian = "little")]
index: u32,
generation: u32,
generation: NonZeroU32,
#[cfg(target_endian = "big")]
index: u32,
}
Expand Down Expand Up @@ -188,8 +189,12 @@ pub(crate) enum AllocAtWithoutReplacement {

impl Entity {
#[cfg(test)]
pub(crate) const fn new(index: u32, generation: u32) -> Entity {
Entity { index, generation }
pub(crate) const fn new(index: u32, generation: u32) -> Result<Entity, &'static str> {
if let Some(generation) = NonZeroU32::new(generation) {
Ok(Entity { index, generation })
} else {
Err("Failed to construct Entity, check that generation > 0")
}
}

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

/// Creates a new entity ID with the specified `index` and a generation of 0.
/// Creates a new entity ID with the specified `index` and a generation of 1.
///
/// # Note
///
Expand All @@ -244,7 +249,7 @@ impl Entity {
pub const fn from_raw(index: u32) -> Entity {
Entity {
index,
generation: 0,
generation: NonZeroU32::MIN,
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that this function's rustdoc says "and a generation of 0", so that probably needs to be updated.

}
}

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

/// Reconstruct an `Entity` previously destructured with [`Entity::to_bits`].
///
/// Only useful when applied to results from `to_bits` in the same instance of an application.
#[inline(always)]
///
/// # Panics
///
/// This method will likely panic if given `u64` values that did not come from [`Entity::to_bits`]
#[inline]
pub const fn from_bits(bits: u64) -> Self {
Self {
generation: (bits >> 32) as u32,
index: bits as u32,
// Construct an Identifier initially to extract the kind from.
let id = Self::try_from_bits(bits);

match id {
Ok(entity) => entity,
Err(_) => panic!("Attempted to initialise invalid bits as an entity"),
}
}

/// Reconstruct an `Entity` previously destructured with [`Entity::to_bits`].
///
/// Only useful when applied to results from `to_bits` in the same instance of an application.
///
/// This method is the fallible counterpart to [`Entity::from_bits`].
#[inline(always)]
pub const fn try_from_bits(bits: u64) -> Result<Self, &'static str> {
if let Some(generation) = NonZeroU32::new((bits >> 32) as u32) {
Ok(Self {
generation,
index: bits as u32,
})
} else {
Err("Invalid generation bits")
}
}

Expand All @@ -285,7 +314,7 @@ impl Entity {
/// given index has been reused (index, generation) pairs uniquely identify a given Entity.
#[inline]
pub const fn generation(self) -> u32 {
self.generation
self.generation.get()
}
}

Expand All @@ -303,8 +332,9 @@ impl<'de> Deserialize<'de> for Entity {
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let id: u64 = serde::de::Deserialize::deserialize(deserializer)?;
Ok(Entity::from_bits(id))
Entity::try_from_bits(id).map_err(D::Error::custom)
}
}

Expand Down Expand Up @@ -350,7 +380,7 @@ impl<'a> Iterator for ReserveEntitiesIterator<'a> {
})
.or_else(|| {
self.index_range.next().map(|index| Entity {
generation: 0,
generation: NonZeroU32::MIN,
index,
})
})
Expand Down Expand Up @@ -498,7 +528,7 @@ impl Entities {
// As `self.free_cursor` goes more and more negative, we return IDs farther
// and farther beyond `meta.len()`.
Entity {
generation: 0,
generation: NonZeroU32::MIN,
index: u32::try_from(self.meta.len() as IdCursor - n).expect("too many entities"),
}
}
Expand Down Expand Up @@ -527,7 +557,7 @@ impl Entities {
let index = u32::try_from(self.meta.len()).expect("too many entities");
self.meta.push(EntityMeta::EMPTY);
Entity {
generation: 0,
generation: NonZeroU32::MIN,
index,
}
}
Expand Down Expand Up @@ -614,7 +644,15 @@ impl Entities {
if meta.generation != entity.generation {
return None;
}
meta.generation += 1;

meta.generation = inc_generation_by(meta.generation, 1);

if meta.generation == NonZeroU32::MIN {
warn!(
"Entity({}) generation wrapped on Entities::free, aliasing may occur",
entity.index
);
}

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

Expand Down Expand Up @@ -644,7 +682,7 @@ impl Entities {
// not reallocated since the generation is incremented in `free`
pub fn contains(&self, entity: Entity) -> bool {
self.resolve_from_id(entity.index())
.map_or(false, |e| e.generation() == entity.generation)
.map_or(false, |e| e.generation() == entity.generation())
}

/// Clears all [`Entity`] from the World.
Expand Down Expand Up @@ -696,7 +734,7 @@ impl Entities {

let meta = &mut self.meta[index as usize];
if meta.location.archetype_id == ArchetypeId::INVALID {
meta.generation += generations;
meta.generation = inc_generation_by(meta.generation, generations);
true
} else {
false
Expand All @@ -720,7 +758,7 @@ impl Entities {
// Returning None handles that case correctly
let num_pending = usize::try_from(-free_cursor).ok()?;
(idu < self.meta.len() + num_pending).then_some(Entity {
generation: 0,
generation: NonZeroU32::MIN,
index,
})
}
Expand Down Expand Up @@ -838,15 +876,15 @@ impl Entities {
#[repr(C)]
struct EntityMeta {
/// The current generation of the [`Entity`].
pub generation: u32,
pub generation: NonZeroU32,
/// The current location of the [`Entity`]
pub location: EntityLocation,
}

impl EntityMeta {
/// meta for **pending entity**
const EMPTY: EntityMeta = EntityMeta {
generation: 0,
generation: NonZeroU32::MIN,
location: EntityLocation::INVALID,
};
}
Expand Down Expand Up @@ -890,14 +928,61 @@ impl EntityLocation {
};
}

/// Offsets a generation value by the specified amount, wrapping to 1 instead of 0
pub(crate) const fn inc_generation_by(lhs: NonZeroU32, rhs: u32) -> NonZeroU32 {
let (lo, hi) = lhs.get().overflowing_add(rhs);
let ret = lo + hi as u32;
Comment on lines +933 to +934
Copy link
Contributor

Choose a reason for hiding this comment

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

Ohh, clever! This codegens really elegantly; nice work 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Kolsky up above came up with it:
#9907 (comment)

I was really surprised at it's elegance, definitely going to be using it in one or two places in my personal projects now that i know about it. I think, unfortunately, given my reading of:
#9797 (comment)

it means we won't be able to keep it (I think)

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, I think it we will be able to do something similar, but we'd need to wrap on a however many bits we have available for the generation segment. So at the moment, that would be 31 bits. So you'd have to do something like:

pub const fn nonzero_wrapping_high_increment(value: NonZeroU32) -> NonZeroU32 {
     let next_value = value.get().wrapping_add(1);
     // Mask the overflow bit
     let overflowed = (next_value & 0x8000_0000) >> 31;

    // Remove the overflow bit from the next value, but then add to it
    unsafe { NonZeroU32::new_unchecked((next_value & 0x7FFF_FFFF) + overflowed) }
}

As long as we know we are only incrementing by one each time, then it should still output fairly terse asm: https://rust.godbolt.org/z/PnYTPfGb6 Basically the same principle as before, just applied to wrapping on 31 bits (or whatever amount of bits we need later)

Copy link
Contributor Author

@notverymoe notverymoe Nov 22, 2023

Choose a reason for hiding this comment

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

Oh I see, that's interesting 🤔 We can definitely use that for the regular increment, it was just nice that this version worked for both slot incrementing and Entities::reserve_generations (which requires an arbitrary increment, but honestly might not even be correct).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tried a version with checked_add, it has a similar instruction count, but is probably more expensive.

// SAFETY:
// - Adding the overflow flag will offet overflows to start at 1 instead of 0
// - The sum of `NonZeroU32::MAX` + `u32::MAX` + 1 (overflow) == `NonZeroU32::MAX`
// - If the operation doesn't overflow, no offsetting takes place
unsafe { NonZeroU32::new_unchecked(ret) }
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn inc_generation_by_is_safe() {
// Correct offsets without overflow
assert_eq!(
inc_generation_by(NonZeroU32::MIN, 1),
NonZeroU32::new(2).unwrap()
);

assert_eq!(
inc_generation_by(NonZeroU32::MIN, 10),
NonZeroU32::new(11).unwrap()
);

// Check that overflows start at 1 correctly
assert_eq!(inc_generation_by(NonZeroU32::MAX, 1), NonZeroU32::MIN);

assert_eq!(
inc_generation_by(NonZeroU32::MAX, 2),
NonZeroU32::new(2).unwrap()
);

// Ensure we can't overflow twice
assert_eq!(
inc_generation_by(NonZeroU32::MAX, u32::MAX),
NonZeroU32::MAX
);
}

#[test]
fn entity_niche_optimization() {
assert_eq!(
std::mem::size_of::<Entity>(),
std::mem::size_of::<Option<Entity>>()
);
}

#[test]
fn entity_bits_roundtrip() {
let e = Entity {
generation: 0xDEADBEEF,
generation: NonZeroU32::new(0xDEADBEEF).unwrap(),
index: 0xBAADF00D,
};
assert_eq!(Entity::from_bits(e.to_bits()), e);
Expand Down Expand Up @@ -933,12 +1018,12 @@ mod tests {
#[test]
fn entity_const() {
const C1: Entity = Entity::from_raw(42);
assert_eq!(42, C1.index);
assert_eq!(0, C1.generation);
assert_eq!(42, C1.index());
assert_eq!(1, C1.generation());

const C2: Entity = Entity::from_bits(0x0000_00ff_0000_00cc);
assert_eq!(0x0000_00cc, C2.index);
assert_eq!(0x0000_00ff, C2.generation);
assert_eq!(0x0000_00cc, C2.index());
assert_eq!(0x0000_00ff, C2.generation());

const C3: u32 = Entity::from_raw(33).index();
assert_eq!(33, C3);
Expand Down Expand Up @@ -969,7 +1054,7 @@ mod tests {
// The very next entity allocated should be a further generation on the same index
let next_entity = entities.alloc();
assert_eq!(next_entity.index(), entity.index());
assert!(next_entity.generation > entity.generation + GENERATIONS);
assert!(next_entity.generation() > entity.generation() + GENERATIONS);
}

#[test]
Expand Down
Loading