bevy/crates/bevy_ecs/src/system/commands/command_queue.rs

329 lines
12 KiB
Rust
Raw Normal View History

Speed up `CommandQueue` by storing commands more densely (#6391) # Objective * Speed up inserting and applying commands. * Halve the stack size of `CommandQueue` to 24 bytes. * Require fewer allocations. ## Solution Store commands and metadata densely within the same buffer. Each command takes up 1 `usize` of metadata, plus the bytes to store the command itself. Zero-sized types take up no space except for the metadata. # Benchmarks All of the benchmarks related to commands. | Bench | Time | % Change | p-value | |----------------------------------------|-----------|--------------|-----------------| | empty_commands/0_entities | 4.7780 ns | -18.381% | 0.00 | | spawn_commands/2000_entities | 233.11 us | -0.9961% | 0.00 | | spawn_commands/4000_entities | 448.38 us | -3.1466% | 0.00 | | spawn_commands/6000_entities | 693.12 us | -0.3978% | _0.52_ | | spawn_commands/8000_entities | 889.48 us | -2.8802% | 0.00 | | insert_commands/insert | 609.95 us | -4.8604% | 0.00 | | insert_commands/insert_batch | 355.54 us | -2.8165% | 0.00 | | fake_commands/2000_commands | 4.8018 us | **-17.802%** | 0.00 | | fake_commands/4000_commands | 9.5969 us | **-17.337%** | 0.00 | | fake_commands/6000_commands | 14.421 us | **-18.454%** | 0.00 | | fake_commands/8000_commands | 19.192 us | **-18.261%** | 0.00 | | sized_commands_0_bytes/2000_commands | 4.0593 us | -4.7145% | 0.00 | | sized_commands_0_bytes/4000_commands | 8.1541 us | -4.9470% | 0.00 | | sized_commands_0_bytes/6000_commands | 12.806 us | -12.017% | 0.00 | | sized_commands_0_bytes/8000_commands | 17.096 us | -14.070% | 0.00 | | sized_commands_12_bytes/2000_commands | 5.3425 us | **-27.632%** | 0.00 | | sized_commands_12_bytes/4000_commands | 10.283 us | **-31.158%** | 0.00 | | sized_commands_12_bytes/6000_commands | 15.339 us | **-31.418%** | 0.00 | | sized_commands_12_bytes/8000_commands | 20.206 us | **-33.133%** | 0.00 | | sized_commands_512_bytes/2000_commands | 99.118 us | -9.9655% | 0.00 | | sized_commands_512_bytes/4000_commands | 201.96 us | -8.8235% | 0.00 | | sized_commands_512_bytes/6000_commands | 300.95 us | -9.2344% | 0.00 | | sized_commands_512_bytes/8000_commands | 404.69 us | -8.4578% | 0.00 |
2023-01-28 01:15:51 +00:00
use std::mem::MaybeUninit;
use bevy_ptr::{OwningPtr, Unaligned};
use super::Command;
use crate::world::World;
struct CommandMeta {
/// SAFETY: The `value` must point to a value of type `T: Command`,
/// where `T` is some specific type that was used to produce this metadata.
Speed up `CommandQueue` by storing commands more densely (#6391) # Objective * Speed up inserting and applying commands. * Halve the stack size of `CommandQueue` to 24 bytes. * Require fewer allocations. ## Solution Store commands and metadata densely within the same buffer. Each command takes up 1 `usize` of metadata, plus the bytes to store the command itself. Zero-sized types take up no space except for the metadata. # Benchmarks All of the benchmarks related to commands. | Bench | Time | % Change | p-value | |----------------------------------------|-----------|--------------|-----------------| | empty_commands/0_entities | 4.7780 ns | -18.381% | 0.00 | | spawn_commands/2000_entities | 233.11 us | -0.9961% | 0.00 | | spawn_commands/4000_entities | 448.38 us | -3.1466% | 0.00 | | spawn_commands/6000_entities | 693.12 us | -0.3978% | _0.52_ | | spawn_commands/8000_entities | 889.48 us | -2.8802% | 0.00 | | insert_commands/insert | 609.95 us | -4.8604% | 0.00 | | insert_commands/insert_batch | 355.54 us | -2.8165% | 0.00 | | fake_commands/2000_commands | 4.8018 us | **-17.802%** | 0.00 | | fake_commands/4000_commands | 9.5969 us | **-17.337%** | 0.00 | | fake_commands/6000_commands | 14.421 us | **-18.454%** | 0.00 | | fake_commands/8000_commands | 19.192 us | **-18.261%** | 0.00 | | sized_commands_0_bytes/2000_commands | 4.0593 us | -4.7145% | 0.00 | | sized_commands_0_bytes/4000_commands | 8.1541 us | -4.9470% | 0.00 | | sized_commands_0_bytes/6000_commands | 12.806 us | -12.017% | 0.00 | | sized_commands_0_bytes/8000_commands | 17.096 us | -14.070% | 0.00 | | sized_commands_12_bytes/2000_commands | 5.3425 us | **-27.632%** | 0.00 | | sized_commands_12_bytes/4000_commands | 10.283 us | **-31.158%** | 0.00 | | sized_commands_12_bytes/6000_commands | 15.339 us | **-31.418%** | 0.00 | | sized_commands_12_bytes/8000_commands | 20.206 us | **-33.133%** | 0.00 | | sized_commands_512_bytes/2000_commands | 99.118 us | -9.9655% | 0.00 | | sized_commands_512_bytes/4000_commands | 201.96 us | -8.8235% | 0.00 | | sized_commands_512_bytes/6000_commands | 300.95 us | -9.2344% | 0.00 | | sized_commands_512_bytes/8000_commands | 404.69 us | -8.4578% | 0.00 |
2023-01-28 01:15:51 +00:00
///
/// `world` is optional to allow this one function pointer to perform double-duty as a drop.
///
Speed up `CommandQueue` by storing commands more densely (#6391) # Objective * Speed up inserting and applying commands. * Halve the stack size of `CommandQueue` to 24 bytes. * Require fewer allocations. ## Solution Store commands and metadata densely within the same buffer. Each command takes up 1 `usize` of metadata, plus the bytes to store the command itself. Zero-sized types take up no space except for the metadata. # Benchmarks All of the benchmarks related to commands. | Bench | Time | % Change | p-value | |----------------------------------------|-----------|--------------|-----------------| | empty_commands/0_entities | 4.7780 ns | -18.381% | 0.00 | | spawn_commands/2000_entities | 233.11 us | -0.9961% | 0.00 | | spawn_commands/4000_entities | 448.38 us | -3.1466% | 0.00 | | spawn_commands/6000_entities | 693.12 us | -0.3978% | _0.52_ | | spawn_commands/8000_entities | 889.48 us | -2.8802% | 0.00 | | insert_commands/insert | 609.95 us | -4.8604% | 0.00 | | insert_commands/insert_batch | 355.54 us | -2.8165% | 0.00 | | fake_commands/2000_commands | 4.8018 us | **-17.802%** | 0.00 | | fake_commands/4000_commands | 9.5969 us | **-17.337%** | 0.00 | | fake_commands/6000_commands | 14.421 us | **-18.454%** | 0.00 | | fake_commands/8000_commands | 19.192 us | **-18.261%** | 0.00 | | sized_commands_0_bytes/2000_commands | 4.0593 us | -4.7145% | 0.00 | | sized_commands_0_bytes/4000_commands | 8.1541 us | -4.9470% | 0.00 | | sized_commands_0_bytes/6000_commands | 12.806 us | -12.017% | 0.00 | | sized_commands_0_bytes/8000_commands | 17.096 us | -14.070% | 0.00 | | sized_commands_12_bytes/2000_commands | 5.3425 us | **-27.632%** | 0.00 | | sized_commands_12_bytes/4000_commands | 10.283 us | **-31.158%** | 0.00 | | sized_commands_12_bytes/6000_commands | 15.339 us | **-31.418%** | 0.00 | | sized_commands_12_bytes/8000_commands | 20.206 us | **-33.133%** | 0.00 | | sized_commands_512_bytes/2000_commands | 99.118 us | -9.9655% | 0.00 | | sized_commands_512_bytes/4000_commands | 201.96 us | -8.8235% | 0.00 | | sized_commands_512_bytes/6000_commands | 300.95 us | -9.2344% | 0.00 | | sized_commands_512_bytes/8000_commands | 404.69 us | -8.4578% | 0.00 |
2023-01-28 01:15:51 +00:00
/// Returns the size of `T` in bytes.
consume_command_and_get_size:
unsafe fn(value: OwningPtr<Unaligned>, world: &mut Option<&mut World>) -> usize,
}
Speed up `CommandQueue` by storing commands more densely (#6391) # Objective * Speed up inserting and applying commands. * Halve the stack size of `CommandQueue` to 24 bytes. * Require fewer allocations. ## Solution Store commands and metadata densely within the same buffer. Each command takes up 1 `usize` of metadata, plus the bytes to store the command itself. Zero-sized types take up no space except for the metadata. # Benchmarks All of the benchmarks related to commands. | Bench | Time | % Change | p-value | |----------------------------------------|-----------|--------------|-----------------| | empty_commands/0_entities | 4.7780 ns | -18.381% | 0.00 | | spawn_commands/2000_entities | 233.11 us | -0.9961% | 0.00 | | spawn_commands/4000_entities | 448.38 us | -3.1466% | 0.00 | | spawn_commands/6000_entities | 693.12 us | -0.3978% | _0.52_ | | spawn_commands/8000_entities | 889.48 us | -2.8802% | 0.00 | | insert_commands/insert | 609.95 us | -4.8604% | 0.00 | | insert_commands/insert_batch | 355.54 us | -2.8165% | 0.00 | | fake_commands/2000_commands | 4.8018 us | **-17.802%** | 0.00 | | fake_commands/4000_commands | 9.5969 us | **-17.337%** | 0.00 | | fake_commands/6000_commands | 14.421 us | **-18.454%** | 0.00 | | fake_commands/8000_commands | 19.192 us | **-18.261%** | 0.00 | | sized_commands_0_bytes/2000_commands | 4.0593 us | -4.7145% | 0.00 | | sized_commands_0_bytes/4000_commands | 8.1541 us | -4.9470% | 0.00 | | sized_commands_0_bytes/6000_commands | 12.806 us | -12.017% | 0.00 | | sized_commands_0_bytes/8000_commands | 17.096 us | -14.070% | 0.00 | | sized_commands_12_bytes/2000_commands | 5.3425 us | **-27.632%** | 0.00 | | sized_commands_12_bytes/4000_commands | 10.283 us | **-31.158%** | 0.00 | | sized_commands_12_bytes/6000_commands | 15.339 us | **-31.418%** | 0.00 | | sized_commands_12_bytes/8000_commands | 20.206 us | **-33.133%** | 0.00 | | sized_commands_512_bytes/2000_commands | 99.118 us | -9.9655% | 0.00 | | sized_commands_512_bytes/4000_commands | 201.96 us | -8.8235% | 0.00 | | sized_commands_512_bytes/6000_commands | 300.95 us | -9.2344% | 0.00 | | sized_commands_512_bytes/8000_commands | 404.69 us | -8.4578% | 0.00 |
2023-01-28 01:15:51 +00:00
/// Densely and efficiently stores a queue of heterogenous types implementing [`Command`].
//
// NOTE: [`CommandQueue`] is implemented via a `Vec<MaybeUninit<u8>>` instead of a `Vec<Box<dyn Command>>`
// as an optimization. Since commands are used frequently in systems as a way to spawn
// entities/components/resources, and it's not currently possible to parallelize these
// due to mutable [`World`] access, maximizing performance for [`CommandQueue`] is
// preferred to simplicity of implementation.
#[derive(Default)]
pub struct CommandQueue {
Speed up `CommandQueue` by storing commands more densely (#6391) # Objective * Speed up inserting and applying commands. * Halve the stack size of `CommandQueue` to 24 bytes. * Require fewer allocations. ## Solution Store commands and metadata densely within the same buffer. Each command takes up 1 `usize` of metadata, plus the bytes to store the command itself. Zero-sized types take up no space except for the metadata. # Benchmarks All of the benchmarks related to commands. | Bench | Time | % Change | p-value | |----------------------------------------|-----------|--------------|-----------------| | empty_commands/0_entities | 4.7780 ns | -18.381% | 0.00 | | spawn_commands/2000_entities | 233.11 us | -0.9961% | 0.00 | | spawn_commands/4000_entities | 448.38 us | -3.1466% | 0.00 | | spawn_commands/6000_entities | 693.12 us | -0.3978% | _0.52_ | | spawn_commands/8000_entities | 889.48 us | -2.8802% | 0.00 | | insert_commands/insert | 609.95 us | -4.8604% | 0.00 | | insert_commands/insert_batch | 355.54 us | -2.8165% | 0.00 | | fake_commands/2000_commands | 4.8018 us | **-17.802%** | 0.00 | | fake_commands/4000_commands | 9.5969 us | **-17.337%** | 0.00 | | fake_commands/6000_commands | 14.421 us | **-18.454%** | 0.00 | | fake_commands/8000_commands | 19.192 us | **-18.261%** | 0.00 | | sized_commands_0_bytes/2000_commands | 4.0593 us | -4.7145% | 0.00 | | sized_commands_0_bytes/4000_commands | 8.1541 us | -4.9470% | 0.00 | | sized_commands_0_bytes/6000_commands | 12.806 us | -12.017% | 0.00 | | sized_commands_0_bytes/8000_commands | 17.096 us | -14.070% | 0.00 | | sized_commands_12_bytes/2000_commands | 5.3425 us | **-27.632%** | 0.00 | | sized_commands_12_bytes/4000_commands | 10.283 us | **-31.158%** | 0.00 | | sized_commands_12_bytes/6000_commands | 15.339 us | **-31.418%** | 0.00 | | sized_commands_12_bytes/8000_commands | 20.206 us | **-33.133%** | 0.00 | | sized_commands_512_bytes/2000_commands | 99.118 us | -9.9655% | 0.00 | | sized_commands_512_bytes/4000_commands | 201.96 us | -8.8235% | 0.00 | | sized_commands_512_bytes/6000_commands | 300.95 us | -9.2344% | 0.00 | | sized_commands_512_bytes/8000_commands | 404.69 us | -8.4578% | 0.00 |
2023-01-28 01:15:51 +00:00
// This buffer densely stores all queued commands.
//
// For each command, one `CommandMeta` is stored, followed by zero or more bytes
// to store the command itself. To interpret these bytes, a pointer must
// be passed to the corresponding `CommandMeta.apply_command_and_get_size` fn pointer.
bytes: Vec<MaybeUninit<u8>>,
}
add more `SAFETY` comments and lint for missing ones in `bevy_ecs` (#4835) # Objective `SAFETY` comments are meant to be placed before `unsafe` blocks and should contain the reasoning of why in this case the usage of unsafe is okay. This is useful when reading the code because it makes it clear which assumptions are required for safety, and makes it easier to spot possible unsoundness holes. It also forces the code writer to think of something to write and maybe look at the safety contracts of any called unsafe methods again to double-check their correct usage. There's a clippy lint called `undocumented_unsafe_blocks` which warns when using a block without such a comment. ## Solution - since clippy expects `SAFETY` instead of `SAFE`, rename those - add `SAFETY` comments in more places - for the last remaining 3 places, add an `#[allow()]` and `// TODO` since I wasn't comfortable enough with the code to justify their safety - add ` #![warn(clippy::undocumented_unsafe_blocks)]` to `bevy_ecs` ### Note for reviewers The first commit only renames `SAFETY` to `SAFE` so it doesn't need a thorough review. https://github.com/bevyengine/bevy/pull/4835/files/cb042a416ecbe5e7d74797449969e064d8a5f13c..55cef2d6fa3aa634667a60f6d5abc16f43f16298 is the diff for all other changes. ### Safety comments where I'm not too familiar with the code https://github.com/bevyengine/bevy/blob/774012ece50e4add4fcc8324ec48bbecf5546c3c/crates/bevy_ecs/src/entity/mod.rs#L540-L546 https://github.com/bevyengine/bevy/blob/774012ece50e4add4fcc8324ec48bbecf5546c3c/crates/bevy_ecs/src/world/entity_ref.rs#L249-L252 ### Locations left undocumented with a `TODO` comment https://github.com/bevyengine/bevy/blob/5dde944a3051426ac69fdedc5699f7da97a7e147/crates/bevy_ecs/src/schedule/executor_parallel.rs#L196-L199 https://github.com/bevyengine/bevy/blob/5dde944a3051426ac69fdedc5699f7da97a7e147/crates/bevy_ecs/src/world/entity_ref.rs#L287-L289 https://github.com/bevyengine/bevy/blob/5dde944a3051426ac69fdedc5699f7da97a7e147/crates/bevy_ecs/src/world/entity_ref.rs#L413-L415 Co-authored-by: Jakob Hellermann <hellermann@sipgate.de>
2022-07-04 14:44:24 +00:00
// SAFETY: All commands [`Command`] implement [`Send`]
unsafe impl Send for CommandQueue {}
add more `SAFETY` comments and lint for missing ones in `bevy_ecs` (#4835) # Objective `SAFETY` comments are meant to be placed before `unsafe` blocks and should contain the reasoning of why in this case the usage of unsafe is okay. This is useful when reading the code because it makes it clear which assumptions are required for safety, and makes it easier to spot possible unsoundness holes. It also forces the code writer to think of something to write and maybe look at the safety contracts of any called unsafe methods again to double-check their correct usage. There's a clippy lint called `undocumented_unsafe_blocks` which warns when using a block without such a comment. ## Solution - since clippy expects `SAFETY` instead of `SAFE`, rename those - add `SAFETY` comments in more places - for the last remaining 3 places, add an `#[allow()]` and `// TODO` since I wasn't comfortable enough with the code to justify their safety - add ` #![warn(clippy::undocumented_unsafe_blocks)]` to `bevy_ecs` ### Note for reviewers The first commit only renames `SAFETY` to `SAFE` so it doesn't need a thorough review. https://github.com/bevyengine/bevy/pull/4835/files/cb042a416ecbe5e7d74797449969e064d8a5f13c..55cef2d6fa3aa634667a60f6d5abc16f43f16298 is the diff for all other changes. ### Safety comments where I'm not too familiar with the code https://github.com/bevyengine/bevy/blob/774012ece50e4add4fcc8324ec48bbecf5546c3c/crates/bevy_ecs/src/entity/mod.rs#L540-L546 https://github.com/bevyengine/bevy/blob/774012ece50e4add4fcc8324ec48bbecf5546c3c/crates/bevy_ecs/src/world/entity_ref.rs#L249-L252 ### Locations left undocumented with a `TODO` comment https://github.com/bevyengine/bevy/blob/5dde944a3051426ac69fdedc5699f7da97a7e147/crates/bevy_ecs/src/schedule/executor_parallel.rs#L196-L199 https://github.com/bevyengine/bevy/blob/5dde944a3051426ac69fdedc5699f7da97a7e147/crates/bevy_ecs/src/world/entity_ref.rs#L287-L289 https://github.com/bevyengine/bevy/blob/5dde944a3051426ac69fdedc5699f7da97a7e147/crates/bevy_ecs/src/world/entity_ref.rs#L413-L415 Co-authored-by: Jakob Hellermann <hellermann@sipgate.de>
2022-07-04 14:44:24 +00:00
// SAFETY: `&CommandQueue` never gives access to the inner commands.
unsafe impl Sync for CommandQueue {}
impl CommandQueue {
/// Push a [`Command`] onto the queue.
#[inline]
pub fn push<C>(&mut self, command: C)
where
C: Command,
{
Speed up `CommandQueue` by storing commands more densely (#6391) # Objective * Speed up inserting and applying commands. * Halve the stack size of `CommandQueue` to 24 bytes. * Require fewer allocations. ## Solution Store commands and metadata densely within the same buffer. Each command takes up 1 `usize` of metadata, plus the bytes to store the command itself. Zero-sized types take up no space except for the metadata. # Benchmarks All of the benchmarks related to commands. | Bench | Time | % Change | p-value | |----------------------------------------|-----------|--------------|-----------------| | empty_commands/0_entities | 4.7780 ns | -18.381% | 0.00 | | spawn_commands/2000_entities | 233.11 us | -0.9961% | 0.00 | | spawn_commands/4000_entities | 448.38 us | -3.1466% | 0.00 | | spawn_commands/6000_entities | 693.12 us | -0.3978% | _0.52_ | | spawn_commands/8000_entities | 889.48 us | -2.8802% | 0.00 | | insert_commands/insert | 609.95 us | -4.8604% | 0.00 | | insert_commands/insert_batch | 355.54 us | -2.8165% | 0.00 | | fake_commands/2000_commands | 4.8018 us | **-17.802%** | 0.00 | | fake_commands/4000_commands | 9.5969 us | **-17.337%** | 0.00 | | fake_commands/6000_commands | 14.421 us | **-18.454%** | 0.00 | | fake_commands/8000_commands | 19.192 us | **-18.261%** | 0.00 | | sized_commands_0_bytes/2000_commands | 4.0593 us | -4.7145% | 0.00 | | sized_commands_0_bytes/4000_commands | 8.1541 us | -4.9470% | 0.00 | | sized_commands_0_bytes/6000_commands | 12.806 us | -12.017% | 0.00 | | sized_commands_0_bytes/8000_commands | 17.096 us | -14.070% | 0.00 | | sized_commands_12_bytes/2000_commands | 5.3425 us | **-27.632%** | 0.00 | | sized_commands_12_bytes/4000_commands | 10.283 us | **-31.158%** | 0.00 | | sized_commands_12_bytes/6000_commands | 15.339 us | **-31.418%** | 0.00 | | sized_commands_12_bytes/8000_commands | 20.206 us | **-33.133%** | 0.00 | | sized_commands_512_bytes/2000_commands | 99.118 us | -9.9655% | 0.00 | | sized_commands_512_bytes/4000_commands | 201.96 us | -8.8235% | 0.00 | | sized_commands_512_bytes/6000_commands | 300.95 us | -9.2344% | 0.00 | | sized_commands_512_bytes/8000_commands | 404.69 us | -8.4578% | 0.00 |
2023-01-28 01:15:51 +00:00
// Stores a command alongside its metadata.
// `repr(C)` prevents the compiler from reordering the fields,
// while `repr(packed)` prevents the compiler from inserting padding bytes.
#[repr(C, packed)]
struct Packed<T: Command> {
meta: CommandMeta,
command: T,
}
Speed up `CommandQueue` by storing commands more densely (#6391) # Objective * Speed up inserting and applying commands. * Halve the stack size of `CommandQueue` to 24 bytes. * Require fewer allocations. ## Solution Store commands and metadata densely within the same buffer. Each command takes up 1 `usize` of metadata, plus the bytes to store the command itself. Zero-sized types take up no space except for the metadata. # Benchmarks All of the benchmarks related to commands. | Bench | Time | % Change | p-value | |----------------------------------------|-----------|--------------|-----------------| | empty_commands/0_entities | 4.7780 ns | -18.381% | 0.00 | | spawn_commands/2000_entities | 233.11 us | -0.9961% | 0.00 | | spawn_commands/4000_entities | 448.38 us | -3.1466% | 0.00 | | spawn_commands/6000_entities | 693.12 us | -0.3978% | _0.52_ | | spawn_commands/8000_entities | 889.48 us | -2.8802% | 0.00 | | insert_commands/insert | 609.95 us | -4.8604% | 0.00 | | insert_commands/insert_batch | 355.54 us | -2.8165% | 0.00 | | fake_commands/2000_commands | 4.8018 us | **-17.802%** | 0.00 | | fake_commands/4000_commands | 9.5969 us | **-17.337%** | 0.00 | | fake_commands/6000_commands | 14.421 us | **-18.454%** | 0.00 | | fake_commands/8000_commands | 19.192 us | **-18.261%** | 0.00 | | sized_commands_0_bytes/2000_commands | 4.0593 us | -4.7145% | 0.00 | | sized_commands_0_bytes/4000_commands | 8.1541 us | -4.9470% | 0.00 | | sized_commands_0_bytes/6000_commands | 12.806 us | -12.017% | 0.00 | | sized_commands_0_bytes/8000_commands | 17.096 us | -14.070% | 0.00 | | sized_commands_12_bytes/2000_commands | 5.3425 us | **-27.632%** | 0.00 | | sized_commands_12_bytes/4000_commands | 10.283 us | **-31.158%** | 0.00 | | sized_commands_12_bytes/6000_commands | 15.339 us | **-31.418%** | 0.00 | | sized_commands_12_bytes/8000_commands | 20.206 us | **-33.133%** | 0.00 | | sized_commands_512_bytes/2000_commands | 99.118 us | -9.9655% | 0.00 | | sized_commands_512_bytes/4000_commands | 201.96 us | -8.8235% | 0.00 | | sized_commands_512_bytes/6000_commands | 300.95 us | -9.2344% | 0.00 | | sized_commands_512_bytes/8000_commands | 404.69 us | -8.4578% | 0.00 |
2023-01-28 01:15:51 +00:00
let meta = CommandMeta {
consume_command_and_get_size: |command, world| {
// SAFETY: According to the invariants of `CommandMeta.consume_command_and_get_size`,
// `command` must point to a value of type `C`.
let command: C = unsafe { command.read_unaligned() };
match world {
// Apply command to the provided world...
Some(world) => command.apply(world),
// ...or discard it.
None => drop(command),
}
Speed up `CommandQueue` by storing commands more densely (#6391) # Objective * Speed up inserting and applying commands. * Halve the stack size of `CommandQueue` to 24 bytes. * Require fewer allocations. ## Solution Store commands and metadata densely within the same buffer. Each command takes up 1 `usize` of metadata, plus the bytes to store the command itself. Zero-sized types take up no space except for the metadata. # Benchmarks All of the benchmarks related to commands. | Bench | Time | % Change | p-value | |----------------------------------------|-----------|--------------|-----------------| | empty_commands/0_entities | 4.7780 ns | -18.381% | 0.00 | | spawn_commands/2000_entities | 233.11 us | -0.9961% | 0.00 | | spawn_commands/4000_entities | 448.38 us | -3.1466% | 0.00 | | spawn_commands/6000_entities | 693.12 us | -0.3978% | _0.52_ | | spawn_commands/8000_entities | 889.48 us | -2.8802% | 0.00 | | insert_commands/insert | 609.95 us | -4.8604% | 0.00 | | insert_commands/insert_batch | 355.54 us | -2.8165% | 0.00 | | fake_commands/2000_commands | 4.8018 us | **-17.802%** | 0.00 | | fake_commands/4000_commands | 9.5969 us | **-17.337%** | 0.00 | | fake_commands/6000_commands | 14.421 us | **-18.454%** | 0.00 | | fake_commands/8000_commands | 19.192 us | **-18.261%** | 0.00 | | sized_commands_0_bytes/2000_commands | 4.0593 us | -4.7145% | 0.00 | | sized_commands_0_bytes/4000_commands | 8.1541 us | -4.9470% | 0.00 | | sized_commands_0_bytes/6000_commands | 12.806 us | -12.017% | 0.00 | | sized_commands_0_bytes/8000_commands | 17.096 us | -14.070% | 0.00 | | sized_commands_12_bytes/2000_commands | 5.3425 us | **-27.632%** | 0.00 | | sized_commands_12_bytes/4000_commands | 10.283 us | **-31.158%** | 0.00 | | sized_commands_12_bytes/6000_commands | 15.339 us | **-31.418%** | 0.00 | | sized_commands_12_bytes/8000_commands | 20.206 us | **-33.133%** | 0.00 | | sized_commands_512_bytes/2000_commands | 99.118 us | -9.9655% | 0.00 | | sized_commands_512_bytes/4000_commands | 201.96 us | -8.8235% | 0.00 | | sized_commands_512_bytes/6000_commands | 300.95 us | -9.2344% | 0.00 | | sized_commands_512_bytes/8000_commands | 404.69 us | -8.4578% | 0.00 |
2023-01-28 01:15:51 +00:00
std::mem::size_of::<C>()
},
Speed up `CommandQueue` by storing commands more densely (#6391) # Objective * Speed up inserting and applying commands. * Halve the stack size of `CommandQueue` to 24 bytes. * Require fewer allocations. ## Solution Store commands and metadata densely within the same buffer. Each command takes up 1 `usize` of metadata, plus the bytes to store the command itself. Zero-sized types take up no space except for the metadata. # Benchmarks All of the benchmarks related to commands. | Bench | Time | % Change | p-value | |----------------------------------------|-----------|--------------|-----------------| | empty_commands/0_entities | 4.7780 ns | -18.381% | 0.00 | | spawn_commands/2000_entities | 233.11 us | -0.9961% | 0.00 | | spawn_commands/4000_entities | 448.38 us | -3.1466% | 0.00 | | spawn_commands/6000_entities | 693.12 us | -0.3978% | _0.52_ | | spawn_commands/8000_entities | 889.48 us | -2.8802% | 0.00 | | insert_commands/insert | 609.95 us | -4.8604% | 0.00 | | insert_commands/insert_batch | 355.54 us | -2.8165% | 0.00 | | fake_commands/2000_commands | 4.8018 us | **-17.802%** | 0.00 | | fake_commands/4000_commands | 9.5969 us | **-17.337%** | 0.00 | | fake_commands/6000_commands | 14.421 us | **-18.454%** | 0.00 | | fake_commands/8000_commands | 19.192 us | **-18.261%** | 0.00 | | sized_commands_0_bytes/2000_commands | 4.0593 us | -4.7145% | 0.00 | | sized_commands_0_bytes/4000_commands | 8.1541 us | -4.9470% | 0.00 | | sized_commands_0_bytes/6000_commands | 12.806 us | -12.017% | 0.00 | | sized_commands_0_bytes/8000_commands | 17.096 us | -14.070% | 0.00 | | sized_commands_12_bytes/2000_commands | 5.3425 us | **-27.632%** | 0.00 | | sized_commands_12_bytes/4000_commands | 10.283 us | **-31.158%** | 0.00 | | sized_commands_12_bytes/6000_commands | 15.339 us | **-31.418%** | 0.00 | | sized_commands_12_bytes/8000_commands | 20.206 us | **-33.133%** | 0.00 | | sized_commands_512_bytes/2000_commands | 99.118 us | -9.9655% | 0.00 | | sized_commands_512_bytes/4000_commands | 201.96 us | -8.8235% | 0.00 | | sized_commands_512_bytes/6000_commands | 300.95 us | -9.2344% | 0.00 | | sized_commands_512_bytes/8000_commands | 404.69 us | -8.4578% | 0.00 |
2023-01-28 01:15:51 +00:00
};
let old_len = self.bytes.len();
// Reserve enough bytes for both the metadata and the command itself.
self.bytes.reserve(std::mem::size_of::<Packed<C>>());
// Pointer to the bytes at the end of the buffer.
// SAFETY: We know it is within bounds of the allocation, due to the call to `.reserve()`.
let ptr = unsafe { self.bytes.as_mut_ptr().add(old_len) };
// Write the metadata into the buffer, followed by the command.
// We are using a packed struct to write them both as one operation.
// SAFETY: `ptr` must be non-null, since it is within a non-null buffer.
// The call to `reserve()` ensures that the buffer has enough space to fit a value of type `C`,
// and it is valid to write any bit pattern since the underlying buffer is of type `MaybeUninit<u8>`.
unsafe {
ptr.cast::<Packed<C>>()
.write_unaligned(Packed { meta, command });
}
// Extend the length of the buffer to include the data we just wrote.
// SAFETY: The new length is guaranteed to fit in the vector's capacity,
// due to the call to `.reserve()` above.
unsafe {
self.bytes
.set_len(old_len + std::mem::size_of::<Packed<C>>());
}
}
/// Execute the queued [`Command`]s in the world.
/// This clears the queue.
#[inline]
pub fn apply(&mut self, world: &mut World) {
// flush the previously queued entities
world.flush();
self.apply_or_drop_queued(Some(world));
}
/// If `world` is [`Some`], this will apply the queued [commands](`Command`).
/// If `world` is [`None`], this will drop the queued [commands](`Command`) (without applying them).
/// This clears the queue.
#[inline]
fn apply_or_drop_queued(&mut self, mut world: Option<&mut World>) {
// The range of pointers of the filled portion of `self.bytes`.
let bytes_range = self.bytes.as_mut_ptr_range();
Speed up `CommandQueue` by storing commands more densely (#6391) # Objective * Speed up inserting and applying commands. * Halve the stack size of `CommandQueue` to 24 bytes. * Require fewer allocations. ## Solution Store commands and metadata densely within the same buffer. Each command takes up 1 `usize` of metadata, plus the bytes to store the command itself. Zero-sized types take up no space except for the metadata. # Benchmarks All of the benchmarks related to commands. | Bench | Time | % Change | p-value | |----------------------------------------|-----------|--------------|-----------------| | empty_commands/0_entities | 4.7780 ns | -18.381% | 0.00 | | spawn_commands/2000_entities | 233.11 us | -0.9961% | 0.00 | | spawn_commands/4000_entities | 448.38 us | -3.1466% | 0.00 | | spawn_commands/6000_entities | 693.12 us | -0.3978% | _0.52_ | | spawn_commands/8000_entities | 889.48 us | -2.8802% | 0.00 | | insert_commands/insert | 609.95 us | -4.8604% | 0.00 | | insert_commands/insert_batch | 355.54 us | -2.8165% | 0.00 | | fake_commands/2000_commands | 4.8018 us | **-17.802%** | 0.00 | | fake_commands/4000_commands | 9.5969 us | **-17.337%** | 0.00 | | fake_commands/6000_commands | 14.421 us | **-18.454%** | 0.00 | | fake_commands/8000_commands | 19.192 us | **-18.261%** | 0.00 | | sized_commands_0_bytes/2000_commands | 4.0593 us | -4.7145% | 0.00 | | sized_commands_0_bytes/4000_commands | 8.1541 us | -4.9470% | 0.00 | | sized_commands_0_bytes/6000_commands | 12.806 us | -12.017% | 0.00 | | sized_commands_0_bytes/8000_commands | 17.096 us | -14.070% | 0.00 | | sized_commands_12_bytes/2000_commands | 5.3425 us | **-27.632%** | 0.00 | | sized_commands_12_bytes/4000_commands | 10.283 us | **-31.158%** | 0.00 | | sized_commands_12_bytes/6000_commands | 15.339 us | **-31.418%** | 0.00 | | sized_commands_12_bytes/8000_commands | 20.206 us | **-33.133%** | 0.00 | | sized_commands_512_bytes/2000_commands | 99.118 us | -9.9655% | 0.00 | | sized_commands_512_bytes/4000_commands | 201.96 us | -8.8235% | 0.00 | | sized_commands_512_bytes/6000_commands | 300.95 us | -9.2344% | 0.00 | | sized_commands_512_bytes/8000_commands | 404.69 us | -8.4578% | 0.00 |
2023-01-28 01:15:51 +00:00
// Pointer that will iterate over the entries of the buffer.
let mut cursor = bytes_range.start;
Speed up `CommandQueue` by storing commands more densely (#6391) # Objective * Speed up inserting and applying commands. * Halve the stack size of `CommandQueue` to 24 bytes. * Require fewer allocations. ## Solution Store commands and metadata densely within the same buffer. Each command takes up 1 `usize` of metadata, plus the bytes to store the command itself. Zero-sized types take up no space except for the metadata. # Benchmarks All of the benchmarks related to commands. | Bench | Time | % Change | p-value | |----------------------------------------|-----------|--------------|-----------------| | empty_commands/0_entities | 4.7780 ns | -18.381% | 0.00 | | spawn_commands/2000_entities | 233.11 us | -0.9961% | 0.00 | | spawn_commands/4000_entities | 448.38 us | -3.1466% | 0.00 | | spawn_commands/6000_entities | 693.12 us | -0.3978% | _0.52_ | | spawn_commands/8000_entities | 889.48 us | -2.8802% | 0.00 | | insert_commands/insert | 609.95 us | -4.8604% | 0.00 | | insert_commands/insert_batch | 355.54 us | -2.8165% | 0.00 | | fake_commands/2000_commands | 4.8018 us | **-17.802%** | 0.00 | | fake_commands/4000_commands | 9.5969 us | **-17.337%** | 0.00 | | fake_commands/6000_commands | 14.421 us | **-18.454%** | 0.00 | | fake_commands/8000_commands | 19.192 us | **-18.261%** | 0.00 | | sized_commands_0_bytes/2000_commands | 4.0593 us | -4.7145% | 0.00 | | sized_commands_0_bytes/4000_commands | 8.1541 us | -4.9470% | 0.00 | | sized_commands_0_bytes/6000_commands | 12.806 us | -12.017% | 0.00 | | sized_commands_0_bytes/8000_commands | 17.096 us | -14.070% | 0.00 | | sized_commands_12_bytes/2000_commands | 5.3425 us | **-27.632%** | 0.00 | | sized_commands_12_bytes/4000_commands | 10.283 us | **-31.158%** | 0.00 | | sized_commands_12_bytes/6000_commands | 15.339 us | **-31.418%** | 0.00 | | sized_commands_12_bytes/8000_commands | 20.206 us | **-33.133%** | 0.00 | | sized_commands_512_bytes/2000_commands | 99.118 us | -9.9655% | 0.00 | | sized_commands_512_bytes/4000_commands | 201.96 us | -8.8235% | 0.00 | | sized_commands_512_bytes/6000_commands | 300.95 us | -9.2344% | 0.00 | | sized_commands_512_bytes/8000_commands | 404.69 us | -8.4578% | 0.00 |
2023-01-28 01:15:51 +00:00
// Reset the buffer, so it can be reused after this function ends.
// In the loop below, ownership of each command will be transferred into user code.
// SAFETY: `set_len(0)` is always valid.
unsafe { self.bytes.set_len(0) };
while cursor < bytes_range.end {
Speed up `CommandQueue` by storing commands more densely (#6391) # Objective * Speed up inserting and applying commands. * Halve the stack size of `CommandQueue` to 24 bytes. * Require fewer allocations. ## Solution Store commands and metadata densely within the same buffer. Each command takes up 1 `usize` of metadata, plus the bytes to store the command itself. Zero-sized types take up no space except for the metadata. # Benchmarks All of the benchmarks related to commands. | Bench | Time | % Change | p-value | |----------------------------------------|-----------|--------------|-----------------| | empty_commands/0_entities | 4.7780 ns | -18.381% | 0.00 | | spawn_commands/2000_entities | 233.11 us | -0.9961% | 0.00 | | spawn_commands/4000_entities | 448.38 us | -3.1466% | 0.00 | | spawn_commands/6000_entities | 693.12 us | -0.3978% | _0.52_ | | spawn_commands/8000_entities | 889.48 us | -2.8802% | 0.00 | | insert_commands/insert | 609.95 us | -4.8604% | 0.00 | | insert_commands/insert_batch | 355.54 us | -2.8165% | 0.00 | | fake_commands/2000_commands | 4.8018 us | **-17.802%** | 0.00 | | fake_commands/4000_commands | 9.5969 us | **-17.337%** | 0.00 | | fake_commands/6000_commands | 14.421 us | **-18.454%** | 0.00 | | fake_commands/8000_commands | 19.192 us | **-18.261%** | 0.00 | | sized_commands_0_bytes/2000_commands | 4.0593 us | -4.7145% | 0.00 | | sized_commands_0_bytes/4000_commands | 8.1541 us | -4.9470% | 0.00 | | sized_commands_0_bytes/6000_commands | 12.806 us | -12.017% | 0.00 | | sized_commands_0_bytes/8000_commands | 17.096 us | -14.070% | 0.00 | | sized_commands_12_bytes/2000_commands | 5.3425 us | **-27.632%** | 0.00 | | sized_commands_12_bytes/4000_commands | 10.283 us | **-31.158%** | 0.00 | | sized_commands_12_bytes/6000_commands | 15.339 us | **-31.418%** | 0.00 | | sized_commands_12_bytes/8000_commands | 20.206 us | **-33.133%** | 0.00 | | sized_commands_512_bytes/2000_commands | 99.118 us | -9.9655% | 0.00 | | sized_commands_512_bytes/4000_commands | 201.96 us | -8.8235% | 0.00 | | sized_commands_512_bytes/6000_commands | 300.95 us | -9.2344% | 0.00 | | sized_commands_512_bytes/8000_commands | 404.69 us | -8.4578% | 0.00 |
2023-01-28 01:15:51 +00:00
// SAFETY: The cursor is either at the start of the buffer, or just after the previous command.
// Since we know that the cursor is in bounds, it must point to the start of a new command.
let meta = unsafe { cursor.cast::<CommandMeta>().read_unaligned() };
// Advance to the bytes just after `meta`, which represent a type-erased command.
// SAFETY: For most types of `Command`, the pointer immediately following the metadata
// is guaranteed to be in bounds. If the command is a zero-sized type (ZST), then the cursor
// might be 1 byte past the end of the buffer, which is safe.
cursor = unsafe { cursor.add(std::mem::size_of::<CommandMeta>()) };
// Construct an owned pointer to the command.
// SAFETY: It is safe to transfer ownership out of `self.bytes`, since the call to `set_len(0)` above
// guarantees that nothing stored in the buffer will get observed after this function ends.
// `cmd` points to a valid address of a stored command, so it must be non-null.
Speed up `CommandQueue` by storing commands more densely (#6391) # Objective * Speed up inserting and applying commands. * Halve the stack size of `CommandQueue` to 24 bytes. * Require fewer allocations. ## Solution Store commands and metadata densely within the same buffer. Each command takes up 1 `usize` of metadata, plus the bytes to store the command itself. Zero-sized types take up no space except for the metadata. # Benchmarks All of the benchmarks related to commands. | Bench | Time | % Change | p-value | |----------------------------------------|-----------|--------------|-----------------| | empty_commands/0_entities | 4.7780 ns | -18.381% | 0.00 | | spawn_commands/2000_entities | 233.11 us | -0.9961% | 0.00 | | spawn_commands/4000_entities | 448.38 us | -3.1466% | 0.00 | | spawn_commands/6000_entities | 693.12 us | -0.3978% | _0.52_ | | spawn_commands/8000_entities | 889.48 us | -2.8802% | 0.00 | | insert_commands/insert | 609.95 us | -4.8604% | 0.00 | | insert_commands/insert_batch | 355.54 us | -2.8165% | 0.00 | | fake_commands/2000_commands | 4.8018 us | **-17.802%** | 0.00 | | fake_commands/4000_commands | 9.5969 us | **-17.337%** | 0.00 | | fake_commands/6000_commands | 14.421 us | **-18.454%** | 0.00 | | fake_commands/8000_commands | 19.192 us | **-18.261%** | 0.00 | | sized_commands_0_bytes/2000_commands | 4.0593 us | -4.7145% | 0.00 | | sized_commands_0_bytes/4000_commands | 8.1541 us | -4.9470% | 0.00 | | sized_commands_0_bytes/6000_commands | 12.806 us | -12.017% | 0.00 | | sized_commands_0_bytes/8000_commands | 17.096 us | -14.070% | 0.00 | | sized_commands_12_bytes/2000_commands | 5.3425 us | **-27.632%** | 0.00 | | sized_commands_12_bytes/4000_commands | 10.283 us | **-31.158%** | 0.00 | | sized_commands_12_bytes/6000_commands | 15.339 us | **-31.418%** | 0.00 | | sized_commands_12_bytes/8000_commands | 20.206 us | **-33.133%** | 0.00 | | sized_commands_512_bytes/2000_commands | 99.118 us | -9.9655% | 0.00 | | sized_commands_512_bytes/4000_commands | 201.96 us | -8.8235% | 0.00 | | sized_commands_512_bytes/6000_commands | 300.95 us | -9.2344% | 0.00 | | sized_commands_512_bytes/8000_commands | 404.69 us | -8.4578% | 0.00 |
2023-01-28 01:15:51 +00:00
let cmd = unsafe {
OwningPtr::<Unaligned>::new(std::ptr::NonNull::new_unchecked(cursor.cast()))
};
// SAFETY: The data underneath the cursor must correspond to the type erased in metadata,
// since they were stored next to each other by `.push()`.
// For ZSTs, the type doesn't matter as long as the pointer is non-null.
let size = unsafe { (meta.consume_command_and_get_size)(cmd, &mut world) };
Speed up `CommandQueue` by storing commands more densely (#6391) # Objective * Speed up inserting and applying commands. * Halve the stack size of `CommandQueue` to 24 bytes. * Require fewer allocations. ## Solution Store commands and metadata densely within the same buffer. Each command takes up 1 `usize` of metadata, plus the bytes to store the command itself. Zero-sized types take up no space except for the metadata. # Benchmarks All of the benchmarks related to commands. | Bench | Time | % Change | p-value | |----------------------------------------|-----------|--------------|-----------------| | empty_commands/0_entities | 4.7780 ns | -18.381% | 0.00 | | spawn_commands/2000_entities | 233.11 us | -0.9961% | 0.00 | | spawn_commands/4000_entities | 448.38 us | -3.1466% | 0.00 | | spawn_commands/6000_entities | 693.12 us | -0.3978% | _0.52_ | | spawn_commands/8000_entities | 889.48 us | -2.8802% | 0.00 | | insert_commands/insert | 609.95 us | -4.8604% | 0.00 | | insert_commands/insert_batch | 355.54 us | -2.8165% | 0.00 | | fake_commands/2000_commands | 4.8018 us | **-17.802%** | 0.00 | | fake_commands/4000_commands | 9.5969 us | **-17.337%** | 0.00 | | fake_commands/6000_commands | 14.421 us | **-18.454%** | 0.00 | | fake_commands/8000_commands | 19.192 us | **-18.261%** | 0.00 | | sized_commands_0_bytes/2000_commands | 4.0593 us | -4.7145% | 0.00 | | sized_commands_0_bytes/4000_commands | 8.1541 us | -4.9470% | 0.00 | | sized_commands_0_bytes/6000_commands | 12.806 us | -12.017% | 0.00 | | sized_commands_0_bytes/8000_commands | 17.096 us | -14.070% | 0.00 | | sized_commands_12_bytes/2000_commands | 5.3425 us | **-27.632%** | 0.00 | | sized_commands_12_bytes/4000_commands | 10.283 us | **-31.158%** | 0.00 | | sized_commands_12_bytes/6000_commands | 15.339 us | **-31.418%** | 0.00 | | sized_commands_12_bytes/8000_commands | 20.206 us | **-33.133%** | 0.00 | | sized_commands_512_bytes/2000_commands | 99.118 us | -9.9655% | 0.00 | | sized_commands_512_bytes/4000_commands | 201.96 us | -8.8235% | 0.00 | | sized_commands_512_bytes/6000_commands | 300.95 us | -9.2344% | 0.00 | | sized_commands_512_bytes/8000_commands | 404.69 us | -8.4578% | 0.00 |
2023-01-28 01:15:51 +00:00
// Advance the cursor past the command. For ZSTs, the cursor will not move.
// At this point, it will either point to the next `CommandMeta`,
// or the cursor will be out of bounds and the loop will end.
// SAFETY: The address just past the command is either within the buffer,
// or 1 byte past the end, so this addition will not overflow the pointer's allocation.
cursor = unsafe { cursor.add(size) };
}
}
/// Take all commands from `other` and append them to `self`, leaving `other` empty
pub fn append(&mut self, other: &mut CommandQueue) {
self.bytes.append(&mut other.bytes);
}
}
impl Drop for CommandQueue {
fn drop(&mut self) {
self.apply_or_drop_queued(None);
}
}
#[cfg(test)]
mod test {
use super::*;
use std::{
panic::AssertUnwindSafe,
sync::{
atomic::{AtomicU32, Ordering},
Arc,
},
};
struct DropCheck(Arc<AtomicU32>);
impl DropCheck {
fn new() -> (Self, Arc<AtomicU32>) {
let drops = Arc::new(AtomicU32::new(0));
(Self(drops.clone()), drops)
}
}
impl Drop for DropCheck {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
impl Command for DropCheck {
fn apply(self, _: &mut World) {}
}
#[test]
fn test_command_queue_inner_drop() {
let mut queue = CommandQueue::default();
let (dropcheck_a, drops_a) = DropCheck::new();
let (dropcheck_b, drops_b) = DropCheck::new();
queue.push(dropcheck_a);
queue.push(dropcheck_b);
assert_eq!(drops_a.load(Ordering::Relaxed), 0);
assert_eq!(drops_b.load(Ordering::Relaxed), 0);
let mut world = World::new();
queue.apply(&mut world);
assert_eq!(drops_a.load(Ordering::Relaxed), 1);
assert_eq!(drops_b.load(Ordering::Relaxed), 1);
}
/// Asserts that inner [commands](`Command`) are dropped on early drop of [`CommandQueue`].
/// Originally identified as an issue in [#10676](https://github.com/bevyengine/bevy/issues/10676)
#[test]
fn test_command_queue_inner_drop_early() {
let mut queue = CommandQueue::default();
let (dropcheck_a, drops_a) = DropCheck::new();
let (dropcheck_b, drops_b) = DropCheck::new();
queue.push(dropcheck_a);
queue.push(dropcheck_b);
assert_eq!(drops_a.load(Ordering::Relaxed), 0);
assert_eq!(drops_b.load(Ordering::Relaxed), 0);
drop(queue);
assert_eq!(drops_a.load(Ordering::Relaxed), 1);
assert_eq!(drops_b.load(Ordering::Relaxed), 1);
}
struct SpawnCommand;
impl Command for SpawnCommand {
fn apply(self, world: &mut World) {
Spawn now takes a Bundle (#6054) # Objective Now that we can consolidate Bundles and Components under a single insert (thanks to #2975 and #6039), almost 100% of world spawns now look like `world.spawn().insert((Some, Tuple, Here))`. Spawning an entity without any components is an extremely uncommon pattern, so it makes sense to give spawn the "first class" ergonomic api. This consolidated api should be made consistent across all spawn apis (such as World and Commands). ## Solution All `spawn` apis (`World::spawn`, `Commands:;spawn`, `ChildBuilder::spawn`, and `WorldChildBuilder::spawn`) now accept a bundle as input: ```rust // before: commands .spawn() .insert((A, B, C)); world .spawn() .insert((A, B, C); // after commands.spawn((A, B, C)); world.spawn((A, B, C)); ``` All existing instances of `spawn_bundle` have been deprecated in favor of the new `spawn` api. A new `spawn_empty` has been added, replacing the old `spawn` api. By allowing `world.spawn(some_bundle)` to replace `world.spawn().insert(some_bundle)`, this opened the door to removing the initial entity allocation in the "empty" archetype / table done in `spawn()` (and subsequent move to the actual archetype in `.insert(some_bundle)`). This improves spawn performance by over 10%: ![image](https://user-images.githubusercontent.com/2694663/191627587-4ab2f949-4ccd-4231-80eb-80dd4d9ad6b9.png) To take this measurement, I added a new `world_spawn` benchmark. Unfortunately, optimizing `Commands::spawn` is slightly less trivial, as Commands expose the Entity id of spawned entities prior to actually spawning. Doing the optimization would (naively) require assurances that the `spawn(some_bundle)` command is applied before all other commands involving the entity (which would not necessarily be true, if memory serves). Optimizing `Commands::spawn` this way does feel possible, but it will require careful thought (and maybe some additional checks), which deserves its own PR. For now, it has the same performance characteristics of the current `Commands::spawn_bundle` on main. **Note that 99% of this PR is simple renames and refactors. The only code that needs careful scrutiny is the new `World::spawn()` impl, which is relatively straightforward, but it has some new unsafe code (which re-uses battle tested BundlerSpawner code path).** --- ## Changelog - All `spawn` apis (`World::spawn`, `Commands:;spawn`, `ChildBuilder::spawn`, and `WorldChildBuilder::spawn`) now accept a bundle as input - All instances of `spawn_bundle` have been deprecated in favor of the new `spawn` api - World and Commands now have `spawn_empty()`, which is equivalent to the old `spawn()` behavior. ## Migration Guide ```rust // Old (0.8): commands .spawn() .insert_bundle((A, B, C)); // New (0.9) commands.spawn((A, B, C)); // Old (0.8): commands.spawn_bundle((A, B, C)); // New (0.9) commands.spawn((A, B, C)); // Old (0.8): let entity = commands.spawn().id(); // New (0.9) let entity = commands.spawn_empty().id(); // Old (0.8) let entity = world.spawn().id(); // New (0.9) let entity = world.spawn_empty(); ```
2022-09-23 19:55:54 +00:00
world.spawn_empty();
}
}
#[test]
fn test_command_queue_inner() {
let mut queue = CommandQueue::default();
queue.push(SpawnCommand);
queue.push(SpawnCommand);
let mut world = World::new();
queue.apply(&mut world);
assert_eq!(world.entities().len(), 2);
// The previous call to `apply` cleared the queue.
// This call should do nothing.
queue.apply(&mut world);
assert_eq!(world.entities().len(), 2);
}
// This has an arbitrary value `String` stored to ensure
// when then command gets pushed, the `bytes` vector gets
// some data added to it.
struct PanicCommand(String);
impl Command for PanicCommand {
fn apply(self, _: &mut World) {
panic!("command is panicking");
}
}
#[test]
fn test_command_queue_inner_panic_safe() {
std::panic::set_hook(Box::new(|_| {}));
let mut queue = CommandQueue::default();
queue.push(PanicCommand("I panic!".to_owned()));
queue.push(SpawnCommand);
let mut world = World::new();
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
queue.apply(&mut world);
}));
// even though the first command panicking.
// the `bytes`/`metas` vectors were cleared.
assert_eq!(queue.bytes.len(), 0);
// Even though the first command panicked, it's still ok to push
// more commands.
queue.push(SpawnCommand);
queue.push(SpawnCommand);
queue.apply(&mut world);
assert_eq!(world.entities().len(), 2);
}
// NOTE: `CommandQueue` is `Send` because `Command` is send.
// If the `Command` trait gets reworked to be non-send, `CommandQueue`
// should be reworked.
// This test asserts that Command types are send.
fn assert_is_send_impl(_: impl Send) {}
fn assert_is_send(command: impl Command) {
assert_is_send_impl(command);
}
#[test]
fn test_command_is_send() {
assert_is_send(SpawnCommand);
}
struct CommandWithPadding(u8, u16);
impl Command for CommandWithPadding {
fn apply(self, _: &mut World) {}
}
#[cfg(miri)]
#[test]
fn test_uninit_bytes() {
let mut queue = CommandQueue::default();
queue.push(CommandWithPadding(0, 0));
let _ = format!("{:?}", queue.bytes);
}
}