From e39fa12ee5884c3a4976994aee2dad92b2af5ae4 Mon Sep 17 00:00:00 2001 From: Marius Metzger Date: Fri, 13 Jun 2025 12:11:24 +0200 Subject: [PATCH] Bench replacement of immutable components with hooks --- benches/benches/bevy_ecs/change_detection.rs | 287 ++++++++++++++++++- 1 file changed, 286 insertions(+), 1 deletion(-) diff --git a/benches/benches/bevy_ecs/change_detection.rs b/benches/benches/bevy_ecs/change_detection.rs index 92f3251abc..392e7b0054 100644 --- a/benches/benches/bevy_ecs/change_detection.rs +++ b/benches/benches/bevy_ecs/change_detection.rs @@ -3,6 +3,8 @@ use core::hint::black_box; use bevy_ecs::{ component::{Component, Mutable}, entity::Entity, + lifecycle::Insert, + observer::{Observer, On}, prelude::{Added, Changed, EntityWorldMut, QueryState}, query::QueryFilter, world::World, @@ -17,7 +19,12 @@ criterion_group!( all_changed_detection, few_changed_detection, none_changed_detection, - multiple_archetype_none_changed_detection + multiple_archetype_none_changed_detection, + all_immutable_replace_detection, + few_immutable_replace_detection, + none_immutable_replace_detection, + all_immutable_replace_no_observer, + few_immutable_replace_no_observer ); macro_rules! modify { @@ -39,6 +46,14 @@ struct Sparse(f32); #[component(storage = "Table")] struct Data(f32); +#[derive(Component, Default, Clone)] +#[component(storage = "Table", immutable)] +struct ImmutableTable(f32); + +#[derive(Component, Default, Clone)] +#[component(storage = "SparseSet", immutable)] +struct ImmutableSparse(f32); + trait BenchModify { fn bench_modify(&mut self) -> f32; } @@ -56,6 +71,22 @@ impl BenchModify for Sparse { } } +trait BenchReplace { + fn bench_replace(&self) -> Self; +} + +impl BenchReplace for ImmutableTable { + fn bench_replace(&self) -> Self { + Self(self.0 + 1f32) + } +} + +impl BenchReplace for ImmutableSparse { + fn bench_replace(&self) -> Self { + Self(self.0 + 1f32) + } +} + const ENTITIES_TO_BENCH_COUNT: &[u32] = &[5000, 50000]; type BenchGroup<'a> = criterion::BenchmarkGroup<'a, criterion::measurement::WallTime>; @@ -70,6 +101,17 @@ fn setup(entity_count: u32) -> World { black_box(world) } +fn setup_with_observer(entity_count: u32) -> World { + let mut world = World::default(); + + world.spawn(Observer::new(|trigger: On| { + black_box(trigger); + })); + + world.spawn_batch((0..entity_count).map(|_| T::default())); + black_box(world) +} + // create a cached query in setup to avoid extra costs in each iter fn generic_filter_query(world: &mut World) -> QueryState { world.query_filtered::() @@ -266,6 +308,249 @@ fn none_changed_detection(criterion: &mut Criterion) { ); } } + +fn all_immutable_replace_detection_generic< + T: Component + Default + Clone + BenchReplace + 'static, +>( + group: &mut BenchGroup, + entity_count: u32, +) { + group.bench_function( + format!("{}_entities_{}", entity_count, core::any::type_name::()), + |bencher| { + bencher.iter_batched_ref( + || { + let mut world = setup_with_observer::(entity_count); + let mut query = world.query::(); + let entities: Vec<_> = query.iter(&world).collect(); + (world, entities) + }, + |(world, entities)| { + for entity in entities { + let component = world + .entity(*entity) + .get::() + .cloned() + .unwrap_or_else(T::default); + let new_component = component.bench_replace(); + black_box(world.entity_mut(*entity).insert(new_component)); + } + }, + criterion::BatchSize::LargeInput, + ); + }, + ); +} + +fn all_immutable_replace_detection(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("all_immutable_replace_detection"); + group.warm_up_time(core::time::Duration::from_millis(500)); + group.measurement_time(core::time::Duration::from_secs(4)); + for &entity_count in ENTITIES_TO_BENCH_COUNT { + generic_bench( + &mut group, + vec![ + Box::new(all_immutable_replace_detection_generic::), + //Box::new(all_immutable_replace_detection_generic::), + ], + entity_count, + ); + } +} + +fn few_immutable_replace_detection_generic< + T: Component + Default + Clone + BenchReplace + 'static, +>( + group: &mut BenchGroup, + entity_count: u32, +) { + let ratio_to_modify = 0.1; + let amount_to_modify = (entity_count as f32 * ratio_to_modify) as usize; + group.bench_function( + format!("{}_entities_{}", entity_count, core::any::type_name::()), + |bencher| { + bencher.iter_batched_ref( + || { + let mut world = setup_with_observer::(entity_count); + let mut query = world.query::(); + let mut entities: Vec<_> = query.iter(&world).collect(); + entities.shuffle(&mut deterministic_rand()); + (world, entities) + }, + |(world, entities)| { + for entity in entities[0..amount_to_modify].iter() { + let component = world + .entity(*entity) + .get::() + .cloned() + .unwrap_or_else(T::default); + let new_component = component.bench_replace(); + black_box(world.entity_mut(*entity).insert(new_component)); + } + }, + criterion::BatchSize::LargeInput, + ); + }, + ); +} + +fn few_immutable_replace_detection(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("few_immutable_replace_detection"); + group.warm_up_time(core::time::Duration::from_millis(500)); + group.measurement_time(core::time::Duration::from_secs(4)); + for &entity_count in ENTITIES_TO_BENCH_COUNT { + generic_bench( + &mut group, + vec![ + Box::new(few_immutable_replace_detection_generic::), + Box::new(few_immutable_replace_detection_generic::), + ], + entity_count, + ); + } +} + +fn none_immutable_replace_detection_generic( + group: &mut BenchGroup, + entity_count: u32, +) { + group.bench_function( + format!("{}_entities_{}", entity_count, core::any::type_name::()), + |bencher| { + bencher.iter_batched_ref( + || { + let world = setup_with_observer::(entity_count); + world + }, + |world| { + // Do nothing - just measure the overhead of having the observer + // without actually replacing any components + world.clear_trackers(); + }, + criterion::BatchSize::LargeInput, + ); + }, + ); +} + +fn none_immutable_replace_detection(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("none_immutable_replace_detection"); + group.warm_up_time(core::time::Duration::from_millis(500)); + group.measurement_time(core::time::Duration::from_secs(4)); + for &entity_count in ENTITIES_TO_BENCH_COUNT { + generic_bench( + &mut group, + vec![ + Box::new(none_immutable_replace_detection_generic::), + Box::new(none_immutable_replace_detection_generic::), + ], + entity_count, + ); + } +} + +// Baseline benchmarks without observers for comparison +fn all_immutable_replace_no_observer_generic< + T: Component + Default + Clone + BenchReplace + 'static, +>( + group: &mut BenchGroup, + entity_count: u32, +) { + group.bench_function( + format!("{}_entities_{}", entity_count, core::any::type_name::()), + |bencher| { + bencher.iter_batched_ref( + || { + let mut world = setup::(entity_count); // No observer + let mut query = world.query::(); + let entities: Vec<_> = query.iter(&world).collect(); + (world, entities) + }, + |(world, entities)| { + for entity in entities { + let component = world + .entity(*entity) + .get::() + .cloned() + .unwrap_or_else(T::default); + let new_component = component.bench_replace(); + black_box(world.entity_mut(*entity).insert(new_component)); + } + }, + criterion::BatchSize::LargeInput, + ); + }, + ); +} + +fn all_immutable_replace_no_observer(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("all_immutable_replace_no_observer"); + group.warm_up_time(core::time::Duration::from_millis(500)); + group.measurement_time(core::time::Duration::from_secs(4)); + for &entity_count in ENTITIES_TO_BENCH_COUNT { + generic_bench( + &mut group, + vec![ + Box::new(all_immutable_replace_no_observer_generic::), + Box::new(all_immutable_replace_no_observer_generic::), + ], + entity_count, + ); + } +} + +fn few_immutable_replace_no_observer_generic< + T: Component + Default + Clone + BenchReplace + 'static, +>( + group: &mut BenchGroup, + entity_count: u32, +) { + let ratio_to_modify = 0.1; + let amount_to_modify = (entity_count as f32 * ratio_to_modify) as usize; + group.bench_function( + format!("{}_entities_{}", entity_count, core::any::type_name::()), + |bencher| { + bencher.iter_batched_ref( + || { + let mut world = setup::(entity_count); // No observer + let mut query = world.query::(); + let mut entities: Vec<_> = query.iter(&world).collect(); + entities.shuffle(&mut deterministic_rand()); + (world, entities) + }, + |(world, entities)| { + for entity in entities[0..amount_to_modify].iter() { + let component = world + .entity(*entity) + .get::() + .cloned() + .unwrap_or_else(T::default); + let new_component = component.bench_replace(); + black_box(world.entity_mut(*entity).insert(new_component)); + } + }, + criterion::BatchSize::LargeInput, + ); + }, + ); +} + +fn few_immutable_replace_no_observer(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("few_immutable_replace_no_observer"); + group.warm_up_time(core::time::Duration::from_millis(500)); + group.measurement_time(core::time::Duration::from_secs(4)); + for &entity_count in ENTITIES_TO_BENCH_COUNT { + generic_bench( + &mut group, + vec![ + Box::new(few_immutable_replace_no_observer_generic::), + Box::new(few_immutable_replace_no_observer_generic::), + ], + entity_count, + ); + } +} + fn insert_if_bit_enabled(entity: &mut EntityWorldMut, i: u16) { if i & (1 << B) != 0 { entity.insert(Data::(1.0));