Skip to content

Commit 20049d4

Browse files
eugineerdalice-i-cecilechescock
authored
Faster entity cloning (#16717)
# Objective #16132 introduced entity cloning functionality, and while it works and is useful, it can be made faster. This is the promised follow-up to improve performance. ## Solution **PREFACE**: This is my first time writing `unsafe` in rust and I have only vague idea about what I'm doing. I would encourage reviewers to scrutinize `unsafe` parts in particular. The solution is to clone component data to an intermediate buffer and use `EntityWorldMut::insert_by_ids` to insert components without additional archetype moves. To facilitate this, `EntityCloner::clone_entity` now reads all components of the source entity and provides clone handlers with the ability to read component data straight from component storage using `read_source_component` and write to an intermediate buffer using `write_target_component`. `ComponentId` is used to check that requested type corresponds to the type available on source entity. Reflect-based handler is a little trickier to pull of: we only have `&dyn Reflect` and no direct access to the underlying data. `ReflectFromPtr` can be used to get `&dyn Reflect` from concrete component data, but to write it we need to create a clone of the underlying data using `Reflect`. For this reason only components that have `ReflectDefault` or `ReflectFromReflect` or `ReflectFromWorld` can be cloned, all other components will be skipped. The good news is that this is actually only a temporary limitation: once #13432 lands we will be able to clone component without requiring one of these `type data`s. This PR also introduces `entity_cloning` benchmark to better compare changes between the PR and main, you can see the results in the **showcase** section. ## Testing - All previous tests passing - Added test for fast reflect clone path (temporary, will be removed after reflection-based cloning lands) - Ran miri ## Showcase Here's a table demonstrating the improvement: | **benchmark** | **main, avg** | **PR, avg** | **change, avg** | | ----------------------- | ------------- | ----------- | --------------- | | many components reflect | 18.505 µs | 2.1351 µs | -89.095% | | hierarchy wide reflect* | 22.778 ms | 4.1875 ms | -81.616% | | hierarchy tall reflect* | 107.24 µs | 26.322 µs | -77.141% | | hierarchy many reflect | 78.533 ms | 9.7415 ms | -87.596% | | many components clone | 1.3633 µs | 758.17 ns | -45.937% | | hierarchy wide clone* | 2.7716 ms | 3.3411 ms | +20.546% | | hierarchy tall clone* | 17.646 µs | 20.190 µs | +17.379% | | hierarchy many clone | 5.8779 ms | 4.2650 ms | -27.439% | *: these benchmarks have entities with only 1 component ## Considerations Once #10154 is resolved a large part of the functionality in this PR will probably become obsolete. It might still be a little bit faster than using command batching, but the complexity might not be worth it. ## Migration Guide - `&EntityCloner` in component clone handlers is changed to `&mut ComponentCloneCtx` to better separate data. - Changed `EntityCloneHandler` from enum to struct and added convenience functions to add default clone and reflect handler more easily. --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com>
1 parent 3ef99cf commit 20049d4

File tree

10 files changed

+932
-188
lines changed

10 files changed

+932
-188
lines changed

benches/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] }
6060
unsafe_op_in_unsafe_fn = "warn"
6161
unused_qualifications = "warn"
6262

63+
[[bench]]
64+
name = "entity_cloning"
65+
path = "benches/bevy_ecs/entity_cloning.rs"
66+
harness = false
67+
6368
[[bench]]
6469
name = "ecs"
6570
path = "benches/bevy_ecs/main.rs"
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
use bevy_ecs::bundle::Bundle;
2+
use bevy_ecs::reflect::AppTypeRegistry;
3+
use bevy_ecs::{component::Component, reflect::ReflectComponent, world::World};
4+
use bevy_hierarchy::{BuildChildren, CloneEntityHierarchyExt};
5+
use bevy_math::Mat4;
6+
use bevy_reflect::{GetTypeRegistration, Reflect};
7+
use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion};
8+
9+
criterion_group!(benches, reflect_benches, clone_benches);
10+
criterion_main!(benches);
11+
12+
#[derive(Component, Reflect, Default, Clone)]
13+
#[reflect(Component)]
14+
struct C1(Mat4);
15+
16+
#[derive(Component, Reflect, Default, Clone)]
17+
#[reflect(Component)]
18+
struct C2(Mat4);
19+
20+
#[derive(Component, Reflect, Default, Clone)]
21+
#[reflect(Component)]
22+
struct C3(Mat4);
23+
24+
#[derive(Component, Reflect, Default, Clone)]
25+
#[reflect(Component)]
26+
struct C4(Mat4);
27+
28+
#[derive(Component, Reflect, Default, Clone)]
29+
#[reflect(Component)]
30+
struct C5(Mat4);
31+
32+
#[derive(Component, Reflect, Default, Clone)]
33+
#[reflect(Component)]
34+
struct C6(Mat4);
35+
36+
#[derive(Component, Reflect, Default, Clone)]
37+
#[reflect(Component)]
38+
struct C7(Mat4);
39+
40+
#[derive(Component, Reflect, Default, Clone)]
41+
#[reflect(Component)]
42+
struct C8(Mat4);
43+
44+
#[derive(Component, Reflect, Default, Clone)]
45+
#[reflect(Component)]
46+
struct C9(Mat4);
47+
48+
#[derive(Component, Reflect, Default, Clone)]
49+
#[reflect(Component)]
50+
struct C10(Mat4);
51+
52+
type ComplexBundle = (C1, C2, C3, C4, C5, C6, C7, C8, C9, C10);
53+
54+
fn hierarchy<C: Bundle + Default + GetTypeRegistration>(
55+
b: &mut Bencher,
56+
width: usize,
57+
height: usize,
58+
clone_via_reflect: bool,
59+
) {
60+
let mut world = World::default();
61+
let registry = AppTypeRegistry::default();
62+
{
63+
let mut r = registry.write();
64+
r.register::<C>();
65+
}
66+
world.insert_resource(registry);
67+
world.register_bundle::<C>();
68+
if clone_via_reflect {
69+
let mut components = Vec::new();
70+
C::get_component_ids(world.components(), &mut |id| components.push(id.unwrap()));
71+
for component in components {
72+
world
73+
.get_component_clone_handlers_mut()
74+
.set_component_handler(
75+
component,
76+
bevy_ecs::component::ComponentCloneHandler::reflect_handler(),
77+
);
78+
}
79+
}
80+
81+
let id = world.spawn(black_box(C::default())).id();
82+
83+
let mut hierarchy_level = vec![id];
84+
85+
for _ in 0..height {
86+
let current_hierarchy_level = hierarchy_level.clone();
87+
hierarchy_level.clear();
88+
for parent_id in current_hierarchy_level {
89+
for _ in 0..width {
90+
let child_id = world
91+
.spawn(black_box(C::default()))
92+
.set_parent(parent_id)
93+
.id();
94+
hierarchy_level.push(child_id)
95+
}
96+
}
97+
}
98+
world.flush();
99+
100+
b.iter(move || {
101+
world.commands().entity(id).clone_and_spawn_with(|builder| {
102+
builder.recursive(true);
103+
});
104+
world.flush();
105+
});
106+
}
107+
108+
fn simple<C: Bundle + Default + GetTypeRegistration>(b: &mut Bencher, clone_via_reflect: bool) {
109+
let mut world = World::default();
110+
let registry = AppTypeRegistry::default();
111+
{
112+
let mut r = registry.write();
113+
r.register::<C>();
114+
}
115+
world.insert_resource(registry);
116+
world.register_bundle::<C>();
117+
if clone_via_reflect {
118+
let mut components = Vec::new();
119+
C::get_component_ids(world.components(), &mut |id| components.push(id.unwrap()));
120+
for component in components {
121+
world
122+
.get_component_clone_handlers_mut()
123+
.set_component_handler(
124+
component,
125+
bevy_ecs::component::ComponentCloneHandler::reflect_handler(),
126+
);
127+
}
128+
}
129+
let id = world.spawn(black_box(C::default())).id();
130+
131+
b.iter(move || {
132+
world.commands().entity(id).clone_and_spawn();
133+
world.flush();
134+
});
135+
}
136+
137+
fn reflect_benches(c: &mut Criterion) {
138+
c.bench_function("many components reflect", |b| {
139+
simple::<ComplexBundle>(b, true);
140+
});
141+
142+
c.bench_function("hierarchy wide reflect", |b| {
143+
hierarchy::<C1>(b, 10, 4, true);
144+
});
145+
146+
c.bench_function("hierarchy tall reflect", |b| {
147+
hierarchy::<C1>(b, 1, 50, true);
148+
});
149+
150+
c.bench_function("hierarchy many reflect", |b| {
151+
hierarchy::<ComplexBundle>(b, 5, 5, true);
152+
});
153+
}
154+
155+
fn clone_benches(c: &mut Criterion) {
156+
c.bench_function("many components clone", |b| {
157+
simple::<ComplexBundle>(b, false);
158+
});
159+
160+
c.bench_function("hierarchy wide clone", |b| {
161+
hierarchy::<C1>(b, 10, 4, false);
162+
});
163+
164+
c.bench_function("hierarchy tall clone", |b| {
165+
hierarchy::<C1>(b, 1, 50, false);
166+
});
167+
168+
c.bench_function("hierarchy many clone", |b| {
169+
hierarchy::<ComplexBundle>(b, 5, 5, false);
170+
});
171+
}

crates/bevy_ecs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ spin = { version = "0.9.8", default-features = false, features = [
133133
] }
134134
tracing = { version = "0.1", default-features = false, optional = true }
135135
log = { version = "0.4", default-features = false }
136+
bumpalo = "3"
136137

137138
[dev-dependencies]
138139
rand = "0.8"

0 commit comments

Comments
 (0)