Skip to content

Commit

Permalink
Use HashMap in some places to improve performance. (#8)
Browse files Browse the repository at this point in the history
Falls back to `BTreeMap` when the `std` feature is off.
Also added more btree vs hashmap benchmarks.
  • Loading branch information
rj00a authored Jan 31, 2024
1 parent c1ac2e7 commit 8e2651a
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- Fixed potential "cannot shadow tuple struct with let binding" errors in derived `Query` and `SystemParam` impls.
- Minor performance improvements.

## 0.1.1 - 2024-01-25

Expand Down
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ categories = ["data-structures", "no-std", "game-development"]

[features]
default = ["std"]
std = []
std = ["dep:ahash"]

[dependencies]
ahash = { version = "0.8.7", optional = true }
bumpalo = "3.14.0"
evenio_macros = { path = "evenio_macros", version = "0.1.1" }
memoffset = "0.9.0"
Expand Down Expand Up @@ -103,6 +104,7 @@ invalid_upcast_comparisons = "warn"
iter_filter_is_ok = "warn"
iter_filter_is_some = "warn"
iter_not_returning_iterator = "warn"
iter_over_hash_type = "warn" # Requires justification
iter_without_into_iter = "warn"
large_stack_arrays = "warn"
large_types_passed_by_value = "warn"
Expand Down
31 changes: 31 additions & 0 deletions benches/btree_vs_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,34 @@ where
let _ = black_box(set.binary_search(&n));
});
}

const ARRAY_LENS: [usize; 10] = [1, 2, 3, 4, 5, 10, 20, 30, 40, 50];
const ARRAY_COUNT: usize = 100;

#[divan::bench(args = ARRAY_LENS)]
fn lookup_u32_array_hashset(bencher: Bencher, array_len: usize) {
let set: AHashSet<Box<[u32]>> = (0..ARRAY_COUNT)
.map(|n| vec![n as u32; n % array_len + 1].into_boxed_slice())
.chain([[].into()])
.collect();

let val = vec![12345; array_len].into_boxed_slice();

bencher.bench(|| {
black_box(set.get(black_box(&*val)));
});
}

#[divan::bench(args = ARRAY_LENS)]
fn lookup_u32_array_btreeset(bencher: Bencher, array_len: usize) {
let set: BTreeSet<Box<[u32]>> = (0..ARRAY_COUNT)
.map(|n| vec![n as u32; n % array_len + 1].into_boxed_slice())
.chain([[].into()])
.collect();

let val = vec![12345; array_len].into_boxed_slice();

bencher.bench(|| {
black_box(set.get(black_box(&*val)));
});
}
15 changes: 8 additions & 7 deletions src/archetype.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! [`Archetype`] and related items.

use alloc::boxed::Box;
use alloc::collections::btree_map::Entry;
use alloc::collections::btree_map::Entry as BTreeEntry;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::vec;
use alloc::vec::Vec;
Expand All @@ -16,6 +16,7 @@ use crate::blob_vec::BlobVec;
use crate::component::{ComponentIdx, Components};
use crate::entity::{Entities, EntityId, EntityLocation};
use crate::event::{EventIdx, EventPtr, TargetedEventIdx};
use crate::map::{Entry, Map};
use crate::prelude::World;
use crate::sparse::SparseIndex;
use crate::sparse_map::SparseMap;
Expand All @@ -41,14 +42,14 @@ use crate::world::UnsafeWorldCell;
#[derive(Debug)]
pub struct Archetypes {
archetypes: Slab<Archetype>,
by_components: BTreeMap<Box<[ComponentIdx]>, ArchetypeIdx>,
by_components: Map<Box<[ComponentIdx]>, ArchetypeIdx>,
}

impl Archetypes {
pub(crate) fn new() -> Self {
Self {
archetypes: Slab::from_iter([(0, Archetype::empty())]),
by_components: BTreeMap::from_iter([(vec![].into_boxed_slice(), ArchetypeIdx::EMPTY)]),
by_components: Map::from_iter([(vec![].into_boxed_slice(), ArchetypeIdx::EMPTY)]),
}
}

Expand Down Expand Up @@ -192,7 +193,7 @@ impl Archetypes {
};

match src_arch.insert_components.entry(component_idx) {
Entry::Vacant(vacant_insert_components) => {
BTreeEntry::Vacant(vacant_insert_components) => {
let Err(idx) = src_arch
.columns
.binary_search_by_key(&component_idx, |c| c.component_idx)
Expand Down Expand Up @@ -236,7 +237,7 @@ impl Archetypes {
Entry::Occupied(o) => *vacant_insert_components.insert(*o.get()),
}
}
Entry::Occupied(o) => *o.get(),
BTreeEntry::Occupied(o) => *o.get(),
}
}

Expand All @@ -257,7 +258,7 @@ impl Archetypes {
};

match src_arch.remove_components.entry(component_idx) {
Entry::Vacant(vacant_remove_components) => {
BTreeEntry::Vacant(vacant_remove_components) => {
if src_arch
.columns
.binary_search_by_key(&component_idx, |c| c.component_idx)
Expand Down Expand Up @@ -307,7 +308,7 @@ impl Archetypes {
Entry::Occupied(o) => *vacant_remove_components.insert(*o.get()),
}
}
Entry::Occupied(o) => *o.get(),
BTreeEntry::Occupied(o) => *o.get(),
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/component.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//! Types for working with [`Component`]s.

use alloc::borrow::Cow;
use alloc::collections::btree_map::Entry;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::collections::BTreeSet;
use core::alloc::Layout;
use core::any::TypeId;
use core::ops::Index;
Expand All @@ -13,6 +12,7 @@ use crate::archetype::Archetype;
use crate::assert::UnwrapDebugChecked;
use crate::drop::DropFn;
use crate::event::{Event, EventId, EventPtr};
use crate::map::{Entry, TypeIdMap};
use crate::prelude::World;
use crate::slot_map::{Key, SlotMap};
use crate::sparse::SparseIndex;
Expand All @@ -36,14 +36,14 @@ use crate::world::UnsafeWorldCell;
#[derive(Debug)]
pub struct Components {
infos: SlotMap<ComponentInfo>,
by_type_id: BTreeMap<TypeId, ComponentId>,
by_type_id: TypeIdMap<ComponentId>,
}

impl Components {
pub(crate) fn new() -> Self {
Self {
infos: SlotMap::new(),
by_type_id: BTreeMap::new(),
by_type_id: TypeIdMap::default(),
}
}

Expand Down
7 changes: 3 additions & 4 deletions src/event.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
//! Types for sending and receiving [`Event`]s.

use alloc::borrow::Cow;
use alloc::collections::btree_map::Entry;
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use alloc::{format, vec};
use core::alloc::Layout;
Expand All @@ -27,6 +25,7 @@ use crate::component::ComponentIdx;
use crate::drop::DropFn;
use crate::entity::EntityId;
use crate::fetch::FetcherState;
use crate::map::{Entry, TypeIdMap};
use crate::prelude::Component;
use crate::query::Query;
use crate::slot_map::{Key, SlotMap};
Expand All @@ -51,15 +50,15 @@ use crate::world::{UnsafeWorldCell, World};
pub struct Events {
untargeted_events: SlotMap<EventInfo>,
targeted_events: SlotMap<EventInfo>,
by_type_id: BTreeMap<TypeId, EventId>,
by_type_id: TypeIdMap<EventId>,
}

impl Events {
pub(crate) fn new() -> Self {
let mut this = Self {
untargeted_events: SlotMap::new(),
targeted_events: SlotMap::new(),
by_type_id: BTreeMap::new(),
by_type_id: TypeIdMap::default(),
};

this.add(EventDescriptor {
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub mod event;
pub mod exclusive;
pub mod fetch;
mod layout_util;
mod map;
pub mod query;
mod slot_map;
pub mod sparse;
Expand Down
68 changes: 68 additions & 0 deletions src/map.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//! Contains the general-purpose map type [`Map`] and specialized map for
//! [`TypeId`] keys [`TypeIdMap`].
//!
//! These maps are hash maps when the `std` feature is enabled and fall back to
//! `BTreeMap` otherwise.

use core::any::TypeId;

#[cfg(feature = "std")]
pub(crate) type Map<K, V> = ahash::AHashMap<K, V>;

#[cfg(not(feature = "std"))]
pub(crate) type Map<K, V> = alloc::collections::BTreeMap<K, V>;

#[cfg(feature = "std")]
pub(crate) type Entry<'a, K, V> = std::collections::hash_map::Entry<'a, K, V>;

#[cfg(not(feature = "std"))]
pub(crate) type Entry<'a, K, V> = alloc::collections::btree_map::Entry<'a, K, V>;

#[cfg(feature = "std")]
pub(crate) type TypeIdMap<V> =
std::collections::HashMap<TypeId, V, core::hash::BuildHasherDefault<TypeIdHasher>>;

#[cfg(not(feature = "std"))]
pub(crate) type TypeIdMap<V> = alloc::collections::BTreeMap<TypeId, V>;

/// A hasher optimized for hashing a single [`TypeId`].
///
/// `TypeId` is already thoroughly hashed, so there's no reason to hash it
/// again. Just leave the bits unchanged.
#[cfg(feature = "std")]
#[derive(Default)]
pub(crate) struct TypeIdHasher {
hash: u64,
}

#[cfg(feature = "std")]
impl core::hash::Hasher for TypeIdHasher {
fn write_u64(&mut self, n: u64) {
debug_assert_eq!(self.hash, 0, "value was already hashed");

self.hash = n;
}

fn write_u128(&mut self, n: u128) {
debug_assert_eq!(self.hash, 0, "value was already hashed");

// Tolerate TypeId being either u64 or u128.
self.hash = n as u64;
}

fn write(&mut self, bytes: &[u8]) {
debug_assert_eq!(self.hash, 0, "value was already hashed");

// This will only be called if TypeId is neither u64 nor u128, which is not
// anticipated. In that case we'll just fall back to using a different
// hash implementation.
let mut hasher = ahash::AHasher::default();

hasher.write(bytes);
self.hash = hasher.finish();
}

fn finish(&self) -> u64 {
self.hash
}
}
6 changes: 3 additions & 3 deletions src/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

use alloc::borrow::Cow;
use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::vec;
use alloc::vec::Vec;
use core::any::TypeId;
Expand All @@ -22,6 +21,7 @@ use crate::bool_expr::BoolExpr;
use crate::component::ComponentIdx;
use crate::event::{Event, EventId, EventIdx, EventPtr, TargetedEventIdx, UntargetedEventIdx};
use crate::exclusive::Exclusive;
use crate::map::TypeIdMap;
use crate::slot_map::{Key, SlotMap};
use crate::sparse::SparseIndex;
use crate::world::{UnsafeWorldCell, World};
Expand All @@ -46,15 +46,15 @@ pub struct Systems {
/// Maps untargeted event indices to the ordered list of systems that handle
/// the event.
by_untargeted_event: Vec<SystemList>,
by_type_id: BTreeMap<TypeId, SystemInfoPtr>,
by_type_id: TypeIdMap<SystemInfoPtr>,
}

impl Systems {
pub(crate) fn new() -> Self {
Self {
infos: SlotMap::new(),
by_untargeted_event: vec![],
by_type_id: BTreeMap::new(),
by_type_id: Default::default(),
}
}

Expand Down

0 comments on commit 8e2651a

Please sign in to comment.