Bench replacement of immutable components with hooks

This commit is contained in:
Marius Metzger 2025-06-13 12:11:24 +02:00
parent 58c276ab44
commit e39fa12ee5

View File

@ -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<const X: u16>(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<T: Component + Default>(entity_count: u32) -> World {
black_box(world)
}
fn setup_with_observer<T: Component + Default + Clone + 'static>(entity_count: u32) -> World {
let mut world = World::default();
world.spawn(Observer::new(|trigger: On<Insert, T>| {
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<F: QueryFilter>(world: &mut World) -> QueryState<Entity, F> {
world.query_filtered::<Entity, F>()
@ -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::<T>()),
|bencher| {
bencher.iter_batched_ref(
|| {
let mut world = setup_with_observer::<T>(entity_count);
let mut query = world.query::<Entity>();
let entities: Vec<_> = query.iter(&world).collect();
(world, entities)
},
|(world, entities)| {
for entity in entities {
let component = world
.entity(*entity)
.get::<T>()
.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::<ImmutableTable>),
//Box::new(all_immutable_replace_detection_generic::<ImmutableSparse>),
],
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::<T>()),
|bencher| {
bencher.iter_batched_ref(
|| {
let mut world = setup_with_observer::<T>(entity_count);
let mut query = world.query::<Entity>();
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::<T>()
.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::<ImmutableTable>),
Box::new(few_immutable_replace_detection_generic::<ImmutableSparse>),
],
entity_count,
);
}
}
fn none_immutable_replace_detection_generic<T: Component + Default + Clone + 'static>(
group: &mut BenchGroup,
entity_count: u32,
) {
group.bench_function(
format!("{}_entities_{}", entity_count, core::any::type_name::<T>()),
|bencher| {
bencher.iter_batched_ref(
|| {
let world = setup_with_observer::<T>(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::<ImmutableTable>),
Box::new(none_immutable_replace_detection_generic::<ImmutableSparse>),
],
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::<T>()),
|bencher| {
bencher.iter_batched_ref(
|| {
let mut world = setup::<T>(entity_count); // No observer
let mut query = world.query::<Entity>();
let entities: Vec<_> = query.iter(&world).collect();
(world, entities)
},
|(world, entities)| {
for entity in entities {
let component = world
.entity(*entity)
.get::<T>()
.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::<ImmutableTable>),
Box::new(all_immutable_replace_no_observer_generic::<ImmutableSparse>),
],
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::<T>()),
|bencher| {
bencher.iter_batched_ref(
|| {
let mut world = setup::<T>(entity_count); // No observer
let mut query = world.query::<Entity>();
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::<T>()
.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::<ImmutableTable>),
Box::new(few_immutable_replace_no_observer_generic::<ImmutableSparse>),
],
entity_count,
);
}
}
fn insert_if_bit_enabled<const B: u16>(entity: &mut EntityWorldMut, i: u16) {
if i & (1 << B) != 0 {
entity.insert(Data::<B>(1.0));