-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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
Let query items borrow from query state to avoid needing to clone #15396
base: main
Are you sure you want to change the base?
Conversation
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.
IIUC, the lack of |
It might be. The issue is that the For what it's worth, I don't think |
crates/bevy_core/src/name.rs
Outdated
@@ -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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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 :/
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.
The tuple behavior is a bug: #14349. |
… they are necessary to work around borrowing QueryState mutably.
Yeah, there are definitely tradeoffs here! Given that (It feels like there must be some way to get the best of both options by dropping the
Sorry, I was wrong; it works fine. I confused the local |
To note, I think |
…mpls of `ReleaseStateQueryData`.
…-queryitem # Conflicts: # crates/bevy_asset/src/asset_changed.rs # crates/bevy_ecs/macros/src/world_query.rs # crates/bevy_ecs/src/query/fetch.rs # crates/bevy_ecs/src/query/filter.rs # crates/bevy_ecs/src/query/mod.rs # crates/bevy_ecs/src/query/world_query.rs # crates/bevy_ecs/src/relationship/relationship_query.rs # crates/bevy_render/src/sync_world.rs
This prevents lens items from borrowing from the state, letting us drop the `ReleaseStateQueryData` requirement.
…yitem-temp # Conflicts: # crates/bevy_ecs/src/system/query.rs
Okay, I think I've finally fixed the problems that were brought up! I've updated this PR, and it should be reviewable now. The issues had been:
|
@@ -20,13 +24,13 @@ use crate::{entity::Entity, query::ReadOnlyQueryData, relationship::Relationship | |||
/// [specify the direction]: crate::event::Event::Traversal | |||
/// [event propagation]: crate::observer::Trigger::propagate | |||
/// [observers]: crate::observer::Observer | |||
pub trait Traversal<D: ?Sized>: ReadOnlyQueryData { | |||
pub trait Traversal<D: ?Sized>: ReadOnlyQueryData + ReleaseStateQueryData { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How come the extra bound is necessary here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Traversal is implemented in terms of get_components()
, which works by creating D::State
in a local, so the returned item can't borrow from the state.
It shouldn't prevent anything useful, since the only QueryData
that prohibits is FilteredEntity(Ref|Mut)
, and those don't have any access at all when default-initialized. Note that it does allow Entity(Ref|Mut)
, so you can get full access, just not partial!
We could loosen the requirement on Traveral
in the future if we need to. The traversal code only needs the query items during traversal, so it would be possible to create D::State
in the traversal code to borrow from. But that means making a different version of get_components()
, and that's a lot of extra code to support a case that isn't actually useful in the first place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I was confused because I was only thinking of traversal in the context of observers, where the Traversal
is usually just a component so we could use world.get::<T>
instead of get_components
.
But I see that in some cases (WindowTraversal
) the traversal query data is not a component
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Conceptually it makes sense to me that the Item can borrow from both the UnsafeWorldCell
or the State
.
I don't have a strong opinion on the sort changes, I actually couldn't find them in the diff
let entity_iter = keyed_query | ||
.into_iter() | ||
.map(|(.., entity)| entity.0) | ||
.collect::<Vec<_>>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is the change to do a collect()
as part of the sort
methods.
I also like how the code for |
…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`.
Objective
Improve the performance of
FilteredEntity(Ref|Mut)
andEntity(Ref|Mut)Except
.FilteredEntityRef
needs anAccess<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 toclone()
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 toWorldQuery::Item
andWorldQuery::Fetch
, similar to the one inSystemParam
, 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 subtraitReleaseStateQueryData
that converts aQueryItem<'w, 's>
toQueryItem<'w, 'static>
, and is implemented for everything exceptFilteredEntity(Ref|Mut)
andEntity(Ref|Mut)Except
.#[derive(QueryData)]
will generateReleaseStateQueryData
implementations that apply when all of the subqueries implementReleaseStateQueryData
.This PR does not actually change the implementation of
FilteredEntity(Ref|Mut)
orEntity(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)

Migration Guide
The
WorldQuery::Item
andWorldQuery::Fetch
associated types and theQueryItem
andROQueryItem
type aliases now have an additional lifetime parameter corresponding to the's
lifetime inQuery
. Manual implementations ofWorldQuery
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 implementingRenderCommand
.Before:
After:
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 affectsget()
,iter()
, and others. To fix the errors, first callQueryState::update_archetypes()
, and then replace a callstate.foo(world, param)
withstate.query_manual(world).foo_inner(param)
. Alternately, you may be able to restructure the code to callstate.query(world)
once and then make multiple calls using theQuery
.Before:
After: