Skip to content

Add earlier early-outs to bundle and component registration - #25295

Open
JaySpruce wants to merge 5 commits into
bevyengine:mainfrom
JaySpruce:bundle_early_out
Open

Add earlier early-outs to bundle and component registration#25295
JaySpruce wants to merge 5 commits into
bevyengine:mainfrom
JaySpruce:bundle_early_out

Conversation

@JaySpruce

Copy link
Copy Markdown
Member

Objective

World::register_bundle is called near the beginning of every bundle-related operation (i.e. insert or remove) to get the BundleId of the bundle, or register the bundle if it's new. However, the check for whether it already exists only happens after a ComponentsRegistrator is created, which happens to contain a Vec. The registrator isn't necessary to check if the bundle exists, so theoretically we're creating an empty Vec for no reason at every insertion or similar operation.

Solution

Add earlier outs, including other similar situations.

A little bit of performance:
Screenshot_20260804_131431

@JaySpruce JaySpruce added A-ECS Entities, components, systems, and events C-Performance A change motivated by improving speed, memory usage or compile times S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Aug 4, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in ECS Aug 4, 2026
@alice-i-cecile
alice-i-cecile requested a review from chescock August 5, 2026 01:50
// Use `register_component_checked` if that's a concern.
#[inline]
pub fn register_component<T: Component>(&mut self) -> ComponentId {
if let Some(&id) = self.indices.get(&TypeId::of::<T>()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good. This has annoyed me before; I should have thought to check if it's a perf improvement.

constructor: impl Fn() -> C + 'static,
) {
let id = components.register_component::<C>();
let id = components.register_component_checked(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add a comment for why this was changed? I can't immediately tell by looking at it, which means that future contributors will be confused too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just gonna revert that part, it doesn't really affect performance

Comment thread crates/bevy_ecs/src/component/register.rs Outdated
@alice-i-cecile alice-i-cecile added X-Uncontroversial This work is generally agreed upon D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Aug 5, 2026
@JaySpruce

Copy link
Copy Markdown
Member Author

Still the same performance (besides noise) after last commit:
Screenshot_20260804_213736

@JaySpruce JaySpruce added S-Needs-Review Needs reviewer attention (from anyone!) to move forward D-Straightforward Simple bug fixes and API improvements, docs, test and examples and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes labels Aug 5, 2026
Comment on lines +164 to +167
if let Some(&id) = self.indices.get(&TypeId::of::<T>()) {
enforce_no_required_components_recursion(self, &self.recursion_check_stack, id);
return id;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be needed, as register_component_checked already does this in its first line, and it only takes constants as inputs (note that here we're inside a ComponentsRegistrator so the Vec was already created.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case I was trying to avoid the ComponentDescriptor::new because it's a big type, but I've just realized it's passing the function pointer and not actually calling it lol. I'll remove this one

Comment on lines +325 to 329
if let Some(id) = self.component_id::<T>() {
return id;
}

self.components_registrator().register_component::<T>()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small note here: I believe in this and the other cases we should be able to skip the same check in the register_component/register_bundle_info/etc etc. However I would not give it too much weight since it will only happen very rarely (when we actually register components) and increase the code complexity unnecessarily.

@chescock chescock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! This is a clear and simple change with a measured performance improvement.

I'm surprised that this helped that much! Creating an empty Vec is pretty cheap since it doesn't allocate, so I would have thought that the cost of the extra HashMap lookup would make it not worth it. It's a good thing we have benchmarks to listen to instead of me :).

return id;
}

self.register_component_checked(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it help to use #[cold] or cold_path() somewhere?

The pattern I've seen before, like in Vec::reserve, is to have a quick check that gets #[inline]d into the caller, and then the slow complex part in a #[cold] function. Then the common case has just the quick check and a branch not taken, as even the code to set up arguments for the expensive call gets pushed out of the hot path.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem to make a difference unfortunately

Comment thread crates/bevy_ecs/src/bundle/info.rs Outdated
Comment thread crates/bevy_ecs/src/world/mod.rs
@JaySpruce JaySpruce added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Aug 5, 2026
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-Performance A change motivated by improving speed, memory usage or compile times D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Uncontroversial This work is generally agreed upon

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

4 participants