Speed up Query::get_many and add benchmarks (#6400)

# Objective

* Add benchmarks for `Query::get_many`.
* Speed up `Query::get_many`.

## Solution

Previously, `get_many` and `get_many_mut` used the method `array::map`, which tends to optimize very poorly. This PR replaces uses of that method with loops.

## Benchmarks

| Benchmark name                       | Execution time | Change from this PR |
|--------------------------------------|----------------|---------------------|
| query_get_many_2/50000_calls_table   | 1.3732 ms      | -24.967%            |
| query_get_many_2/50000_calls_sparse  | 1.3826 ms      | -24.572%            |
| query_get_many_5/50000_calls_table   | 2.6833 ms      | -30.681%            |
| query_get_many_5/50000_calls_sparse  | 2.9936 ms      | -30.672%            |
| query_get_many_10/50000_calls_table  | 5.7771 ms      | -36.950%            |
| query_get_many_10/50000_calls_sparse | 7.4345 ms      | -36.987%            |
This commit is contained in:
JoJoJet 2022-11-01 03:51:41 +00:00
parent e7719bf245
commit 3d6706f86d
3 changed files with 78 additions and 26 deletions

View file

@ -27,4 +27,7 @@ criterion_group!(
query_get_component_simple,
query_get_component,
query_get,
query_get_many::<2>,
query_get_many::<5>,
query_get_many::<10>,
);

View file

@ -402,3 +402,58 @@ pub fn query_get(criterion: &mut Criterion) {
group.finish();
}
pub fn query_get_many<const N: usize>(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group(&format!("query_get_many_{N}"));
group.warm_up_time(std::time::Duration::from_millis(500));
group.measurement_time(std::time::Duration::from_secs(2 * N as u64));
for entity_count in RANGE.map(|i| i * 10_000) {
group.bench_function(format!("{}_calls_table", entity_count), |bencher| {
let mut world = World::default();
let mut entity_groups: Vec<_> = (0..entity_count)
.map(|_| [(); N].map(|_| world.spawn(Table::default()).id()))
.collect();
entity_groups.shuffle(&mut deterministic_rand());
let mut query = SystemState::<Query<&Table>>::new(&mut world);
let query = query.get(&world);
bencher.iter(|| {
let mut count = 0;
for comp in entity_groups
.iter()
.filter_map(|&ids| query.get_many(ids).ok())
{
black_box(comp);
count += 1;
black_box(count);
}
assert_eq!(black_box(count), entity_count);
});
});
group.bench_function(format!("{}_calls_sparse", entity_count), |bencher| {
let mut world = World::default();
let mut entity_groups: Vec<_> = (0..entity_count)
.map(|_| [(); N].map(|_| world.spawn(Sparse::default()).id()))
.collect();
entity_groups.shuffle(&mut deterministic_rand());
let mut query = SystemState::<Query<&Sparse>>::new(&mut world);
let query = query.get(&world);
bencher.iter(|| {
let mut count = 0;
for comp in entity_groups
.iter()
.filter_map(|&ids| query.get_many(ids).ok())
{
black_box(comp);
count += 1;
black_box(count);
}
assert_eq!(black_box(count), entity_count);
});
});
}
}

View file

@ -11,7 +11,7 @@ use bevy_tasks::ComputeTaskPool;
#[cfg(feature = "trace")]
use bevy_utils::tracing::Instrument;
use fixedbitset::FixedBitSet;
use std::{borrow::Borrow, fmt};
use std::{borrow::Borrow, fmt, mem::MaybeUninit};
use super::{NopWorldQuery, QueryItem, QueryManyIter, ROQueryItem, ReadOnlyWorldQuery};
@ -438,24 +438,22 @@ impl<Q: WorldQuery, F: ReadOnlyWorldQuery> QueryState<Q, F> {
last_change_tick: u32,
change_tick: u32,
) -> Result<[ROQueryItem<'w, Q>; N], QueryEntityError> {
// SAFETY: fetch is read-only
// and world must be validated
let array_of_results = entities.map(|entity| {
self.as_readonly()
.get_unchecked_manual(world, entity, last_change_tick, change_tick)
});
let mut values = [(); N].map(|_| MaybeUninit::uninit());
// TODO: Replace with TryMap once https://github.com/rust-lang/rust/issues/79711 is stabilized
// If any of the get calls failed, bubble up the error
for result in &array_of_results {
match result {
Ok(_) => (),
Err(error) => return Err(*error),
}
for (value, entity) in std::iter::zip(&mut values, entities) {
// SAFETY: fetch is read-only
// and world must be validated
let item = self.as_readonly().get_unchecked_manual(
world,
entity,
last_change_tick,
change_tick,
)?;
*value = MaybeUninit::new(item);
}
// Since we have verified that all entities are present, we can safely unwrap
Ok(array_of_results.map(|result| result.unwrap()))
// SAFETY: Each value has been fully initialized.
Ok(values.map(|x| x.assume_init()))
}
/// Gets the query results for the given [`World`] and array of [`Entity`], where the last change and
@ -484,19 +482,15 @@ impl<Q: WorldQuery, F: ReadOnlyWorldQuery> QueryState<Q, F> {
}
}
let array_of_results = entities
.map(|entity| self.get_unchecked_manual(world, entity, last_change_tick, change_tick));
let mut values = [(); N].map(|_| MaybeUninit::uninit());
// If any of the get calls failed, bubble up the error
for result in &array_of_results {
match result {
Ok(_) => (),
Err(error) => return Err(*error),
}
for (value, entity) in std::iter::zip(&mut values, entities) {
let item = self.get_unchecked_manual(world, entity, last_change_tick, change_tick)?;
*value = MaybeUninit::new(item);
}
// Since we have verified that all entities are present, we can safely unwrap
Ok(array_of_results.map(|result| result.unwrap()))
// SAFETY: Each value has been fully initialized.
Ok(values.map(|x| x.assume_init()))
}
/// Returns an [`Iterator`] over the query results for the given [`World`].