 a830530be4
			
		
	
	
		a830530be4
		
			
		
	
	
	
	
		
			
			# Objective First of all, this PR took heavy inspiration from #7760 and #5715. It intends to also fix #5569, but with a slightly different approach. This also fixes #9335 by reexporting `DynEq`. ## Solution The advantage of this API is that we can intern a value without allocating for zero-sized-types and for enum variants that have no fields. This PR does this automatically in the `SystemSet` and `ScheduleLabel` derive macros for unit structs and fieldless enum variants. So this should cover many internal and external use cases of `SystemSet` and `ScheduleLabel`. In these optimal use cases, no memory will be allocated. - The interning returns a `Interned<dyn SystemSet>`, which is just a wrapper around a `&'static dyn SystemSet`. - `Hash` and `Eq` are implemented in terms of the pointer value of the reference, similar to my first approach of anonymous system sets in #7676. - Therefore, `Interned<T>` does not implement `Borrow<T>`, only `Deref`. - The debug output of `Interned<T>` is the same as the interned value. Edit: - `AppLabel` is now also interned and the old `derive_label`/`define_label` macros were replaced with the new interning implementation. - Anonymous set ids are reused for different `Schedule`s, reducing the amount of leaked memory. ### Pros - `InternedSystemSet` and `InternedScheduleLabel` behave very similar to the current `BoxedSystemSet` and `BoxedScheduleLabel`, but can be copied without an allocation. - Many use cases don't allocate at all. - Very fast lookups and comparisons when using `InternedSystemSet` and `InternedScheduleLabel`. - The `intern` module might be usable in other areas. - `Interned{ScheduleLabel, SystemSet, AppLabel}` does implement `{ScheduleLabel, SystemSet, AppLabel}`, increasing ergonomics. ### Cons - Implementors of `SystemSet` and `ScheduleLabel` still need to implement `Hash` and `Eq` (and `Clone`) for it to work. ## Changelog ### Added - Added `intern` module to `bevy_utils`. - Added reexports of `DynEq` to `bevy_ecs` and `bevy_app`. ### Changed - Replaced `BoxedSystemSet` and `BoxedScheduleLabel` with `InternedSystemSet` and `InternedScheduleLabel`. - Replaced `impl AsRef<dyn ScheduleLabel>` with `impl ScheduleLabel`. - Replaced `AppLabelId` with `InternedAppLabel`. - Changed `AppLabel` to use `Debug` for error messages. - Changed `AppLabel` to use interning. - Changed `define_label`/`derive_label` to use interning. - Replaced `define_boxed_label`/`derive_boxed_label` with `define_label`/`derive_label`. - Changed anonymous set ids to be only unique inside a schedule, not globally. - Made interned label types implement their label trait. ### Removed - Removed `define_boxed_label` and `derive_boxed_label`. ## Migration guide - Replace `BoxedScheduleLabel` and `Box<dyn ScheduleLabel>` with `InternedScheduleLabel` or `Interned<dyn ScheduleLabel>`. - Replace `BoxedSystemSet` and `Box<dyn SystemSet>` with `InternedSystemSet` or `Interned<dyn SystemSet>`. - Replace `AppLabelId` with `InternedAppLabel` or `Interned<dyn AppLabel>`. - Types manually implementing `ScheduleLabel`, `AppLabel` or `SystemSet` need to implement: - `dyn_hash` directly instead of implementing `DynHash` - `as_dyn_eq` - Pass labels to `World::try_schedule_scope`, `World::schedule_scope`, `World::try_run_schedule`. `World::run_schedule`, `Schedules::remove`, `Schedules::remove_entry`, `Schedules::contains`, `Schedules::get` and `Schedules::get_mut` by value instead of by reference. --------- Co-authored-by: Joseph <21144246+JoJoJet@users.noreply.github.com> Co-authored-by: Carter Anderson <mcanders1@gmail.com>
		
			
				
	
	
		
			287 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			287 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! Provides types used to statically intern immutable values.
 | |
| //!
 | |
| //! Interning is a pattern used to save memory by deduplicating identical values,
 | |
| //! speed up code by shrinking the stack size of large types,
 | |
| //! and make comparisons for any type as fast as integers.
 | |
| 
 | |
| use std::{
 | |
|     fmt::Debug,
 | |
|     hash::Hash,
 | |
|     ops::Deref,
 | |
|     sync::{OnceLock, PoisonError, RwLock},
 | |
| };
 | |
| 
 | |
| use crate::HashSet;
 | |
| 
 | |
| /// An interned value. Will stay valid until the end of the program and will not drop.
 | |
| ///
 | |
| /// For details on interning, see [the module level docs](self).
 | |
| ///
 | |
| /// # Comparisons
 | |
| ///
 | |
| /// Interned values use reference equality, meaning they implement [`Eq`]
 | |
| /// and [`Hash`] regardless of whether `T` implements these traits.
 | |
| /// Two interned values are only guaranteed to compare equal if they were interned using
 | |
| /// the same [`Interner`] instance.
 | |
| // NOTE: This type must NEVER implement Borrow since it does not obey that trait's invariants.
 | |
| ///
 | |
| /// ```
 | |
| /// # use bevy_utils::intern::*;
 | |
| /// #[derive(PartialEq, Eq, Hash, Debug)]
 | |
| /// struct Value(i32);
 | |
| /// impl Internable for Value {
 | |
| ///     // ...
 | |
| /// # fn leak(&self) -> &'static Self { Box::leak(Box::new(Value(self.0))) }
 | |
| /// # fn ref_eq(&self, other: &Self) -> bool { std::ptr::eq(self, other ) }
 | |
| /// # fn ref_hash<H: std::hash::Hasher>(&self, state: &mut H) { std::ptr::hash(self, state); }
 | |
| /// }
 | |
| /// let interner_1 = Interner::new();
 | |
| /// let interner_2 = Interner::new();
 | |
| /// // Even though both values are identical, their interned forms do not
 | |
| /// // compare equal as they use different interner instances.
 | |
| /// assert_ne!(interner_1.intern(&Value(42)), interner_2.intern(&Value(42)));
 | |
| /// ```
 | |
| pub struct Interned<T: ?Sized + 'static>(pub &'static T);
 | |
| 
 | |
| impl<T: ?Sized> Deref for Interned<T> {
 | |
|     type Target = T;
 | |
| 
 | |
|     fn deref(&self) -> &Self::Target {
 | |
|         self.0
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<T: ?Sized> Clone for Interned<T> {
 | |
|     fn clone(&self) -> Self {
 | |
|         *self
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<T: ?Sized> Copy for Interned<T> {}
 | |
| 
 | |
| // Two Interned<T> should only be equal if they are clones from the same instance.
 | |
| // Therefore, we only use the pointer to determine equality.
 | |
| impl<T: ?Sized + Internable> PartialEq for Interned<T> {
 | |
|     fn eq(&self, other: &Self) -> bool {
 | |
|         self.0.ref_eq(other.0)
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<T: ?Sized + Internable> Eq for Interned<T> {}
 | |
| 
 | |
| // Important: This must be kept in sync with the PartialEq/Eq implementation
 | |
| impl<T: ?Sized + Internable> Hash for Interned<T> {
 | |
|     fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
 | |
|         self.0.ref_hash(state);
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<T: ?Sized + Debug> Debug for Interned<T> {
 | |
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | |
|         self.0.fmt(f)
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<T> From<&Interned<T>> for Interned<T> {
 | |
|     fn from(value: &Interned<T>) -> Self {
 | |
|         *value
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// A trait for internable values.
 | |
| ///
 | |
| /// This is used by [`Interner<T>`] to create static references for values that are interned.
 | |
| pub trait Internable: Hash + Eq {
 | |
|     /// Creates a static reference to `self`, possibly leaking memory.
 | |
|     fn leak(&self) -> &'static Self;
 | |
| 
 | |
|     /// Returns `true` if the two references point to the same value.
 | |
|     fn ref_eq(&self, other: &Self) -> bool;
 | |
| 
 | |
|     /// Feeds the reference to the hasher.
 | |
|     fn ref_hash<H: std::hash::Hasher>(&self, state: &mut H);
 | |
| }
 | |
| 
 | |
| impl Internable for str {
 | |
|     fn leak(&self) -> &'static Self {
 | |
|         let str = self.to_owned().into_boxed_str();
 | |
|         Box::leak(str)
 | |
|     }
 | |
| 
 | |
|     fn ref_eq(&self, other: &Self) -> bool {
 | |
|         self.as_ptr() == other.as_ptr() && self.len() == other.len()
 | |
|     }
 | |
| 
 | |
|     fn ref_hash<H: std::hash::Hasher>(&self, state: &mut H) {
 | |
|         self.len().hash(state);
 | |
|         self.as_ptr().hash(state);
 | |
|     }
 | |
| }
 | |
| 
 | |
| /// A thread-safe interner which can be used to create [`Interned<T>`] from `&T`
 | |
| ///
 | |
| /// For details on interning, see [the module level docs](self).
 | |
| ///
 | |
| /// The implementation ensures that two equal values return two equal [`Interned<T>`] values.
 | |
| ///
 | |
| /// To use an [`Interner<T>`], `T` must implement [`Internable`].
 | |
| pub struct Interner<T: ?Sized + 'static>(OnceLock<RwLock<HashSet<&'static T>>>);
 | |
| 
 | |
| impl<T: ?Sized> Interner<T> {
 | |
|     /// Creates a new empty interner
 | |
|     pub const fn new() -> Self {
 | |
|         Self(OnceLock::new())
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<T: Internable + ?Sized> Interner<T> {
 | |
|     /// Return the [`Interned<T>`] corresponding to `value`.
 | |
|     ///
 | |
|     /// If it is called the first time for `value`, it will possibly leak the value and return an
 | |
|     /// [`Interned<T>`] using the obtained static reference. Subsequent calls for the same `value`
 | |
|     /// will return [`Interned<T>`] using the same static reference.
 | |
|     pub fn intern(&self, value: &T) -> Interned<T> {
 | |
|         let lock = self.0.get_or_init(Default::default);
 | |
|         {
 | |
|             let set = lock.read().unwrap_or_else(PoisonError::into_inner);
 | |
|             if let Some(value) = set.get(value) {
 | |
|                 return Interned(*value);
 | |
|             }
 | |
|         }
 | |
|         {
 | |
|             let mut set = lock.write().unwrap_or_else(PoisonError::into_inner);
 | |
|             if let Some(value) = set.get(value) {
 | |
|                 Interned(*value)
 | |
|             } else {
 | |
|                 let leaked = value.leak();
 | |
|                 set.insert(leaked);
 | |
|                 Interned(leaked)
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| impl<T: ?Sized> Default for Interner<T> {
 | |
|     fn default() -> Self {
 | |
|         Self::new()
 | |
|     }
 | |
| }
 | |
| 
 | |
| #[cfg(test)]
 | |
| mod tests {
 | |
|     use std::{
 | |
|         collections::hash_map::DefaultHasher,
 | |
|         hash::{Hash, Hasher},
 | |
|     };
 | |
| 
 | |
|     use crate::intern::{Internable, Interned, Interner};
 | |
| 
 | |
|     #[test]
 | |
|     fn zero_sized_type() {
 | |
|         #[derive(PartialEq, Eq, Hash, Debug)]
 | |
|         pub struct A;
 | |
| 
 | |
|         impl Internable for A {
 | |
|             fn leak(&self) -> &'static Self {
 | |
|                 &A
 | |
|             }
 | |
| 
 | |
|             fn ref_eq(&self, other: &Self) -> bool {
 | |
|                 std::ptr::eq(self, other)
 | |
|             }
 | |
| 
 | |
|             fn ref_hash<H: std::hash::Hasher>(&self, state: &mut H) {
 | |
|                 std::ptr::hash(self, state);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         let interner = Interner::default();
 | |
|         let x = interner.intern(&A);
 | |
|         let y = interner.intern(&A);
 | |
|         assert_eq!(x, y);
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn fieldless_enum() {
 | |
|         #[derive(PartialEq, Eq, Hash, Debug, Clone)]
 | |
|         pub enum A {
 | |
|             X,
 | |
|             Y,
 | |
|         }
 | |
| 
 | |
|         impl Internable for A {
 | |
|             fn leak(&self) -> &'static Self {
 | |
|                 match self {
 | |
|                     A::X => &A::X,
 | |
|                     A::Y => &A::Y,
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             fn ref_eq(&self, other: &Self) -> bool {
 | |
|                 std::ptr::eq(self, other)
 | |
|             }
 | |
| 
 | |
|             fn ref_hash<H: std::hash::Hasher>(&self, state: &mut H) {
 | |
|                 std::ptr::hash(self, state);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         let interner = Interner::default();
 | |
|         let x = interner.intern(&A::X);
 | |
|         let y = interner.intern(&A::Y);
 | |
|         assert_ne!(x, y);
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn static_sub_strings() {
 | |
|         let str = "ABC ABC";
 | |
|         let a = &str[0..3];
 | |
|         let b = &str[4..7];
 | |
|         // Same contents
 | |
|         assert_eq!(a, b);
 | |
|         let x = Interned(a);
 | |
|         let y = Interned(b);
 | |
|         // Different pointers
 | |
|         assert_ne!(x, y);
 | |
|         let interner = Interner::default();
 | |
|         let x = interner.intern(a);
 | |
|         let y = interner.intern(b);
 | |
|         // Same pointers returned by interner
 | |
|         assert_eq!(x, y);
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn same_interned_instance() {
 | |
|         let a = Interned("A");
 | |
|         let b = a;
 | |
| 
 | |
|         assert_eq!(a, b);
 | |
| 
 | |
|         let mut hasher = DefaultHasher::default();
 | |
|         a.hash(&mut hasher);
 | |
|         let hash_a = hasher.finish();
 | |
| 
 | |
|         let mut hasher = DefaultHasher::default();
 | |
|         b.hash(&mut hasher);
 | |
|         let hash_b = hasher.finish();
 | |
| 
 | |
|         assert_eq!(hash_a, hash_b);
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn same_interned_content() {
 | |
|         let a = Interned::<str>(Box::leak(Box::new("A".to_string())));
 | |
|         let b = Interned::<str>(Box::leak(Box::new("A".to_string())));
 | |
| 
 | |
|         assert_ne!(a, b);
 | |
|     }
 | |
| 
 | |
|     #[test]
 | |
|     fn different_interned_content() {
 | |
|         let a = Interned::<str>("A");
 | |
|         let b = Interned::<str>("B");
 | |
| 
 | |
|         assert_ne!(a, b);
 | |
|     }
 | |
| }
 |