Skip empty archetype/table (#14749)

# Objective

- As sander commneted on discord
[link](https://discord.com/channels/691052431525675048/749335865876021248/1273414144091230228),

![image](https://github.com/user-attachments/assets/62f2b6f3-1aaf-49d9-bafa-bf62b83b10be)





## Performance

![image](https://github.com/user-attachments/assets/11122940-1547-42ae-9576-0e1a93fd9f5f)

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Giacomo Stevanato <giaco.stevanato@gmail.com>
This commit is contained in:
re0312 2024-08-15 22:07:20 +08:00 committed by GitHub
parent d3ffb2a5c0
commit 3bd039e821
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 116 additions and 2 deletions

View file

@ -2,6 +2,7 @@ use criterion::criterion_main;
mod components; mod components;
mod events; mod events;
mod fragmentation;
mod iteration; mod iteration;
mod observers; mod observers;
mod scheduling; mod scheduling;
@ -11,6 +12,7 @@ criterion_main!(
components::components_benches, components::components_benches,
events::event_benches, events::event_benches,
iteration::iterations_benches, iteration::iterations_benches,
fragmentation::fragmentation_benches,
observers::observer_benches, observers::observer_benches,
scheduling::scheduling_benches, scheduling::scheduling_benches,
world::world_benches, world::world_benches,

View file

@ -0,0 +1,99 @@
use bevy_ecs::prelude::*;
use bevy_ecs::system::SystemState;
use criterion::*;
use glam::*;
use std::hint::black_box;
criterion_group!(fragmentation_benches, iter_frag_empty);
#[derive(Component, Default)]
struct Table<const X: usize = 0>(usize);
#[derive(Component, Default)]
#[component(storage = "SparseSet")]
struct Sparse<const X: usize = 0>(usize);
fn flip_coin() -> bool {
rand::random::<bool>()
}
fn iter_frag_empty(c: &mut Criterion) {
let mut group = c.benchmark_group("iter_fragmented(4096)_empty");
group.warm_up_time(std::time::Duration::from_millis(500));
group.measurement_time(std::time::Duration::from_secs(4));
group.bench_function("foreach_table", |b| {
let mut world = World::new();
spawn_empty_frag_archetype::<Table>(&mut world);
let mut q: SystemState<Query<(Entity, &Table)>> =
SystemState::<Query<(Entity, &Table<0>)>>::new(&mut world);
let query = q.get(&world);
b.iter(move || {
let mut res = 0;
query.iter().for_each(|(e, t)| {
res += e.to_bits();
black_box(t);
});
});
});
group.bench_function("foreach_sparse", |b| {
let mut world = World::new();
spawn_empty_frag_archetype::<Sparse>(&mut world);
let mut q: SystemState<Query<(Entity, &Sparse)>> =
SystemState::<Query<(Entity, &Sparse<0>)>>::new(&mut world);
let query = q.get(&world);
b.iter(move || {
let mut res = 0;
query.iter().for_each(|(e, t)| {
res += e.to_bits();
black_box(t);
});
});
});
group.finish();
fn spawn_empty_frag_archetype<T: Component + Default>(world: &mut World) {
for i in 0..65536 {
let mut e = world.spawn_empty();
if flip_coin() {
e.insert(Table::<1>(0));
}
if flip_coin() {
e.insert(Table::<2>(0));
}
if flip_coin() {
e.insert(Table::<3>(0));
}
if flip_coin() {
e.insert(Table::<4>(0));
}
if flip_coin() {
e.insert(Table::<5>(0));
}
if flip_coin() {
e.insert(Table::<6>(0));
}
if flip_coin() {
e.insert(Table::<7>(0));
}
if flip_coin() {
e.insert(Table::<8>(0));
}
if flip_coin() {
e.insert(Table::<9>(0));
}
if flip_coin() {
e.insert(Table::<10>(0));
}
if flip_coin() {
e.insert(Table::<11>(0));
}
if flip_coin() {
e.insert(Table::<12>(0));
}
e.insert(T::default());
if i != 0 {
e.despawn();
}
}
}
}

View file

@ -68,6 +68,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> {
where where
Func: FnMut(B, D::Item<'w>) -> B, Func: FnMut(B, D::Item<'w>) -> B,
{ {
if table.is_empty() {
return accum;
}
assert!( assert!(
rows.end <= u32::MAX as usize, rows.end <= u32::MAX as usize,
"TableRow is only valid up to u32::MAX" "TableRow is only valid up to u32::MAX"
@ -120,6 +123,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> {
where where
Func: FnMut(B, D::Item<'w>) -> B, Func: FnMut(B, D::Item<'w>) -> B,
{ {
if archetype.is_empty() {
return accum;
}
let table = self.tables.get(archetype.table_id()).debug_checked_unwrap(); let table = self.tables.get(archetype.table_id()).debug_checked_unwrap();
D::set_archetype( D::set_archetype(
&mut self.cursor.fetch, &mut self.cursor.fetch,
@ -186,6 +192,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> {
where where
Func: FnMut(B, D::Item<'w>) -> B, Func: FnMut(B, D::Item<'w>) -> B,
{ {
if archetype.is_empty() {
return accum;
}
assert!( assert!(
rows.end <= u32::MAX as usize, rows.end <= u32::MAX as usize,
"TableRow is only valid up to u32::MAX" "TableRow is only valid up to u32::MAX"
@ -1716,6 +1725,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> {
if self.current_row == self.current_len { if self.current_row == self.current_len {
let table_id = self.storage_id_iter.next()?.table_id; let table_id = self.storage_id_iter.next()?.table_id;
let table = tables.get(table_id).debug_checked_unwrap(); let table = tables.get(table_id).debug_checked_unwrap();
if table.is_empty() {
continue;
}
// SAFETY: `table` is from the world that `fetch/filter` were created for, // SAFETY: `table` is from the world that `fetch/filter` were created for,
// `fetch_state`/`filter_state` are the states that `fetch/filter` were initialized with // `fetch_state`/`filter_state` are the states that `fetch/filter` were initialized with
unsafe { unsafe {
@ -1725,7 +1737,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> {
self.table_entities = table.entities(); self.table_entities = table.entities();
self.current_len = table.entity_count(); self.current_len = table.entity_count();
self.current_row = 0; self.current_row = 0;
continue;
} }
// SAFETY: set_table was called prior. // SAFETY: set_table was called prior.
@ -1752,6 +1763,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> {
if self.current_row == self.current_len { if self.current_row == self.current_len {
let archetype_id = self.storage_id_iter.next()?.archetype_id; let archetype_id = self.storage_id_iter.next()?.archetype_id;
let archetype = archetypes.get(archetype_id).debug_checked_unwrap(); let archetype = archetypes.get(archetype_id).debug_checked_unwrap();
if archetype.is_empty() {
continue;
}
let table = tables.get(archetype.table_id()).debug_checked_unwrap(); let table = tables.get(archetype.table_id()).debug_checked_unwrap();
// SAFETY: `archetype` and `tables` are from the world that `fetch/filter` were created for, // SAFETY: `archetype` and `tables` are from the world that `fetch/filter` were created for,
// `fetch_state`/`filter_state` are the states that `fetch/filter` were initialized with // `fetch_state`/`filter_state` are the states that `fetch/filter` were initialized with
@ -1772,7 +1786,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> {
self.archetype_entities = archetype.entities(); self.archetype_entities = archetype.entities();
self.current_len = archetype.len(); self.current_len = archetype.len();
self.current_row = 0; self.current_row = 0;
continue;
} }
// SAFETY: set_archetype was called prior. // SAFETY: set_archetype was called prior.