Can the same subsumption pre-sorting optimization (BTreeSet with custom Ord) applied to Universal transducers be applied to parameterized transducers for performance improvements?
No, optimization not needed. The parameterized transducers already use an optimal implementation that is superior to the BTreeSet approach for their use case.
File: src/transducer/state.rs
Data Structure:
pub struct State {
positions: SmallVec<[Position; 8]>,
}
Key characteristics:
\le 8$ positions (no heap allocation)binary_search + insert\mathcal{O}(\log n)$ binary search per insertData Structure:
pub struct UniversalState<V: PositionVariant> {
positions: BTreeSet<UniversalPosition<V>>,
max_distance: u8,
}
Key characteristics:
Ord implementation\mathcal{O}(\log n)$ tree operationsSource: docs/optimization/SUBSUMPTION_OPTIMIZATION_REPORT.md
Current SmallVec implementation (parameterized transducers):
| Position Count | Time | Throughput |
|---|---|---|
| n=10 | ~360ns | - |
| n=50 | ~1.7µs | 29.24 Melem/s |
| n=100 | ~2.6µs | - |
| n=200 | ~4.3µs | - |
Performance characteristics:
\mathcal{O}(1)$ best case with early exit\mathcal{O}(\text{kn})$ average case where k << nFrom 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.
| Aspect | SmallVec (Current) | BTreeSet (Alternative) |
|---|---|---|
| Insert | $\mathcal{O}(\log n)$ search + $\mathcal{O}(n)$ shift | $\mathcal{O}(\log n)$ tree operations |
| Memory | Stack $(\le 8),$ then heap | Always heap allocated |
| Cache locality | Excellent (contiguous) | Good (tree nodes) |
| Allocation overhead | None for $\le 8$ positions | Every insert allocates |
| Iteration | $\mathcal{O}(n)$ sequential | $\mathcal{O}(n)$ in-order |
Small states $(\le 8)$ | Optimal | Overkill (heap overhead) |
| Large states (>8) | Competitive | Competitive |
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.
Source: docs/research/universal-levenshtein/SUBSUMPTION_COMPARISON_JAVA_VS_RUST.md
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`$ positionsAlternative (Java-style):
positions: Option<Box<Position>> // Linked list like JavaAnalysis:
- 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)
State size distribution favors SmallVec
\le 8$ positionsCache locality advantage
Already benchmarked and proven
Simplicity and maintainability
Asymptotic equivalence
\mathcal{O}(\text{kn})$ with early terminationtake_while() optimization doesn't provide measurable benefitHeap allocation overhead
Tree node overhead
No performance gain
\mathcal{O}(\text{kn})$ complexityAPI complexity
Keep the current SmallVec implementation for parameterized transducers.
Rationale:
(\le 8$ positions)| Aspect | SmallVec (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)$ shifts | Heap, $\mathcal{O}(\log n)$ tree ops | Tie |
| Memory overhead | Minimal (90% stack) | Always heap + metadata | SmallVec |
| Cache locality | Excellent | Good | SmallVec |
| Early termination | $\mathcal{O}(\text{kn})$ with online check | $\mathcal{O}(k)$ with take_while() | Tie |
| Code simplicity | Simple Vec API | Custom Ord required | SmallVec |
| Overall | Optimal | Overkill | SmallVec |
docs/research/universal-levenshtein/SUBSUMPTION_OPTIMIZATION.mddocs/research/universal-levenshtein/SUBSUMPTION_COMPARISON_JAVA_VS_RUST.mddocs/optimization/SUBSUMPTION_OPTIMIZATION_REPORT.mddocs/research/universal-levenshtein/SUBSUMPTION_BTREESET_TEST_FIXES.mdUniversal transducers (BTreeSet):
(errors, offset) enables powerful early terminationtake_while() is beneficialParameterized transducers (SmallVec):
\le 8$ positions) make stack allocation dominantThe 2025-10-29 subsumption analysis provided empirical evidence that:
Without this data, we might have assumed BTreeSet would be an improvement.
The BTreeSet optimization for Universal transducers was:
For parameterized transducers:
File: src/transducer/state.rs
insert() with online subsumptionFile: src/transducer/universal/state.rs
add_position() with error-based early terminationFile: src/transducer/universal/position.rs
Parameterized:
benches/subsumption_benchmarks.rs: Online vs batch comparisondocs/optimization/SUBSUMPTION_OPTIMIZATION_REPORT.md: Full analysisUniversal:
docs/research/universal-levenshtein/SUBSUMPTION_OPTIMIZATION.md: BTreeSet designdocs/research/universal-levenshtein/SUBSUMPTION_COMPARISON_JAVA_VS_RUST.md: Java comparisonCan 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 |