Date: 2025-10-30 Status: ✅ Phase 2 Complete - 15-39% Improvement Achieved Current Performance: 94ns (short), 374-492ns (medium)
Based on comprehensive benchmarking, profiling, and research, this document outlines the optimization strategy for Levenshtein distance functions. Priority is given to high-impact, low-effort improvements.
Current: Default hasher (SipHash for security) Proposed: FxHash (faster for small keys)
Rationale:
Implementation:
use std::hash::BuildHasherDefault;
use rustc_hash::FxHasher;
type FastHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FxHasher>>;
Expected gain: 10-15ns per cached operation (~10-15% for recursive)
Files to modify:
src/distance/mod.rs: Change HashMap to use FxHasherCargo.toml: Add rustc-hash = "1.1" dependencyCurrent: Compiler decides inlining Proposed: Force inline for small, hot functions
Targets:
#[inline(always)]
fn substring_from(s: &str, char_offset: usize) -> &str { ... }
#[inline(always)]
fn SymmetricPair::new(a: &str, b: &str) -> Self { ... }
Rationale:
Expected gain: 5-10ns per operation (~5-10%)
Files to modify:
src/distance/mod.rs: Add #[inline(always)] annotationsCurrent: Vec<char> allocation for each string
Proposed: SmallVec<[char; 32]> (stack allocation for small strings)
Rationale:
Implementation:
use smallvec::SmallVec;
// Instead of:
let source_chars: Vec<char> = source.chars().collect();
// Use:
let source_chars: SmallVec<[char; 32]> = source.chars().collect();
Expected gain: 20-30ns for short strings (~20-30%)
Note: smallvec is already a dependency!
Files to modify:
src/distance/mod.rs: Replace Vec<char> with SmallVec<[char; 32]>Current: Code exists but commented out
Proposed: Integrate strip_common_affixes() into recursive functions
Rationale:
Expected gain: 10-50% for strings with common suffixes (variable)
Files to modify:
src/distance/mod.rs: Uncomment and integrate suffix strippingTarget: Inner loop of DP matrix computation Approach: Compute 4-16 cells in parallel using SIMD instructions
Options:
std::simd (Rust standard library, nightly)
packed_simd2 (crate, stable)
std::simdRaw intrinsics (std::arch)
Recommendation: Start with packed_simd2 (stable + portable)
Current scalar code:
for j in 1..=n {
let cost = if source_chars[i - 1] == target_chars[j - 1] { 0 } else { 1 };
curr_row[j] = (prev_row[j] + 1) // deletion
.min(curr_row[j - 1] + 1) // insertion
.min(prev_row[j - 1] + cost); // substitution
}
Vectorized code (process 8 cells at once with AVX2):
use packed_simd::u32x8;
// Process 8 cells in parallel
for j in (1..=n).step_by(8) {
let prev = u32x8::from_slice_unaligned(&prev_row[j..j+8]);
let curr = u32x8::from_slice_unaligned(&curr_row[j-1..j+7]);
let diag = u32x8::from_slice_unaligned(&prev_row[j-1..j+7]);
let cost_vec = /* compute match/mismatch cost vector */;
// Parallel min operations
let result = (prev + 1).min(curr + 1).min(diag + cost_vec);
result.write_to_slice_unaligned(&mut curr_row[j..j+8]);
}
Challenges:
Expected speedup: 2-4x for strings > 20 chars
Feature flag approach:
[features]
simd = ["packed_simd2"]
Problem: Unbounded cache growth Solution: Least Recently Used (LRU) eviction policy
Implementation options:
lru crate (simple, battle-tested)use lru::LruCache;
pub struct MemoCache {
cache: RwLock<LruCache<SymmetricPair, usize>>,
}
HashMap + doubly-linked listRecommendation: Use lru crate
Expected benefit: Bounded memory usage, no performance regression
Goal: Monitor cache effectiveness
Metrics to track:
Implementation:
pub struct CacheStats {
pub hits: AtomicUsize,
pub misses: AtomicUsize,
pub evictions: AtomicUsize,
}
impl MemoCache {
pub fn stats(&self) -> CacheStats { ... }
pub fn hit_rate(&self) -> f64 { ... }
}
Usage:
let cache = create_memo_cache();
// ... perform queries ...
println!("Hit rate: {:.2}%", cache.hit_rate() * 100.0);
Current: Unbounded (grows indefinitely) Proposed: Configurable max size (default: 1000 entries)
Implementation:
pub fn create_memo_cache_with_capacity(capacity: usize) -> MemoCache {
MemoCache::with_capacity(capacity)
}
Tuning guidance:
Approach: Use actual usage patterns to guide compiler optimizations
Steps:
cargo pgo buildcargo pgo optimizeExpected gain: 5-15% across the board
Effort: Low (mostly automated)
Ukkonen's Algorithm:
When beneficial:
Implementation:
pub fn ukkonen_distance(source: &str, target: &str, max_k: usize) -> Option<usize>
Expected gain: 2-10x for small k values
Problem: Cache locality degrades for very long strings Solution: Process DP matrix in blocks (cache-oblivious algorithm)
Only beneficial for: Strings > 1000 chars (rare in fuzzy search)
Effort: High, benefit: Marginal for typical use case
Goal: Verify exact equivalence with C++ implementation
Approach:
Test corpus:
Validation criteria:
| Optimization | Effort | Expected Gain | Priority | Phase |
|---|---|---|---|---|
| FxHash | 1 hour | 10-15% recursive | ⭐⭐⭐⭐⭐ | 2 |
| SmallVec | 2 hours | 20-30% short strings | ⭐⭐⭐⭐⭐ | 2 |
| Inline annotations | 1 hour | 5-10% | ⭐⭐⭐⭐ | 2 |
| Suffix elimination | 2 hours | 10-50% (variable) | ⭐⭐⭐⭐ | 2 |
| SIMD vectorization | 3-5 days | 2-4x medium/long | ⭐⭐⭐⭐ | 3 |
| LRU eviction | 1 day | Better memory | ⭐⭐⭐⭐ | 4 |
| Cache statistics | 4 hours | Monitoring | ⭐⭐⭐ | 4 |
| PGO | 1 day | 5-15% | ⭐⭐⭐ | 5 |
| Ukkonen's | 3-5 days | 2-10x (k small) | ⭐⭐ | 5 |
| Block processing | 1 week | Marginal | ⭐ | 5 |
Deliverable: 30-50% speedup for typical workloads
Deliverable: 2-4x speedup for medium/long strings
Deliverable: Production-ready optimized implementation
Short strings: 99 ns
Medium strings: 740 ns
Long strings: ~5 µs
Short strings: 70 ns (-30%)
Medium strings: 520 ns (-30%)
Long strings: ~3.5 µs (-30%)
Short strings: 70 ns (no change, too small for SIMD)
Medium strings: 200 ns (-70% from baseline!)
Long strings: ~1 µs (-80% from baseline!)
Short strings: 60 ns (-40% from baseline)
Medium strings: 150 ns (-80% from baseline)
Long strings: ~800 ns (-84% from baseline)
Target: Competitive with hand-tuned assembly implementations!
The optimization roadmap is clear and achievable:
Recommended approach: Implement Phase 2 first, then reassess whether Phase 3-5 are needed based on actual performance requirements.
Current status: Production-ready baseline established. Ready to optimize! 🚀
Generated: 2025-10-30 Based on: Comprehensive benchmarking, profiling, and research
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 |