Skip to content

Commit b508b5c

Browse files
committed
Skip empty archetypes and tables when iterating over queries (#4724)
# Objective Speed up queries that are fragmented over many empty archetypes and tables. ## Solution Add a early-out to check if the table or archetype is empty before iterating over it. This adds an extra branch for every archetype matched, but skips setting the archetype/table to the underlying state and any iteration over it. This may not be worth it for the default `Query::iter` and maybe even the `Query::for_each` implementations, but this definitely avoids scheduling unnecessary tasks in the `Query::par_for_each` case. Ideally, `matched_archetypes` should only contain archetypes where there's actually work to do, but this would add a `O(n)` flat cost to every call to `update_archetypes` that scales with the number of matched archetypes. TODO: Benchmark
1 parent 7989cb2 commit b508b5c

File tree

2 files changed

+261
-0
lines changed

2 files changed

+261
-0
lines changed
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
use bevy_ecs::{
2+
component::Component,
3+
prelude::*,
4+
schedule::{Stage, SystemStage},
5+
world::World,
6+
};
7+
use bevy_tasks::{ComputeTaskPool, TaskPool};
8+
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
9+
10+
criterion_group!(benches, empty_archetypes);
11+
criterion_main!(benches);
12+
13+
#[derive(Component)]
14+
struct A<const N: u16>(f32);
15+
16+
fn iter(
17+
query: Query<(
18+
&A<0>,
19+
&A<1>,
20+
&A<2>,
21+
&A<3>,
22+
&A<4>,
23+
&A<5>,
24+
&A<6>,
25+
&A<7>,
26+
&A<8>,
27+
&A<9>,
28+
&A<10>,
29+
&A<11>,
30+
&A<12>,
31+
)>,
32+
) {
33+
for comp in query.iter() {
34+
black_box(comp);
35+
}
36+
}
37+
38+
fn for_each(
39+
query: Query<(
40+
&A<0>,
41+
&A<1>,
42+
&A<2>,
43+
&A<3>,
44+
&A<4>,
45+
&A<5>,
46+
&A<6>,
47+
&A<7>,
48+
&A<8>,
49+
&A<9>,
50+
&A<10>,
51+
&A<11>,
52+
&A<12>,
53+
)>,
54+
) {
55+
query.for_each(|comp| {
56+
black_box(comp);
57+
});
58+
}
59+
60+
fn par_for_each(
61+
task_pool: Res<ComputeTaskPool>,
62+
query: Query<(
63+
&A<0>,
64+
&A<1>,
65+
&A<2>,
66+
&A<3>,
67+
&A<4>,
68+
&A<5>,
69+
&A<6>,
70+
&A<7>,
71+
&A<8>,
72+
&A<9>,
73+
&A<10>,
74+
&A<11>,
75+
&A<12>,
76+
)>,
77+
) {
78+
query.par_for_each(&*task_pool, 64, |comp| {
79+
black_box(comp);
80+
});
81+
}
82+
83+
fn setup(parallel: bool, setup: impl FnOnce(&mut SystemStage)) -> (World, SystemStage) {
84+
let mut world = World::new();
85+
let mut stage = SystemStage::parallel();
86+
if parallel {
87+
world.insert_resource(ComputeTaskPool(TaskPool::default()));
88+
}
89+
setup(&mut stage);
90+
(world, stage)
91+
}
92+
93+
/// create `count` entities with distinct archetypes
94+
fn add_archetypes(world: &mut World, count: u16) {
95+
for i in 0..count {
96+
let mut e = world.spawn();
97+
e.insert(A::<0>(1.0));
98+
e.insert(A::<1>(1.0));
99+
e.insert(A::<2>(1.0));
100+
e.insert(A::<3>(1.0));
101+
e.insert(A::<4>(1.0));
102+
e.insert(A::<5>(1.0));
103+
e.insert(A::<6>(1.0));
104+
e.insert(A::<7>(1.0));
105+
e.insert(A::<8>(1.0));
106+
e.insert(A::<9>(1.0));
107+
e.insert(A::<10>(1.0));
108+
e.insert(A::<11>(1.0));
109+
e.insert(A::<12>(1.0));
110+
if i & 1 << 1 != 0 {
111+
e.insert(A::<13>(1.0));
112+
}
113+
if i & 1 << 2 != 0 {
114+
e.insert(A::<14>(1.0));
115+
}
116+
if i & 1 << 3 != 0 {
117+
e.insert(A::<15>(1.0));
118+
}
119+
if i & 1 << 4 != 0 {
120+
e.insert(A::<16>(1.0));
121+
}
122+
if i & 1 << 5 != 0 {
123+
e.insert(A::<18>(1.0));
124+
}
125+
if i & 1 << 6 != 0 {
126+
e.insert(A::<19>(1.0));
127+
}
128+
if i & 1 << 7 != 0 {
129+
e.insert(A::<20>(1.0));
130+
}
131+
if i & 1 << 8 != 0 {
132+
e.insert(A::<21>(1.0));
133+
}
134+
if i & 1 << 9 != 0 {
135+
e.insert(A::<22>(1.0));
136+
}
137+
if i & 1 << 10 != 0 {
138+
e.insert(A::<23>(1.0));
139+
}
140+
if i & 1 << 11 != 0 {
141+
e.insert(A::<24>(1.0));
142+
}
143+
if i & 1 << 12 != 0 {
144+
e.insert(A::<25>(1.0));
145+
}
146+
if i & 1 << 13 != 0 {
147+
e.insert(A::<26>(1.0));
148+
}
149+
if i & 1 << 14 != 0 {
150+
e.insert(A::<27>(1.0));
151+
}
152+
if i & 1 << 15 != 0 {
153+
e.insert(A::<28>(1.0));
154+
}
155+
}
156+
}
157+
158+
fn empty_archetypes(criterion: &mut Criterion) {
159+
let mut group = criterion.benchmark_group("empty_archetypes");
160+
for archetype_count in [10, 100, 500, 1000, 2000, 5000, 10000] {
161+
let (mut world, mut stage) = setup(true, |stage| {
162+
stage.add_system(iter);
163+
});
164+
add_archetypes(&mut world, archetype_count);
165+
world.clear_entities();
166+
let mut e = world.spawn();
167+
e.insert(A::<0>(1.0));
168+
e.insert(A::<1>(1.0));
169+
e.insert(A::<2>(1.0));
170+
e.insert(A::<3>(1.0));
171+
e.insert(A::<4>(1.0));
172+
e.insert(A::<5>(1.0));
173+
e.insert(A::<6>(1.0));
174+
e.insert(A::<7>(1.0));
175+
e.insert(A::<8>(1.0));
176+
e.insert(A::<9>(1.0));
177+
e.insert(A::<10>(1.0));
178+
e.insert(A::<11>(1.0));
179+
e.insert(A::<12>(1.0));
180+
stage.run(&mut world);
181+
group.bench_with_input(
182+
BenchmarkId::new("iter", archetype_count),
183+
&archetype_count,
184+
|bencher, &_| {
185+
bencher.iter(|| {
186+
stage.run(&mut world);
187+
})
188+
},
189+
);
190+
}
191+
for archetype_count in [10, 100, 500, 1000, 2000, 5000, 10000] {
192+
let (mut world, mut stage) = setup(true, |stage| {
193+
stage.add_system(for_each);
194+
});
195+
add_archetypes(&mut world, archetype_count);
196+
world.clear_entities();
197+
let mut e = world.spawn();
198+
e.insert(A::<0>(1.0));
199+
e.insert(A::<1>(1.0));
200+
e.insert(A::<2>(1.0));
201+
e.insert(A::<3>(1.0));
202+
e.insert(A::<4>(1.0));
203+
e.insert(A::<5>(1.0));
204+
e.insert(A::<6>(1.0));
205+
e.insert(A::<7>(1.0));
206+
e.insert(A::<8>(1.0));
207+
e.insert(A::<9>(1.0));
208+
e.insert(A::<10>(1.0));
209+
e.insert(A::<11>(1.0));
210+
e.insert(A::<12>(1.0));
211+
stage.run(&mut world);
212+
group.bench_with_input(
213+
BenchmarkId::new("for_each", archetype_count),
214+
&archetype_count,
215+
|bencher, &_| {
216+
bencher.iter(|| {
217+
stage.run(&mut world);
218+
})
219+
},
220+
);
221+
}
222+
for archetype_count in [10, 100, 500, 1000, 2000, 5000, 10000] {
223+
let (mut world, mut stage) = setup(true, |stage| {
224+
stage.add_system(par_for_each);
225+
});
226+
add_archetypes(&mut world, archetype_count);
227+
world.clear_entities();
228+
let mut e = world.spawn();
229+
e.insert(A::<0>(1.0));
230+
e.insert(A::<1>(1.0));
231+
e.insert(A::<2>(1.0));
232+
e.insert(A::<3>(1.0));
233+
e.insert(A::<4>(1.0));
234+
e.insert(A::<5>(1.0));
235+
e.insert(A::<6>(1.0));
236+
e.insert(A::<7>(1.0));
237+
e.insert(A::<8>(1.0));
238+
e.insert(A::<9>(1.0));
239+
e.insert(A::<10>(1.0));
240+
e.insert(A::<11>(1.0));
241+
e.insert(A::<12>(1.0));
242+
stage.run(&mut world);
243+
group.bench_with_input(
244+
BenchmarkId::new("par_for_each", archetype_count),
245+
&archetype_count,
246+
|bencher, &_| {
247+
bencher.iter(|| {
248+
stage.run(&mut world);
249+
})
250+
},
251+
);
252+
}
253+
}

crates/bevy_ecs/src/query/state.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,10 @@ impl<Q: WorldQuery, F: ReadOnlyWorldQuery> QueryState<Q, F> {
983983
let tables = &world.storages().tables;
984984
for table_id in &self.matched_table_ids {
985985
let table = &tables[*table_id];
986+
if table.is_empty() {
987+
continue;
988+
}
989+
986990
let mut offset = 0;
987991
while offset < table.entity_count() {
988992
let func = func.clone();
@@ -1030,6 +1034,10 @@ impl<Q: WorldQuery, F: ReadOnlyWorldQuery> QueryState<Q, F> {
10301034
for archetype_id in &self.matched_archetype_ids {
10311035
let mut offset = 0;
10321036
let archetype = &archetypes[*archetype_id];
1037+
if archetype.is_empty() {
1038+
continue;
1039+
}
1040+
10331041
while offset < archetype.len() {
10341042
let func = func.clone();
10351043
let len = batch_size.min(archetype.len() - offset);

0 commit comments

Comments
 (0)