Skip to content

Conversation

@hymm
Copy link
Contributor

@hymm hymm commented Nov 7, 2025

Objective

  • Add a checked version of EntityMut::get_components_mut and EntityWorldMut::get_components_mut that does not allocate

Solution

  • Add a iterator over the access type to QueryData. This is then used to iterate over the pairs of access to check if they are compatible or not.

Testing

  • Added a unit test

Bench checked vs unchecked (50000 entities)

#components unchecked checked times slower
2 509 us 1123 us 2.2x
5 903 us 2902us 3.2x
10 1700 us 11424 us 6.72x

so at 10 components each call was taking about 0.22us vs 0.03 us


ToDo

  • add release note
  • add migration guide
  • add macro for more benches
  • add bench results to pr description
  • look into if this will help with uncached queries
  • see if we can optimize it a bit

@hymm hymm mentioned this pull request Nov 7, 2025
@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events M-Release-Note Work that should be called out in the blog due to impact S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Nov 9, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Nov 9, 2025

It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note.

Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes.

github-merge-queue bot pushed a commit that referenced this pull request Nov 13, 2025
# Objective

- As part of #21780, I need a way to iterate over the component ids of a
bundle for `Entity*Except` conflict checking without allocating. Pulled
this out as it changes some unrelated code too.

## Solution

- Change `Bundle::component_ids` and `Bundle::get_component_ids` to
return an iterator instead of taking a closure. In theory I would expect
this to compile to the same asm. I would also argue that using an
iterator is a more natural api for this than the closure. It probably
took a closure before because expressing that the iterator doesn't
capture the `&mut ComponentRegistrator` lifetime wasn't possible without
the `use` syntax.
- Removed some #[allow(deprecated)] in the Bundle macro that was missed.

## Testing

- Checked the asm for `hook_on_add` in the observers example for to
confirm it was still the same. This is a pretty simple example though,
so not sure how good of a check this is.
- None of the code touched are in any hot paths, but ran the spawn and
insert benches. Any changes seem to be in the noise.
@hymm hymm force-pushed the get_components_mut branch from d0fda0c to caf7606 Compare November 15, 2025 01:38
ItsDoot pushed a commit to ItsDoot/bevy that referenced this pull request Nov 15, 2025
# Objective

- As part of bevyengine#21780, I need a way to iterate over the component ids of a
bundle for `Entity*Except` conflict checking without allocating. Pulled
this out as it changes some unrelated code too.

## Solution

- Change `Bundle::component_ids` and `Bundle::get_component_ids` to
return an iterator instead of taking a closure. In theory I would expect
this to compile to the same asm. I would also argue that using an
iterator is a more natural api for this than the closure. It probably
took a closure before because expressing that the iterator doesn't
capture the `&mut ComponentRegistrator` lifetime wasn't possible without
the `use` syntax.
- Removed some #[allow(deprecated)] in the Bundle macro that was missed.

## Testing

- Checked the asm for `hook_on_add` in the observers example for to
confirm it was still the same. This is a pretty simple example though,
so not sure how good of a check this is.
- None of the code touched are in any hot paths, but ran the spawn and
insert benches. Any changes seem to be in the noise.
@hymm hymm marked this pull request as ready for review November 15, 2025 19:53
Copy link
Contributor

@chescock chescock left a comment

Choose a reason for hiding this comment

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

This is a very clever idea! I'm a little curious just how much faster it is than doing a naive check like in #20273.

/// Accesses [`Component`](crate::prelude::Component) data
Component(EcsAccessLevel),
/// Accesses [`Resource`](crate::prelude::Resource) data
Resource(ResourceAccessLevel),
Copy link
Contributor

Choose a reason for hiding this comment

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

We could probably ignore resource access for this. We don't allow mutable access (#17116), so it will never conflict. And it's never valid to access resources through EntityMut::get_components_mut anyway (#20315).

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum QueryAccessError {
/// Component was not registered on world
ComponentNotRegistered,
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that since adding a component to an entity registers it, ComponentNotRegistered is just a special case of EntityDoesNotMatch. I don't think a caller can ever really do anything differently in those cases, so I'd be inclined to remove the ComponentNotRegistered variant and just return EntityDoesNotMatch in that case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You can technically call this method before the component has been registered. I hit this while I was writing my tests. Is the average user likely to hit this? probably not. But the way you would fix this is by registering the component which is not what you would do for EntityDoesNotMatch.

Copy link
Contributor

Choose a reason for hiding this comment

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

You can technically call this method before the component has been registered. I hit this while I was writing my tests. Is the average user likely to hit this? probably not. But the way you would fix this is by registering the component which is not what you would do for EntityDoesNotMatch.

But if the component hasn't been registered, then it hasn't been inserted onto any entity, which means it hasn't been inserted onto the current entity. So registering it will just change the error to EntityDoesNotMatch. And inserting the component onto that entity will fix either error.

... oh, except for Option<D> or Has<C>, which might fail instead of returning None or false. Blah.

@hymm
Copy link
Contributor Author

hymm commented Nov 15, 2025

I'm a little curious just how much faster it is than doing a naive check like in #20273.

I didn't do exactly that, but I did try the algorithm in this pr with fixedbitsets and it was significantly slower at 2 and 5 components. iirc it was around 30us for both. Around the same at 10 components and faster at 16.

ItsDoot pushed a commit to ItsDoot/bevy that referenced this pull request Nov 16, 2025
# Objective

- As part of bevyengine#21780, I need a way to iterate over the component ids of a
bundle for `Entity*Except` conflict checking without allocating. Pulled
this out as it changes some unrelated code too.

## Solution

- Change `Bundle::component_ids` and `Bundle::get_component_ids` to
return an iterator instead of taking a closure. In theory I would expect
this to compile to the same asm. I would also argue that using an
iterator is a more natural api for this than the closure. It probably
took a closure before because expressing that the iterator doesn't
capture the `&mut ComponentRegistrator` lifetime wasn't possible without
the `use` syntax.
- Removed some #[allow(deprecated)] in the Bundle macro that was missed.

## Testing

- Checked the asm for `hook_on_add` in the observers example for to
confirm it was still the same. This is a pretty simple example though,
so not sure how good of a check this is.
- None of the code touched are in any hot paths, but ran the spawn and
insert benches. Any changes seem to be in the noise.
ItsDoot pushed a commit to ItsDoot/bevy that referenced this pull request Nov 16, 2025
# Objective

- As part of bevyengine#21780, I need a way to iterate over the component ids of a
bundle for `Entity*Except` conflict checking without allocating. Pulled
this out as it changes some unrelated code too.

## Solution

- Change `Bundle::component_ids` and `Bundle::get_component_ids` to
return an iterator instead of taking a closure. In theory I would expect
this to compile to the same asm. I would also argue that using an
iterator is a more natural api for this than the closure. It probably
took a closure before because expressing that the iterator doesn't
capture the `&mut ComponentRegistrator` lifetime wasn't possible without
the `use` syntax.
- Removed some #[allow(deprecated)] in the Bundle macro that was missed.

## Testing

- Checked the asm for `hook_on_add` in the observers example for to
confirm it was still the same. This is a pretty simple example though,
so not sure how good of a check this is.
- None of the code touched are in any hot paths, but ran the spawn and
insert benches. Any changes seem to be in the noise.
@alice-i-cecile
Copy link
Member

I didn't do exactly that, but I did try the algorithm in this pr with fixedbitsets and it was significantly slower at 2 and 5 components. iirc it was around 30us for both. Around the same at 10 components and faster at 16.

Good call: we should prioritize the performance of the 2-5 component use case.

@alice-i-cecile
Copy link
Member

@hymm, this looks pretty much ready for review :) Want to get this passing CI and then get a full review from me?

Copy link
Contributor

@chescock chescock left a comment

Choose a reason for hiding this comment

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

Awesome!

}
}
} else {
// we can optimize small sizes by caching the iteration result in an array on the stack
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this actually help? For the really small sizes, I would have expected the compiler to unroll the whole thing anyway.

Copy link
Contributor Author

@hymm hymm Nov 17, 2025

Choose a reason for hiding this comment

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

yeah, it's like 50% faster at 10 components. I think it's mostly the calls to Components::component_id.

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah, it's like 50% faster at 10 components. The call to Components::component_id is surprisingly expensive.

Yeah, but the component_id call is now done only once in get_state, right? So the ComponentIds are already cached on the stack, and this is just caching the trivial conversions to EcsAccessType.

Anyway, if it's 50% faster then we should definitely keep it!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

looked at the assembly a little. Best I can tell is that it's inlining way more stuff. I see 10 is_compatible checks vs just 3. I see some other changes, but don't fully understand them to know if they're helping the speed.

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-Feature A new feature, making something new possible M-Release-Note Work that should be called out in the blog due to impact S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants