- String interning: all hot paths operate on
u32IDs (symbol_id,pattern_id). NoString::eqin beam search or cost computation. - Parallel infer:
spma inferparallelizes sequence scoring with rayon. Sequences are independent — embarrassingly parallel. Stdout buffered and flushed after all workers complete. - mimalloc: global allocator replaced with mimalloc for parallel allocation throughput.
- MDL cache: total E cost cached during training beam passes — avoids a full extra pass to build
e_distribution. - Infer match_log reuse: level-0
beam_searchresult'smatch_logseeded directly into the N-level loop; level=1 extracts pid_seq from it without a second beam call. - SymbolIndex on GrammarLevel: inverted index
symbol_id → [(pattern_idx, pos)]built once per level, eliminates per-callHashMaprebuild inbeam_search. Measured gain on HDFS 1k/446k: <1% — bottleneck is beam candidate expansion, not index construction.
Observed on HDFS (1k training corpus, 446k infer, release build, Apple Silicon):
- Training (1k sequences): ~138s user, ~1m48s wall (sequential, 128% CPU)
- Infer (446k sequences, parallel, baseline): ~1816s user, ~123s wall (1480% CPU, 16 cores)
- Infer (446k sequences, parallel, after H+I+J): ~637s user, ~42s wall (~2.85× user, ~2.9× wall)
cargo-instruments Time Profiler, inverted call tree:
| Self time | Function | Root cause |
|---|---|---|
| ~80s total | hashbrown::find_inner, SipHasher::write, hash_one, make_hash, probe_seq |
old_cursors/new_cursors: HashMap<usize,usize> in PartialAlignment — cloned per candidate per symbol in beam inner loop |
| ~25s | <f64>::log2 |
MDL cost recomputed per candidate per extension — no caching |
After each fix, re-profiled to confirm:
| Profile pass | Top consumer | Action |
|---|---|---|
| Baseline | 40.5% Map::fold, 35.9% hash_one |
HashMap cursors dominate |
| After H (Vec cursors) | 81.2% Vec::from_iter |
Per-sequence pid_freq: HashMap still allocating |
| After pid_freq Vec | 88.3% LocalKey::with |
TLS overhead exceeded benefit — reverted |
| After grow-only Vec buffers | 83.7% infer, 8.5% log2 |
Healthy: hot path owns CPU, no single bottleneck |
H — Vec cursors in beam.rs
- Removed dead
old_cursors: HashMap<usize,usize>(written but never read) new_cursors: HashMap<usize,usize>→Vec<u16>with sentinelu16::MAXcovered_new: Vec<bool>→[u64; 8]bitmask (512-symbol limit)- Clone is now memcpy — no heap allocation in beam inner loop
I — log2 hoisting and pid_freq Vec in engine.rs
fallback_pid = log2(n_prev_pats)hoisted outside per-sequence loop (level-constant)log2_total = log2(total_pid)cached once per level per sequencepid_freq: HashMap<u32,u32>→Vec<u32>indexed by pattern id- Grow-only
pid_freq_buf/pid_costs_bufallocated once perinfercall, reused across all levels and sequences
J — TLS attempted, reverted
Thread-local RefCell buffers caused 88.3% LocalKey::with overhead on macOS. Reverted in favour of grow-only Vecs per infer call.
B — Structure of Arrays layout Replace AoS with flat Vecs + offset table. Expected 3–5x on cost computation and frequency updates.
pub struct PatternStore {
symbol_data: Vec<u32>, // all symbols concatenated
symbol_offsets: Vec<u32>, // symbol_data[offsets[i]..offsets[i+1]] = pattern i
frequencies: Vec<u32>,
total_costs: Vec<f64>,
}D — Parallel training (independent of B) Training beam passes are currently sequential per level. Each new pattern is independent within a level — could parallelize with rayon, keeping grammar update sequential. Does not require the SoA refactor (B) — can be done on the current AoS layout.
E — SIMD symbol comparison (after B)
AVX2: 8 u32 symbols per instruction.
F — Arena allocator for beam candidates (low priority)
bumpalo arena for short-lived PartialAlignment structs. Only relevant at beam_k > 50.
C ✓ done (no gain). Profile ✓ done. H ✓, I ✓, J attempted+reverted. 2.9× wall speedup achieved. Remaining: B → D → E → F.