-
-
Notifications
You must be signed in to change notification settings - Fork 3.5k
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
Allow Option<Entity>
to leverage niche optimization
#3029
Changes from 1 commit
5740099
7ad7eaa
43e990d
e3784e5
4767f08
2e201a9
ab84dff
3535468
080fef5
829ae66
6323185
41c3e80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -33,6 +33,7 @@ use std::{ | |||||
convert::TryFrom, | ||||||
fmt, mem, | ||||||
sync::atomic::{AtomicI64, Ordering}, | ||||||
num::NonZeroU32, | ||||||
}; | ||||||
|
||||||
/// Lightweight unique ID of an entity. | ||||||
|
@@ -46,7 +47,7 @@ use std::{ | |||||
/// [`Query::get`](crate::system::Query::get) and related methods. | ||||||
#[derive(Clone, Copy, Hash, Eq, Ord, PartialEq, PartialOrd)] | ||||||
pub struct Entity { | ||||||
pub(crate) generation: u32, | ||||||
pub(crate) generation: NonZeroU32, | ||||||
pub(crate) id: u32, | ||||||
} | ||||||
|
||||||
|
@@ -57,7 +58,7 @@ pub enum AllocAtWithoutReplacement { | |||||
} | ||||||
|
||||||
impl Entity { | ||||||
/// Creates a new entity reference with a generation of 0. | ||||||
/// Creates a new entity reference with a generation of 1. | ||||||
/// | ||||||
/// # Note | ||||||
/// | ||||||
|
@@ -66,7 +67,10 @@ impl Entity { | |||||
/// only be used for sharing entities across apps, and only when they have a scheme | ||||||
/// worked out to share an ID space (which doesn't happen by default). | ||||||
pub fn new(id: u32) -> Entity { | ||||||
Entity { id, generation: 0 } | ||||||
Entity { | ||||||
id, | ||||||
generation: unsafe{ NonZeroU32::new_unchecked(1) } | ||||||
} | ||||||
} | ||||||
|
||||||
/// Convert to a form convenient for passing outside of rust. | ||||||
|
@@ -76,17 +80,17 @@ impl Entity { | |||||
/// | ||||||
/// No particular structure is guaranteed for the returned bits. | ||||||
pub fn to_bits(self) -> u64 { | ||||||
u64::from(self.generation) << 32 | u64::from(self.id) | ||||||
u64::from(self.generation.get()) << 32 | u64::from(self.id) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
/// 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. | ||||||
pub fn from_bits(bits: u64) -> Self { | ||||||
Self { | ||||||
generation: (bits >> 32) as u32, | ||||||
pub fn from_bits(bits: u64) -> Option<Self> { | ||||||
Some(Self{ | ||||||
generation: NonZeroU32::new((bits >> 32) as u32)?, | ||||||
id: bits as u32, | ||||||
} | ||||||
}) | ||||||
} | ||||||
|
||||||
/// Return a transiently unique identifier. | ||||||
|
@@ -104,7 +108,7 @@ impl Entity { | |||||
/// given id has been reused (id, generation) pairs uniquely identify a given Entity. | ||||||
#[inline] | ||||||
pub fn generation(self) -> u32 { | ||||||
self.generation | ||||||
self.generation.get() | ||||||
} | ||||||
} | ||||||
|
||||||
|
@@ -147,7 +151,10 @@ impl<'a> Iterator for ReserveEntitiesIterator<'a> { | |||||
generation: self.meta[id as usize].generation, | ||||||
id, | ||||||
}) | ||||||
.or_else(|| self.id_range.next().map(|id| Entity { generation: 0, id })) | ||||||
.or_else(|| self.id_range.next().map(|id| Entity { | ||||||
generation: unsafe{ NonZeroU32::new_unchecked(1) }, | ||||||
id, | ||||||
})) | ||||||
} | ||||||
|
||||||
fn size_hint(&self) -> (usize, Option<usize>) { | ||||||
|
@@ -265,7 +272,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: unsafe{ NonZeroU32::new_unchecked(1) }, | ||||||
id: u32::try_from(self.meta.len() as i64 - n).expect("too many entities"), | ||||||
} | ||||||
} | ||||||
|
@@ -293,7 +300,10 @@ impl Entities { | |||||
} else { | ||||||
let id = u32::try_from(self.meta.len()).expect("too many entities"); | ||||||
self.meta.push(EntityMeta::EMPTY); | ||||||
Entity { generation: 0, id } | ||||||
Entity { | ||||||
generation: unsafe{ NonZeroU32::new_unchecked(1) }, | ||||||
id, | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
|
@@ -373,7 +383,12 @@ impl Entities { | |||||
if meta.generation != entity.generation { | ||||||
return None; | ||||||
} | ||||||
meta.generation += 1; | ||||||
|
||||||
meta.generation = unsafe{ NonZeroU32::new_unchecked( | ||||||
meta.generation.get() | ||||||
.checked_add(1) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if this addition fails, we should instead choose not to add the entity to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only issue I can see is that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm yeah, well spotted. I guess EntityMeta could contain an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be the dead generation way, avoids unwraps on reallocating free'd but not dead entities: meta.generation = NonZeroU32::new(meta.generation.get().saturating_add(1)).unwrap();
if meta.generation != GENERATION_DEAD {
self.pending.push(entity.id);
let new_free_cursor = self.pending.len() as i64;
*self.free_cursor.get_mut() = new_free_cursor;
self.len -= 1;
}
Some(mem::replace(&mut meta.location, EntityMeta::EMPTY.location)) Optimized, this is the same cost as just a saturating_add, so it correctly removes the NonZeroU32::new/unwrap. I'll give the Option option a go now There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry for the delay, dinner! Option method was a little more complex, but it's the more correct solution for sure. Thought there were a few issues revealed by using it, but, it turns out that in all those cases we were accessing Only issue I can foresee is that I don't think the optimizer can handle this case, since it won't know that pending can only contain indexes to options with a value, so all those additional unwraps probably won't be optimized out. I don't think that switching to a raw u32 would "fix" that here either, and I'm not even really sure this would be a bottleneck anyway. Beats a panic or bug though, that's for sure. |
||||||
.unwrap_or(1)) | ||||||
}; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would the safe version optimize to the same asm as this code here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure, I'll confirm There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't appear to optimize to the same code unfortunately, though I might've done it wrong: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possible solution: std::num::NonZeroU32::new(match num.get() {
u32::MAX => 1,
v => v+1
}).unwrap();
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the dead slot, we don't actually need to unwrap where. But we do need to unwrap when we allocate from the freelist. I'm not sure if that's particularly performant - but it's safe! |
||||||
|
||||||
let loc = mem::replace(&mut meta.location, EntityMeta::EMPTY.location); | ||||||
|
||||||
|
@@ -401,7 +416,7 @@ impl Entities { | |||||
// not reallocated since the generation is incremented in `free` | ||||||
pub fn contains(&self, entity: Entity) -> bool { | ||||||
self.resolve_from_id(entity.id()) | ||||||
.map_or(false, |e| e.generation() == entity.generation) | ||||||
.map_or(false, |e| e.generation == entity.generation) | ||||||
} | ||||||
|
||||||
pub fn clear(&mut self) { | ||||||
|
@@ -442,7 +457,10 @@ impl Entities { | |||||
// If this entity was manually created, then free_cursor might be positive | ||||||
// Returning None handles that case correctly | ||||||
let num_pending = usize::try_from(-free_cursor).ok()?; | ||||||
(idu < self.meta.len() + num_pending).then(|| Entity { generation: 0, id }) | ||||||
(idu < self.meta.len() + num_pending).then(|| Entity { | ||||||
generation: unsafe{ NonZeroU32::new_unchecked(1) }, | ||||||
id | ||||||
}) | ||||||
} | ||||||
} | ||||||
|
||||||
|
@@ -518,13 +536,13 @@ impl Entities { | |||||
|
||||||
#[derive(Copy, Clone, Debug)] | ||||||
pub struct EntityMeta { | ||||||
pub generation: u32, | ||||||
pub generation: NonZeroU32, | ||||||
pub location: EntityLocation, | ||||||
} | ||||||
|
||||||
impl EntityMeta { | ||||||
const EMPTY: EntityMeta = EntityMeta { | ||||||
generation: 0, | ||||||
generation: unsafe{ NonZeroU32::new_unchecked(1) }, | ||||||
location: EntityLocation { | ||||||
archetype_id: ArchetypeId::INVALID, | ||||||
index: usize::MAX, // dummy value, to be filled in | ||||||
|
@@ -549,10 +567,53 @@ mod tests { | |||||
#[test] | ||||||
fn entity_bits_roundtrip() { | ||||||
let e = Entity { | ||||||
generation: 0xDEADBEEF, | ||||||
generation: NonZeroU32::new(0xDEADBEEF).unwrap(), | ||||||
id: 0xBAADF00D, | ||||||
}; | ||||||
assert_eq!(Entity::from_bits(e.to_bits()), e); | ||||||
assert_eq!(Entity::from_bits(e.to_bits()).unwrap(), e); | ||||||
} | ||||||
|
||||||
#[test] | ||||||
fn entity_bad_bits() { | ||||||
let bits: u64 = 0xBAADF00D; | ||||||
assert_eq!(Entity::from_bits(bits), None); | ||||||
} | ||||||
|
||||||
#[test] | ||||||
fn entity_option_size_optimized() { | ||||||
assert_eq!( | ||||||
core::mem::size_of::<Option<Entity>>(), | ||||||
core::mem::size_of::<Entity>() | ||||||
); | ||||||
} | ||||||
|
||||||
#[test] | ||||||
fn entities_generation_increment() { | ||||||
let mut entities = Entities::default(); | ||||||
|
||||||
let entity_old = entities.alloc(); | ||||||
entities.free(entity_old); | ||||||
let entity_new = entities.alloc(); | ||||||
|
||||||
assert_eq!(entity_old.id, entity_new.id ); | ||||||
assert_eq!(entity_old.generation() + 1, entity_new.generation()); | ||||||
} | ||||||
|
||||||
#[test] | ||||||
fn entities_generation_wrap() { | ||||||
let mut entities = Entities::default(); | ||||||
let mut entity_old = entities.alloc(); | ||||||
|
||||||
// Modify generation on entity and entities to cause overflow on free | ||||||
entity_old.generation = NonZeroU32::new(u32::MAX).unwrap(); | ||||||
entities.meta[entity_old.id as usize].generation = entity_old.generation; | ||||||
entities.free(entity_old); | ||||||
|
||||||
// Get just free-d entity back | ||||||
let entity_new = entities.alloc(); | ||||||
|
||||||
assert_eq!(entity_old.id, entity_new.id); | ||||||
assert_eq!(entity_new.generation(), 1); | ||||||
} | ||||||
|
||||||
#[test] | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe
as this is pretty much guaranteed to be optimized away anyway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely should be optimized away like that, but I'm not sure about with debug/non-optimized builds - though I don't imagine it being a bottleneck.
The other option is to introduce a constant in the file, ie.
GENERATION_INITIAL
, which we almost have anyway via theEntityMeta::EMPTY
constant since unsafe is required in that context anyway.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just checked, compiler seems happy to optimize that even at
opt-level=1
but won't at 0:https://godbolt.org/z/E8EY5Mj8s
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A constant should work. Something like
const GENERATION_ONE: NonZeroU32 = if let Some(gen) = NonZeroU32::new(1) { gen } else { panic!() };
should work in safe code and optimize even with opt-level=0. It will require rustc 1.57 for thepanic!()
though, but replacingpanic!()
with[][1]
works even with the current rustc 1.56.