Resource storage#24058
Conversation
2a42510 to
7f528bd
Compare
7f528bd to
b9e6d72
Compare
b9e6d72 to
40e78dc
Compare
| insert_just_failed: SyncUnsafeCell<bool>, | ||
| /// capacity: 1 | ||
| /// length: 1 if populated, 0 otherwise | ||
| data: Column, |
There was a problem hiding this comment.
A 1-element column is rather silly, I know. I'll inline it if requested.
There was a problem hiding this comment.
Unless there's performance gains here I think this is simpler.
| #[derive(Default)] | ||
| pub struct ResourceStorages { | ||
| /// Column is always one element long. | ||
| resources: SparseSet<ComponentId, ResourceStorage>, |
There was a problem hiding this comment.
I haven't put too much consideration into what map data structure is best here yet.
There was a problem hiding this comment.
I haven't put too much consideration into what map data structure is best here yet.
I would expect a SparseSet like this to be pretty efficient, and it's similar to what we had before resources-as-entities.
40e78dc to
ffd3350
Compare
| self.data.replace(ROW, value, change_tick, caller); | ||
| } | ||
| Populated(_) => { | ||
| self.insert_just_failed = SyncUnsafeCell::new(true); |
There was a problem hiding this comment.
I think ignoring the inserted values like this will be unsound. The hook will move the archetype back soon after, but other hooks and observers can briefly see a world where the duplicate resource entity is in an archetype with the resource component but does not have its own value.
That is, it's possible to query for &ResourceValue in a hook, and the get_with_entity(entity).debug_checked_unwrap() lines will be UB on the duplicate entities.
Unless we have a way to prevent the initial archetype move, I think we might need some auxiliary storage somewhere to hold the duplicate values temporarily. :(
Maybe we could store the values in the Column, and have something like duplicates: Vec<Entity> that we have to check in queries? That wouldn't affect resource-specific APIs like Res since they'd never be the real resource values, and it wouldn't allocate anything when there aren't duplicates. I think the main cost would be some extra codegen in queries.
There was a problem hiding this comment.
Fundamentally, I think we need to move this check to occur earlier. Preventing the inital archetype move is the cleanest and most robust way to do this.
I suspect that this is possible: we just need to scan ResourceStorage for pre-existing resources before archetype moves. It looks like ResourceStorage::insert, BundleInfo::write_components and BundleInserter::insert / spawn_at are the methods we would need to touch.
There was a problem hiding this comment.
Fundamentally, I think we need to move this check to occur earlier. Preventing the inital archetype move is the cleanest and most robust way to do this.
Currently trying this out. Looks like it should be possible to do so soundly and without perf penality, I just need to be very careful and quadruple check everything (and be very clear in the safety comments).
There was a problem hiding this comment.
Not entirely sure which of the two is the best solution.
I've implemented that invalid writes don't happen in the first place. Fails Stacked Borrows, but valid under Tree Borrows. I'll need to do more investigation to figure out the exact cause and find a workaround.
|
Please add yourself and this PR to the resources-as-components release notes. This is a critical part of the story. |
alice-i-cecile
left a comment
There was a problem hiding this comment.
I really like the core idea, but we need to a) fix the insert_just_failed messiness b) rerun benchmarks comparing to #24077 c) do a sanity check on many_foxes or something.
# Objective Part of the bevyengine#23988 and bevyengine#24058 saga. We attempt to speed up resource access. ## Solution When messing around with bevyengine#23988 I noticed that changing the storage type mattered a lot for the benchmarks. ## Testing Added benchmarks from bevyengine#24058 and got the following micro benchmarks compared to main: ``` ecs::resources::get time: [6.3584 ns 6.3840 ns 6.4097 ns] change: [−10.625% −10.075% −9.6652%] (p = 0.00 < 0.05) Performance has improved. Found 9 outliers among 100 measurements (9.00%) 1 (1.00%) low mild 8 (8.00%) high mild ecs::resources::get_mut time: [7.4181 ns 7.4343 ns 7.4515 ns] change: [−39.895% −39.304% −38.809%] (p = 0.00 < 0.05) Performance has improved. Found 7 outliers among 100 measurements (7.00%) 5 (5.00%) high mild 2 (2.00%) high severe ecs::resources::insert_remove time: [89.515 ns 89.654 ns 89.815 ns] change: [−20.163% −16.527% −11.930%] (p = 0.00 < 0.05) Performance has improved. Found 10 outliers among 100 measurements (10.00%) 2 (2.00%) low severe 1 (1.00%) low mild 3 (3.00%) high mild 4 (4.00%) high severe ``` If someone wants to double-check these numbers, I encourage you to do so.
# Objective Part of the bevyengine#23988 and bevyengine#24058 saga. We attempt to speed up resource access. ## Solution When messing around with bevyengine#23988 I noticed that changing the storage type mattered a lot for the benchmarks. ## Testing Added benchmarks from bevyengine#24058 and got the following micro benchmarks compared to main: ``` ecs::resources::get time: [6.3584 ns 6.3840 ns 6.4097 ns] change: [−10.625% −10.075% −9.6652%] (p = 0.00 < 0.05) Performance has improved. Found 9 outliers among 100 measurements (9.00%) 1 (1.00%) low mild 8 (8.00%) high mild ecs::resources::get_mut time: [7.4181 ns 7.4343 ns 7.4515 ns] change: [−39.895% −39.304% −38.809%] (p = 0.00 < 0.05) Performance has improved. Found 7 outliers among 100 measurements (7.00%) 5 (5.00%) high mild 2 (2.00%) high severe ecs::resources::insert_remove time: [89.515 ns 89.654 ns 89.815 ns] change: [−20.163% −16.527% −11.930%] (p = 0.00 < 0.05) Performance has improved. Found 10 outliers among 100 measurements (10.00%) 2 (2.00%) low severe 1 (1.00%) low mild 3 (3.00%) high mild 4 (4.00%) high severe ``` If someone wants to double-check these numbers, I encourage you to do so.
|
Cutting from the milestone: the perf regression is already mostly addressed, and we're still not sure that we want this on top of resources-as-components. I still want this, but it's not worth blocking the release on. |
|
If we introduce it later, it will be a big breaking change since users can start using it for different entities. I think it's quite a useful feature, see #24058 (comment). Unless we add a separate support for singletone components. |
This functionality will still work after this PR: it's just changing "how is the data on an entity stored", not "which entities can resources be associated with". Insertion, querying and so on will all work just as before: this is purely an internals change. We can and probably should add a test for this pattern however.
This was already the case in Bevy 0.1-0.18. I also don't think that this will make things dramatically harder for networking. Resources are special, and warrant special treatment. |
Ah, so users will be able to insert resources on any entities, they will just be stored differently internally? Then I just misunderstood the description (I haven't looked into the code yet, sorry).
That is true, and both Replicon and Lightyear don't have resource replication right now because we were waiting for components-as-resources to land. With the current main, resource replication works just like component replication, with no special handling required (here is the branch for Replicon). However, if this PR only affects the storage, almost no special logic will be needed even after the PR. I'll just iterate over components as usual, get the storage, and obtain the pointer. I'll only have to add one more case for |
To be clear: This isn't how it works on main, the insertion gets undone. We could make it work that way, but it would be in conflict with the plan for components-as-entities: There a resource is attached to itself. |
|
You're right, my bad: bevy/crates/bevy_ecs/src/resource.rs Lines 144 to 173 in 0376e71 I didn't have much time to play with main in an actual project and for some reason I assumed it works this way. Thanks for the clarification and sorry for the fuss! |
c1f02fb to
e617265
Compare
Objective
Resources as components tracking issue: #19731
#20934 changed resources to be components stored in sparse set storage.
This means accessing a resource looks up
TypeId→ComponentId,ComponentId→Entiy&SparseSet,Entity→TableRowandTableRow→ component data. This seems to have caused a performance regression: #23039Solution
Keep resources as components, but remove
ResourceEntitiesin favor of a dedicated storage type for resource components. Looking up a resource now looks more likeTypeId→ComponentId,ComponentId→ component data.Micro-benchmarks vs main (9bd7e1):
Running systems with
Res/ResMutsystem params should also be faster.Related work
#23988 makes components themselves entities. Besides letting you apply ecs tools like relationships to components, it would also remove
ResourceEntities.This would result in part of the speedup seen here. Whether having a dedicated resource storage is worth it can should be reconsidered when components-as-entities is in.
Testing
Testing of correctness is pretty well covered by the existing tests that supported entities as resources.
I'd like someone to validate the performance improvements though. I also haven't measured if this impacts build time, binary size or memory usage, but I don't expect significant changes there.