Skip to content

Let query items borrow from query state to avoid needing to clone #15396

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

Merged
merged 31 commits into from
Jun 16, 2025

Conversation

chescock
Copy link
Contributor

@chescock chescock commented Sep 23, 2024

Objective

Improve the performance of FilteredEntity(Ref|Mut) and Entity(Ref|Mut)Except.

FilteredEntityRef needs an Access<ComponentId> to determine what components it can access. There is one stored in the query state, but query items cannot borrow from the state, so it has to clone() the access for each row. Cloning the access involves memory allocations and can be expensive.

Solution

Let query items borrow from their query state.

Add an 's lifetime to WorldQuery::Item and WorldQuery::Fetch, similar to the one in SystemParam, and provide &'s Self::State to the fetch so that it can borrow from the state.

Unfortunately, there are a few cases where we currently return query items from temporary query states: the sorted iteration methods create a temporary state to query the sort keys, and the EntityRef::components<Q>() methods create a temporary state for their query.

To allow these to continue to work with most QueryData implementations, introduce a new subtrait ReleaseStateQueryData that converts a QueryItem<'w, 's> to QueryItem<'w, 'static>, and is implemented for everything except FilteredEntity(Ref|Mut) and Entity(Ref|Mut)Except.

#[derive(QueryData)] will generate ReleaseStateQueryData implementations that apply when all of the subqueries implement ReleaseStateQueryData.

This PR does not actually change the implementation of FilteredEntity(Ref|Mut) or Entity(Ref|Mut)Except! That will be done as a follow-up PR so that the changes are easier to review. I have pushed the changes as chescock#5.

Testing

I ran performance traces of many_foxes, both against main and against chescock#5, both including #15282. These changes do appear to make generalized animation a bit faster:

(Red is main, yellow is chescock#5)
image

Migration Guide

The WorldQuery::Item and WorldQuery::Fetch associated types and the QueryItem and ROQueryItem type aliases now have an additional lifetime parameter corresponding to the 's lifetime in Query. Manual implementations of WorldQuery will need to update the method signatures to include the new lifetimes. Other uses of the types will need to be updated to include a lifetime parameter, although it can usually be passed as '_. In particular, ROQueryItem is used when implementing RenderCommand.

Before:

fn render<'w>(
    item: &P,
    view: ROQueryItem<'w, Self::ViewQuery>,
    entity: Option<ROQueryItem<'w, Self::ItemQuery>>,
    param: SystemParamItem<'w, '_, Self::Param>,
    pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult;

After:

fn render<'w>(
    item: &P,
    view: ROQueryItem<'w, '_, Self::ViewQuery>,
    entity: Option<ROQueryItem<'w, '_, Self::ItemQuery>>,
    param: SystemParamItem<'w, '_, Self::Param>,
    pass: &mut TrackedRenderPass<'w>,
) -> RenderCommandResult;

Methods on QueryState that take &mut self may now result in conflicting borrows if the query items capture the lifetime of the mutable reference. This affects get(), iter(), and others. To fix the errors, first call QueryState::update_archetypes(), and then replace a call state.foo(world, param) with state.query_manual(world).foo_inner(param). Alternately, you may be able to restructure the code to call state.query(world) once and then make multiple calls using the Query.

Before:

let mut state: QueryState<_, _> = ...;
let d1 = state.get(world, e1);
let d2 = state.get(world, e2); // Error: cannot borrow `state` as mutable more than once at a time
println!("{d1:?}");
println!("{d2:?}");

After:

let mut state: QueryState<_, _> = ...;

state.update_archetypes(world);
let d1 = state.get_manual(world, e1);
let d2 = state.get_manual(world, e2);
// OR
state.update_archetypes(world);
let d1 = state.query(world).get_inner(e1);
let d2 = state.query(world).get_inner(e2);
// OR
let query = state.query(world);
let d1 = query.get_inner(e1);
let d1 = query.get_inner(e2);

println!("{d1:?}");
println!("{d2:?}");

That takes a mutable borrow of the QueryState, which will conflict once query items may borrow the state.
This allows them to borrow from the query state, which can avoid expensive clones in some cases.
@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events C-Performance A change motivated by improving speed, memory usage or compile times M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Sep 23, 2024
@alice-i-cecile alice-i-cecile added this to the 0.15 milestone Sep 23, 2024
@Victoronz
Copy link
Contributor

Victoronz commented Sep 23, 2024

IIUC, the lack of ReleaseStateQueryData impls for FilteredEntityRef|Mut means they can no longer be used in the sort methods. This would mean removing support for sorting with untyped QueryData, which is important for some people. Is this change possible without removing that feature?

@chescock
Copy link
Contributor Author

IIUC, the lack of ReleaseStateQueryData impls for FilteredEntityRef|Mut means they can no longer be used in the sort methods. This would mean removing support for sorting with untyped QueryData, which is important for some people. Is this change possible without removing that feature?

It might be. The issue is that the L::Items borrow from the state, and they get included in the returned Iterator<Item=Entity>. But they aren't actually used after the sort method returns. The simplest fix would be to collect() entity_iter into a Vec to drop the L::Items, but I saw you had tried to avoid that in #13443.

For what it's worth, I don't think FilteredEntityRef|Mut will actually work with the sort() methods right now, which is why I hadn't been worried about it. They do transmute_filtered::<(L, Entity), F>(), and calling set_access() on (FilteredEntityRef, Entity) doesn't pass it down to the FilteredEntityRef. Also, transmute_filtered() calls set_access() before update_component_access(), so the access is empty anyway.

@@ -229,9 +229,9 @@ mod tests {
let e2 = world.spawn(name.clone()).id();
let mut query = world.query::<NameOrEntity>();
let d1 = query.get(&world, e1).unwrap();
let d2 = query.get(&world, e2).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

This needing to be moved is suspicious. Can you not call get multiple times on an immutable borrow of the world anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not in this case, but I promise it's not as bad as it looks!

QueryState::get() takes &mut self so that it can call update_archetypes(). And NameOrEntity uses #[derive(QueryData)], which now generates an item struct that includes the 's lifetime. So the item is treated as having a mutable borrow of the QueryState.

This doesn't affect Query::get(), which always takes &self.

And this doesn't affect concrete types where QueryData::Item doesn't include 's. That is, it only affects generic parameters, #[derive(QueryData)] types, and (in the future) FilteredEntityRef|Mut. If we use let mut query = world.query::<(Option<&Name>, Entity)>(); here, it will compile.

And there is a workaround in this case, which is to call query.update_archetypes() manually and then use query.get_manual(), which takes &self and so will allow overlapping borrows.

But, yeah, it is a breaking change here.

Copy link
Contributor

@hymm hymm Sep 28, 2024

Choose a reason for hiding this comment

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

After digesting this for a bit, maybe it isn't such a bad change. get takes an &mut self so hopefully it won't be that surprising for new users if you can't get multiple items out of it. And the workaround is more performant, so leading users to that is probably better.

But we should document the workaround on QueryState::get and in the migration guide.

I haven't done a full review yet, but hopefully will get to that soon.

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 not also affect query iteration? As in, for the affected types, any item would have to be dropped before another can be retrieved?
(Like what happens in QueryManyIter::fetch_next(), where item lifetimes are shrunk like this intentionally)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does this not also affect query iteration?

No, it doesn't. Query has a &'s QueryState, so we can copy that reference and hand it out with the full 's lifetime. This change only affects methods that take &mut QueryState.

(The issue with things like QueryManyIter::fetch_next() and Query::iter_mut() are that the UnsafeWorldCell<'w> is acting like a &'w mut, so we can't copy it and instead have to reborrow it with a shorter 'w lifetime.)

Copy link
Contributor

@Victoronz Victoronz Sep 29, 2024

Choose a reason for hiding this comment

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

Okay, so this does not present an issue within individual iterators.
However, this will be a breaking change to more methods of QueryState than what you've currently listed:

  • get
  • get_single (no checked manual version)
  • get_many (no manual version)
  • single (no manual version)
  • iter
  • iter_many
  • iter_combinations (no manual version)
  • par_iter (no manual version)
  • and naturally, any method that takes at least &'a QueryState while the query items returned by one of the above is live.

The lack of manual versions can be considered API holes for some of the above, however not all of them (f.e. single)

Do I understand correctly?

If so, I think the ability to mix these methods of QueryState contains important use cases, and I would see those as more valuable than this change in its current form :/

@BenjaminBrienen BenjaminBrienen added the D-Complex Quite challenging from either a design or technical perspective. Ask for help! label Sep 28, 2024
@Victoronz
Copy link
Contributor

Victoronz commented Sep 28, 2024

IIUC, the lack of ReleaseStateQueryData impls for FilteredEntityRef|Mut means they can no longer be used in the sort methods. This would mean removing support for sorting with untyped QueryData, which is important for some people. Is this change possible without removing that feature?

It might be. The issue is that the L::Items borrow from the state, and they get included in the returned Iterator<Item=Entity>. But they aren't actually used after the sort method returns. The simplest fix would be to collect() entity_iter into a Vec to drop the L::Items, but I saw you had tried to avoid that in #13443.

Yeah, an additional collect would regress sort performance. That would make this change a perf trade-off, not a pure gain. By how much, would have to be benchmarked.

For what it's worth, I don't think FilteredEntityRef|Mut will actually work with the sort() methods right now, which is why I hadn't been worried about it. They do transmute_filtered::<(L, Entity), F>(), and calling set_access() on (FilteredEntityRef, Entity) doesn't pass it down to the FilteredEntityRef. Also, transmute_filtered() calls set_access() before update_component_access(), so the access is empty anyway.

The tuple behavior is a bug: #14349. Entity, EntityLocation and &Archetype should always be available QueryData, regardless of access.
That access is not propagated to a transmuted FilteredEntityRef|Mut sounds like another bug, does FilteredEntityRef|Mut even work with transmutes in the first place?

@alice-i-cecile alice-i-cecile added the X-Contentious There are nontrivial implications that should be thought through label Sep 28, 2024
@chescock
Copy link
Contributor Author

Yeah, an additional collect would regress sort performance. That would make this change a perf trade-off, not a pure gain. By how much, would have to be benchmarked.

Yeah, there are definitely tradeoffs here! Given that sort doesn't actually work with FilteredEntityRef right now, I'd like to leave the ReleaseStateQueryData constraint in place on those methods. If we decide to merge this and we fix #14349, then we can revisit the constraint.

(It feels like there must be some way to get the best of both options by dropping the L::Items in place before the method returns without allocating a new Vec, but I don't yet see how to make it work. ManuallyDrop<T> still captures lifetimes.)

That access is not propagated to a transmuted FilteredEntityRef|Mut sounds like another bug, does FilteredEntityRef|Mut even work with transmutes in the first place?

Sorry, I was wrong; it works fine. I confused the local component_access, which was empty, with self.component_access, which gets passed to set_access.

@Victoronz
Copy link
Contributor

Victoronz commented Sep 29, 2024

To note, I think super let could also solve the kind of problem this PR tackles, once we get some form of it in Rust.

@alice-i-cecile alice-i-cecile added X-Controversial There is active debate or serious implications around merging this PR and removed X-Contentious There are nontrivial implications that should be thought through labels Sep 29, 2024
@alice-i-cecile alice-i-cecile modified the milestones: 0.15, 0.16 Oct 8, 2024
@chescock chescock added 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 Mar 7, 2025
chescock added 3 commits March 7, 2025 13:48
…yitem

# Conflicts:
#	crates/bevy_core_pipeline/src/deferred/node.rs
#	crates/bevy_core_pipeline/src/experimental/mip_generation/mod.rs
#	crates/bevy_core_pipeline/src/prepass/node.rs
#	crates/bevy_ecs/src/query/iter.rs
#	crates/bevy_ecs/src/query/state.rs
#	crates/bevy_ecs/src/system/query.rs
#	crates/bevy_ecs/src/world/entity_ref.rs
#	crates/bevy_input_focus/src/lib.rs
#	crates/bevy_picking/src/events.rs
Add `const IS_READ_ONLY` to `NonReleaseQueryData`.
…yitem

# Conflicts:
#	crates/bevy_ecs/src/query/state.rs
#	crates/bevy_ecs/src/system/query.rs
chescock added 3 commits June 16, 2025 09:43
…yitem

# Conflicts:
#	crates/bevy_ecs/src/name.rs
#	crates/bevy_ecs/src/query/fetch.rs
#	crates/bevy_ecs/src/query/state.rs
#	crates/bevy_ecs/src/system/query.rs
#	crates/bevy_ecs/src/system/system_param.rs
#	crates/bevy_ecs/src/world/entity_ref.rs
…yitem

# Conflicts:
#	crates/bevy_ecs/src/system/query.rs
#	crates/bevy_ecs/src/world/unsafe_world_cell.rs
#	crates/bevy_input_focus/src/lib.rs
@chescock chescock added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Jun 16, 2025
Copy link
Contributor

@ecoskey ecoskey left a comment

Choose a reason for hiding this comment

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

Looks good! I'm no ecs expert so I can't comment on the architectural concerns, but it seems like most of that was addressed, and the migration isn't too bad. With #19602 on the horizon I think this has a lot more upsides than downsides

for #read_only_struct_name #user_ty_generics #user_where_clauses
// Make these HRTBs with an unused lifetime parameter to allow trivial constraints
// See https://github.com/rust-lang/rust/issues/48214
where #(for<'__a> #field_types: #path::query::QueryData<ReadOnly: #path::query::ReleaseStateQueryData>,)* {
Copy link
Contributor

Choose a reason for hiding this comment

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

Afaik blanket impls like this tend to give unhelpful errors when they go unfulfilled. In the future maybe we should add a #[query_data(release_state)] and remove the per-field bounds, so any errors happen at the macro call site instead of at use sites?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, I hadn't considered what the errors would look like! I'd still lean towards implementing this automatically for now, though. Very few APIs actually require it, so I'd expect it to be forgotten if it isn't automatic, but most QueryData will implement it successfully.

@ecoskey ecoskey 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 Jun 16, 2025
@Victoronz
Copy link
Contributor

Victoronz commented Jun 16, 2025

IIUC, this change will mean that the consuming methods will not work for what is not ReleaseStateQueryData. Given that the current default way of iterating over Query uses a consuming method to iterate, users will effectively not be able to use the default for loop over FilteredEntityRef and other non-ReleaseStateQueryData types.

Example of what fails:

pub fn foo(query: Query<FilteredEntityRef>) {
    for entity_ref in query {
        // ...
    }
}

Note: I say "default" as in, what we intend for users to reach for first. Using &query in the for loop should work, but that is not obvious to an unsuspecting user, and becomes inconsistent with for loops elsewhere.

@chescock
Copy link
Contributor Author

IIUC, this change will mean that the consuming methods will not work for what is not ReleaseStateQueryData.

That will still work fine! Query has an &'s QueryState, and the items will borrow from that.

The issue is if you create a QueryState locally, such as in a QueryLens, the items will borrow from it. So you'll no longer be able to return FilteredEntityRefs created from a local lens, like:

fn foo(query: Query<&Transform>) -> FilteredEntityRef {
    let lens = query.transmute_lens_inner::<FilteredEntityRef>();
    let query = lens.query_inner();
    query.single_inner().unwrap()
}

If we also do #18162, then the new consuming methods on QueryLens won't be able to work with non-ReleaseStateQueryData. But those methods don't exist at all today, so that' not a breaking change :).

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Jun 16, 2025
@Victoronz
Copy link
Contributor

IIUC, this change will mean that the consuming methods will not work for what is not ReleaseStateQueryData.

That will still work fine! Query has an &'s QueryState, and the items will borrow from that.

The issue is if you create a QueryState locally, such as in a QueryLens, the items will borrow from it. So you'll no longer be able to return FilteredEntityRefs created from a local lens, like:

You're right, I was missing that context.

If we also do #18162, then the new consuming methods on QueryLens won't be able to work with non-ReleaseStateQueryData. But those methods don't exist at all today, so that' not a breaking change :).

Even if its not a breaking change, it is still inconsistent!

@alice-i-cecile
Copy link
Member

Really excellent work. This is extremely complex, but you've done a great job navigating that in your code, docs, review comments and PR description. Thanks!

Merged via the queue into bevyengine:main with commit f7e112a Jun 16, 2025
36 checks passed
github-merge-queue bot pushed a commit that referenced this pull request Jun 19, 2025
# Objective

Unblock #18162.

#15396 added the `'s` lifetime to `QueryData::Item` to make it possible
for query items to borrow from the state. The state isn't passed
directly to `QueryData::fetch()`, so it also added the `'s` lifetime to
`WorldQuery::Fetch` so that we can pass the borrows through there.

Unfortunately, having `WorldQuery::Fetch` borrow from the state makes it
impossible to have owned state, because we store the state and the
`Fetch` in the same `struct` during iteration.

## Solution

Undo the change to add the `'s` lifetime to `WorldQuery::Fetch`.

Instead, add a `&'s Self::State` parameter to `QueryData::fetch()` and
`QueryFilter::filter_fetch()` so that borrows from the state can be
passed directly to query items.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Emerson Coskey <emerson@coskey.dev>
Trashtalk217 pushed a commit to Trashtalk217/bevy that referenced this pull request Jul 10, 2025
# Objective

Unblock bevyengine#18162.

bevyengine#15396 added the `'s` lifetime to `QueryData::Item` to make it possible
for query items to borrow from the state. The state isn't passed
directly to `QueryData::fetch()`, so it also added the `'s` lifetime to
`WorldQuery::Fetch` so that we can pass the borrows through there.

Unfortunately, having `WorldQuery::Fetch` borrow from the state makes it
impossible to have owned state, because we store the state and the
`Fetch` in the same `struct` during iteration.

## Solution

Undo the change to add the `'s` lifetime to `WorldQuery::Fetch`.

Instead, add a `&'s Self::State` parameter to `QueryData::fetch()` and
`QueryFilter::filter_fetch()` so that borrows from the state can be
passed directly to query items.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Emerson Coskey <emerson@coskey.dev>
github-merge-queue bot pushed a commit that referenced this pull request Jul 21, 2025
…wing `Access` (#20111)

# Objective

Improve the performance of queries using `FilteredEntityRef`,
`FilteredEntityMut`, `EntityRefExcept`, and `EntityMutExcept`. In
particular, this appears to speed up `bevy_animation::animate_targets`
by 10% in many-foxes.

`FilteredEntity(Ref|Mut)` needs to store an `Access` to determine which
components may be accessed. Prior to #15396, this required cloning the
`Access` for each instance. Now, we can borrow the `Access` from the
query state and make cheap pointer copies.

`Entity(Ref|Mut)Except` avoided needing to clone an `Access` by calling
functions on the `Bundle` trait. Unfortunately, that meant we needed to
convert from a type to a `ComponentId` for every component in the bundle
on every check. Now, we can do those conversions up front and pass
references to an `Access`.

Finally, fix a bug where `Entity(Ref|Mut)Except` would not initialize
their components during `init_state`. I noticed this while updating
`init_state` and fixed it while I was there. That was normally harmless
because the components would be registered elsewhere, but a system like
`fn system(_q1: Query<EntityMutExcept<C>>, _q2: Query<&mut C>) {}` would
fail to find the `ComponentId` for `C` and not exclude it from the
access for `q1`, and then panic with conflicting access from `q2`.

## Solution

Change `FilteredEntityRef` and `FilteredEntityMut` to store `&'s Access`
instead of `Access`, and change `EntityRefExcept` and `EntityMutExcept`
to store an extra `&'s Access`.

This adds the `'s` lifetime to those four types, and most changes are
adding lifetimes as appropriate.

Change the `WorldQuery::State` for `Entity(Ref|Mut)Except` to store an
`Access` that can be borrowed from, replacing the
`SmallVec<[ComponentId; 4]>` that was used only to set the query access.

To support the conversions from `EntityRef` and `EntityMut`, we need to
be able to create a `&'static Access` for read-all or write-all. I could
not change `fn read_all_components()` to be `const` because it called
the non-`const` `FixedBitSet::clear()`, so I created separate
constructor functions.

## Testing

Ran `cargo run --example many_foxes --features bevy/trace_tracy
--release` before and after, and compared the results of
`animate_targets`, since that is the only in-engine use of
`EntityMutExcept` and was the motivation for creating it.

Yellow is this PR, red is main: 

<img width="695" height="690" alt="image"
src="https://github.com/user-attachments/assets/24531a3f-65bf-46d0-baa5-29ea9e56b16a"
/>
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-Complex Quite challenging from either a design or technical perspective. Ask for help! M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Contentious There are nontrivial implications that should be thought through
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants