Skip to content
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

Update entity cloning benchmarks #17084

Merged
merged 9 commits into from
Jan 2, 2025
Merged
5 changes: 0 additions & 5 deletions benches/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,6 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] }
unsafe_op_in_unsafe_fn = "warn"
unused_qualifications = "warn"

[[bench]]
name = "entity_cloning"
path = "benches/bevy_ecs/entity_cloning.rs"
harness = false

[[bench]]
name = "ecs"
path = "benches/bevy_ecs/main.rs"
Expand Down
249 changes: 156 additions & 93 deletions benches/benches/bevy_ecs/entity_cloning.rs
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();
}
2 changes: 2 additions & 0 deletions benches/benches/bevy_ecs/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use criterion::criterion_main;
mod change_detection;
mod components;
mod empty_archetypes;
mod entity_cloning;
mod events;
mod fragmentation;
mod iteration;
Expand All @@ -21,6 +22,7 @@ criterion_main!(
change_detection::benches,
components::benches,
empty_archetypes::benches,
entity_cloning::benches,
events::benches,
iteration::benches,
fragmentation::benches,
Expand Down
Loading