Skip to content

Commit

Permalink
Update entity cloning benchmarks (bevyengine#17084)
Browse files Browse the repository at this point in the history
# 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


![image](https://github.com/user-attachments/assets/4e3d7d98-015a-4974-ae16-363cf1b9423c)

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:


![image](https://github.com/user-attachments/assets/bd8513de-3922-432f-b3dd-1b1b7750bdb5)


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
BD103 authored and mrchantey committed Feb 4, 2025
1 parent 1e5909a commit 483a71c
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 98 deletions.
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

0 comments on commit 483a71c

Please sign in to comment.