This document analyzes the performance characteristics of the position transition functions
for all three Levenshtein automaton algorithms: Standard, Transposition, and MergeAndSplit.
File: src/transducer/transition.rs
transition_position() - Entry point that dispatches to algorithm-specific functionstransition_standard() - Standard Levenshtein (insert, delete, substitute)transition_transposition() - Transposition variant (adds character swaps)transition_merge_split() - Merge/split variant (char merge/split operations)
characteristic_vector() - Computes match vector for dictionary character vs queryindex_of_match() - Finds first match in characteristic vector (multi-char deletion optimization)epsilon_closure() - Adds positions reachable by deletion without consuming dict charstransition_state() - Transitions entire statetransition_state_pooled() - Pool-aware version for allocation reuse
Location: transition.rs:123-192
Operations:
- Substitution:
(i, e) → (i+1, e+1) when query[i] ≠ dict_char - Insertion:
(i, e) → (i, e+1) (consume dict char, don't advance query) - Deletion:
(i, e) → (i+1, e+1) (skip query char) - Match:
(i, e) → (i+1, e) when query[i] == dict_char
Optimizations:
- Multi-character deletion via
index_of_match() - skips multiple query chars in one operation - Early exit when match found immediately
- Returns 1-3 positions typically (SmallVec avoids heap allocation)
Complexity:
- Best case: O(1) with immediate match
- Typical: O(k) where k = min(max_distance - e + 1, remaining_query_length)
- Worst case: O(max_distance) for scanning characteristic vector
Potential Bottlenecks:
index_of_match() linear scan (but bounded by max_distance, typically ≤3)- Multiple Position allocations per transition
- Branching on cases (may cause branch mispredictions)
Location: transition.rs:198-323
Additional Operations:
- Transposition:
(i, e) → (i+2, e+1) via special intermediate state - Uses
is_special flag to track transposition progress
Complexity:
- Similar to Standard, plus transposition logic
- Additional branch checks for
is_special flag - Returns 1-4 positions depending on state
Potential Bottlenecks:
- More complex branching (3 main cases × subcases)
- Special position state tracking adds overhead
- More positions to process per transition (up to 4)
Location: transition.rs:330-426
Additional Operations:
- Merge: Two query chars → one dict char:
(i, e) → (i+2, e+1) - Split: One query char → two dict chars (via special state)
- Uses
is_special flag for split operation tracking
Complexity:
- Similar to Transposition
- Branch complexity between Standard and Transposition
- Returns 1-4 positions
Potential Bottlenecks:
- Similar branching complexity to Transposition
- Special state handling overhead
Function: characteristic_vector() (transition.rs:22-36)
fn characteristic_vector<'a>(
dict_char: u8,
query: &[u8],
window_size: usize,
offset: usize,
buffer: &'a mut [bool; 8],
) -> &'a [bool]
Optimization: Stack-allocated buffer (no heap allocation)
Complexity: O(min(window_size, 8)) ≈ O(max_distance)
Typical Performance:
- window_size usually ≤ 4 (max_distance ≤ 3 is common)
- Highly cache-friendly (small buffer, sequential access)
- Simple comparison operations
Potential Optimizations:
- SIMD for parallel character comparisons (if window_size > 4)
- Lookup table for common characters (if profiling shows char comparison is hot)
Function: index_of_match() (transition.rs:105-112)
fn index_of_match(cv: &[bool], start: usize, limit: usize) -> Option<usize>
Purpose: Multi-character deletion optimization
Complexity: O(limit), where limit ≤ max_distance + 1
Typical Performance:
- limit usually ≤ 4
- Early exit common (match found quickly)
- Simple boolean checks
Potential Optimizations:
- SIMD scan for first
true value - Bit-packing characteristic vector (use u8 instead of [bool; 8])
Function: epsilon_closure_mut() (transition.rs:433-465)
Purpose: Add positions reachable by deletion without consuming dictionary characters
Complexity: O(n × m) where:
- n = number of positions in state
- m = average positions added per position (typically 1-2)
Optimization: In-place modification, uses State::insert() for deduplication
Typical Performance:
- Most states: 2-5 positions → 3-7 positions after closure
- State::insert() early exit helps (subsumption checking)
- SmallVec avoids heap allocation for typical sizes
Potential Bottlenecks:
- Repeated calls to State::insert() (but this is already optimized)
- Position duplication checks
- Growing
to_process vec
Potential Optimizations:
- Batch insertion with deduplication
- Bit-set for tracking processed positions
- Pre-compute epsilon closure patterns for common cases
Location: transition.rs:509-551
Process:
- Compute epsilon closure (adds deletion positions)
- For each position in expanded state:
- Compute characteristic vector
- Transition position
- Insert resulting positions into next state
- Return next state (or None if empty)
Complexity: O(n × k × m) where:
- n = positions in current state
- k = cost of epsilon closure (typically ~1.5x)
- m = cost of transition_position (typically 1-4 positions returned)
Typical Path:
state (3 positions)
→ epsilon_closure (5 positions)
→ transition each (5 × 2 = 10 position transitions)
→ insert into next_state (State::insert with subsumption)
→ next_state (4 positions after subsumption)
Potential Bottlenecks:
- Epsilon closure allocation: Creates new state (though uses SmallVec)
- Repeated characteristic vector: Computed for each position (but cached in buffer)
- State::insert overhead: Called many times per transition
- Position allocation: Up to 4 positions allocated per input position
Location: transition.rs:580-638
Optimization: Uses StatePool for allocation reuse
Benefits:
- Eliminates Vec allocations for states
- Reuses expanded_state and next_state allocations
- ~6-10% performance improvement over non-pooled version
Complexity: Same as transition_state() but with lower constant factors
- Algorithms: Standard, Transposition, MergeAndSplit
- Query Lengths: 4, 8, 16, 32 characters
- Max Distances: 0, 1, 2, 3, 5, 10
- Position Counts: 1, 3, 5, 10 positions per state
- Match Patterns:
- Immediate match (best case)
- No match (worst case)
- Partial match (average case)
- characteristic_vector - measure vector computation overhead
- index_of_match - measure multi-char deletion scan
- transition_standard - individual position transition
- transition_transposition - with special positions
- transition_merge_split - with special positions
- epsilon_closure - deletion position expansion
- transition_state - full state transition
- transition_state_pooled - pooled vs non-pooled comparison
- Time per transition (nanoseconds)
- Throughput (transitions/second)
- Positions generated per input position
- SmallVec heap allocations (should be 0 for typical cases)
Based on code analysis, likely hot spots:
- Called many times per state transition
- But we've already proven this is optimal!
- Creates intermediate state
- Processes each position
- Multiple State::insert() calls
- Complex control flow in all three algorithms
- Many conditional branches
- May cause branch mispredictions
- Called for every position in expanded state
- Simple but frequent operation
- SmallVec should handle typical cases
- But may heap-allocate for large states
-
SIMD Characteristic Vector
- Use SIMD to compare 8 bytes at once
- Only beneficial if max_distance > 4
- May not help for typical cases (max_distance ≤ 3)
-
Bit-Packed Characteristic Vector
- Use u8 instead of [bool; 8]
- Faster index_of_match with
trailing_zeros() - Smaller memory footprint
- Better cache behavior
-
Pre-computed Transition Tables
- Cache common transition patterns
- Key: (position, characteristic_vector, algorithm)
- Value: resulting positions
- Effective for repeated queries
-
Batch Position Insertion
- Collect all next positions, then bulk insert
- May reduce subsumption checking overhead
- Trade-off with subsumption early exit
-
Epsilon Closure Caching
- Cache epsilon closure results per state
- Key: (positions, max_distance, algorithm)
- High memory cost, questionable benefit
-
Branch Optimization
- Restructure conditionals to reduce mispredictions
- Use likely/unlikely hints
- Profile-guided optimization
-
Custom Allocator for Positions
- Position is small (16 bytes)
- Could use arena allocator
- SmallVec already handles well
-
Parallel State Transition
- Process positions in parallel
- Only beneficial for very large states (n > 20)
- Rare in practice
- ✅ Analyze transition function implementations
- ⏳ Create comprehensive benchmark suite
- ⏳ Run benchmarks and generate flame graphs
- ⏳ Identify actual bottlenecks (may differ from theoretical analysis)
- ⏳ Implement targeted optimizations for hot paths
- ⏳ Validate performance improvements
- C++ Implementation:
liblevenshtein-cpp/src/liblevenshtein/transducer/position_transition.cpp - Rust Implementation:
src/transducer/transition.rs - State Implementation:
src/transducer/state.rs - Position Implementation:
src/transducer/position.rs