forked from bevyengine/bevy
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update entity cloning benchmarks (bevyengine#17084)
# Objective - `entity_cloning` was separated from the rest of the ECS benchmarks. - There was some room for improvement in the benchmarks themselves. - Part of bevyengine#16647. ## Solution - Merge `entity_cloning` into the rest of the ECS benchmarks. - Apply the `bench!` macro to all benchmark names.\ - Reorganize benchmarks and their helper functions, with more comments than before. - Remove all the extra component definitions (`C2`, `C3`, etc.), and just leave one. Now all entities have exactly one component. ## Testing ```sh # List all entity cloning benchmarks, to verify their names have updated. cargo bench -p benches --bench ecs entity_cloning -- --list # Test benchmarks by running them once. cargo test -p benches --bench ecs entity_cloning # Run all benchmarks (takes about a minute). cargo bench -p benches --bench ecs entity_cloning ``` --- ## Showcase  Interestingly, using `Clone` instead of `Reflect` appears to be 2-2.5 times faster. Furthermore, there were noticeable jumps in time when running the benchmarks:  I theorize this is because the `World` is allocating more space for all the entities, but I don't know for certain. Neat!
- Loading branch information
Showing
3 changed files
with
158 additions
and
98 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,173 +1,236 @@ | ||
use core::hint::black_box; | ||
|
||
use benches::bench; | ||
use bevy_ecs::bundle::Bundle; | ||
use bevy_ecs::component::ComponentCloneHandler; | ||
use bevy_ecs::reflect::AppTypeRegistry; | ||
use bevy_ecs::{component::Component, reflect::ReflectComponent, world::World}; | ||
use bevy_ecs::{component::Component, world::World}; | ||
use bevy_hierarchy::{BuildChildren, CloneEntityHierarchyExt}; | ||
use bevy_math::Mat4; | ||
use bevy_reflect::{GetTypeRegistration, Reflect}; | ||
use criterion::{criterion_group, criterion_main, Bencher, Criterion}; | ||
use criterion::{criterion_group, Bencher, Criterion, Throughput}; | ||
|
||
criterion_group!(benches, reflect_benches, clone_benches); | ||
criterion_main!(benches); | ||
criterion_group!( | ||
benches, | ||
single, | ||
hierarchy_tall, | ||
hierarchy_wide, | ||
hierarchy_many, | ||
); | ||
|
||
#[derive(Component, Reflect, Default, Clone)] | ||
#[reflect(Component)] | ||
struct C1(Mat4); | ||
|
||
#[derive(Component, Reflect, Default, Clone)] | ||
#[reflect(Component)] | ||
struct C2(Mat4); | ||
|
||
#[derive(Component, Reflect, Default, Clone)] | ||
#[reflect(Component)] | ||
struct C3(Mat4); | ||
|
||
#[derive(Component, Reflect, Default, Clone)] | ||
#[reflect(Component)] | ||
struct C4(Mat4); | ||
|
||
#[derive(Component, Reflect, Default, Clone)] | ||
#[reflect(Component)] | ||
struct C5(Mat4); | ||
|
||
#[derive(Component, Reflect, Default, Clone)] | ||
#[reflect(Component)] | ||
struct C6(Mat4); | ||
|
||
#[derive(Component, Reflect, Default, Clone)] | ||
#[reflect(Component)] | ||
struct C7(Mat4); | ||
|
||
#[derive(Component, Reflect, Default, Clone)] | ||
#[reflect(Component)] | ||
struct C8(Mat4); | ||
|
||
#[derive(Component, Reflect, Default, Clone)] | ||
#[reflect(Component)] | ||
struct C9(Mat4); | ||
|
||
#[derive(Component, Reflect, Default, Clone)] | ||
#[reflect(Component)] | ||
struct C10(Mat4); | ||
|
||
type ComplexBundle = (C1, C2, C3, C4, C5, C6, C7, C8, C9, C10); | ||
|
||
fn hierarchy<C: Bundle + Default + GetTypeRegistration>( | ||
/// Sets the [`ComponentCloneHandler`] for all explicit and required components in a bundle `B` to | ||
/// use the [`Reflect`] trait instead of [`Clone`]. | ||
fn set_reflect_clone_handler<B: Bundle + GetTypeRegistration>(world: &mut World) { | ||
// Get mutable access to the type registry, creating it if it does not exist yet. | ||
let registry = world.get_resource_or_init::<AppTypeRegistry>(); | ||
|
||
// Recursively register all components in the bundle to the reflection type registry. | ||
{ | ||
let mut r = registry.write(); | ||
r.register::<B>(); | ||
} | ||
|
||
// Recursively register all components in the bundle, then save the component IDs to a list. | ||
// This uses `contributed_components()`, meaning both explicit and required component IDs in | ||
// this bundle are saved. | ||
let component_ids: Vec<_> = world.register_bundle::<B>().contributed_components().into(); | ||
|
||
let clone_handlers = world.get_component_clone_handlers_mut(); | ||
|
||
// Overwrite the clone handler for all components in the bundle to use `Reflect`, not `Clone`. | ||
for component in component_ids { | ||
clone_handlers.set_component_handler(component, ComponentCloneHandler::reflect_handler()); | ||
} | ||
} | ||
|
||
/// A helper function that benchmarks running the [`EntityCommands::clone_and_spawn()`] command on a | ||
/// bundle `B`. | ||
/// | ||
/// The bundle must implement [`Default`], which is used to create the first entity that gets cloned | ||
/// in the benchmark. | ||
/// | ||
/// If `clone_via_reflect` is false, this will use the default [`ComponentCloneHandler`] for all | ||
/// components (which is usually [`ComponentCloneHandler::clone_handler()`]). If `clone_via_reflect` | ||
/// is true, it will overwrite the handler for all components in the bundle to be | ||
/// [`ComponentCloneHandler::reflect_handler()`]. | ||
fn bench_clone<B: Bundle + Default + GetTypeRegistration>( | ||
b: &mut Bencher, | ||
width: usize, | ||
height: usize, | ||
clone_via_reflect: bool, | ||
) { | ||
let mut world = World::default(); | ||
let registry = AppTypeRegistry::default(); | ||
{ | ||
let mut r = registry.write(); | ||
r.register::<C>(); | ||
|
||
if clone_via_reflect { | ||
set_reflect_clone_handler::<B>(&mut world); | ||
} | ||
world.insert_resource(registry); | ||
world.register_bundle::<C>(); | ||
|
||
// Spawn the first entity, which will be cloned in the benchmark routine. | ||
let id = world.spawn(B::default()).id(); | ||
|
||
b.iter(|| { | ||
// Queue the command to clone the entity. | ||
world.commands().entity(black_box(id)).clone_and_spawn(); | ||
|
||
// Run the command. | ||
world.flush(); | ||
}); | ||
} | ||
|
||
/// A helper function that benchmarks running the [`EntityCommands::clone_and_spawn()`] command on a | ||
/// bundle `B`. | ||
/// | ||
/// As compared to [`bench_clone()`], this benchmarks recursively cloning an entity with several | ||
/// children. It does so by setting up an entity tree with a given `height` where each entity has a | ||
/// specified number of `children`. | ||
/// | ||
/// For example, setting `height` to 5 and `children` to 1 creates a single chain of entities with | ||
/// no siblings. Alternatively, setting `height` to 1 and `children` to 5 will spawn 5 direct | ||
/// children of the root entity. | ||
fn bench_clone_hierarchy<B: Bundle + Default + GetTypeRegistration>( | ||
b: &mut Bencher, | ||
height: usize, | ||
children: usize, | ||
clone_via_reflect: bool, | ||
) { | ||
let mut world = World::default(); | ||
|
||
if clone_via_reflect { | ||
let mut components = Vec::new(); | ||
C::get_component_ids(world.components(), &mut |id| components.push(id.unwrap())); | ||
for component in components { | ||
world | ||
.get_component_clone_handlers_mut() | ||
.set_component_handler( | ||
component, | ||
bevy_ecs::component::ComponentCloneHandler::reflect_handler(), | ||
); | ||
} | ||
set_reflect_clone_handler::<B>(&mut world); | ||
} | ||
|
||
let id = world.spawn(black_box(C::default())).id(); | ||
// Spawn the first entity, which will be cloned in the benchmark routine. | ||
let id = world.spawn(B::default()).id(); | ||
|
||
let mut hierarchy_level = vec![id]; | ||
|
||
// Set up the hierarchy tree by spawning all children. | ||
for _ in 0..height { | ||
let current_hierarchy_level = hierarchy_level.clone(); | ||
|
||
hierarchy_level.clear(); | ||
|
||
for parent_id in current_hierarchy_level { | ||
for _ in 0..width { | ||
let child_id = world | ||
.spawn(black_box(C::default())) | ||
.set_parent(parent_id) | ||
.id(); | ||
for _ in 0..children { | ||
let child_id = world.spawn(B::default()).set_parent(parent_id).id(); | ||
|
||
hierarchy_level.push(child_id); | ||
} | ||
} | ||
} | ||
|
||
// Flush all `set_parent()` commands. | ||
world.flush(); | ||
|
||
b.iter(move || { | ||
world.commands().entity(id).clone_and_spawn_with(|builder| { | ||
builder.recursive(true); | ||
}); | ||
b.iter(|| { | ||
world | ||
.commands() | ||
.entity(black_box(id)) | ||
.clone_and_spawn_with(|builder| { | ||
// Make the clone command recursive, so children are cloned as well. | ||
builder.recursive(true); | ||
}); | ||
|
||
world.flush(); | ||
}); | ||
} | ||
|
||
fn simple<C: Bundle + Default + GetTypeRegistration>(b: &mut Bencher, clone_via_reflect: bool) { | ||
let mut world = World::default(); | ||
let registry = AppTypeRegistry::default(); | ||
{ | ||
let mut r = registry.write(); | ||
r.register::<C>(); | ||
// Each benchmark runs twice: using either the `Clone` or `Reflect` traits to clone entities. This | ||
// constant represents this as an easy array that can be used in a `for` loop. | ||
const SCENARIOS: [(&str, bool); 2] = [("clone", false), ("reflect", true)]; | ||
|
||
/// Benchmarks cloning a single entity with 10 components and no children. | ||
fn single(c: &mut Criterion) { | ||
let mut group = c.benchmark_group(bench!("single")); | ||
|
||
// We're cloning 1 entity. | ||
group.throughput(Throughput::Elements(1)); | ||
|
||
for (id, clone_via_reflect) in SCENARIOS { | ||
group.bench_function(id, |b| { | ||
bench_clone::<ComplexBundle>(b, clone_via_reflect); | ||
}); | ||
} | ||
world.insert_resource(registry); | ||
world.register_bundle::<C>(); | ||
if clone_via_reflect { | ||
let mut components = Vec::new(); | ||
C::get_component_ids(world.components(), &mut |id| components.push(id.unwrap())); | ||
for component in components { | ||
world | ||
.get_component_clone_handlers_mut() | ||
.set_component_handler( | ||
component, | ||
bevy_ecs::component::ComponentCloneHandler::reflect_handler(), | ||
); | ||
} | ||
|
||
group.finish(); | ||
} | ||
|
||
/// Benchmarks cloning an an entity and its 50 descendents, each with only 1 component. | ||
fn hierarchy_tall(c: &mut Criterion) { | ||
let mut group = c.benchmark_group(bench!("hierarchy_tall")); | ||
|
||
// We're cloning both the root entity and its 50 descendents. | ||
group.throughput(Throughput::Elements(51)); | ||
|
||
for (id, clone_via_reflect) in SCENARIOS { | ||
group.bench_function(id, |b| { | ||
bench_clone_hierarchy::<C1>(b, 50, 1, clone_via_reflect); | ||
}); | ||
} | ||
let id = world.spawn(black_box(C::default())).id(); | ||
|
||
b.iter(move || { | ||
world.commands().entity(id).clone_and_spawn(); | ||
world.flush(); | ||
}); | ||
group.finish(); | ||
} | ||
|
||
fn reflect_benches(c: &mut Criterion) { | ||
c.bench_function("many components reflect", |b| { | ||
simple::<ComplexBundle>(b, true); | ||
}); | ||
/// Benchmarks cloning an an entity and its 50 direct children, each with only 1 component. | ||
fn hierarchy_wide(c: &mut Criterion) { | ||
let mut group = c.benchmark_group(bench!("hierarchy_wide")); | ||
|
||
c.bench_function("hierarchy wide reflect", |b| { | ||
hierarchy::<C1>(b, 10, 4, true); | ||
}); | ||
// We're cloning both the root entity and its 50 direct children. | ||
group.throughput(Throughput::Elements(51)); | ||
|
||
c.bench_function("hierarchy tall reflect", |b| { | ||
hierarchy::<C1>(b, 1, 50, true); | ||
}); | ||
for (id, clone_via_reflect) in SCENARIOS { | ||
group.bench_function(id, |b| { | ||
bench_clone_hierarchy::<C1>(b, 1, 50, clone_via_reflect); | ||
}); | ||
} | ||
|
||
c.bench_function("hierarchy many reflect", |b| { | ||
hierarchy::<ComplexBundle>(b, 5, 5, true); | ||
}); | ||
group.finish(); | ||
} | ||
|
||
fn clone_benches(c: &mut Criterion) { | ||
c.bench_function("many components clone", |b| { | ||
simple::<ComplexBundle>(b, false); | ||
}); | ||
/// Benchmarks cloning a large hierarchy of entities with several children each. Each entity has 10 | ||
/// components. | ||
fn hierarchy_many(c: &mut Criterion) { | ||
let mut group = c.benchmark_group(bench!("hierarchy_many")); | ||
|
||
c.bench_function("hierarchy wide clone", |b| { | ||
hierarchy::<C1>(b, 10, 4, false); | ||
}); | ||
// We're cloning 364 entities total. This number was calculated by manually counting the number | ||
// of entities spawned in `bench_clone_hierarchy()` with a `println!()` statement. :) | ||
group.throughput(Throughput::Elements(364)); | ||
|
||
c.bench_function("hierarchy tall clone", |b| { | ||
hierarchy::<C1>(b, 1, 50, false); | ||
}); | ||
for (id, clone_via_reflect) in SCENARIOS { | ||
group.bench_function(id, |b| { | ||
bench_clone_hierarchy::<ComplexBundle>(b, 5, 3, clone_via_reflect); | ||
}); | ||
} | ||
|
||
c.bench_function("hierarchy many clone", |b| { | ||
hierarchy::<ComplexBundle>(b, 5, 5, false); | ||
}); | ||
group.finish(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters