Skip to content

Commit 41f3ee7

Browse files
Improve time complexity of get_component_mut (#22572)
# Objective - Addresses #22483. ## Solution Add a bloom filter to `has_conflicts` as a pre-check to see if we need to check a given access against every other access or not. If the access doesn't hit any component or resource a previous access does, we know we don't need to check it element-by-element. Well-formed calls to get_components_mut should then be linear in time taken to check for conflicts, as they would always pass the pre-check. The filter used is exported in the `bevy_utils` API as it is not specific to components or resources. ## Testing Tested via `cargo test`, where the get_component_mut tests pass or panic as expected. The get_component_mut benchmarks show >=20% improvements at as little as 5 components compared to the existing fallback for smaller sets. The larger, 32 component benchmark added shows massive improvements. In general, the new pre-check filter means we're only about 4-5x slower than not checking at all. ``` ecs::world::world_get::world_query_get_components_mut/10_components_50000_entities time: [6.5252 ms 6.5314 ms 6.5388 ms] change: [−34.225% −33.954% −33.747%] (p = 0.00 < 0.05) Performance has improved. [...] ecs::world::world_get::world_query_get_components_mut/32_components time: [21.608 ms 21.696 ms 21.828 ms] change: [−92.284% −92.236% −92.186%] (p = 0.00 < 0.05) Performance has improved. ``` | bench | mean (prev, with fallback threshold) | mean (post, always complex) | delta | |-|-|-|-| | 2_components | 770.73 us | 925.90 us | +19.62% | | unchecked_2_components | 288.66 us | 288.71 us | -0.84% | | 5_components | 2.960 ms | 2.286 ms | -22.77% | | unchecked_5_components | 505.45 us | 521.56 us | +2.14% | | 10_components | 9.889 ms | 6.531 ms | -33.95% | | unchecked_10_components | 1.452 ms | 1.538 ms | +5.48% | | 32_components | 279.44 ms | 21.696 ms | -92.24% | | unchecked_32_components | 4.460 ms | 4.458 ms | -0.05%
1 parent 543b305 commit 41f3ee7

File tree

6 files changed

+485
-51
lines changed

6 files changed

+485
-51
lines changed

benches/benches/bevy_ecs/world/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@ criterion_group!(
3939
query_get_components_mut_2,
4040
query_get_components_mut_5,
4141
query_get_components_mut_10,
42+
query_get_components_mut_32,
4243
entity_set_build_and_lookup,
4344
);

benches/benches/bevy_ecs/world/world_get.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,3 +420,80 @@ macro_rules! query_get_components_mut {
420420
query_get_components_mut!(query_get_components_mut_2, 2);
421421
query_get_components_mut!(query_get_components_mut_5, 5);
422422
query_get_components_mut!(query_get_components_mut_10, 10);
423+
424+
// I'd like to do this as a macro, but we're bounded by the QueryData tuple size limit
425+
pub fn query_get_components_mut_32(criterion: &mut Criterion) {
426+
#[expect(
427+
clippy::identity_op,
428+
clippy::erasing_op,
429+
reason = "Clippy complains that, at some point in the 32 component
430+
bench, C32/RefC32 expand to 0 * 16 or 0 * 4 or 0. The
431+
alternative is to make the bounds 2..(n + 2) which is
432+
much less readable."
433+
)]
434+
type C32 = seq!(I in 0..2 {
435+
( #(
436+
seq!(J in 0..4 {
437+
( #(
438+
seq!(K in 0..4 {
439+
( #( WideTable::<{I * 16 + J * 4 + K}>, )* )
440+
}),
441+
)* )
442+
}),
443+
)* )
444+
});
445+
#[expect(
446+
clippy::identity_op,
447+
clippy::erasing_op,
448+
reason = "Clippy complains that, at some point in the 32 component
449+
bench, C32/RefC32 expand to 0 * 16 or 0 * 4 or 0. The
450+
alternative is to make the bounds 2..(n + 2) which is
451+
much less readable."
452+
)]
453+
type RefC32<'a> = seq!(I in 0..2 {
454+
( #(
455+
seq!(J in 0..4 {
456+
( #(
457+
seq!(K in 0..4 {
458+
( #( &'a WideTable::<{I * 16 + J * 4 + K}>, )* )
459+
}),
460+
)* )
461+
}),
462+
)* )
463+
});
464+
let mut group = criterion.benchmark_group(bench!("world_query_get_components_mut"));
465+
group.warm_up_time(core::time::Duration::from_millis(500));
466+
group.measurement_time(core::time::Duration::from_secs(4));
467+
468+
for entity_count in RANGE.map(|i| i * 10_000) {
469+
let (mut world, entities) = setup_wide::<C32>(entity_count);
470+
let mut query = world.query::<EntityMut>();
471+
group.bench_function("32_components", |bencher| {
472+
bencher.iter(|| {
473+
for entity in &entities {
474+
assert!(query
475+
.get_mut(&mut world, *entity)
476+
.unwrap()
477+
.get_components_mut::<RefC32>()
478+
.is_ok());
479+
}
480+
});
481+
});
482+
group.bench_function("unchecked_32_components", |bencher| {
483+
bencher.iter(|| {
484+
for entity in &entities {
485+
// SAFETY: no duplicate components are listed
486+
unsafe {
487+
assert!(query
488+
.get_mut(&mut world, *entity)
489+
.unwrap()
490+
.get_components_mut_unchecked::<RefC32>()
491+
.is_ok());
492+
}
493+
}
494+
});
495+
});
496+
}
497+
498+
group.finish();
499+
}

crates/bevy_ecs/src/query/access.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ impl<'a> Debug for FormattedBitSet<'a> {
4343
///
4444
/// Used internally to ensure soundness during system initialization and execution.
4545
/// See the [`is_compatible`](Access::is_compatible) and [`get_conflicts`](Access::get_conflicts) functions.
46-
#[derive(Eq, PartialEq, Default)]
46+
#[derive(Eq, PartialEq, Default, Hash)]
4747
pub struct Access {
4848
/// All accessed components, or forbidden components if
4949
/// `Self::component_read_and_writes_inverted` is set.

0 commit comments

Comments
 (0)