This document analyzes the performance characteristics of state operations beyond the core insert() function (which was already optimized in the subsumption analysis).
File: src/transducer/state.rs
pub struct State {
positions: SmallVec<[Position; 8]>,
}
State::new() - lines 26-31pub fn new() -> Self {
Self {
positions: SmallVec::new(),
}
}
Complexity: O(1) Usage: Initial state creation Performance: Optimal - just allocates SmallVec
State::single(position) - lines 33-38pub fn single(position: Position) -> Self {
let mut positions = SmallVec::new();
positions.push(position);
Self { positions }
}
Complexity: O(1) Usage: Create state with one position Performance: Optimal - one allocation + one push
State::from_positions(positions) - lines 43-49pub fn from_positions(mut positions: Vec<Position>) -> Self {
positions.sort();
positions.dedup();
Self {
positions: SmallVec::from_vec(positions),
}
}
Complexity: O(n log n) for sort Usage: Batch state creation Performance: Standard sort/dedup, reasonable for batch ops
State::insert() - lines 82-99Status: ✅ Already optimized (see SUBSUMPTION_OPTIMIZATION_REPORT.md) Performance: O(kn) where k << n, 3.3x faster than alternatives
State::merge() - lines 102-106pub fn merge(&mut self, other: &State, algorithm: Algorithm) {
for position in &other.positions {
self.insert(*position, algorithm);
}
}
Complexity: O(m × kn) where m = positions in other state Usage: Combine two states Performance: Depends on insert() which is already optimal Potential Concern: Multiple insert() calls could be batched
State::clear() - lines 147-149pub fn clear(&mut self) {
self.positions.clear();
}
Complexity: O(1) Usage: StatePool reuse Performance: Optimal - just resets length
State::copy_from() - lines 162-168pub fn copy_from(&mut self, other: &State) {
self.positions.clear();
self.positions.reserve(other.positions.len());
for pos in &other.positions {
self.positions.push(*pos); // Copy, not clone
}
}
Complexity: O(n) Usage: StatePool reuse with copy Performance: Good - uses reserve + copy Potential Optimization: Could use slice copy instead of loop
State::head() - lines 109-111pub fn head(&self) -> Option<&Position> {
self.positions.first()
}
Complexity: O(1) Performance: Optimal
State::positions() - lines 115-117pub fn positions(&self) -> &[Position] {
&self.positions
}
Complexity: O(1) Performance: Optimal - just returns slice reference
State::is_empty() - lines 121-123pub fn is_empty(&self) -> bool {
self.positions.is_empty()
}
Complexity: O(1) Performance: Optimal
State::len() - lines 127-129pub fn len(&self) -> usize {
self.positions.len()
}
Complexity: O(1) Performance: Optimal
State::iter() - lines 132-134pub fn iter(&self) -> impl Iterator<Item = &Position> {
self.positions.iter()
}
Complexity: O(1) to create iterator Performance: Optimal - zero-cost abstraction
State::min_distance() - lines 174-186pub fn min_distance(&self) -> Option<usize> {
self.positions.first().map(|first| {
// Fast path: if we only have one position, return it immediately
if self.positions.len() == 1 {
return first.num_errors;
}
// Otherwise, find the minimum
self.positions.iter().map(|p| p.num_errors).min().unwrap()
})
}
Complexity:
Usage: Find minimum error count in state Performance: Good - has fast path for single position Potential Optimization: Could cache min_errors if frequently called
State::infer_distance() - lines 193-210pub fn infer_distance(&self, query_length: usize) -> Option<usize> {
// Fast path: single position (common case)
if self.positions.len() == 1 {
let p = &self.positions[0];
let remaining = query_length.saturating_sub(p.term_index);
return Some(p.num_errors + remaining);
}
// General case: find minimum across all positions
self.positions
.iter()
.map(|p| {
let remaining = query_length.saturating_sub(p.term_index);
p.num_errors + remaining
})
.min()
}
Complexity:
Usage: Calculate final edit distance at end of dictionary term Performance: Good - has fast path, uses iterator min() Optimization: Well-designed with fast path
State::infer_prefix_distance() - lines 220-237pub fn infer_prefix_distance(&self, query_length: usize) -> Option<usize> {
// Fast path: single position
if self.positions.len() == 1 {
let p = &self.positions[0];
return if p.term_index >= query_length {
Some(p.num_errors)
} else {
None
};
}
// General case: find minimum among positions that consumed the full query
self.positions
.iter()
.filter(|p| p.term_index >= query_length)
.map(|p| p.num_errors)
.min()
}
Complexity:
Usage: Calculate distance for prefix matching Performance: Good - has fast path, uses filter + min() Optimization: Well-designed
new() - O(1)single() - O(1)insert() - Already optimized (subsumption analysis)clear() - O(1)head() - O(1)positions() - O(1)is_empty() - O(1)len() - O(1)iter() - O(1) to createmin_distance() - O(1) for n=1, O(n) otherwiseinfer_distance() - O(1) for n=1, O(n) otherwiseinfer_prefix_distance() - O(1) for n=1, O(n) otherwisecopy_from() - lines 162-168
merge() - lines 102-106
from_positions() - lines 43-49
1. Create initial state: State::single(Position::new(0, 0))
2. Transition states: transition_state() [already benchmarked at ~75ns]
3. Query distance: infer_distance() or infer_prefix_distance()
4. Cleanup: Drop (automatic)
1. Allocate state from pool
2. clear() existing state
3. copy_from() or manual population via insert()
4. Use state
5. Return to pool
The StatePool pattern is used in transition_state_pooled() for allocation reuse.
min_distance() - single vs multiple positionsinfer_distance() - single vs multiple positionsinfer_prefix_distance() - single vs multiple positionscopy_from() - measure memcpy overheadmerge() - measure vs hypothetical batch insertcopy_from() loop vs slice copymerge() multiple insert vs batchmin_distance() with cached vs computedBased on transition benchmark results (~75ns full state transition), the state operations are likely not bottlenecks because:
If profiling a full dictionary query:
infer_distance() - Called for every accepted dictionary term
copy_from() in StatePool - If used frequently
merge() - If used (not clear from codebase)
copy_from() slice copy
Cached min_distance
Based on the transition benchmark results showing ~75ns full state transitions, and given that:
Prediction: State operations are already well-optimized and are not bottlenecks.
The likely final conclusion will be:
"State operations are already efficient. The O(1) operations are optimal, and the O(n) operations have appropriate fast paths. No optimization is needed unless profiling identifies specific hot spots."
src/transducer/state.rsSUBSUMPTION_OPTIMIZATION_REPORT.mdTRANSITION_OPTIMIZATION_REPORT.mdsrc/transducer/position.rsCan 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 |