perf(game): eliminate per-event allocations via ScratchArena

Game::likelihoods previously allocated four Vecs (teams, diffs, ties,
margins) on every call. Batch now owns one ScratchArena reused across
all Game::new calls in the iteration loop; likelihoods() clears and
extends the arena buffers instead of allocating fresh.

For log_evidence (called infrequently), a local ScratchArena is created
per invocation so the method signature stays &self.

Also: add #[derive(Debug)] to TeamMessage and DiffMessage (required by
ScratchArena's own Debug derive).

Part of T0 engine redesign.
This commit is contained in:
2026-04-24 07:24:29 +02:00
parent 49d2b317da
commit b1e0fcb817
6 changed files with 234 additions and 61 deletions

44
src/arena.rs Normal file
View File

@@ -0,0 +1,44 @@
use crate::message::{DiffMessage, TeamMessage};
/// Reusable scratch buffers for `Game::likelihoods`.
///
/// The four Vecs previously allocated fresh on every `Game::new` call —
/// `teams`, `diffs`, `ties`, `margins` — are now borrowed from this arena,
/// reset between uses. A `Batch` owns one arena; all events in the slice
/// share it across the convergence iterations.
#[derive(Debug, Default)]
pub struct ScratchArena {
pub(crate) teams: Vec<TeamMessage>,
pub(crate) diffs: Vec<DiffMessage>,
pub(crate) ties: Vec<bool>,
pub(crate) margins: Vec<f64>,
}
impl ScratchArena {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub(crate) fn reset(&mut self) {
self.teams.clear();
self.diffs.clear();
self.ties.clear();
self.margins.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reset_keeps_capacity() {
let mut arena = ScratchArena::new();
arena.teams.push(TeamMessage::default());
let cap = arena.teams.capacity();
arena.reset();
assert_eq!(arena.teams.len(), 0);
assert_eq!(arena.teams.capacity(), cap);
}
}