bevyengine / bevyengine/bevy

Early system return outperforming conditional execution

Open
#20,421 2 comments 0 reactions 0 assignees View on GitHub
A-ECS A-Tasks C-Bug C-Performance S-Needs-Investigation
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 22h
Merged PRs (30d)
161

Description

## Bevy version

0.16.1

## What you did

As I find early return expressions to be a little more ergonomic than writing extra systems for conditional system execution, I was wondering on performance caveats.

I made 10,000 entities with transform and 10,000 without and let systems using filtered queries to both run (or better: I rejected their run through conditions). I used a resource holding a bool. This is either passed to the system directly for an immediate return or it is passed to a condition check system that has the same simple if expression checking this struct's content bool. I let the app with conditional system and a separate one with internal return run for 10 sec each and compared the methods of conditionally running the systems.

The difference is significant in lightweight systems that are returned early/not run with 12 µs vs 10 µs favouring run_if. As a redittor pointed out, however, the cost may be higher when scheduling several tasks for multitasking is involved where creating a task only for cancelling it should have a high overhead.

When I checked this specifically by adding three dummy systems, however, things got confusing. When I added three systems doing nothing but calling the same queries and then returning, we get about 24 µs for the early return vs 28->90 µs for conditional execution.

## What went wrong

I would assume that the run_if implementation should be just as efficient if not more than an early return in systems.

## Additional information

### Wild theory

Is the schedule affected by the conditional execution? I would assume this would get more expensive in any realistic app with dozens or more systems, early returns would always be the way to go.

If this is true, conditional systems could perhaps be rewired to function as interface for an early return rather than inducing a rescheduling?

### Log

Single system
---- dev_tools::bench::bench_if_vs_early_return stdout ----
Early return frame time: 12.12 µs
Conditional system frame time: 9.93 µs

With three dummy systems

Run 1
---- dev_tools::bench::bench_if_vs_early_return stdout ----
Early return frame time: 25.79 µs
Conditional system frame time: 28.68 µs

Run 2
---- dev_tools::bench::bench_if_vs_early_return stdout ----
Early return frame time: 24.65 µs
Conditional system frame time: 96.84 µs

Run 3
---- dev_tools::bench::bench_if_vs_early_return stdout ----
Early return frame time: 24.93 µs
Conditional system frame time: 63.38 µs

### Benchmark

```rust
use bevy::prelude::*;

#[test]
fn bench_if_vs_early_return() {
use std::time::Duration;

const BENCH_SECS: u64 = 10;
const WITH_DUMMIES: bool = true;

#[derive(Resource, Reflect, Debug, Default)]
#[reflect(Resource)]
struct Toggle(bool);

fn dummy1(
q1: Query>,
q2: Query>,
toggle: Res,
) {
for e1 in q1 {
for e2 in q2 {
return;
}
}
}

fn dummy2(
q1: Query>,
q2: Query>,
toggle: Res,
) {
for e1 in q1 {
for e2 in q2 {
return;
}
}
}
fn dummy3(
q1: Query>,
q2: Query>,
toggle: Res,
) {
for e1 in q1 {
for e2 in q2 {
return;
}
}
}

fn system_internal_if(
q1: Query>,
q2: Query>,
toggle: Res,
) {
if toggle.0 {
for e1 in q1 {
for e2 in q2 {
println!("{} {}", e1, e2);
}
}
}
}

fn run_condition(toggle: Res) -> bool {
toggle.0
}

fn system_conditional(
q1: Query>,
q2: Query>,
) {
for e1 in q1 {
for e2 in q2 {
println!("{} {}", e1, e2);
}
}
}

let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.init_resource::();
app.add_systems(Update, system_internal_if);
if WITH_DUMMIES {
app.add_systems(Update, (dummy1, dummy2, dummy3));
}

for _ in 0..10000 {
app.world_mut().spawn(Transform::default());
}
for _ in 0..10000 {
app.world_mut().spawn_empty();
}

let mut time = Duration::from_secs(0);
let mut frame_count = 0;
while time < Duration::from_secs(BENCH_SECS) {
app.update();
frame_count += 1;
time += app.world().resource::

let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.init_resource::();
app.add_systems(Update, system_conditional.run_if(run_condition));
if WITH_DUMMIES {
app.add_systems(Update, (dummy1, dummy2, dummy3));
}

for _ in 0..10000 {
app.world_mut().spawn(Transform::default());
}
for _ in 0..10000 {
app.world_mut().spawn_empty();
}

let mut time = Duration::from_secs(0);
let mut frame_count = 0;
while time < Duration::from_secs(BENCH_SECS) {
app.update();
frame_count += 1;
time += app.world().resource::

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.