refactor(batch): replace HashMap<Index, Skill> with dense SkillStore
SkillStore is a Vec<Skill>-backed dense store with a parallel present mask, indexed directly by Index.0. Eliminates per-iteration hashing in the within-slice convergence loop; O(1) array lookup replaces O(1) amortised hash lookup with better cache behaviour. Iteration order is now ascending-by-Index (was arbitrary for HashMap); EP fixed point is order-independent so posteriors are unchanged. Part of T0 engine redesign.
This commit is contained in:
3
src/storage/mod.rs
Normal file
3
src/storage/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod skill_store;
|
||||
|
||||
pub(crate) use skill_store::SkillStore;
|
||||
128
src/storage/skill_store.rs
Normal file
128
src/storage/skill_store.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use crate::Index;
|
||||
use crate::batch::Skill;
|
||||
|
||||
/// Dense Vec-backed store for per-agent skill state within a TimeSlice.
|
||||
///
|
||||
/// Indexed directly by Index.0, eliminating HashMap hashing in the inner
|
||||
/// convergence loop. Uses a parallel `present` mask so iteration skips
|
||||
/// absent slots without incurring per-slot Option overhead in the hot path.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SkillStore {
|
||||
skills: Vec<Skill>,
|
||||
present: Vec<bool>,
|
||||
n_present: usize,
|
||||
}
|
||||
|
||||
impl SkillStore {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn ensure_capacity(&mut self, idx: usize) {
|
||||
if idx >= self.skills.len() {
|
||||
self.skills.resize_with(idx + 1, Skill::default);
|
||||
self.present.resize(idx + 1, false);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, idx: Index, skill: Skill) {
|
||||
self.ensure_capacity(idx.0);
|
||||
if !self.present[idx.0] {
|
||||
self.n_present += 1;
|
||||
}
|
||||
self.skills[idx.0] = skill;
|
||||
self.present[idx.0] = true;
|
||||
}
|
||||
|
||||
pub fn get(&self, idx: Index) -> Option<&Skill> {
|
||||
if idx.0 < self.present.len() && self.present[idx.0] {
|
||||
Some(&self.skills[idx.0])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, idx: Index) -> Option<&mut Skill> {
|
||||
if idx.0 < self.present.len() && self.present[idx.0] {
|
||||
Some(&mut self.skills[idx.0])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, idx: Index) -> bool {
|
||||
idx.0 < self.present.len() && self.present[idx.0]
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.n_present
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.n_present == 0
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (Index, &Skill)> {
|
||||
self.present.iter().enumerate().filter_map(|(i, &p)| {
|
||||
if p {
|
||||
Some((Index(i), &self.skills[i]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (Index, &mut Skill)> {
|
||||
self.skills
|
||||
.iter_mut()
|
||||
.zip(self.present.iter())
|
||||
.enumerate()
|
||||
.filter_map(|(i, (s, &p))| if p { Some((Index(i), s)) } else { None })
|
||||
}
|
||||
|
||||
pub fn keys(&self) -> impl Iterator<Item = Index> + '_ {
|
||||
self.present
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, &p)| if p { Some(Index(i)) } else { None })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn insert_then_get() {
|
||||
let mut store = SkillStore::new();
|
||||
let idx = Index(3);
|
||||
store.insert(idx, Skill::default());
|
||||
assert!(store.contains(idx));
|
||||
assert_eq!(store.len(), 1);
|
||||
assert!(store.get(idx).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_returns_none() {
|
||||
let store = SkillStore::new();
|
||||
assert!(store.get(Index(0)).is_none());
|
||||
assert!(!store.contains(Index(42)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter_skips_absent_slots() {
|
||||
let mut store = SkillStore::new();
|
||||
store.insert(Index(0), Skill::default());
|
||||
store.insert(Index(5), Skill::default());
|
||||
let keys: Vec<Index> = store.keys().collect();
|
||||
assert_eq!(keys, vec![Index(0), Index(5)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_insert_does_not_double_count() {
|
||||
let mut store = SkillStore::new();
|
||||
store.insert(Index(2), Skill::default());
|
||||
store.insert(Index(2), Skill::default());
|
||||
assert_eq!(store.len(), 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user