The in-place mutation API would eliminate the 21.73% State cloning overhead by reusing State allocations instead of cloning. This requires significant API changes and refactoring but provides deterministic performance gains.
Complexity: High Impact: High (21.73% overhead → 0%) Breaking Change: Yes Estimated Effort: 2-3 days implementation + testing
State cloning: 21.73% of runtime
State::clone: 7.60% (Vec allocation + copy)PathMapNode::clone: 6.70% (path Vec clones)Intersection creation: Remaining overhead1. State Transition (src/transducer/transition.rs:203)
pub fn transition_state(
state: &State, // ← Borrowed
dict_char: u8,
query: &[u8],
max_distance: usize,
algorithm: Algorithm,
) -> Option<State> { // ← Returns new State
let expanded_state = epsilon_closure(state, ...); // ← CLONE #1
let mut next_state = State::new();
for position in expanded_state.positions() {
// ... compute next positions ...
next_state.insert(next_pos);
}
Some(next_state) // ← Return new allocation
}
2. Epsilon Closure (src/transducer/transition.rs:193)
fn epsilon_closure(state: &State, ...) -> State {
let mut result = state.clone(); // ← CLONE #2
epsilon_closure_mut(&mut result, ...);
result
}
3. Query Iterator (src/transducer/query.rs:83)
fn queue_children(&mut self, intersection: &Box<Intersection<N>>) {
for (label, child_node) in intersection.node.edges() {
if let Some(next_state) = transition_state(
&intersection.state, // ← Borrow current state
label, query, max_distance, algorithm
) {
// Create new Intersection with new State
let child = Box::new(Intersection::with_parent(
label,
child_node,
next_state, // ← Move new State
parent_box,
));
self.queue.push(child);
}
}
}
Total Clones Per Edge:
epsilon_closure clones the input statetransition_state creates new stateIntersection::with_parent takes ownershipFor a typical query with 100 state transitions → 200+ State clones
Instead of cloning States, reuse allocations from a pool.
// New: State allocation pool
pub struct StatePool {
// Recycled states ready for reuse
pool: Vec<State>,
// Statistics
allocations: usize,
reuses: usize,
}
impl StatePool {
pub fn new() -> Self {
Self {
pool: Vec::with_capacity(16), // Start with small pool
allocations: 0,
reuses: 0,
}
}
/// Get a state (from pool or allocate new)
pub fn acquire(&mut self) -> State {
if let Some(mut state) = self.pool.pop() {
state.clear(); // Clear positions but keep allocation
self.reuses += 1;
state
} else {
self.allocations += 1;
State::new()
}
}
/// Return a state to the pool for reuse
pub fn release(&mut self, state: State) {
if self.pool.len() < 32 { // Cap pool size
self.pool.push(state);
}
}
}
Before (current):
pub fn transition_state(
state: &State,
dict_char: u8,
query: &[u8],
max_distance: usize,
algorithm: Algorithm,
) -> Option<State>
After (in-place mutation):
pub fn transition_state_pooled(
state: &State, // Input (still borrowed)
pool: &mut StatePool, // ← NEW: Pool for allocations
dict_char: u8,
query: &[u8],
max_distance: usize,
algorithm: Algorithm,
) -> Option<State> // Returns pooled state
Implementation:
pub fn transition_state_pooled(
state: &State,
pool: &mut StatePool,
dict_char: u8,
query: &[u8],
max_distance: usize,
algorithm: Algorithm,
) -> Option<State> {
let window_size = max_distance + 1;
let query_length = query.len();
// Get a state from pool (reuses allocation!)
let mut expanded_state = pool.acquire();
// In-place epsilon closure (no clone!)
epsilon_closure_into(state, &mut expanded_state, query_length, max_distance);
// Get another state from pool
let mut next_state = pool.acquire();
let mut cv_buffer = [false; 8];
for position in expanded_state.positions() {
let offset = position.term_index;
let cv = characteristic_vector(dict_char, query, window_size, offset, &mut cv_buffer);
let next_positions = transition_position(position, cv, query_length, max_distance, algorithm);
for next_pos in next_positions {
next_state.insert(next_pos);
}
}
// Return expanded_state to pool (no longer needed)
pool.release(expanded_state);
if next_state.is_empty() {
pool.release(next_state);
None
} else {
Some(next_state)
}
}
/// Compute epsilon closure into a target state (no allocation)
fn epsilon_closure_into(
source: &State,
target: &mut State,
query_length: usize,
max_distance: usize,
) {
// Copy positions from source to target
for pos in source.positions() {
target.insert(pos.clone()); // Position is small (Copy?)
}
// Apply epsilon closure in-place
epsilon_closure_mut(target, query_length, max_distance);
}
impl State {
/// Clear all positions (keeps allocation)
pub fn clear(&mut self) {
self.positions.clear(); // Vec::clear keeps capacity
}
/// Copy positions from another state
pub fn copy_from(&mut self, other: &State) {
self.positions.clear();
for pos in other.positions() {
self.positions.push(pos.clone());
}
}
}
Current:
fn queue_children(&mut self, intersection: &Box<Intersection<N>>) {
for (label, child_node) in intersection.node.edges() {
if let Some(next_state) = transition_state(
&intersection.state,
label, &self.query, self.max_distance, self.algorithm
) {
let child = Box::new(Intersection::with_parent(
label, child_node, next_state, parent_box
));
self.queue.push(child);
}
}
}
Modified:
// Add pool to CandidateIterator
pub struct CandidateIterator<'a, D, N>
where
D: Dictionary<Node = N>,
N: DictionaryNode,
{
// ... existing fields ...
state_pool: StatePool, // ← NEW
}
fn queue_children(&mut self, intersection: &Box<Intersection<N>>) {
for (label, child_node) in intersection.node.edges() {
if let Some(next_state) = transition_state_pooled(
&intersection.state,
&mut self.state_pool, // ← Pass pool
label, &self.query, self.max_distance, self.algorithm
) {
// ... rest same ...
}
}
// When intersection is done, return its state to pool
// (requires additional lifecycle management)
}
Problem: When can we return a State to the pool?
Currently, Intersection owns its State:
pub struct Intersection<N: DictionaryNode> {
pub state: State, // Owned
// ...
}
Solution A: Reference Counting
pub struct Intersection<N: DictionaryNode> {
pub state: Rc<State>, // Shared ownership
// ...
}
Solution B: Explicit Lifecycle
impl Drop for Intersection<N> {
fn drop(&mut self) {
// Problem: Can't access pool from here!
// Would need global pool or different approach
}
}
Solution C: Pool Per Query (Recommended)
Public API Changes:
// Old (current)
pub fn transition_state(
state: &State,
dict_char: u8,
query: &[u8],
max_distance: usize,
algorithm: Algorithm,
) -> Option<State>
// New (with pool)
pub fn transition_state_pooled(
state: &State,
pool: &mut StatePool, // ← NEW PARAMETER
dict_char: u8,
query: &[u8],
max_distance: usize,
algorithm: Algorithm,
) -> Option<State>
Migration Strategy:
transition_state as deprecated wrappertransition_state_pooledEven with State pooling, we still clone Position objects:
for pos in source.positions() {
target.insert(pos.clone()); // ← Still cloning
}
Optimization: Make Position Copy
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Position {
pub term_index: usize, // 8 bytes
pub num_errors: usize, // 8 bytes
pub is_special: bool, // 1 byte
} // Total: ~17 bytes → good for Copy
If Position is Copy, then:
target.insert(*pos); // Bitwise copy, no allocation
State::clone: 21.73% total
1. Eliminate Vec Allocations (6.00%)
Vec::clear() keeps capacity2. Reduce Position Copies (7.44% → ~2%)
Copy reduces overhead3. Orthogonal to PathMapNode (6.70%)
Realistic improvement: 10-15% overall performance gain
StatePool structureacquire() and release()Copy (if possible)State::clear() and helperstransition_state_pooled()epsilon_closure_into()CandidateIteratorqueue_children() to use pooltransition_stateRisk: StatePool management adds overhead
Mitigation:
Risk: Pool grows indefinitely
Mitigation:
Risk: Code becomes harder to maintain
Mitigation:
Risk: StatePool is not thread-safe
Current State: Queries are single-threaded Mitigation:
| Approach | Complexity | Impact | Breaking | Risk |
|---|---|---|---|---|
| In-Place Mutation | High | 10-15% | Yes | Medium |
| SmallVec (Phase 4) | Low | Mixed | No | Proven failed |
| Arc<Vec> paths | Low | 5% | No | Low |
| Copy-on-Write | Very High | Unknown | Yes | High |
Verdict: In-place mutation is the highest-impact option remaining, but requires significant work.
Reasons:
Alternative: Try Arc<Vec<u8>> for paths first (5% gain, no API break, low risk)
The in-place mutation API is technically feasible and would provide 10-15% performance improvement by eliminating State cloning overhead. However, it requires:
Given current performance is already excellent, this optimization should only be pursued if:
Status: Design complete, awaiting decision to implement.
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 |