Skip to content

Commit cf8f9be

Browse files
Improve realism of entity benches by warming up the entity allocator (#22639)
# Objective As per [this](#18670 (comment)) comment on #18670, this PR attempts to make entity related benchmarks more realistic by warming up the entity allocator. This helps test the freelist in the entity allocator. ## Solution This PR introduces a new `WorldBuilder` type that starts with `World::new`, allows configuration options for warming up the world via the builder pattern, and then builds the warmed up, realistic world. For now, the only available "warm up" is for entities, but we could also add functionality in the future to cache bundle info, pre-create tables, etc to make our benchmarks more realistic. That is, more closely match the performance of a running app, rather than an app at startup. The current implementation for entity warmups allocates some entities and frees them in a random order. It also spawns the highest allocated entity index to prepare `Entities`'s location storage, etc. This involves using `rng` (deterministically), but without this, the entities are allocated in a linear index order (0, 1, 2, ...), which is unrealistic and extremely cache friendly (so it probably makes an impact in performance not desirable for a benchmark). The major downsides here are that the benches take a little longer to run now and that startup/caching time is no longer benchmarked. That is for example, that benchmarking despawning only one entity used to tell us some information about performance of allocating the free list (amongst other one time actions). Now, that information is lost since the world is already warmed up. In practice, for N values of entities, it used to be the case that a higher N showed the performance of the operation, and a lower N showed the performance of the operation + any registration/caching costs. Now, the different N values only tell us more about how well the CPU accommodates a batch of the operation. Currently in Bevy, making a change might make the `...1_entity` benches much worse but the `...1000_entities` much much better because the change added some new caching. The inverse is also common. With this PR, that will no longer be the case, at least for entities and whatever else we add to the `WorldBuilder` in the future. And that change may or may not be desirable. ## Testing Ran a sampling of the benchmarks.
1 parent 0dab4fb commit cf8f9be

File tree

5 files changed

+119
-10
lines changed

5 files changed

+119
-10
lines changed

benches/benches/bevy_ecs/main.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,80 @@ criterion_main!(
3232
world::benches,
3333
param::benches,
3434
);
35+
36+
mod world_builder {
37+
use bevy_ecs::world::World;
38+
use rand::{rngs::SmallRng, seq::SliceRandom, SeedableRng};
39+
40+
/// This builder generates a "hot"/realistic [`World`].
41+
///
42+
/// Using [`World::new`] creates a "cold" world.
43+
/// That is, the world has a fresh entity allocator, no registered components, and generally no accumulated entropy.
44+
/// When a cold world is used in a benchmark, much of what is benched is registration and caching costs,
45+
/// and what is not benched is the cost of the accumulated entropy in world storage, entity allocators, etc.
46+
///
47+
/// Use this in benches that are meant to reflect realistic, common, non-startup scenarios (Ex: spawn scenes, query entities, etc).
48+
/// Prefer [`World::new`] when creating benches for start-up costs (Ex: component registration, table creation time, etc).
49+
///
50+
/// Note that this does have a performance cost over [`World::new`], so this should not be used in a benchmark's routine, only in its setup.
51+
///
52+
/// Which parts of the world are sped up is highly configurable in the interest of doing the minimal work to warm up a world for a particular benchmark.
53+
/// (For example, despawn benches wouldn't benefit from warming up world storage.)
54+
pub struct WorldBuilder {
55+
world: World,
56+
rng: SmallRng,
57+
max_expected_entities: u32,
58+
}
59+
60+
impl WorldBuilder {
61+
/// Starts the builder.
62+
pub fn new() -> Self {
63+
Self {
64+
world: World::new(),
65+
rng: SmallRng::seed_from_u64(2039482342342),
66+
max_expected_entities: 10_000,
67+
}
68+
}
69+
70+
/// Sets the maximum expected entities that will interact with the world.
71+
/// By default this is `10_000`.
72+
pub fn with_max_expected_entities(mut self, max_expected_entities: u32) -> Self {
73+
self.max_expected_entities = max_expected_entities;
74+
self
75+
}
76+
77+
/// Warms up the entity allocator to give out arbitrary entity ids instead of sequential ones.
78+
/// This also pre-allocates room in `Entities`.
79+
pub fn warm_up_entity_allocator(mut self) -> Self {
80+
// allocate
81+
let mut entities = Vec::new();
82+
entities.reserve_exact(self.max_expected_entities as usize);
83+
entities.extend(
84+
self.world
85+
.entity_allocator()
86+
.alloc_many(self.max_expected_entities),
87+
);
88+
89+
// Spawn the high index to warm up `Entities`.
90+
let Some(high_index) = entities.last_mut() else {
91+
// There were no expected entities.
92+
return self;
93+
};
94+
self.world.spawn_empty_at(*high_index).unwrap();
95+
*high_index = self.world.try_despawn_no_free(*high_index).unwrap();
96+
97+
// free
98+
entities.shuffle(&mut self.rng);
99+
entities
100+
.drain(..)
101+
.for_each(|e| self.world.entity_allocator_mut().free(e));
102+
103+
self
104+
}
105+
106+
/// Finishes the builder to get the warmed up world.
107+
pub fn build(self) -> World {
108+
self.world
109+
}
110+
}
111+
}

benches/benches/bevy_ecs/world/commands.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use bevy_ecs::{
77
};
88
use criterion::Criterion;
99

10+
use crate::world_builder::WorldBuilder;
11+
1012
#[derive(Component)]
1113
struct A;
1214
#[derive(Component)]
@@ -38,7 +40,10 @@ pub fn spawn_commands(criterion: &mut Criterion) {
3840

3941
for entity_count in [100, 1_000, 10_000] {
4042
group.bench_function(format!("{entity_count}_entities"), |bencher| {
41-
let mut world = World::default();
43+
let mut world = WorldBuilder::new()
44+
.with_max_expected_entities(entity_count)
45+
.warm_up_entity_allocator()
46+
.build();
4247
let mut command_queue = CommandQueue::default();
4348

4449
bencher.iter(|| {
@@ -69,7 +74,10 @@ pub fn nonempty_spawn_commands(criterion: &mut Criterion) {
6974

7075
for entity_count in [100, 1_000, 10_000] {
7176
group.bench_function(format!("{entity_count}_entities"), |bencher| {
72-
let mut world = World::default();
77+
let mut world = WorldBuilder::new()
78+
.with_max_expected_entities(entity_count)
79+
.warm_up_entity_allocator()
80+
.build();
7381
let mut command_queue = CommandQueue::default();
7482

7583
bencher.iter(|| {
@@ -100,7 +108,10 @@ pub fn insert_commands(criterion: &mut Criterion) {
100108

101109
let entity_count = 10_000;
102110
group.bench_function("insert", |bencher| {
103-
let mut world = World::default();
111+
let mut world = WorldBuilder::new()
112+
.with_max_expected_entities(entity_count)
113+
.warm_up_entity_allocator()
114+
.build();
104115
let mut command_queue = CommandQueue::default();
105116
let mut entities = Vec::new();
106117
for _ in 0..entity_count {
@@ -118,7 +129,10 @@ pub fn insert_commands(criterion: &mut Criterion) {
118129
});
119130
});
120131
group.bench_function("insert_batch", |bencher| {
121-
let mut world = World::default();
132+
let mut world = WorldBuilder::new()
133+
.with_max_expected_entities(entity_count)
134+
.warm_up_entity_allocator()
135+
.build();
122136
let mut command_queue = CommandQueue::default();
123137
let mut entities = Vec::new();
124138
for _ in 0..entity_count {
@@ -127,7 +141,7 @@ pub fn insert_commands(criterion: &mut Criterion) {
127141

128142
bencher.iter(|| {
129143
let mut commands = Commands::new(&mut command_queue, &world);
130-
let mut values = Vec::with_capacity(entity_count);
144+
let mut values = Vec::with_capacity(entity_count as usize);
131145
for entity in &entities {
132146
values.push((*entity, (Matrix::default(), Vec3::default())));
133147
}

benches/benches/bevy_ecs/world/despawn.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use bevy_ecs::prelude::*;
22
use criterion::{BatchSize, Criterion};
33
use glam::*;
44

5+
use crate::world_builder::WorldBuilder;
6+
57
#[derive(Component)]
68
struct A(Mat4);
79
#[derive(Component)]
@@ -16,7 +18,10 @@ pub fn world_despawn(criterion: &mut Criterion) {
1618
group.bench_function(format!("{entity_count}_entities"), |bencher| {
1719
bencher.iter_batched_ref(
1820
|| {
19-
let mut world = World::default();
21+
let mut world = WorldBuilder::new()
22+
.with_max_expected_entities(entity_count)
23+
.warm_up_entity_allocator()
24+
.build();
2025
let entities: Vec<Entity> = world
2126
.spawn_batch(
2227
(0..entity_count).map(|_| (A(Mat4::default()), B(Vec4::default()))),

benches/benches/bevy_ecs/world/despawn_recursive.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use bevy_ecs::prelude::*;
22
use criterion::{BatchSize, Criterion};
33
use glam::*;
44

5+
use crate::world_builder::WorldBuilder;
6+
57
#[derive(Component)]
68
struct A(Mat4);
79
#[derive(Component)]
@@ -16,7 +18,10 @@ pub fn world_despawn_recursive(criterion: &mut Criterion) {
1618
group.bench_function(format!("{entity_count}_entities"), |bencher| {
1719
bencher.iter_batched_ref(
1820
|| {
19-
let mut world = World::default();
21+
let mut world = WorldBuilder::new()
22+
.with_max_expected_entities(entity_count)
23+
.warm_up_entity_allocator()
24+
.build();
2025
let parent_ents = (0..entity_count)
2126
.map(|_| {
2227
world

benches/benches/bevy_ecs/world/spawn.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use bevy_ecs::prelude::*;
22
use criterion::Criterion;
33
use glam::*;
44

5+
use crate::world_builder::WorldBuilder;
6+
57
#[derive(Component, Clone)]
68
struct A(Mat4);
79
#[derive(Component, Clone)]
@@ -14,7 +16,10 @@ pub fn world_spawn(criterion: &mut Criterion) {
1416

1517
for entity_count in [1, 100, 10_000] {
1618
group.bench_function(format!("{entity_count}_entities"), |bencher| {
17-
let mut world = World::default();
19+
let mut world = WorldBuilder::new()
20+
.with_max_expected_entities(entity_count)
21+
.warm_up_entity_allocator()
22+
.build();
1823
bencher.iter(|| {
1924
for _ in 0..entity_count {
2025
world.spawn((A(Mat4::default()), B(Vec4::default())));
@@ -33,12 +38,15 @@ pub fn world_spawn_batch(criterion: &mut Criterion) {
3338

3439
for batch_count in [1, 100, 1000, 10_000] {
3540
group.bench_function(format!("{batch_count}_entities"), |bencher| {
36-
let mut world = World::default();
41+
let mut world = WorldBuilder::new()
42+
.with_max_expected_entities(batch_count)
43+
.warm_up_entity_allocator()
44+
.build();
3745
bencher.iter(|| {
3846
for _ in 0..(10_000 / batch_count) {
3947
world.spawn_batch(std::iter::repeat_n(
4048
(A(Mat4::default()), B(Vec4::default())),
41-
batch_count,
49+
batch_count as usize,
4250
));
4351
}
4452
});

0 commit comments

Comments
 (0)