Skip to content

QueryData/QueryFilter refactors and query lookahead#22500

Open
ecoskey wants to merge 33 commits into
bevyengine:mainfrom
ecoskey:feature/query_lookahead
Open

QueryData/QueryFilter refactors and query lookahead#22500
ecoskey wants to merge 33 commits into
bevyengine:mainfrom
ecoskey:feature/query_lookahead

Conversation

@ecoskey
Copy link
Copy Markdown
Contributor

@ecoskey ecoskey commented Jan 14, 2026

Objective

Make query iteration faster, more flexible, and give query terms more control over how to iterate.

This PR implements "chunk-based query lookahead". Instead of checking the conditions for each row row-wise, we now check them column-wise, iterating forward for each term until we find the next contiguous "chunk" of valid rows. By passing the chunk returned by each query term to the next one, we can gradually narrow down our search area, like if we were short-circuiting in a row-wise check.

This works fine for normal query iteration (and is even faster by default in some cases!), but the main point is it gives query terms more control over how they iterate. In particular, this pattern is perfect for searching in something like a segment tree! The goal is for custom query terms (and changed detection eventually!) to be able to greatly speed up iteration where they need to.

It also might******* be more likely to auto-vectorize plain condition checking, there were vectorized loops in some of the asm dumps but I wasn't able to verify that it was condition checking in particular.

Solution

  • move IS_ARCHETYPAL onto WorldQuery
  • move all conditional logic into WorldQuery::matches, leaving QueryFilter empty, and
    removing the Option wrapper from QueryData::fetch.
  • add WorldQuery::find_table_chunk and find_archetype_chunk to let query terms determine
    if/how to seek ahead in the query.
  • rewrite QueryIter::fold/for_each to use chunk-based iteration.

NOTE: I had some strange performance issues with QueryIter::next, so I left it as-is for now. I saw as much as a 50% performance hit in some benchmarks, and I was able to (mostly) fix it, but at the cost of a similar hit to for_each. It feels like a weird bug though, so it seems worth coming back to eventually.

Testing

  • Ran examples

  • Ran benchmarks:

  • none_changed_detection: 10-20% faster or no change vs main

  • few_changed_detection: 40-55% faster vs main

  • all_changed_detection: ~20% faster vs main for table iteration, ~20% slower for sparse (that feels like a bug, will investigate)

  • iter_simple: generally no change, 15% faster for wide_sparse_set, 3-4% slower for
    foreach_hybrid and par_hybrid

  • iter_frag: generally no change, a few were faster by 5-7% and a few were slower by 5-10%

  • heavy_compute: no change vs main

Needs real world testing! I don't have a good representative benchmark myself.

Future Work

  • implement a fast wide segment tree for change ticks, and implement proper lookahead for Added and Changed
  • look into tracking "last-checked" ranges for each query term, so work during lookahead doesn't get lost/redone.
  • opt-in change ticks (WIP)
  • add lookahead to Query::next
  • controversial: after this change we could totally merge the two generics on Query into one

@ecoskey ecoskey changed the title QueryData/QueryFilter refactors, and query lookahead QueryData/QueryFilter refactors and query lookahead Jan 14, 2026
@ecoskey ecoskey added A-ECS Entities, components, systems, and events C-Performance A change motivated by improving speed, memory usage or compile times S-Needs-Benchmarking This set of changes needs performance benchmarking to double-check that they help S-Needs-Review Needs reviewer attention (from anyone!) to move forward D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Waiting-on-SME This is currently waiting for an SME to resolve something controversial and removed S-Needs-Benchmarking This set of changes needs performance benchmarking to double-check that they help labels Jan 14, 2026
@ecoskey ecoskey added the M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide label Jan 14, 2026
@alice-i-cecile alice-i-cecile added X-Needs-SME This type of work requires an SME to approve it. M-Release-Note Work that should be called out in the blog due to impact labels Jan 14, 2026
@ecoskey
Copy link
Copy Markdown
Contributor Author

ecoskey commented Jan 21, 2026

  • move all conditional logic into WorldQuery::matches, leaving QueryFilter empty, and
    removing the Option wrapper from QueryData::fetch.

Hmm, this approach wouldn't work well with my plan for Nested Queries (#21557). The idea there is that we'd evaluate Query<Parent<&T>> by doing a get on a nested Query<&T>. But if we separate matches from fetch, then we'd have to evaluate the nested query twice!

Would it work to leave QueryData::fetch returning an Option? So QueryData would have two ways to filter, one that takes advantage of lookahead (matches) and one that can be combined with the fetch (Option).

(Or... do we ever need lookahead on QueryData, or just on QueryFilter? If we only need it on QueryFilter, then another option is to put matches and find_table_chunk on QueryFilter instead of WorldQuery, and just have completely separate filtering mechanisms.)

I think I'm leaning towards something like option 1. What if we added a try_fetch method that defaults to matches().then(|| fetch())? That would simplify the diff a bit in a few places anyways.

@chescock chescock mentioned this pull request Jan 21, 2026

#[doc(hidden)]
#[derive(Clone)]
pub struct AnyOfFetch<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.

This isn't directly related to this PR, but I've been wondering whether we should change the implementation of AnyOf to delegate to Option and Or somehow. You wound up basically having to duplicate the find_table_chunk and find_archetype_chunk implementations between AnyOf and Or.

Comment thread crates/bevy_asset/src/asset_changed.rs Outdated
Comment thread crates/bevy_ecs/src/query/world_query.rs Outdated
Comment thread crates/bevy_ecs/src/query/world_query.rs Outdated
/// `table_row` must be in the range of the current table and archetype.
/// - There must not be simultaneous conflicting component access registered in `update_component_access`.
#[inline(always)]
unsafe fn try_fetch<'w, 's>(
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.

I don't think I see how this approach to try_fetch avoids to double-query problem with nested queries. We'll be able to override Parent::try_fetch to make a single call, which will help for get. But regular iteration will use find_table_chunk and fetch, which will call matches and fetch, which will wind up invoking the nested query twice.

I think we might want to keep the mechanisms separate, so that Query<Parent<&T>, Changed<&U>> can use lookahead to evaluate Changed<&U> but then make nested Parent<&T> queries one-by-one. So, add matches, but keep fetch returning Option, and query iteration code needs to handle both ways of filtering.

But also: Nested queries don't exist yet! I don't want you to feel like you need to handle my pet project in this PR, and I'm happy to rework this again if this PR gets merged and then we decide we want nested queries. It will just seem a little silly if I do a PR to make fetch return an Option, and then you remove the Option in this PR, and then I add it back again in a third PR :).

Copy link
Copy Markdown
Contributor Author

@ecoskey ecoskey Jan 30, 2026

Choose a reason for hiding this comment

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

True, it won't speed up chunked iteration for &Parent. Though, currently chunked iteration only applies to for_each anyways! That was originally because of some perf issues I found, but I'd be fine keeping it that way tbh. It kind of makes sense to have different optimizations apply to different kinds of queries. It was kind of jank to shove all the chunking logic into QueryIterationCursor anyways lol.

@alice-i-cecile alice-i-cecile added this to the 0.19 milestone Feb 1, 2026
@cart cart added this to ECS Feb 12, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in ECS Feb 12, 2026
@alice-i-cecile alice-i-cecile added the S-Needs-Goal This should have a C-Goal and should not continue until it has one label Feb 12, 2026
@alice-i-cecile alice-i-cecile moved this from Needs SME Triage to SME Triaged in ECS Feb 12, 2026
@alice-i-cecile alice-i-cecile removed this from the 0.19 milestone Mar 16, 2026
@cart cart closed this May 5, 2026
@github-project-automation github-project-automation Bot moved this from SME Triaged to Done in ECS May 5, 2026
@cart cart reopened this May 5, 2026
@github-project-automation github-project-automation Bot moved this from Done to Needs SME Triage in ECS May 5, 2026
pull Bot pushed a commit to octoape/bevy that referenced this pull request May 8, 2026
# Objective

- Migration guide merged in release-content

## Solution

- Move it
- Add a CI check that will block new merges

Opened PRs that sill change that folder:
- bevyengine#23467
- bevyengine#23445
- bevyengine#23373
- bevyengine#23137
- bevyengine#23132
- bevyengine#23056
- bevyengine#22917
- bevyengine#22852
- bevyengine#22782
- bevyengine#22670
- bevyengine#22557
- bevyengine#22500
- bevyengine#21929
- bevyengine#21912
- bevyengine#21897
- bevyengine#21893
- bevyengine#21890
- bevyengine#21889
- bevyengine#21839
- bevyengine#21811
- bevyengine#21772

None of them are likely to be merged in the 0.19
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-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide M-Release-Note Work that should be called out in the blog due to impact S-Needs-Goal This should have a C-Goal and should not continue until it has one S-Needs-Review Needs reviewer attention (from anyone!) to move forward S-Waiting-on-SME This is currently waiting for an SME to resolve something controversial X-Needs-SME This type of work requires an SME to approve it.

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

4 participants