Liking cljdoc? Tell your friends :D

Parameterized Transducer Subsumption Optimization Decision

Question

Can the same subsumption pre-sorting optimization (BTreeSet with custom Ord) applied to Universal transducers be applied to parameterized transducers for performance improvements?

Answer

No, optimization not needed. The parameterized transducers already use an optimal implementation that is superior to the BTreeSet approach for their use case.


Analysis

Current Implementation

File: src/transducer/state.rs

Data Structure:

pub struct State {
    positions: SmallVec<[Position; 8]>,
}

Key characteristics:

  • Stack-allocated for $\le 8$ positions (no heap allocation)
  • Maintained in sorted order via binary_search + insert
  • Online subsumption during insertion
  • Explicit sorting: $\mathcal{O}(\log n)$ binary search per insert

Alternative: BTreeSet Approach (Universal Transducers)

Data Structure:

pub struct UniversalState<V: PositionVariant> {
    positions: BTreeSet<UniversalPosition<V>>,
    max_distance: u8,
}

Key characteristics:

  • Heap-allocated red-black tree
  • Automatic sorting via custom Ord implementation
  • Online subsumption during insertion
  • Automatic sorting: $\mathcal{O}(\log n)$ tree operations

Performance Comparison

Benchmark Results (2025-10-29)

Source: docs/optimization/SUBSUMPTION_OPTIMIZATION_REPORT.md

Current SmallVec implementation (parameterized transducers):

Position CountTimeThroughput
n=10~360ns-
n=50~1.7µs29.24 Melem/s
n=100~2.6µs-
n=200~4.3µs-

Performance characteristics:

  • 3.3x faster than batch unsubsumption (Java-style)
  • ✅ Stack allocation for small states (no heap overhead)
  • ✅ Excellent cache locality (contiguous memory)
  • ✅ $\mathcal{O}(1)$ best case with early exit
  • ✅ $\mathcal{O}(\text{kn})$ average case where k << n

State Size Distribution (Real-World)

From profiling analysis of dictionary queries:

2-5 positions:   70% of states (SmallVec sweet spot!)
6-8 positions:   20% of states (still stack-allocated)
9-15 positions:   9% of states (heap, but still fast)
>15 positions:   <1% of states (rare, large distances)

Key insight: 90% of states have $\le 8$ positions, making SmallVec's stack allocation optimal.


Comparison: SmallVec vs BTreeSet

Data Structure Trade-offs

AspectSmallVec (Current)BTreeSet (Alternative)
Insert$\mathcal{O}(\log n)$ search + $\mathcal{O}(n)$ shift$\mathcal{O}(\log n)$ tree operations
MemoryStack $(\le 8),$ then heapAlways heap allocated
Cache localityExcellent (contiguous)Good (tree nodes)
Allocation overheadNone for $\le 8$ positionsEvery insert allocates
Iteration$\mathcal{O}(n)$ sequential$\mathcal{O}(n)$ in-order
Small states $(\le 8)$OptimalOverkill (heap overhead)
Large states (>8)CompetitiveCompetitive

Algorithmic Equivalence

Both approaches achieve the same early termination optimizations:

SmallVec (src/transducer/state.rs:82-100):

pub fn insert(&mut self, position: Position, algorithm: Algorithm, query_length: usize) {
    // Early termination: check if subsumed
    for existing in &self.positions {
        if existing.subsumes(&position, algorithm, query_length) {
            return;  // O(1) best case
        }
    }

    // Remove subsumed positions
    self.positions.retain(|p| !position.subsumes(p, algorithm, query_length));

    // Binary search + insert
    let insert_pos = self.positions.binary_search(&position).unwrap_or_else(|pos| pos);
    self.positions.insert(insert_pos, position);
}

BTreeSet (src/transducer/universal/state.rs:155-182):

pub fn add_position(&mut self, pos: UniversalPosition<V>) {
    let pos_errors = pos.errors();

    // Early termination: only check positions with more errors
    self.positions.retain(|p| {
        if p.errors() <= pos_errors {
            true  // Cannot be subsumed
        } else {
            !subsumes(&pos, p, self.max_distance)
        }
    });

    // Early termination: only check positions with fewer errors
    let is_subsumed = self.positions.iter()
        .take_while(|p| p.errors() < pos_errors)
        .any(|p| subsumes(p, &pos, self.max_distance));

    if !is_subsumed {
        self.positions.insert(pos);  // O(log n)
    }
}

Key difference: BTreeSet can use take_while() for error-based early termination because positions are sorted by (errors, offset). However, SmallVec achieves similar $\mathcal{O}(\text{kn})$ performance through online subsumption without this optimization.


Java Implementation Comparison

Source: docs/research/universal-levenshtein/SUBSUMPTION_COMPARISON_JAVA_VS_RUST.md

Java Approach

Data structure: Linked list + explicit merge sort Strategy: Batch unsubsumption (sort → remove subsumed) Complexity: $\mathcal{O}(n \log n)$ + $\mathcal{O}(n*k)$ where k << n

Recommendation from analysis (lines 442-459):

5. 🔄 Consider Linked List for Parameterized (Optional)

For the parameterized transducers (Schulz & Mihov 2002), which use SmallVec<[Position; 8]>:

Current:

positions: SmallVec<[Position; 8]>  // Stack allocation for $`\le 8`$ positions

Alternative (Java-style):

positions: Option<Box<Position>>  // Linked list like Java

Analysis:

  • SmallVec is better for small states (stack allocation, cache-friendly)
  • Linked list is better for large states (no reallocation, $\mathcal{O}(1)$ remove during iteration)
  • Recommendation: Keep SmallVec (most states are small)

Decision Rationale

Why SmallVec is Optimal for Parameterized Transducers

  1. State size distribution favors SmallVec

    • 90% of states have $\le 8$ positions
    • Stack allocation eliminates heap overhead for 90% of cases
    • No allocator calls for typical queries
  2. Cache locality advantage

    • Contiguous memory for positions
    • Better prefetching
    • Fewer cache misses than tree structures
  3. Already benchmarked and proven

    • 3.3x faster than batch approaches
    • Empirical data from 2025-10-29 optimization analysis
    • Real-world profiling confirms distribution
  4. Simplicity and maintainability

    • Standard Vec-like API
    • No custom Ord implementation needed
    • Clear, idiomatic Rust code
  5. Asymptotic equivalence

    • Both SmallVec and BTreeSet achieve $\mathcal{O}(\text{kn})$ with early termination
    • BTreeSet's take_while() optimization doesn't provide measurable benefit
    • SmallVec's constant factors are better for small n

Why NOT to Switch to BTreeSet

  1. Heap allocation overhead

    • BTreeSet always allocates on heap
    • SmallVec is stack-allocated for 90% of cases
    • Allocation is expensive relative to subsumption checks
  2. Tree node overhead

    • Red-black tree nodes have metadata (color, pointers)
    • SmallVec has minimal overhead (just capacity + length)
  3. No performance gain

    • Both achieve same $\mathcal{O}(\text{kn})$ complexity
    • SmallVec has better constant factors for small n
    • Error-based early termination doesn't help enough to offset costs
  4. API complexity

    • BTreeSet requires custom Ord implementation
    • SmallVec uses standard comparison
    • More code to maintain

Conclusion

Final Recommendation

Keep the current SmallVec implementation for parameterized transducers.

Rationale:

  1. ✅ Already optimal for 90% of states $(\le 8$ positions)
  2. ✅ Benchmarked 3.3x faster than alternative approaches
  3. ✅ Superior cache locality and memory efficiency
  4. ✅ Simpler code, easier to maintain
  5. ✅ Asymptotically equivalent to BTreeSet
  6. ✅ Java analysis confirms SmallVec is correct choice

Summary Table

AspectSmallVec (Current)BTreeSet (Alternative)Winner
Small states $(\le 8)$Stack allocated, $\mathcal{O}(1)$Heap allocated, $\mathcal{O}(\log n)$SmallVec
Large states (>8)Heap, $\mathcal{O}(n)$ shiftsHeap, $\mathcal{O}(\log n)$ tree opsTie
Memory overheadMinimal (90% stack)Always heap + metadataSmallVec
Cache localityExcellentGoodSmallVec
Early termination$\mathcal{O}(\text{kn})$ with online check$\mathcal{O}(k)$ with take_while()Tie
Code simplicitySimple Vec APICustom Ord requiredSmallVec
OverallOptimalOverkillSmallVec

Related Documentation

  • Universal BTreeSet optimization: docs/research/universal-levenshtein/SUBSUMPTION_OPTIMIZATION.md
  • Java comparison: docs/research/universal-levenshtein/SUBSUMPTION_COMPARISON_JAVA_VS_RUST.md
  • Parameterized benchmarks: docs/optimization/SUBSUMPTION_OPTIMIZATION_REPORT.md
  • Test fixes: docs/research/universal-levenshtein/SUBSUMPTION_BTREESET_TEST_FIXES.md

Lessons Learned

1. Different Data Structures for Different Use Cases

Universal transducers (BTreeSet):

  • Parameter-free algorithm with relative offsets
  • Positions sorted by (errors, offset) enables powerful early termination
  • Error-based filtering via take_while() is beneficial
  • State sizes more varied, heap allocation acceptable

Parameterized transducers (SmallVec):

  • Absolute term indices, simpler subsumption
  • Small state sizes (90% $\le 8$ positions) make stack allocation dominant
  • Online subsumption sufficient without error-based filtering
  • Cache locality more important than tree structure

2. Benchmark Before Optimizing

The 2025-10-29 subsumption analysis provided empirical evidence that:

  • Current implementation is 3.3x faster than alternatives
  • State size distribution heavily favors stack allocation
  • Real-world performance confirms theoretical analysis

Without this data, we might have assumed BTreeSet would be an improvement.

3. "Premature Optimization is the Root of All Evil"

The BTreeSet optimization for Universal transducers was:

  • ✅ Necessary: Error-based early termination provides real benefit
  • ✅ Validated: Tests confirmed correctness
  • ✅ Measured: Benchmarks showed improvement

For parameterized transducers:

  • ❌ Not necessary: SmallVec already optimal
  • ✅ Already validated: Existing benchmarks prove performance
  • ❌ No improvement expected: Same asymptotic complexity, worse constants

Appendix: Code References

Current Parameterized Implementation

File: src/transducer/state.rs

  • Line 18-23: SmallVec definition
  • Line 82-100: insert() with online subsumption
  • Line 52-77: Design rationale and performance data

Universal BTreeSet Implementation

File: src/transducer/universal/state.rs

  • Line 83-119: BTreeSet definition
  • Line 155-182: add_position() with error-based early termination

File: src/transducer/universal/position.rs

  • Line 207-228: Custom Ord implementation for (errors, offset) sorting

Benchmark Files

Parameterized:

  • benches/subsumption_benchmarks.rs: Online vs batch comparison
  • docs/optimization/SUBSUMPTION_OPTIMIZATION_REPORT.md: Full analysis

Universal:

  • docs/research/universal-levenshtein/SUBSUMPTION_OPTIMIZATION.md: BTreeSet design
  • docs/research/universal-levenshtein/SUBSUMPTION_COMPARISON_JAVA_VS_RUST.md: Java comparison

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