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

Synchronized component registration #17569

Open
wants to merge 48 commits into
base: main
Choose a base branch
from

Conversation

ElliottjPierce
Copy link

@ElliottjPierce ElliottjPierce commented Jan 28, 2025

Objective

Registering components currently requires mutable access to a world, but conceptually, registering a component doesn't change the world's state. As discussed on discord, we want to fix that discrepancy by allowing component registration with only a read reference to the world.

If that can be done, this will open up the ability to create read only queries and could be the basis of resolving similar battles with the borrow checker.

Solution

Other solutions have been brought up in discord, but this PR attempts to solve the problem by synchronizing component registration itself.

Components now stores ComponentsData and StagedComponents. ComponentsData is effectively the previous components implementation. When a component is registered or required components are set, we lock the staged components, queue those modifications on the stage, and release the lock. Eventually (during SubApp::update for now), we apply those queued changes to the normal ComponentsData, clearing the staged changes. When we are just reading data, including registering an already registered component, the lock is only engaged if the requested data still lives in the staged changes. Effectively, the lock is almost never hit unless there is some registration process happening.

For performance, the original Components implementation has been kept, and both the new and the old are available via an abstraction trait, ComponentsView. When registering in bulk, the previous implementation may be slightly faster, and it's pretty simple to use that one instead. Additionally, when a lock needs to be used for registration, it uses a new ComponentsLock. That lock is kept between nearby registrations by default, so there is minimal locking overhead.

There is some duplicated code between different implementations of ComponentsView. We could abstract it away, but I wanted to leave it for now since we may choose to let the implementations diverge.

Testing

Current tests pass for me, but no new tests were created.

We will probably want to benchmark this vs the old implementation, but I wanted to leave that up to someone who knows better exactly what situations to benchmark.

Pros

  • Fixes the problem.
  • Is still very fast.
  • Fully backwards compatible (components can still be registered at full speed if you have mutable access).

Cons

  • More complexity.
  • Can be slower in some situations.
  • Harder to get Component info out due to locking guards being dropped. This will fix the problem eventually.

Migration Guide

To make catching bugs and butterfly effects easier, I went ahead and adapted multiple signatures. For example, many signatures changed from &mut to &. Also, Component::register_required_components now accepts a ComponentsView instead of Components. We can spread some of these changes over multiple PRs. It was just easier for me to follow it with the changes.

Previously, when a sparse set component was registered, its set was created. Now, the set is created when the component is inserted (or spawned) in an entity. I did this during BundleInfo creation (Maybe there's a better place). At any rate, lots of code depended on registered components having valid sets. Now the rule is spawned components have valid sets. I updated relevant usages and safety comments, but more testing should probably be done. For now, it passes my "smell test."

I realized the new components would need to know about previously registered components, so I simplified.
I deleted a lot of mostly unused utility functions too since it's so painful to implement until https://doc.rust-lang.org/std/sync/struct.MappedRwLockReadGuard.html is stabalized.
Added back in the mutable versions.
Keep lock during frequent registrations.
Before, required components could be read directly from ComponentInfo. But sicne there could be staged changes to required components, this could be wrong. Hence, that interface was removed, and now, required components can be requested directly.
The new ones were cloned from the original so merging is not needed.
these were mostly originally inlined, but it was dropped along the way.
@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use X-Controversial There is active debate or serious implications around merging this PR S-Needs-Benchmarking This set of changes needs performance benchmarking to double-check that they help D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 28, 2025
@ElliottjPierce
Copy link
Author

CI should fail right now since we still need to create ComponentSparseSets. Right now we aren't doing that, so the dead code causes a failure. The unwrap I mentioned in sparse set queries may also be causing crashes in examples.

@ElliottjPierce
Copy link
Author

The work I just finished doing should ensure there is no performance regression. When registering with mutable access to Components, it checks if there's any staged changes, and if not, it uses the previous implementation.

The only costs would be if we use the synchronous registration methods or if we are getting data for a type that was recently registered. I think we can keep this to a minimum by only using the synchronous methods when we need to (ie. read-only queries.)

Then again, with a bit more work, it might be fast enough to just use the synchronized methods out of convenience. I think the next step is to benchmark.

@ElliottjPierce
Copy link
Author

I just added some basic benchmarks, and it looks good. Really good.

Screenshot 2025-01-29 at 11 36 12 AM

  • Raw is the previous Components implementation (still available in the new one).
  • Mut is when you have mutable access to Components and know you you will be registering in bulk.
  • Locked is when you have immutable access to Components and know you you will be registering in bulk.
  • Full is when you have mutable access to Components and don't know that you're registering in bulk.
  • Full_Staged is the same as full, but it has to stage changes instead of applying them directly.
  • Synced is when you have immutable access to Components and don't know that you're registering in bulk.

To me, these benchmarks indicate that when I adapted the previous implementation to work in ComponentsMut, I may have written if to run faster than before. Raw is slower than Mut and Full is slower than Full_Staged which seem to support this.

In a future PR, it may be worth trying to improve the performance of the old registration.

What the benchmarks show is that this optional synchronization is effectively free and that it can be made even faster.

Also note that these benchmarks are in the worst possible order. They ones with no required components are registered before the ones that require them. If the order was reversed, there would be even less locking, and I suspect it would run faster.

@ElliottjPierce
Copy link
Author

At this point, the PR is feature complete IMO.

Benches

Here are the current benches for my M2 Max:
Screenshot 2025-01-29 at 2 35 47 PM
If anyone wants to run other benches, feel free. There are a few places that need to lock on a hot path, like some component insertions, but they only lock if the data they need is staged, so I don't expect that to be a problem.

Needs Careful Review

I'm still relatively new to the guts of bevy's ecs, to it is definitely possible that I made a logic mistake somewhere. I would especially appreciate feedback on the following:

  • Feedback from someone more familiar with atomic operations to make sure I had the right approach with Components::staged_changed, especially my choice of ordering.
  • Feedback on where I cleaned Components. Should we do this more often than SupApp::update? Maybe also during mutable registration?
  • Feedback on where I created the sparse sets. Right now we do it at BundleInfo::new, but we could do it at component registration within BundleInfo::new at the cost of cloning component info. I don't think the extra memory would be worth it, but I'm open to feedback.
  • Feedback on naming and ComponentsView trait ecosystem. There may be a better way to organize this. I just threw it together as a prototype.

Future work

  • Open up signatures. Lots of things still need &mut Components but can be changed to &mut impl ComponentsViewExclusive. (Or &Components if we want to be specific at the potential cost of performance.)
  • Open up signatures that still get &mut World for registration. Now they only need &World.
  • Replace TryQuery API with a synchronous version of initing QueryState. We should keep the current version with &mut World too IMO to prevent unneeded locking.
  • Improve performance of ComponentsData. For some reason ComponentsMut is performing faster, even though it is doing more. There is room for improvement here.
  • Use Either crate to make accessing Components without necessarily locking it easier. For example, if you have &Components and want to get a impl ComponentsViewRef, right now you must use lock_read, but if Components::staged_changed is false, you could just get the inner &ComponentsData.
  • When this is stabilized, we can improve or replace ComponentInfoRef.
  • Components.rs is now getting pretty long. It may be worth re-organizing it, but IDK.

@alice-i-cecile alice-i-cecile removed the S-Needs-Benchmarking This set of changes needs performance benchmarking to double-check that they help label Jan 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Controversial There is active debate or serious implications around merging this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants