Skip to content

Conversation

@james7132
Copy link
Member

@james7132 james7132 commented Jul 30, 2025

Objective

Improve the performance of Bevy's async task executor and clean up the code surrounding TaskPool::scope. Fixes #20421.

Solution

  • Inline a copy of async_executor's code
  • Move the TLS LocalExecutor and ThreadExecutor into the core Executor implementation, and poll their queues in Executor::run.
  • Use !Send types for local queues (Mutex -> UnsafeCell, ConcurrentQueue -> VecDeque).
  • Avoid extra Arc clones by using &'static references to thread-local queues.
  • Avoid extra contention on the global injector queue, and minimize extra allocations by opportunistically pushing onto thread-local queues when done on "executor-owned" threads.

This is a breaking change.

  • ThreadExecutor is now ThreadSpawner and forbids ticking and running the executor, only allowing spawning tasks to the target thread.
  • Executor and LocalExecutor wrappers are no longer public.

Testing

I've only tested this against a few examples: 3d_scene and many_foxes. This current implementation does seems to break animation systems. EDIT: That seems to be #20383.

Performance Testing

All of the benchmarks including a multithreaded schedule run are included below. Overall, this seems to be significant win whenever there are a very large number of tasks (i.e. the empty_archetypes/par_for_each` benchmarks), with some being upwards of 2x faster due to lower contention.

group                                                                                                     bevy_executor                            main
-----                                                                                                     -------------                            ----
added_archetypes/archetype_count/100                                                                      1.00     36.8±1.13µs        ? ?/sec      1.00     36.8±2.14µs        ? ?/sec
added_archetypes/archetype_count/1000                                                                     1.04   399.8±15.31µs        ? ?/sec      1.00   384.9±19.62µs        ? ?/sec
added_archetypes/archetype_count/10000                                                                    1.09     11.0±0.95ms        ? ?/sec      1.00     10.1±1.24ms        ? ?/sec
busy_systems/01x_entities_03_systems                                                                      1.00     18.6±0.56µs        ? ?/sec      2.16     40.3±1.18µs        ? ?/sec
busy_systems/01x_entities_09_systems                                                                      1.00     49.8±1.41µs        ? ?/sec      1.58     79.0±1.81µs        ? ?/sec
busy_systems/01x_entities_15_systems                                                                      1.00     84.0±1.46µs        ? ?/sec      1.39    116.4±2.48µs        ? ?/sec
busy_systems/03x_entities_03_systems                                                                      1.00     30.7±1.12µs        ? ?/sec      1.78     54.6±1.68µs        ? ?/sec
busy_systems/03x_entities_09_systems                                                                      1.00     83.3±2.12µs        ? ?/sec      1.50    125.3±3.44µs        ? ?/sec
busy_systems/03x_entities_15_systems                                                                      1.00    140.2±5.51µs        ? ?/sec      1.35    189.2±6.06µs        ? ?/sec
busy_systems/05x_entities_03_systems                                                                      1.00     50.8±1.00µs        ? ?/sec      1.53     77.7±6.65µs        ? ?/sec
busy_systems/05x_entities_09_systems                                                                      1.00    132.3±2.39µs        ? ?/sec      1.42    187.4±6.30µs        ? ?/sec
busy_systems/05x_entities_15_systems                                                                      1.00    217.3±5.15µs        ? ?/sec      1.29    279.7±5.10µs        ? ?/sec
contrived/01x_entities_03_systems                                                                         1.00      9.5±0.31µs        ? ?/sec      1.67     15.8±1.12µs        ? ?/sec
contrived/01x_entities_09_systems                                                                         1.00     23.1±0.65µs        ? ?/sec      1.56     36.1±5.34µs        ? ?/sec
contrived/01x_entities_15_systems                                                                         1.00     36.5±1.19µs        ? ?/sec      1.51     55.2±2.02µs        ? ?/sec
contrived/03x_entities_03_systems                                                                         1.00     21.1±1.03µs        ? ?/sec      1.70     35.9±2.03µs        ? ?/sec
contrived/03x_entities_09_systems                                                                         1.00     47.8±3.19µs        ? ?/sec      1.57     75.1±1.82µs        ? ?/sec
contrived/03x_entities_15_systems                                                                         1.00    77.7±13.55µs        ? ?/sec      1.54    119.7±9.98µs        ? ?/sec
contrived/05x_entities_03_systems                                                                         1.00     27.0±0.96µs        ? ?/sec      1.54     41.7±2.99µs        ? ?/sec
contrived/05x_entities_09_systems                                                                         1.00     67.1±4.48µs        ? ?/sec      1.57    105.7±5.74µs        ? ?/sec
contrived/05x_entities_15_systems                                                                         1.00   107.1±10.20µs        ? ?/sec      1.52   163.2±10.30µs        ? ?/sec
empty_archetypes/for_each/10                                                                              1.11  1792.3±56.15ns        ? ?/sec      1.00  1607.9±121.49ns        ? ?/sec
empty_archetypes/for_each/100                                                                             1.04  1752.9±71.42ns        ? ?/sec      1.00  1683.9±117.60ns        ? ?/sec
empty_archetypes/for_each/1000                                                                            1.00  1801.1±160.99ns        ? ?/sec     1.36      2.5±1.27µs        ? ?/sec
empty_archetypes/for_each/10000                                                                           1.00     13.3±0.55µs        ? ?/sec      1.40     18.6±1.52µs        ? ?/sec
empty_archetypes/iter/10                                                                                  1.02  1655.2±47.72ns        ? ?/sec      1.00  1624.9±138.81ns        ? ?/sec
empty_archetypes/iter/100                                                                                 1.00  1694.9±46.59ns        ? ?/sec      1.03  1746.4±202.97ns        ? ?/sec
empty_archetypes/iter/1000                                                                                1.00  1752.9±97.17ns        ? ?/sec      1.52      2.7±0.48µs        ? ?/sec
empty_archetypes/iter/10000                                                                               1.00      8.9±1.19µs        ? ?/sec      2.31     20.6±2.13µs        ? ?/sec
empty_archetypes/par_for_each/10                                                                          1.00      5.1±0.87µs        ? ?/sec      3.78     19.3±1.98µs        ? ?/sec
empty_archetypes/par_for_each/100                                                                         1.00      5.9±0.89µs        ? ?/sec      2.18     12.9±1.54µs        ? ?/sec
empty_archetypes/par_for_each/1000                                                                        1.00      5.3±0.39µs        ? ?/sec      3.68     19.5±1.76µs        ? ?/sec
empty_archetypes/par_for_each/10000                                                                       1.00     21.4±1.09µs        ? ?/sec      1.66     35.5±1.46µs        ? ?/sec
empty_commands/0_entities                                                                                 1.00      6.1±0.17ns        ? ?/sec      1.05      6.4±0.73ns        ? ?/sec
empty_systems/0_systems                                                                                   1.02     10.4±0.47ns        ? ?/sec      1.00     10.1±0.49ns        ? ?/sec
empty_systems/1000_systems                                                                                1.47   603.4±38.38µs        ? ?/sec      1.00   411.6±12.47µs        ? ?/sec
empty_systems/100_systems                                                                                 1.20     44.4±2.15µs        ? ?/sec      1.00     37.0±0.94µs        ? ?/sec
empty_systems/10_systems                                                                                  1.00      4.3±0.76µs        ? ?/sec      2.33     10.1±0.56µs        ? ?/sec
empty_systems/2_systems                                                                                   1.00  1964.4±158.36ns        ? ?/sec     2.94      5.8±0.96µs        ? ?/sec
empty_systems/4_systems                                                                                   1.00      2.6±0.13µs        ? ?/sec      3.46      8.9±0.92µs        ? ?/sec
for_each_par_iter/threads/1                                                                               1.00     11.1±0.17ms        ? ?/sec      1.89     20.9±1.50ms        ? ?/sec
for_each_par_iter/threads/16                                                                              1.01      3.1±0.10ms        ? ?/sec      1.00      3.1±0.10ms        ? ?/sec
for_each_par_iter/threads/2                                                                               1.00      7.6±0.14ms        ? ?/sec      1.49     11.4±0.29ms        ? ?/sec
for_each_par_iter/threads/32                                                                              1.01      2.2±0.04ms        ? ?/sec      1.00      2.2±0.06ms        ? ?/sec
for_each_par_iter/threads/4                                                                               1.00      4.8±0.16ms        ? ?/sec      1.61      7.6±0.16ms        ? ?/sec
for_each_par_iter/threads/8                                                                               1.00      4.6±0.74ms        ? ?/sec      1.03      4.7±0.10ms        ? ?/sec
many_maps_iter                                                                                            1.34     30.7±6.03ms        ? ?/sec      1.00     23.0±1.02ms        ? ?/sec
many_maps_par_iter/threads/1                                                                              1.00     11.6±0.37ms        ? ?/sec      1.83     21.2±1.42ms        ? ?/sec
many_maps_par_iter/threads/16                                                                             1.00      2.1±0.08ms        ? ?/sec      1.50      3.1±0.11ms        ? ?/sec
many_maps_par_iter/threads/2                                                                              1.00      7.7±0.11ms        ? ?/sec      1.48     11.4±0.30ms        ? ?/sec
many_maps_par_iter/threads/32                                                                             1.00      2.0±0.03ms        ? ?/sec      1.09      2.2±0.02ms        ? ?/sec
many_maps_par_iter/threads/4                                                                              1.00      4.7±0.08ms        ? ?/sec      1.66      7.8±0.12ms        ? ?/sec
many_maps_par_iter/threads/8                                                                              1.00      3.1±0.13ms        ? ?/sec      1.58      4.9±0.14ms        ? ?/sec
par_iter_simple/hybrid                                                                                    1.00     81.6±4.33µs        ? ?/sec      1.01     82.3±6.10µs        ? ?/sec
par_iter_simple/with_0_fragment                                                                           1.13     51.7±4.35µs        ? ?/sec      1.00     45.6±3.59µs        ? ?/sec
par_iter_simple/with_1000_fragment                                                                        1.04     67.8±3.77µs        ? ?/sec      1.00     65.1±5.16µs        ? ?/sec
par_iter_simple/with_100_fragment                                                                         1.09     53.2±3.21µs        ? ?/sec      1.00     48.8±3.43µs        ? ?/sec
par_iter_simple/with_10_fragment                                                                          1.08     51.4±4.00µs        ? ?/sec      1.00     47.5±5.10µs        ? ?/sec
param/combinator_system/8_dyn_params_system                                                               1.00  1429.6±60.37ns        ? ?/sec      1.16  1651.4±217.47ns        ? ?/sec
param/combinator_system/8_piped_systems                                                                   1.00  1382.3±62.79ns        ? ?/sec      1.18  1633.6±52.53ns        ? ?/sec
param/combinator_system/8_variant_param_set_system                                                        1.00  1375.6±49.81ns        ? ?/sec      1.31  1801.9±1117.63ns        ? ?/sec
run_condition/no/1000_systems                                                                             1.14     38.8±3.10µs        ? ?/sec      1.00     34.2±0.42µs        ? ?/sec
run_condition/no/100_systems                                                                              1.09      2.2±0.13µs        ? ?/sec      1.00      2.0±0.07µs        ? ?/sec
run_condition/no/10_systems                                                                               1.15   301.7±16.65ns        ? ?/sec      1.00   263.0±10.48ns        ? ?/sec
run_condition/yes/1000_systems                                                                            1.00   427.3±58.83µs        ? ?/sec      1.27   542.9±39.23µs        ? ?/sec
run_condition/yes/100_systems                                                                             1.00     37.9±1.33µs        ? ?/sec      1.22     46.2±3.51µs        ? ?/sec
run_condition/yes/10_systems                                                                              1.00      5.2±0.40µs        ? ?/sec      1.41      7.3±0.56µs        ? ?/sec
run_condition/yes_using_query/1000_systems                                                                1.00   413.4±22.99µs        ? ?/sec      1.38   570.8±77.20µs        ? ?/sec
run_condition/yes_using_query/100_systems                                                                 1.00     40.6±2.63µs        ? ?/sec      1.06     43.0±3.78µs        ? ?/sec
run_condition/yes_using_query/10_systems                                                                  1.00      6.0±1.24µs        ? ?/sec      1.25      7.4±0.52µs        ? ?/sec
run_condition/yes_using_resource/1000_systems                                                             1.07   423.0±29.47µs        ? ?/sec      1.00   394.9±29.42µs        ? ?/sec
run_condition/yes_using_resource/100_systems                                                              1.04     38.8±2.51µs        ? ?/sec      1.00     37.3±1.54µs        ? ?/sec
run_condition/yes_using_resource/10_systems                                                               1.00      5.4±0.51µs        ? ?/sec      1.95     10.5±0.75µs        ? ?/sec
run_empty_schedule/MultiThreaded                                                                          1.00      9.7±0.14ns        ? ?/sec      1.43     13.9±0.25ns        ? ?/sec
run_empty_schedule/Simple                                                                                 1.00     10.7±0.10ns        ? ?/sec      1.37     14.7±0.50ns        ? ?/sec
run_empty_schedule/SingleThreaded                                                                         1.00     12.5±0.22ns        ? ?/sec      1.34     16.8±1.34ns        ? ?/sec
schedule/base                                                                                             1.00     18.1±1.10µs        ? ?/sec      2.18     39.6±1.89µs        ? ?/sec

Future Work

@james7132 james7132 added this to the 0.18 milestone Jul 30, 2025
@james7132 james7132 requested a review from hymm July 30, 2025 04:00
@james7132 james7132 added C-Performance A change motivated by improving speed, memory usage or compile times C-Code-Quality A section of code that is hard to understand or change A-Tasks Tools for parallel and async work M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide X-Controversial There is active debate or serious implications around merging this PR S-Needs-Benchmarking This set of changes needs performance benchmarking to double-check that they help labels Jul 30, 2025
@github-actions
Copy link
Contributor

You added a new feature but didn't update the readme. Please run cargo run -p build-templated-pages -- update features to update it, and commit the file change.

F: Future + 'a,
F::Output: 'a,
{
// SAFETY: Original implementation missing safety documentation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this safety comment mean?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was copied over from the other edge_executor spawn_* variants. It otherwise was missing the safety justification..

@NthTensor
Copy link
Contributor

As I'm starting my review of this, I want to leave some broad observations.

  • All of the changes on the bevy side of things look great. This will simplify moving stuff off the main thread and in many cases are identical to changes I have in my prototype forte integration branch.
  • Changing single-threaded scopes to store a list of tasks seems like a pretty good idea. Might be worth splitting that off and merging it for 0.16, assuming there's still time.
  • The vendored version of async_tasks has significant differences from the current crate. I'm pleased to see it, but these will still require a fairly in-depth review.

I think I'll be focusing on the vendored code going forward. The other changes all look pretty reasonable.

Copy link
Contributor

@NthTensor NthTensor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot of work, and is a substantial improvement to both bevy_tasks and imo the original async_executor code. The safety comments all look correct to me, and I can't see any issues.

The original async_executor leaves something to be desired in terms of documentation. I'd prefer if documented all the types and functions in the vendored module. But I won't block on that.

@NthTensor
Copy link
Contributor

Uh, for some reason my review comments don't seem to be loading.

Not sure why that is. Anyone else seeing them?

@hymm
Copy link
Contributor

hymm commented Aug 30, 2025

Uh, for some reason my review comments don't seem to be loading.

Not sure why that is. Anyone else seeing them?

I only see one comment on the code.

@NthTensor
Copy link
Contributor

Looks like github ate them. I can still see them in codespaces, so I guess I'll have to copy them over? Kind of annoying.

Give me a sec.

// `Waker`.
let (runnable, task) = unsafe {
Builder::new()
.propagate_panic(true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason I thought this was the default behavior, but I looked it up and I was wrong.

This is correct, and I'll have to adjust forte to match.

Comment on lines +921 to +937
fn steal<T>(src: &ConcurrentQueue<T>, dest: &ConcurrentQueue<T>) {
// Half of `src`'s length rounded up.
let mut count = src.len();

if count > 0 {
if let Some(capacity) = dest.capacity() {
// Don't steal more than fits into the queue.
count = count.min(capacity- dest.len());
}

// Steal tasks.
for _ in 0..count {
let Ok(val) = src.pop() else { break };
assert!(dest.push(val).is_ok());
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to consider using a queue built for work-stealing, like crossbeam_deque or st3.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree we should try testing them out, but I do think this should be reserved for a later PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

Comment on lines 835 to 867
// Try the local queue.
if let Ok(r) = self.local_state.stealable_queue.pop() {
return Some(r);
}

// Try stealing from the global queue.
if let Ok(r) = self.state.queue.pop() {
steal(&self.state.queue, &self.local_state.stealable_queue);
return Some(r);
}

// Try stealing from other runners.
if let Ok(stealer_queues) = self.state.stealer_queues.try_read() {
// Pick a random starting point in the iterator list and rotate the list.
let n = stealer_queues.len();
let start = _rng.usize(..n);
let iter = stealer_queues
.iter()
.chain(stealer_queues.iter())
.skip(start)
.take(n);

// Remove this runner's local queue.
let iter =
iter.filter(|local| !core::ptr::eq(**local, &self.local_state.stealable_queue));

// Try stealing from each local queue in the list.
for local in iter {
steal(*local, &self.local_state.stealable_queue);
if let Ok(r) = self.local_state.stealable_queue.pop() {
return Some(r);
}
}
Copy link
Contributor

@NthTensor NthTensor Aug 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, because this is from the original implementation. I somewhat disagree with the ordering here. I'm pretty sure rayon takes from the local queue, then from peers, then from the global queue, whereas this is local-global-peers. The rational for this is that it's better to finish your current work (stuff already queued on workers) before you start something new (by pulling from the injector queue).

Can we run some benchmarks with alternative orderings at some point?

@james7132
Copy link
Member Author

Uh, for some reason my review comments don't seem to be loading.

Not sure why that is. Anyone else seeing them?

A set of identical comments have been made on #20649, maybe that's where they went?

@NthTensor
Copy link
Contributor

Ah shoot, your right.

Copy link
Contributor

@hymm hymm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good. I have some questions about some of the code I don't quite understand and still want to do some manual testing before I approve.

#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
bevy_tasks::tick_global_task_pools_on_main_thread();
}
while app.plugins_state() == PluginsState::Adding {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm might need to revert the tick_global_task_pools_on_main_thread changes, some of the examples are failling. May need to implement a block_on based on Executor::run here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I investigated why we needed the tick_global_task_pools_on_main_thread in the runners. It started in #8336 and was added to run the IoTask for initializing the renderer. This works in native single threaded, because we run the tasks inline in the spawn. On web this works since we detach the task and poll for the RenderDevice in finish. So the one case I can think of where this might matter is if we're running multithreaded, but we don't spawn any worker threads. I'd be a bit surprised if that's what's happening in ci, since I thought we spawned at least 1 thread in each pool by default.

#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
bevy_tasks::tick_global_task_pools_on_main_thread();
}
while app.plugins_state() == PluginsState::Adding {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should use hint::spin_loop

#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
bevy_tasks::tick_global_task_pools_on_main_thread();
}
while app.plugins_state() == PluginsState::Adding {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this be able to make progress to spawned tasks in single threaded mode? I thought that was why tick_global_task_pools_on_main_thread was added originally.

/// See [`Self::scope`] for more details in general about how scopes work.
pub fn scope_with_executor<'env, F, T>(
&self,
tick_task_pool_executor: bool,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This option was originally added to prevent main thread systems from blocking the render thread scope from finishing and vis versa (when false). Is removing this going to be a regression?

I think the ideal would be that the thread that the scope runs on would only run the tasks spawned on the scope, but not sure how feasible that would be. (Alternatively we could move to using async scope and systems to allow finishing the scope on any thread.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may very well be a regression there. The run call below would add the main/render thread to the list of executor threads without any block to what can run on them. Overall though, I don't think it's a great idea to be blocking those threads from running ComputeTaskPool tasks. Constrained systems with fewer cores (4 or fewer) would likely see underutilization of their hardware. Your suggestion of moving to async schedule runners to avoid the owning thread from hard-blocking might be a better approach. This may require "merging" adjacent schedules though.

I think the ideal would be that the thread that the scope runs on would only run the tasks spawned on the scope, but not sure how feasible that would be. (Alternatively we could move to using async scope and systems to allow finishing the scope on any thread.)

With this PR, there's a softer prioritization. Tasks will be scheduled locally to the current thread, if possible, so assuming no other thread steals those tasks, the scope will be the one to execute them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we merge this we should make an issue to look into fixing this as this will increase frame variance.

Copy link
Contributor

@hymm hymm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First Schedule:
image

This is the histogram for the First schedule. Yellow is this pr. The tail of yellow bad times is due to the main thread running render schedule systems.

Main schedule:
image

This problem extends into the main schedule. But we don't see this problem at the frame level since we're render bound.

Frame:
image

The overall execution time hasn't changed much, but the overall distribution is tighter.

We do see improvements in the schedule execution if we ignore the long tail problems, when the timing is broken down more. So I think this is worth merging it. I think if we made a async scope and a Schedule::run_async then the wins would be more clear. (might also need async systems to run schedules). That work is blocked by removing nonsend resources.

Approving, but the hang on ci needs to be fixed.

@james7132 james7132 added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Oct 3, 2025
@alice-i-cecile alice-i-cecile added X-Contentious There are nontrivial implications that should be thought through and removed X-Controversial There is active debate or serious implications around merging this PR labels Oct 6, 2025
Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the right direction: we need more control over this area of the code base if we're going to reduce system / task overhead, and even these initial improvements are quite nice.

I intend to merge this as we have a consensus of experts, but I'll ping maintainers and check the temperature first.

@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it labels Oct 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Tasks Tools for parallel and async work C-Code-Quality A section of code that is hard to understand or change C-Performance A change motivated by improving speed, memory usage or compile times M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged X-Contentious There are nontrivial implications that should be thought through

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Early system return outperforming conditional execution

5 participants