Skip to content

Benchmark replacement of immutable components with hooks #19616

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 286 additions & 1 deletion benches/benches/bevy_ecs/change_detection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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;
}
Expand All @@ -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>;
Expand All @@ -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>()
Expand Down Expand Up @@ -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));
Expand Down
Loading