-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Contiguous access #21984
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
base: main
Are you sure you want to change the base?
Contiguous access #21984
Conversation
|
This pr just enables slices from tables to be returned directly when applicable, it doesn't implement any batches and it doesn't ensure any specific (other than rust's) alignment (yet these slices may be used to apply simd).
This pr doesn't deal with any alignments but (as of my understanding) you can always take sub-slices which would meet your alignment requirements. And just referring to the issue #21861, even without any specific alignment the code gets vectorized.
No, the returned slices do not have any specific (other than rust's) alignment requirements. |
|
The solution looks promising to solve issue #21861. If you want to use SIMD instructions explicitly, alignment is something you usually have to manage yourself (with an aligned allocator or a peeled prologue). Auto-vectorization won’t “update” the alignment for you – it just uses whatever alignment it can prove and otherwise emits unaligned loads. From that perspective, a contiguous slice is already sufficient; fully aligned SIMD is a separate concern on top of that. |
hymm
left a comment
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 is not a full review, but onboard with the general approach in this pr. Overall this is fairly straightforward. I imagine we'll eventually want to have some simd aligned storage, but in the meantime users can probably align their components manually.
|
You added a new example but didn't add metadata for it. Please update the root Cargo.toml file. |
I'm sort of talking about #22500, and sort of not. Let me try to better articulate my thoughts here... It seems like right now, the main point of this PR is to enable a specific optimization for table iteration, which is absolutely valuable! And you're right that that doesn't really apply to sparse set components. In that lens, it also makes sense why you've opted to name it "contiguous access" rather than "storage/archetype iteration". I've been looking at this PR a slightly different way. To me, the valuable core idea here is removing the inner loop of query iteration, and handing that off to the user to allow them to do whatever they want. On the one hand, that enables accessing raw slices of components, but I think there's value beyond that! For example, if a user had some setup where they need to process per-archetype data in a query, for example for some kind of cache, they might not care if some of the terms hand back a sparse set rather than a dense column. In that case, framing this feature as "iterating over archetypes" makes a lot of sense I think! We could still allow contiguous access where available, but allow ourselves more flexibility with how the feature is used. With that framing, I think you could even implement something like #22500 in userspace! In that sense, I think this pr works well to the same goal of giving users better control over query iteration, though I don't think it actually needs to (or should) iterate over chunks as suggested by @chescock. Now, none of these points are blockers for this PR, but maybe worth considering for future direction. I think there's a lot of potential here 🙂
We should definitely still have a way to access slices directly. My point here was more about replacing the |
hymm
left a comment
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.
The recent changes fixed my reservations with this pr. Just some nits left.
crates/bevy_ecs/src/query/fetch.rs
Outdated
| ) -> Self::Contiguous<'w, 's> { | ||
| fetch.components.extract( | ||
| |table| { | ||
| // SAFETY: set_table was previously called |
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.
| // SAFETY: set_table was previously called | |
| // SAFETY: Caller ensures `set_table` was previously called |
| last_run: Tick, | ||
| ) -> Self { | ||
| Self { | ||
| // SAFETY: The invariants are upheld by the caller. |
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.
You should list each safety invariant for each unsafe block here as they might apply to different invariants of from_slice_ptrs. We want to be able to trace how each invariant is passed through.
examples/ecs/contiguous_query.rs
Outdated
| @@ -0,0 +1,55 @@ | |||
| //! Demonstrates how contiguous queries work | |||
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.
I think this needs some description of why you would use a contiguous query and what they are.
|
|
||
| `Query` and `QueryState` have new methods `contiguous_iter`, `contiguous_iter_mut` and `contiguous_iter_inner`, which allows querying contiguously (i.e., over tables). For it to work the query data must implement `ContiguousQueryData` and the query filter `ArchetypeFilter`. When a contiguous iterator is used, the iterator will jump over whole tables, returning corresponding data. Some notable implementors of `ContiguousQueryData` are `&T` and `&mut T`, returning `&[T]` and `ContiguousMut<T>` correspondingly, where the latter structure lets you get a mutable slice of components as well as corresponding ticks. Some notable implementors of `ArchetypeFilter` are `With<T>` and `Without<T>` and notable types **not implementing** it are `Changed<T>` and `Added<T>`. | ||
|
|
||
| This is for example useful, when an operation must be applied on a big amount of entities lying in the same tables, which allows for the compiler to auto-vectorize the code, thus speeding it up. |
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 is for example useful, when an operation must be applied on a big amount of entities lying in the same tables, which allows for the compiler to auto-vectorize the code, thus speeding it up. | |
| For example, this is useful when an operation must be applied on a large amount of entities lying in the same tables, which allows for the compiler to auto-vectorize the code, thus speeding it up. |
chescock
left a comment
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 looks good! My comments are mostly just thoughts on more polish for the Contiguous(Ref|Mut) types, and shouldn't block this PR.
| pub(crate) changed: &'w [Tick], | ||
| #[expect( | ||
| unused, | ||
| reason = "ZST in release mode, for the back-portability with ComponentTicksRef" |
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 unused lint is surprising. I wouldn't have expected this to be different from the other slices. ... Oh, I see, there are accessor methods for data_slice() and added_ticks_slice(), but there's no changed_by_slice()!
Do you want to add changed_by_slice() to ContiguousRef and ContiguousMut, and then remove the expect?
It might even make sense to add methods like pub fn get(&self, index: usize) -> Ref<'_, T> that take an index and pack everything into a Ref or Mut, but maybe anyone who wants those would use iter instead of contiguous_iter anyway.
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.
I missed the DetectChanges trait which has a method to return changed_by values, added the methods to return changed_by values.
crates/bevy_ecs/src/query/fetch.rs
Outdated
| /// # Safety | ||
| /// | ||
| /// - The result of [`ContiguousQueryData::fetch_contiguous`] must represent the same result as if | ||
| /// [`QueryData::fetch`] was executed for each entity of the set table |
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 actually need to be a safety requirement? It's clearly a bug not to do this, but I don't see how it would cause UB if it returned some other unrelated data.
If we do keep this as a safety requirement, then the safety comments on the impls might need some changes. The one on &mut T in particular seems out-of-date.
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.
Made it a safe trait
|
|
||
| /// Data type returned by [`ContiguousQueryData::fetch_contiguous`](crate::query::ContiguousQueryData::fetch_contiguous) for [`Ref<T>`]. | ||
| #[derive(Clone)] | ||
| pub struct ContiguousRef<'w, T> { |
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.
Is there a pub way to construct these? I bet someone will ask for one to be added later, but I think it's reasonable to leave it out until someone does.
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.
See ContiguousRef::new
| impl<'w, T> ContiguousRef<'w, T> { | ||
| /// Returns the data slice. | ||
| #[inline] | ||
| pub fn data_slice(&self) -> &[T] { |
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.
You can extend the lifetimes returned from ContiguousRef, since & references are Copy.
| pub fn data_slice(&self) -> &[T] { | |
| pub fn data_slice(&self) -> &'w [T] { |
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.
added method ContiguousRef::into_inner which returns &'w [T]
|
|
||
| /// Data type returned by [`ContiguousQueryData::fetch_contiguous`](crate::query::ContiguousQueryData::fetch_contiguous) for [`Ref<T>`]. | ||
| #[derive(Clone)] | ||
| pub struct ContiguousRef<'w, T> { |
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.
Would it make sense to impl Deref<Target = [T]> for ContiguousRef and ContiguousMut?
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.
Implemented Deref<Target = [T]>, DerefMut<Target = [T]>, AsRef<[T]>, AsMut<[T]> and IntoIterator<Item = &T|&mut T> for ContiguousRef and ContiguousMut. Traits returning mutable references also update change ticks automatically, added bypass_change_detection as well.
Objective
Enables accessing slices from tables directly via Queries.
Fixes: #21861
Solution
One new trait:
ContiguousQueryDataallows to fetch all values from tables all at once (an implementation for&Treturns a slice of components in the set table, for&mut Treturns a mutable slice of components in the set table as well as a struct with methods to set update ticks (to match thefetchimplementation))Methods
contiguous_iter,contiguous_iter_mutand similar inQueryandQueryStatemaking possible to iterate using these traits.Macro
QueryDatawas updated to support contiguous items whencontiguous(target)attribute is added (a target can beall,mutableandimmutable, refer to thecustom_query_paramexample)Testing
sparse_set_contiguous_querytest verifies that you can't usenext_contiguouswith sparse set componentstest_contiguous_query_datatest verifies that returned values are validbase_contiguousbenchmark (file is namediter_simple_contiguous.rs)base_no_detectionbenchmark (file is namediter_simple_no_detection.rs)base_no_detection_contiguousbenchmark (file is namediter_simple_no_detection_contiguous.rs)base_contiguous_avx2benchmark (file is namediter_simple_contiguous_avx2.rs)Showcase
Examples
contiguous_query,custom_query_paramExample
Benchmarks
Code for
basebenchmark:Iterating over 10000 entities from a single table and increasing a 3-dimensional vector from component
Positionby a 3-dimensional vector from componentVelocitybypass_change_detection()methodUsing contiguous 'iterator' makes the program a little bit faster and it can be further vectorized to make it even faster