diff --git a/.gitignore b/.gitignore index bfd162d2e..603a928a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Generated by Cargo /target/ Cargo.lock +.criterion # Generated by mdbook /book/book/ diff --git a/.travis.yml b/.travis.yml index 57d94c229..96cb9b9ed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: rust rust: - nightly +- beta - stable cache: @@ -16,8 +17,8 @@ before_script: - export PATH="$PATH:$HOME/.cargo/bin" script: -- cargo build --features="common,serde,rudy" --verbose -- cargo test --features="common,serde,rudy" --verbose +- cargo build --verbose --features "common serde rudy" +- cargo test --verbose --features "common serde rudy" - if [ "$TRAVIS_RUST_VERSION" == "nightly" ]; then cargo build --all-features --verbose; cargo test --all-features --verbose; diff --git a/Cargo.toml b/Cargo.toml index e5e39ffde..56643ecd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ features = ["common", "serde"] [dev-dependencies] cgmath = { version = "0.14", features = ["eders"] } +criterion = "0.2" ron = "0.1.3" rand = "0.3" serde_json = "1.0" @@ -79,6 +80,16 @@ required-features = ["serde"] name = "saveload" required-features = ["serde"] +[[bench]] +name = "benches_main" +harness = false + +[[bench]] +name = "parallel" + +[[bench]] +name = "world" +harness = false + [workspace] members = ["specs-derive"] - diff --git a/README.md b/README.md index 1c1ce36de..dfa5768a3 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ Unlike most other ECS libraries out there, it provides other and you can use barriers to force several stages in system execution * high performance for real-world applications +Minimum Rust version: 1.21 + ## [Link to the book][book] [book]: https://slide-rs.github.io/specs/ diff --git a/benches/benches_main.rs b/benches/benches_main.rs new file mode 100644 index 000000000..ff6fa7c17 --- /dev/null +++ b/benches/benches_main.rs @@ -0,0 +1,26 @@ +#![feature(test)] + +#[macro_use] +extern crate criterion; +extern crate specs; +extern crate test; + +macro_rules! group { + ($name:ident,$($benches:path),*) => { + pub fn $name(c: &mut Criterion) { + $( + $benches(c); + )* + } + }; +} + +mod storage_cmp; +mod storage_sparse; + +pub use test::black_box; + +use storage_cmp::benches_storages; +use storage_sparse::benches_sparse; + +criterion_main!(benches_storages, benches_sparse); diff --git a/benches/storage.rs b/benches/storage.rs deleted file mode 100644 index cd88ff89e..000000000 --- a/benches/storage.rs +++ /dev/null @@ -1,130 +0,0 @@ -#![feature(test)] - -extern crate specs; -extern crate test; - -macro_rules! setup { - ($num:expr => [ $( $comp:ty ),* ] ) => { - pub fn setup(insert: bool, sparsity: u32) -> (World, Vec) { - let mut w = World::new(); - $( - w.register::<$comp>(); - )* - - let eids: Vec<_> = (0..$num) - .map(|i| { - let mut builder = w.create_entity(); - if insert && i % sparsity == 0 { - $( - builder = builder.with::<$comp>(<$comp>::default()); - )* - } - builder.build() - }) - .collect(); - - (w, eids) - } - } -} - -macro_rules! gap { - ( $name:ident => $sparsity:expr ) => { - mod $name { - use super::{CompInt, CompBool, setup}; - use test::{Bencher, black_box}; - - #[bench] - fn insert(bencher: &mut Bencher) { - let (world, entities) = setup(false, $sparsity); - let mut ints = world.write::(); - let mut bools = world.write::(); - - bencher.iter(move || { - for &entity in &entities { - ints.insert(entity, CompInt::default()); - bools.insert(entity, CompBool::default()); - } - }); - } - #[bench] - fn remove(bencher: &mut Bencher) { - let (world, entities) = setup(true, $sparsity); - let mut ints = world.write::(); - let mut bools = world.write::(); - - bencher.iter(move || { - for &entity in &entities { - ints.remove(entity); - bools.remove(entity); - } - }); - } - #[bench] - fn get(bencher: &mut Bencher) { - let (world, entities) = setup(true, $sparsity); - let ints = world.read::(); - let bools = world.read::(); - - bencher.iter(move || { - for &entity in &entities { - black_box(ints.get(entity)); - black_box(bools.get(entity)); - } - }); - } - } - } -} - -macro_rules! tests { - ($mod:ident => $storage:ty) => { - mod $mod { - use specs::prelude::*; - - pub static NUM: u32 = 100_000; - - pub struct CompInt(u32); - pub struct CompBool(bool); - - impl Default for CompInt { - fn default() -> Self { - CompInt(0) - } - } - - impl Default for CompBool { - fn default() -> Self { - CompBool(true) - } - } - - impl Component for CompInt { - type Storage = $storage; - } - impl Component for CompBool { - type Storage = $storage; - } - - setup!(NUM => [ CompInt, CompBool ]); - - gap!(sparse_1 => 1); - gap!(sparse_2 => 2); - gap!(sparse_4 => 4); - gap!(sparse_8 => 8); - gap!(sparse_128 => 128); - gap!(sparse_256 => 256); - gap!(sparse_512 => 512); - gap!(sparse_1024 => 1024); - gap!(sparse_10000 => 10_000); - gap!(sparse_50000 => 50_000); - } - } -} - -tests!(vec_storage => ::specs::storage::VecStorage); -tests!(dense_vec_storage => ::specs::storage::DenseVecStorage); -tests!(hashmap_storage => ::specs::storage::HashMapStorage); -tests!(btree_storage => ::specs::storage::BTreeStorage); -#[cfg(feature = "rudy")] -tests!(rudy_storage => ::specs::storage::RudyStorage); diff --git a/benches/storage_cmp.rs b/benches/storage_cmp.rs new file mode 100644 index 000000000..842470c31 --- /dev/null +++ b/benches/storage_cmp.rs @@ -0,0 +1,257 @@ +use criterion::{Bencher, Criterion}; +use specs::prelude::*; +use specs::storage; + +use super::black_box; + +fn storage_insert(b: &mut Bencher, num: usize) +where + C: Component + Default, + C::Storage: Default, +{ + b.iter_with_setup( + || { + let mut world = World::new(); + + world.register::(); + + world + }, + |world| { + let entities = world.entities(); + let mut storage = world.write::(); + + for e in entities.create_iter().take(num) { + storage.insert(e, C::default()); + } + }, + ) +} + +fn storage_remove(b: &mut Bencher, num: usize) +where + C: Component + Default, + C::Storage: Default, +{ + b.iter_with_setup( + || { + let mut world = World::new(); + + world.register::(); + + { + let entities = world.entities(); + let mut storage = world.write::(); + + for e in entities.create_iter().take(num) { + storage.insert(e, C::default()); + } + } + + world + }, + |world| { + let entities = world.entities(); + let mut storage = world.write::(); + + for e in (&*entities).join() { + storage.remove(e); + } + }, + ) +} + +fn storage_get(b: &mut Bencher, num: usize) +where + C: Component + Default, + C::Storage: Default, +{ + b.iter_with_setup( + || { + let mut world = World::new(); + + world.register::(); + + { + let entities = world.entities(); + let mut storage = world.write::(); + + for e in entities.create_iter().take(num) { + storage.insert(e, C::default()); + } + } + + world + }, + |world| { + let entities = world.entities(); + let storage = world.read::(); + + for e in (&*entities).join() { + black_box(storage.get(e)); + } + }, + ) +} + +macro_rules! decl_comp { + ($bytes:expr, $store:ident) => { + #[derive(Default)] + struct Comp { + _x: [u8; $bytes], + } + + impl Component for Comp { + type Storage = storage::$store; + } + }; +} + +macro_rules! insert { + ($b:ident, $num:expr, $bytes:expr, $store:ident) => { + { + decl_comp!($bytes, $store); + + storage_insert::($b, $num) + } + }; +} + +macro_rules! remove { + ($b:ident, $num:expr, $bytes:expr, $store:ident) => { + { + decl_comp!($bytes, $store); + + storage_remove::($b, $num) + } + }; +} + +macro_rules! get { + ($b:ident, $num:expr, $bytes:expr, $store:ident) => { + { + decl_comp!($bytes, $store); + + storage_get::($b, $num) + } + }; +} + +#[cfg_attr(rustfmt, rustfmt_skip)] +fn insert_benches(c: &mut Criterion) { + c.bench_function_over_inputs( + "insert 1b/dense", + |b, &&i| insert!(b, i, 1, DenseVecStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "insert 1b/btree", + |b, &&i| insert!(b, i, 1, BTreeStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "insert 1b/hash", + |b, &&i| insert!(b, i, 1, HashMapStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "insert 1b/vec", + |b, &&i| insert!(b, i, 1, VecStorage), + &[1, 16, 64, 256, 1024], + ); + + c.bench_function_over_inputs( + "insert 32b/dense", + |b, &&i| insert!(b, i, 32, DenseVecStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "insert 32b/btree", + |b, &&i| insert!(b, i, 32, BTreeStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "insert 32b/hash", + |b, &&i| insert!(b, i, 32, HashMapStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "insert 32b/vec", + |b, &&i| insert!(b, i, 32, VecStorage), + &[1, 16, 64, 256, 1024], + ); +} + +#[cfg_attr(rustfmt, rustfmt_skip)] +fn remove_benches(c: &mut Criterion) { + c.bench_function_over_inputs( + "remove 1b/dense", + |b, &&i| remove!(b, i, 1, DenseVecStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "remove 1b/btree", + |b, &&i| remove!(b, i, 1, BTreeStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "remove 1b/hash", + |b, &&i| remove!(b, i, 1, HashMapStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "remove 1b/vec", + |b, &&i| remove!(b, i, 1, VecStorage), + &[1, 16, 64, 256, 1024], + ); + + c.bench_function_over_inputs( + "remove 32b/dense", + |b, &&i| remove!(b, i, 32, DenseVecStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "remove 32b/btree", + |b, &&i| remove!(b, i, 32, BTreeStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "remove 32b/hash", + |b, &&i| remove!(b, i, 32, HashMapStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "remove 32b/vec", + |b, &&i| remove!(b, i, 32, VecStorage), + &[1, 16, 64, 256, 1024], + ); +} + +#[cfg_attr(rustfmt, rustfmt_skip)] +fn get_benches(c: &mut Criterion) { + c.bench_function_over_inputs( + "get 1b/dense", + |b, &&i| remove!(b, i, 1, DenseVecStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "get 1b/btree", + |b, &&i| remove!(b, i, 1, BTreeStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "get 1b/hash", + |b, &&i| remove!(b, i, 1, HashMapStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "get 1b/vec", + |b, &&i| remove!(b, i, 1, VecStorage), + &[1, 16, 64, 256, 1024], + ); + + c.bench_function_over_inputs( + "get 32b/dense", + |b, &&i| get!(b, i, 32, DenseVecStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "get 32b/btree", + |b, &&i| get!(b, i, 32, BTreeStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "get 32b/hash", + |b, &&i| get!(b, i, 32, HashMapStorage), + &[1, 16, 64, 256, 1024], + ).bench_function_over_inputs( + "get 32b/vec", + |b, &&i| get!(b, i, 32, VecStorage), + &[1, 16, 64, 256, 1024], + ); +} + +criterion_group!(benches_storages, insert_benches, remove_benches, get_benches); diff --git a/benches/storage_sparse.rs b/benches/storage_sparse.rs new file mode 100644 index 000000000..77ba9fa81 --- /dev/null +++ b/benches/storage_sparse.rs @@ -0,0 +1,167 @@ +use criterion::{Bencher, Criterion}; + +use super::black_box; + +macro_rules! setup { + ($num:expr => [ $( $comp:ty ),* ] ) => { + pub fn setup(filter: bool, insert: bool, sparsity: u32) -> (World, Vec) { + let mut w = World::new(); + $( + w.register::<$comp>(); + )* + + let eids: Vec<_> = (0..$num) + .flat_map(|i| { + let mut builder = w.create_entity(); + if insert { + if i % sparsity == 0 { + $( + builder = builder.with::<$comp>(<$comp>::default()); + )* + } + } + + if !filter || i % sparsity == 0 { + Some(builder.build()) + } else { + None + } + }) + .collect(); + + (w, eids) + } + } +} + +macro_rules! gap { + ( $storage:ident, $name:ident => $sparsity:expr ) => { + mod $name { + use super::{CompInt, CompBool, setup}; + use super::super::{Bencher, Criterion, black_box}; + + fn insert(bencher: &mut Bencher) { + let (world, entities) = setup(true, false, $sparsity); + let mut ints = world.write::(); + let mut bools = world.write::(); + + bencher.iter(move || { + for &entity in &entities { + ints.insert(entity, CompInt::default()); + bools.insert(entity, CompBool::default()); + } + }); + } + + fn remove(bencher: &mut Bencher) { + let (world, entities) = setup(true, true, $sparsity); + let mut ints = world.write::(); + let mut bools = world.write::(); + + bencher.iter(move || { + for &entity in &entities { + ints.remove(entity); + bools.remove(entity); + } + }); + } + + fn get(bencher: &mut Bencher) { + let (world, entities) = setup(false, true, $sparsity); + let ints = world.read::(); + let bools = world.read::(); + + bencher.iter(move || { + for &entity in &entities { + black_box(ints.get(entity)); + black_box(bools.get(entity)); + } + }); + } + + pub fn benches(c: &mut Criterion) { + c.bench_function( + &format!("sparse insert {}/{}", $sparsity, stringify!($storage)), + |b| insert(b), + ).bench_function( + &format!("sparse remove {}/{}", $sparsity, stringify!($storage)), + |b| remove(b), + ).bench_function( + &format!("sparse get {}/{}", $sparsity, stringify!($storage)), + |b| get(b), + ); + } + } + } +} + +macro_rules! tests { + ($mod:ident => $storage:ident) => { + mod $mod { + use criterion::Criterion; + use specs::prelude::*; + + pub static NUM: u32 = 100_000; + + pub struct CompInt(u32); + pub struct CompBool(bool); + + impl Default for CompInt { + fn default() -> Self { + CompInt(0) + } + } + + impl Default for CompBool { + fn default() -> Self { + CompBool(true) + } + } + + impl Component for CompInt { + type Storage = ::specs::storage::$storage; + } + impl Component for CompBool { + type Storage = ::specs::storage::$storage; + } + + setup!(NUM => [ CompInt, CompBool ]); + + gap!($storage, sparse_1 => 1); + gap!($storage, sparse_2 => 2); + gap!($storage, sparse_4 => 4); + gap!($storage, sparse_8 => 8); + gap!($storage, sparse_128 => 128); + gap!($storage, sparse_256 => 256); + gap!($storage, sparse_512 => 512); + gap!($storage, sparse_1024 => 1024); + gap!($storage, sparse_10000 => 10_000); + gap!($storage, sparse_50000 => 50_000); + + group!(benches, + sparse_1::benches, + sparse_2::benches, + sparse_4::benches, + sparse_8::benches, + sparse_128::benches, + sparse_256::benches, + sparse_512::benches, + sparse_1024::benches, + sparse_10000::benches, + sparse_50000::benches + ); + } + } +} + +tests!(vec_storage => VecStorage); +tests!(dense_vec_storage => DenseVecStorage); +tests!(hashmap_storage => HashMapStorage); +tests!(btree_storage => BTreeStorage); + +criterion_group!(benches_sparse, + vec_storage::benches, + dense_vec_storage::benches, + hashmap_storage::benches, + btree_storage::benches +); diff --git a/benches/world.rs b/benches/world.rs index 0d3a8ecdd..76412ac7f 100644 --- a/benches/world.rs +++ b/benches/world.rs @@ -1,9 +1,12 @@ #![feature(test)] +#[macro_use] +extern crate criterion; extern crate rayon; extern crate specs; extern crate test; +use criterion::{Bencher, Criterion}; use specs::prelude::*; use specs::storage::HashMapStorage; @@ -30,55 +33,72 @@ fn create_world() -> World { w } -#[bench] -fn world_build(b: &mut test::Bencher) { +fn world_build(b: &mut Bencher) { b.iter(World::new); } -#[bench] -fn create_now(b: &mut test::Bencher) { - let mut w = World::new(); - b.iter(|| w.create_entity().build()); +fn create_now(b: &mut Bencher) { + b.iter_with_large_setup( + || World::new(), + |mut w| { + w.create_entity().build(); + }, + ); } -#[bench] -fn create_now_with_storage(b: &mut test::Bencher) { - let mut w = create_world(); - b.iter(|| w.create_entity().with(CompInt(0)).build()); +fn create_now_with_storage(b: &mut Bencher) { + b.iter_with_large_setup( + || create_world(), + |mut w| { + w.create_entity().with(CompInt(0)).build(); + }, + ); } -#[bench] -fn create_pure(b: &mut test::Bencher) { - let w = World::new(); - b.iter(|| w.entities().create()); +fn create_pure(b: &mut Bencher) { + b.iter_with_large_setup( + || World::new(), + |w| { + w.entities().create(); + }, + ); } -#[bench] -fn delete_now(b: &mut test::Bencher) { - let mut w = World::new(); - let mut eids: Vec<_> = (0..10_000_000).map(|_| w.create_entity().build()).collect(); - b.iter(|| { - if let Some(id) = eids.pop() { - w.delete_entity(id).unwrap() - } - }); +fn delete_now(b: &mut Bencher) { + b.iter_with_setup( + || { + let mut w = create_world(); + let eids: Vec<_> = (0..100).map(|_| w.create_entity().build()).collect(); + + (w, eids) + }, + |(mut w, mut eids)| { + if let Some(id) = eids.pop() { + w.delete_entity(id).unwrap() + } + }, + ); } -#[bench] -fn delete_now_with_storage(b: &mut test::Bencher) { - let mut w = create_world(); - let mut eids: Vec<_> = (0..10_000_000) - .map(|_| w.create_entity().with(CompInt(1)).build()) - .collect(); - b.iter(|| { - if let Some(id) = eids.pop() { - w.delete_entity(id).unwrap() - } - }); +fn delete_now_with_storage(b: &mut Bencher) { + b.iter_with_setup( + || { + let mut w = create_world(); + let eids: Vec<_> = (0..100) + .map(|_| w.create_entity().with(CompInt(1)).build()) + .collect(); + + (w, eids) + }, + |(mut w, mut eids)| { + if let Some(id) = eids.pop() { + w.delete_entity(id).unwrap() + } + }, + ); } -#[bench] -fn delete_later(b: &mut test::Bencher) { +fn delete_later(b: &mut Bencher) { let mut w = World::new(); let mut eids: Vec<_> = (0..10_000_000).map(|_| w.create_entity().build()).collect(); b.iter(|| { @@ -88,16 +108,14 @@ fn delete_later(b: &mut test::Bencher) { }); } -#[bench] -fn maintain_noop(b: &mut test::Bencher) { +fn maintain_noop(b: &mut Bencher) { let mut w = World::new(); b.iter(|| { w.maintain(); }); } -#[bench] -fn maintain_add_later(b: &mut test::Bencher) { +fn maintain_add_later(b: &mut Bencher) { let mut w = World::new(); b.iter(|| { w.entities().create(); @@ -105,8 +123,7 @@ fn maintain_add_later(b: &mut test::Bencher) { }); } -#[bench] -fn maintain_delete_later(b: &mut test::Bencher) { +fn maintain_delete_later(b: &mut Bencher) { let mut w = World::new(); let mut eids: Vec<_> = (0..10_000_000).map(|_| w.create_entity().build()).collect(); b.iter(|| { @@ -117,8 +134,7 @@ fn maintain_delete_later(b: &mut test::Bencher) { }); } -#[bench] -fn join_single_threaded(b: &mut test::Bencher) { +fn join_single_threaded(b: &mut Bencher) { use test::black_box; let mut world = World::new(); @@ -139,8 +155,7 @@ fn join_single_threaded(b: &mut test::Bencher) { }) } -#[bench] -fn join_multi_threaded(b: &mut test::Bencher) { +fn join_multi_threaded(b: &mut Bencher) { use rayon::prelude::*; use test::black_box; @@ -161,3 +176,22 @@ fn join_multi_threaded(b: &mut test::Bencher) { }) }) } + +fn world_benchmarks(c: &mut Criterion) { + c.bench_function("world build", world_build) + .bench_function("create now", create_now) + .bench_function("create pure", create_pure) + .bench_function("create now with storage", create_now_with_storage) + .bench_function("delete now", delete_now) + .bench_function("delete now with storage", delete_now_with_storage) + .bench_function("delete later", delete_later) + .bench_function("maintain noop", maintain_noop) + .bench_function("maintain add later", maintain_add_later) + .bench_function("maintain delete later", maintain_delete_later) + .bench_function("join single threaded", join_single_threaded) + .bench_function("join multi threaded", join_multi_threaded); +} + +criterion_group!(world, world_benchmarks); + +criterion_main!(world);