Liking cljdoc? Tell your friends :D

Position Transition Function Analysis

Overview

This document analyzes the performance characteristics of the position transition functions for all three Levenshtein automaton algorithms: Standard, Transposition, and MergeAndSplit.

Implementation Location

File: src/transducer/transition.rs

Main Functions

  1. transition_position() - Entry point that dispatches to algorithm-specific functions
  2. transition_standard() - Standard Levenshtein (insert, delete, substitute)
  3. transition_transposition() - Transposition variant (adds character swaps)
  4. transition_merge_split() - Merge/split variant (char merge/split operations)

Supporting Functions

  • characteristic_vector() - Computes match vector for dictionary character vs query
  • index_of_match() - Finds first match in characteristic vector (multi-char deletion optimization)
  • epsilon_closure() - Adds positions reachable by deletion without consuming dict chars
  • transition_state() - Transitions entire state
  • transition_state_pooled() - Pool-aware version for allocation reuse

Algorithm-Specific Analysis

1. Standard Algorithm

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)

2. Transposition Algorithm

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)

3. MergeAndSplit Algorithm

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

Performance Characteristics

Characteristic Vector Computation

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)

Index of Match

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])

Epsilon Closure

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

State Transition Analysis

transition_state()

Location: transition.rs:509-551

Process:

  1. Compute epsilon closure (adds deletion positions)
  2. For each position in expanded state:
    • Compute characteristic vector
    • Transition position
    • Insert resulting positions into next state
  3. 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:

  1. Epsilon closure allocation: Creates new state (though uses SmallVec)
  2. Repeated characteristic vector: Computed for each position (but cached in buffer)
  3. State::insert overhead: Called many times per transition
  4. Position allocation: Up to 4 positions allocated per input position

transition_state_pooled()

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


Benchmark Plan

Test Dimensions

  1. Algorithms: Standard, Transposition, MergeAndSplit
  2. Query Lengths: 4, 8, 16, 32 characters
  3. Max Distances: 0, 1, 2, 3, 5, 10
  4. Position Counts: 1, 3, 5, 10 positions per state
  5. Match Patterns:
    • Immediate match (best case)
    • No match (worst case)
    • Partial match (average case)

Specific Benchmarks

  1. characteristic_vector - measure vector computation overhead
  2. index_of_match - measure multi-char deletion scan
  3. transition_standard - individual position transition
  4. transition_transposition - with special positions
  5. transition_merge_split - with special positions
  6. epsilon_closure - deletion position expansion
  7. transition_state - full state transition
  8. transition_state_pooled - pooled vs non-pooled comparison

Metrics

  • Time per transition (nanoseconds)
  • Throughput (transitions/second)
  • Positions generated per input position
  • SmallVec heap allocations (should be 0 for typical cases)

Expected Bottlenecks

Based on code analysis, likely hot spots:

1. State::insert() (Already Optimized)

  • Called many times per state transition
  • But we've already proven this is optimal!

2. Epsilon Closure

  • Creates intermediate state
  • Processes each position
  • Multiple State::insert() calls

3. Transition Position Branching

  • Complex control flow in all three algorithms
  • Many conditional branches
  • May cause branch mispredictions

4. Characteristic Vector Computation

  • Called for every position in expanded state
  • Simple but frequent operation

5. Position Allocation

  • SmallVec should handle typical cases
  • But may heap-allocate for large states

Optimization Opportunities

Potential Optimizations (Priority Order)

High Priority

  1. 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)
  2. Bit-Packed Characteristic Vector

    • Use u8 instead of [bool; 8]
    • Faster index_of_match with trailing_zeros()
    • Smaller memory footprint
    • Better cache behavior
  3. Pre-computed Transition Tables

    • Cache common transition patterns
    • Key: (position, characteristic_vector, algorithm)
    • Value: resulting positions
    • Effective for repeated queries

Medium Priority

  1. Batch Position Insertion

    • Collect all next positions, then bulk insert
    • May reduce subsumption checking overhead
    • Trade-off with subsumption early exit
  2. Epsilon Closure Caching

    • Cache epsilon closure results per state
    • Key: (positions, max_distance, algorithm)
    • High memory cost, questionable benefit
  3. Branch Optimization

    • Restructure conditionals to reduce mispredictions
    • Use likely/unlikely hints
    • Profile-guided optimization

Low Priority

  1. Custom Allocator for Positions

    • Position is small (16 bytes)
    • Could use arena allocator
    • SmallVec already handles well
  2. Parallel State Transition

    • Process positions in parallel
    • Only beneficial for very large states (n > 20)
    • Rare in practice

Next Steps

  1. ✅ Analyze transition function implementations
  2. ⏳ Create comprehensive benchmark suite
  3. ⏳ Run benchmarks and generate flame graphs
  4. ⏳ Identify actual bottlenecks (may differ from theoretical analysis)
  5. ⏳ Implement targeted optimizations for hot paths
  6. ⏳ Validate performance improvements

References

  • 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

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close