-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Revisit labels again #5569
Comments
This is possible under #5377, and it could be made much easier (and derivable) with some design changes. Most label types are incredibly simple and don't need boxing, so we should only box the types that actually need it IMO. |
#5377 continues #4957 though, doesn't it? I want to return to a state where different values mean different labels, without macro attributes, etc. I think it's fine if label types have to be POD, but that's an irrelevant detail. Alternative 1 is the same idea as the rest of the post, except we wouldn't pass around a reference to the map, it would just be a global variable. Like, instead of trying to make the ID type handle everything awkwardly, I would prefer we intern one // the `u64` here would be a hash of the value
pub struct SystemLabelId(TypeId, u64);` |
If it solves the problem, I don't see why that's a concern.
We don't need anything complex like that. Here's an example of a basic label that we should all be familiar with. Since it's so basic, heap allocations aren't necessary. I believe that the vast majority of labels will look like this. #[derive(SystemLabel)]
pub struct MyLabel; Here's an (approximate) example of a more-complex label, which must be stored on the heap. We can do that without having to change the design of the label traits. pub struct OurLabel(Vec<i32>);
static MAP: RwLock<HashMap<u64, Vec<i32>>> = stuff();
impl SystemLabel for OurLabel {
fn data(&self) -> u64 {
let hash = calculate_hash(&self.0);
MAP.write().entry(hash).or_insert_with(|| self.0.clone());
hash
}
fn fmt(data: u64, f: &mut Formatter) -> fmt::Result {
let val = MAP.read().get(&data)?;
write!(f, "{val:?}")
}
}
// I believe this can be made even simpler even, hashing ID's might not be necessary.
// I'd have to iterate. We don't need to force every label to allocate, we can do it as an implementation detail for some types. This isn't currently supported by the derive macro, but it can be. Does this solve your problem? Edit: Downcasting is even possible, and ergonomic. |
Gonna try and revive this thread -- I think we should consider making a decision on this issue. We need to resolve the current usability problems with labels, but should we improve upon #4957, or should we revert it? Reverting it would make deriving labels really annoying again ( I think either direction is acceptable. |
I'm thinking we should do this for all label traits:
Something like this: /// `TypeId` + output of `DynHash` given the `DefaultHasher`.
/// Is different for every value.
struct RawValueHash(TypeId, u64);
/// Lightweight + printable identifier.
pub struct MyLabelTraitId {
hash: RawValueHash,
str: &'static str, // Can also use the hash to pull from the map instead of caching it here.
} Basically a mix of stuff we've talked about before. We'd allocate one string for every label (upon first use), except in cases where we already have a |
Like this, I believe. type Interner = RwLock<HashMap<RawValueHash, Cow<'static, str>>>;
static INTERNER: OnceCell<Interner> = OnceCell::new();
pub trait SystemSet: DynHash + Debug {
fn id(&self) -> SystemSetId {
let key = self.hash();
let mut map = INTERNER.get_or_init(|| default()).write().unwrap();
let str = map
.entry(key)
.or_insert_with(|| {
// Things like system-type sets can implement this differently to skip the heap allocation.
let string = format!("{:?}", self);
let str: &'static mut str = string.into_boxed_str().leak();
str.into()
})
.deref();
SystemSetId::new(key, str)
}
} Like I think minimizing heap allocations is a win even if there is still one per label. If done this way, manually implementing the trait is almost never required. All of our label traits can even share this one map. It sucks that the derive ends up being long, but I'd rather that than trying to workaround limitations. Is it possible to have an attribute macro that fills in the missing derives? |
We could just make |
# 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>
# Objective First of all, this PR took heavy inspiration from bevyengine#7760 and bevyengine#5715. It intends to also fix bevyengine#5569, but with a slightly different approach. This also fixes bevyengine#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 bevyengine#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>
# Objective First of all, this PR took heavy inspiration from bevyengine#7760 and bevyengine#5715. It intends to also fix bevyengine#5569, but with a slightly different approach. This also fixes bevyengine#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 bevyengine#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>
What problem does this solve or what need does it fill?
#4957 made labels much cheaper, but also made them much less capable. By nature, the design moves away from using
Hash
andPartialEq
impls to using(TypeId, &'static str)
tuples for comparisons. (#5377 recovers constant-time performance by adding au64
, so the&'static str
or formatter can just be there for log/debug purposes).Beyond simple fieldless enum variants, this change basically prevents using different values to mean different labels, e.g.
MyLabel(T)
, which I think is a big ergonomics hit.Under bevyengine/rfcs#45, the plan for state machines is to facilitate linking each state to a pair of on-enter and on-exit schedules, which would run from within an exclusive system handling state transitions. The implementation for this in #4391 was looking extremely simple.
The
apply_state_transition
system just reads the state resources, then loads and runs their appropriate schedules (if they exist). What was really nice was that it was just a convenient export of something users could do on their own.However, this is no longer possible, and I'm not sure how to adapt it for the current 0.8 build. Since I don't know
S
, I don't know how to produce a unique&'static str
/u64
for each label value. There's probably some way to do it, but having to do a special manual impl for a simple wrapper type feels ridiculous. As one of its main authors, I believe that what's coming with stageless will lead to apps having more labels, so increasing friction here by reducing what users can do with even POD types seems counterproductive.It's true that schedule construction had too much boxing, but those costs were infrequent, basically once at startup, so I'm skeptical the tradeoff we made was worth it.
What solution would you like?
I haven't figured out an obvious solution yet, but the essence of the problem is that:
All the boxing happened in the descriptor API methods, where we had to collect all
SystemLabel
trait objects into a struct before handing it to theSystemStage
, so we ended up boxing each and every use of a label.(edit) Removing that excessive boxing seems to have been the main reason people supported the new scheme.
Since
add_system
is a method onSystemStage
, could we add aHashMap<SystemLabelId, Box<dyn SystemLabel>>
field and somehow pass a&mut
reference to it into the descriptor process? If we can do that, we'd only box each label once and different values could mean different labels again. The*LabelId
types can just wrap(type_id, value_hash)
tuples (currently same size as au128
).What alternative(s) have you considered?
OnceCell
, and have the descriptor methods access that. It's fine for a system label value to have the same ID, program-wide.The text was updated successfully, but these errors were encountered: