Optimization: Cache Character Vectors (H2) Target: 8.47% of cycles (Iterator::collect 4.08% + cfree 4.39%) Expected Improvement: 5-8% overall speedup Effort: Medium (API changes required) Date: November 18, 2025
H2: Repeated chars().collect() calls cause 10-15% overhead
Evidence from Phase 1:
chars().collect() found in state.rsIterator::collect: 4.08% (allocation)cfree: 4.39% (deallocation of Vec)Root Cause: Every call to successor generation methods allocates a new Vec<char> by calling word_slice.chars().collect(), which is immediately freed after use.
pub fn accepts(&self, word: &str, input: &str) -> bool {
// ...
for (i, input_char) in input.chars().enumerate() {
let subword = self.relevant_subword(word, i + 1);
state.transition(&self.operations, &bit_vector, word, &subword, input_char, i + 1);
// ^^^^ ^^^^^^^^
// Each transition call
// leads to 20+ chars().collect()
}
}
Inside transition() → successors() → successors_i_type():
// This is repeated 20+ times per transition!
let word_chars: Vec<char> = word_slice.chars().collect();
pub fn accepts(&self, word: &str, input: &str) -> bool {
// Pre-compute ONCE
let word_chars: Vec<char> = word.chars().collect();
for (i, input_char) in input.chars().enumerate() {
let subword = self.relevant_subword(word, i + 1);
state.transition(&self.operations, &bit_vector, word, &word_chars, &subword, input_char, i + 1);
// ^^^^^^^^^^^
// Pass cached vector
}
}
Inside successor methods:
// Use slice instead of re-allocating!
// Before: let word_chars: Vec<char> = word_slice.chars().collect();
// After: Just use word_chars[start..end] directly
GeneralizedAutomaton::accepts() ✅File: src/transducer/generalized/automaton.rs
Location: Line 292
Changes:
let word_chars: Vec<char> = word.chars().collect(); after line 326&word_chars to state.transition() call at line 348Signature change: None (internal only)
GeneralizedState::transition()File: src/transducer/generalized/state.rs
Location: ~line 136 (based on earlier read)
Changes:
word_chars: &[char]word_chars to self.successors() callSignature:
// Before
pub fn transition(
&self,
operations: &OperationSet,
bit_vector: &CharacteristicVector,
full_word: &str,
word_slice: &str,
input_char: char,
query_length: usize,
) -> Option<Self>
// After
pub fn transition(
&self,
operations: &OperationSet,
bit_vector: &CharacteristicVector,
full_word: &str,
word_chars: &[char], // NEW: pre-computed character vector
word_slice: &str,
input_char: char,
query_length: usize,
) -> Option<Self>
GeneralizedState::successors()File: src/transducer/generalized/state.rs
Changes:
word_chars: &[char]successors_d_type()successors_i_type() ← Main target (2.75% of cycles)successors_s_type()successors_t_type()Signature:
// Before
pub fn successors(
&self,
operations: &OperationSet,
bit_vector: &CharacteristicVector,
full_word: &str,
word_slice: &str,
input_char: char,
query_length: usize,
) -> Self
// After
pub fn successors(
&self,
operations: &OperationSet,
bit_vector: &CharacteristicVector,
full_word: &str,
word_chars: &[char], // NEW
word_slice: &str,
input_char: char,
query_length: usize,
) -> Self
successors_i_type() ← CRITICALFile: src/transducer/generalized/state.rs
Location: Large method (~320 lines), contains most char().collect() calls
Changes:
word_chars: &[char]let word_chars: Vec<char> = word_slice.chars().collect();
With character vector slicing based on position in word
Challenge: Need to map word_slice positions to indices in word_chars
Approach:
word_slice.as_ptr() vs full_word.as_ptr()Files: src/transducer/generalized/state.rs
Methods:
successors_d_type() - DELETE operationssuccessors_s_type() - SUBSTITUTE operationssuccessors_t_type() - TRANSPOSE operations (phonetic)Changes: Same as Step 4 - add word_chars parameter and replace char().collect()
CharacteristicVector usage (if needed)File: src/transducer/generalized/bit_vector.rs
Locations: Lines 350, 418
Analysis Needed: Check if CharacteristicVector also benefits from character vector caching.
Command:
RUSTFLAGS="-C target-cpu=native" cargo test
Expected: All 725+ tests pass
If failures: Debug and fix signature mismatches
Command:
RUSTFLAGS="-C target-cpu=native" taskset -c 0 cargo bench --bench generalized_automaton_benchmarks
Expected Improvement:
Output: docs/optimization/optimized_h2_generalized_automaton.txt
Command:
RUSTFLAGS="-C target-cpu=native" taskset -c 0 cargo flamegraph \
--bench generalized_automaton_benchmarks \
--output docs/optimization/flamegraphs/optimized_h2.svg -- --bench
Analysis:
baseline_standard.svgIterator::collect should drop from 4.08% to near 0%cfree should drop from 4.39% correspondinglyFile: docs/optimization/PHASE3_H2_RESULTS.md
Contents:
Issue: word_slice is a substring of word, but we need indices into word_chars
Solutions:
Option A: Calculate byte offset and convert to char index
let byte_offset = word_slice.as_ptr() as usize - word.as_ptr() as usize;
let char_offset = word[..byte_offset].chars().count();
Option B: Pass explicit indices
// In accepts():
let (start_idx, end_idx) = self.relevant_subword_indices(word, i + 1);
let subword_chars = &word_chars[start_idx..end_idx];
Recommendation: Option B (cleaner, more explicit)
Issue: Some operations use full_word.chars().collect() for lookups
Solution: Also pass full word_chars and use slicing
Issue: Changing public API signatures
Solution: These are internal methods (not pub), so no external API break
Minimum:
Target:
Stretch:
If optimization fails or causes issues:
optimization-h2 branch before changesEstimated: 2-3 hours
Start: Ready to begin Dependencies: None (Phase 1 complete) Blockers: None identified
First Action: Create git branch optimization-h2 and begin Step 1
Status: ⏳ Ready to implement Next: Create git branch and start Step 1
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |