Skip to content

Commit 3cb5176

Browse files
chescockandrewzhurov
authored andcommitted
Stop using ArchetypeComponentId in the executor (bevyengine#16885)
# Objective Stop using `ArchetypeComponentId` in the executor. These IDs will grow even more quickly with relations, and the size may start to degrade performance. ## Solution Have systems expose their `FilteredAccessSet<ComponentId>`, and have the executor use that to determine which systems conflict. This can be determined statically, so determine all conflicts during initialization and only perform bit tests when running. ## Testing I ran many_foxes and didn't see any performance changes. It's probably worth testing this with a wider range of realistic schedules to see whether the reduced concurrency has a cost in practice, but I don't know what sort of test cases to use. ## Migration Guide The schedule will now prevent systems from running in parallel if there *could* be an archetype that they conflict on, even if there aren't actually any. For example, these systems will now conflict even if no entity has both `Player` and `Enemy` components: ```rust fn player_system(query: Query<(&mut Transform, &Player)>) {} fn enemy_system(query: Query<(&mut Transform, &Enemy)>) {} ``` To allow them to run in parallel, use `Without` filters, just as you would to allow both queries in a single system: ```rust // Either one of these changes alone would be enough fn player_system(query: Query<(&mut Transform, &Player), Without<Enemy>>) {} fn enemy_system(query: Query<(&mut Transform, &Enemy), Without<Player>>) {} ```
1 parent 320e8b9 commit 3cb5176

File tree

11 files changed

+172
-65
lines changed

11 files changed

+172
-65
lines changed

crates/bevy_ecs/src/query/access.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,14 @@ impl<T: SparseSetIndex> Clone for FilteredAccessSet<T> {
12891289
}
12901290

12911291
impl<T: SparseSetIndex> FilteredAccessSet<T> {
1292+
/// Creates an empty [`FilteredAccessSet`].
1293+
pub const fn new() -> Self {
1294+
Self {
1295+
combined_access: Access::new(),
1296+
filtered_accesses: Vec::new(),
1297+
}
1298+
}
1299+
12921300
/// Returns a reference to the unfiltered access of the entire set.
12931301
#[inline]
12941302
pub fn combined_access(&self) -> &Access<T> {
@@ -1412,10 +1420,7 @@ impl<T: SparseSetIndex> FilteredAccessSet<T> {
14121420

14131421
impl<T: SparseSetIndex> Default for FilteredAccessSet<T> {
14141422
fn default() -> Self {
1415-
Self {
1416-
combined_access: Default::default(),
1417-
filtered_accesses: Vec::new(),
1418-
}
1423+
Self::new()
14191424
}
14201425
}
14211426

crates/bevy_ecs/src/schedule/executor/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::{
1818
component::{ComponentId, Tick},
1919
error::{BevyError, ErrorContext, Result},
2020
prelude::{IntoSystemSet, SystemSet},
21-
query::Access,
21+
query::{Access, FilteredAccessSet},
2222
schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet},
2323
system::{ScheduleSystem, System, SystemIn, SystemParamValidationError},
2424
world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World},
@@ -174,6 +174,10 @@ impl System for ApplyDeferred {
174174
const { &Access::new() }
175175
}
176176

177+
fn component_access_set(&self) -> &FilteredAccessSet<ComponentId> {
178+
const { &FilteredAccessSet::new() }
179+
}
180+
177181
fn archetype_component_access(&self) -> &Access<ArchetypeComponentId> {
178182
// This system accesses no archetype components.
179183
const { &Access::new() }

crates/bevy_ecs/src/schedule/executor/multi_threaded.rs

Lines changed: 79 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use alloc::{boxed::Box, vec::Vec};
22
use bevy_platform::sync::Arc;
33
use bevy_tasks::{ComputeTaskPool, Scope, TaskPool, ThreadExecutor};
4-
use bevy_utils::{default, syncunsafecell::SyncUnsafeCell};
4+
use bevy_utils::syncunsafecell::SyncUnsafeCell;
55
use concurrent_queue::ConcurrentQueue;
66
use core::{any::Any, panic::AssertUnwindSafe};
77
use fixedbitset::FixedBitSet;
@@ -13,10 +13,8 @@ use std::sync::{Mutex, MutexGuard};
1313
use tracing::{info_span, Span};
1414

1515
use crate::{
16-
archetype::ArchetypeComponentId,
1716
error::{default_error_handler, BevyError, ErrorContext, Result},
1817
prelude::Resource,
19-
query::Access,
2018
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
2119
system::ScheduleSystem,
2220
world::{unsafe_world_cell::UnsafeWorldCell, World},
@@ -62,8 +60,13 @@ impl<'env, 'sys> Environment<'env, 'sys> {
6260
/// Per-system data used by the [`MultiThreadedExecutor`].
6361
// Copied here because it can't be read from the system when it's running.
6462
struct SystemTaskMetadata {
65-
/// The [`ArchetypeComponentId`] access of the system.
66-
archetype_component_access: Access<ArchetypeComponentId>,
63+
/// The set of systems whose `component_access_set()` conflicts with this one.
64+
conflicting_systems: FixedBitSet,
65+
/// The set of systems whose `component_access_set()` conflicts with this system's conditions.
66+
/// Note that this is separate from `conflicting_systems` to handle the case where
67+
/// a system is skipped by an earlier system set condition or system stepping,
68+
/// and needs access to run its conditions but not for itself.
69+
condition_conflicting_systems: FixedBitSet,
6770
/// Indices of the systems that directly depend on the system.
6871
dependents: Vec<usize>,
6972
/// Is `true` if the system does not access `!Send` data.
@@ -97,8 +100,8 @@ pub struct MultiThreadedExecutor {
97100
pub struct ExecutorState {
98101
/// Metadata for scheduling and running system tasks.
99102
system_task_metadata: Vec<SystemTaskMetadata>,
100-
/// Union of the accesses of all currently running systems.
101-
active_access: Access<ArchetypeComponentId>,
103+
/// The set of systems whose `component_access_set()` conflicts with this system set's conditions.
104+
set_condition_conflicting_systems: Vec<FixedBitSet>,
102105
/// Returns `true` if a system with non-`Send` access is running.
103106
local_thread_running: bool,
104107
/// Returns `true` if an exclusive system is running.
@@ -164,7 +167,8 @@ impl SystemExecutor for MultiThreadedExecutor {
164167
state.system_task_metadata = Vec::with_capacity(sys_count);
165168
for index in 0..sys_count {
166169
state.system_task_metadata.push(SystemTaskMetadata {
167-
archetype_component_access: default(),
170+
conflicting_systems: FixedBitSet::with_capacity(sys_count),
171+
condition_conflicting_systems: FixedBitSet::with_capacity(sys_count),
168172
dependents: schedule.system_dependents[index].clone(),
169173
is_send: schedule.systems[index].is_send(),
170174
is_exclusive: schedule.systems[index].is_exclusive(),
@@ -174,6 +178,60 @@ impl SystemExecutor for MultiThreadedExecutor {
174178
}
175179
}
176180

181+
{
182+
#[cfg(feature = "trace")]
183+
let _span = info_span!("calculate conflicting systems").entered();
184+
for index1 in 0..sys_count {
185+
let system1 = &schedule.systems[index1];
186+
for index2 in 0..index1 {
187+
let system2 = &schedule.systems[index2];
188+
if !system2
189+
.component_access_set()
190+
.is_compatible(system1.component_access_set())
191+
{
192+
state.system_task_metadata[index1]
193+
.conflicting_systems
194+
.insert(index2);
195+
state.system_task_metadata[index2]
196+
.conflicting_systems
197+
.insert(index1);
198+
}
199+
}
200+
201+
for index2 in 0..sys_count {
202+
let system2 = &schedule.systems[index2];
203+
if schedule.system_conditions[index1].iter().any(|condition| {
204+
!system2
205+
.component_access_set()
206+
.is_compatible(condition.component_access_set())
207+
}) {
208+
state.system_task_metadata[index1]
209+
.condition_conflicting_systems
210+
.insert(index2);
211+
}
212+
}
213+
}
214+
215+
state.set_condition_conflicting_systems.clear();
216+
state.set_condition_conflicting_systems.reserve(set_count);
217+
for set_idx in 0..set_count {
218+
let mut conflicting_systems = FixedBitSet::with_capacity(sys_count);
219+
for sys_index in 0..sys_count {
220+
let system = &schedule.systems[sys_index];
221+
if schedule.set_conditions[set_idx].iter().any(|condition| {
222+
!system
223+
.component_access_set()
224+
.is_compatible(condition.component_access_set())
225+
}) {
226+
conflicting_systems.insert(sys_index);
227+
}
228+
}
229+
state
230+
.set_condition_conflicting_systems
231+
.push(conflicting_systems);
232+
}
233+
}
234+
177235
state.num_dependencies_remaining = Vec::with_capacity(sys_count);
178236
}
179237

@@ -257,7 +315,6 @@ impl SystemExecutor for MultiThreadedExecutor {
257315

258316
debug_assert!(state.ready_systems.is_clear());
259317
debug_assert!(state.running_systems.is_clear());
260-
state.active_access.clear();
261318
state.evaluated_sets.clear();
262319
state.skipped_systems.clear();
263320
state.completed_systems.clear();
@@ -345,9 +402,9 @@ impl ExecutorState {
345402
fn new() -> Self {
346403
Self {
347404
system_task_metadata: Vec::new(),
405+
set_condition_conflicting_systems: Vec::new(),
348406
num_running_systems: 0,
349407
num_dependencies_remaining: Vec::new(),
350-
active_access: default(),
351408
local_thread_running: false,
352409
exclusive_running: false,
353410
evaluated_sets: FixedBitSet::new(),
@@ -368,8 +425,6 @@ impl ExecutorState {
368425
self.finish_system_and_handle_dependents(result);
369426
}
370427

371-
self.rebuild_active_access();
372-
373428
// SAFETY:
374429
// - `finish_system_and_handle_dependents` has updated the currently running systems.
375430
// - `rebuild_active_access` locks access for all currently running systems.
@@ -488,37 +543,30 @@ impl ExecutorState {
488543
{
489544
for condition in &mut conditions.set_conditions[set_idx] {
490545
condition.update_archetype_component_access(world);
491-
if !condition
492-
.archetype_component_access()
493-
.is_compatible(&self.active_access)
494-
{
495-
return false;
496-
}
546+
}
547+
if !self.set_condition_conflicting_systems[set_idx].is_disjoint(&self.running_systems) {
548+
return false;
497549
}
498550
}
499551

500552
for condition in &mut conditions.system_conditions[system_index] {
501553
condition.update_archetype_component_access(world);
502-
if !condition
503-
.archetype_component_access()
504-
.is_compatible(&self.active_access)
505-
{
506-
return false;
507-
}
554+
}
555+
if !system_meta
556+
.condition_conflicting_systems
557+
.is_disjoint(&self.running_systems)
558+
{
559+
return false;
508560
}
509561

510562
if !self.skipped_systems.contains(system_index) {
511563
system.update_archetype_component_access(world);
512-
if !system
513-
.archetype_component_access()
514-
.is_compatible(&self.active_access)
564+
if !system_meta
565+
.conflicting_systems
566+
.is_disjoint(&self.running_systems)
515567
{
516568
return false;
517569
}
518-
519-
self.system_task_metadata[system_index]
520-
.archetype_component_access
521-
.clone_from(system.archetype_component_access());
522570
}
523571

524572
true
@@ -648,9 +696,6 @@ impl ExecutorState {
648696
context.system_completed(system_index, res, system);
649697
};
650698

651-
self.active_access
652-
.extend(&system_meta.archetype_component_access);
653-
654699
if system_meta.is_send {
655700
context.scope.spawn(task);
656701
} else {
@@ -741,15 +786,6 @@ impl ExecutorState {
741786
}
742787
}
743788
}
744-
745-
fn rebuild_active_access(&mut self) {
746-
self.active_access.clear();
747-
for index in self.running_systems.ones() {
748-
let system_meta = &self.system_task_metadata[index];
749-
self.active_access
750-
.extend(&system_meta.archetype_component_access);
751-
}
752-
}
753789
}
754790

755791
fn apply_deferred(

crates/bevy_ecs/src/system/adapter_system.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ where
131131
self.system.component_access()
132132
}
133133

134+
fn component_access_set(
135+
&self,
136+
) -> &crate::query::FilteredAccessSet<crate::component::ComponentId> {
137+
self.system.component_access_set()
138+
}
139+
134140
#[inline]
135141
fn archetype_component_access(
136142
&self,

crates/bevy_ecs/src/system/combinator.rs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::{
55
archetype::ArchetypeComponentId,
66
component::{ComponentId, Tick},
77
prelude::World,
8-
query::Access,
8+
query::{Access, FilteredAccessSet},
99
schedule::InternedSystemSet,
1010
system::{input::SystemInput, SystemIn, SystemParamValidationError},
1111
world::unsafe_world_cell::UnsafeWorldCell,
@@ -114,21 +114,21 @@ pub struct CombinatorSystem<Func, A, B> {
114114
a: A,
115115
b: B,
116116
name: Cow<'static, str>,
117-
component_access: Access<ComponentId>,
117+
component_access_set: FilteredAccessSet<ComponentId>,
118118
archetype_component_access: Access<ArchetypeComponentId>,
119119
}
120120

121121
impl<Func, A, B> CombinatorSystem<Func, A, B> {
122122
/// Creates a new system that combines two inner systems.
123123
///
124124
/// The returned system will only be usable if `Func` implements [`Combine<A, B>`].
125-
pub const fn new(a: A, b: B, name: Cow<'static, str>) -> Self {
125+
pub fn new(a: A, b: B, name: Cow<'static, str>) -> Self {
126126
Self {
127127
_marker: PhantomData,
128128
a,
129129
b,
130130
name,
131-
component_access: Access::new(),
131+
component_access_set: FilteredAccessSet::default(),
132132
archetype_component_access: Access::new(),
133133
}
134134
}
@@ -148,7 +148,11 @@ where
148148
}
149149

150150
fn component_access(&self) -> &Access<ComponentId> {
151-
&self.component_access
151+
self.component_access_set.combined_access()
152+
}
153+
154+
fn component_access_set(&self) -> &FilteredAccessSet<ComponentId> {
155+
&self.component_access_set
152156
}
153157

154158
fn archetype_component_access(&self) -> &Access<ArchetypeComponentId> {
@@ -211,8 +215,10 @@ where
211215
fn initialize(&mut self, world: &mut World) {
212216
self.a.initialize(world);
213217
self.b.initialize(world);
214-
self.component_access.extend(self.a.component_access());
215-
self.component_access.extend(self.b.component_access());
218+
self.component_access_set
219+
.extend(self.a.component_access_set().clone());
220+
self.component_access_set
221+
.extend(self.b.component_access_set().clone());
216222
}
217223

218224
fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) {
@@ -343,7 +349,7 @@ pub struct PipeSystem<A, B> {
343349
a: A,
344350
b: B,
345351
name: Cow<'static, str>,
346-
component_access: Access<ComponentId>,
352+
component_access_set: FilteredAccessSet<ComponentId>,
347353
archetype_component_access: Access<ArchetypeComponentId>,
348354
}
349355

@@ -354,12 +360,12 @@ where
354360
for<'a> B::In: SystemInput<Inner<'a> = A::Out>,
355361
{
356362
/// Creates a new system that pipes two inner systems.
357-
pub const fn new(a: A, b: B, name: Cow<'static, str>) -> Self {
363+
pub fn new(a: A, b: B, name: Cow<'static, str>) -> Self {
358364
Self {
359365
a,
360366
b,
361367
name,
362-
component_access: Access::new(),
368+
component_access_set: FilteredAccessSet::default(),
363369
archetype_component_access: Access::new(),
364370
}
365371
}
@@ -379,7 +385,11 @@ where
379385
}
380386

381387
fn component_access(&self) -> &Access<ComponentId> {
382-
&self.component_access
388+
self.component_access_set.combined_access()
389+
}
390+
391+
fn component_access_set(&self) -> &FilteredAccessSet<ComponentId> {
392+
&self.component_access_set
383393
}
384394

385395
fn archetype_component_access(&self) -> &Access<ArchetypeComponentId> {
@@ -443,8 +453,10 @@ where
443453
fn initialize(&mut self, world: &mut World) {
444454
self.a.initialize(world);
445455
self.b.initialize(world);
446-
self.component_access.extend(self.a.component_access());
447-
self.component_access.extend(self.b.component_access());
456+
self.component_access_set
457+
.extend(self.a.component_access_set().clone());
458+
self.component_access_set
459+
.extend(self.b.component_access_set().clone());
448460
}
449461

450462
fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) {

0 commit comments

Comments
 (0)