Version: 0.9.1 Last Updated: 2026-06-19
This document summarizes the key performance optimizations implemented in liblevenshtein-rust.
The library has undergone extensive performance optimization, achieving:
Problem: Expensive cloning of path vectors during traversal
Solution: Use Arc<Vec<u8>> for shared ownership of paths
Impact:
Implementation: src/transducer/query.rs, src/dictionary/dawg_query.rs
// Before: Deep copy on every transition
let mut new_path = self.path.clone();
new_path.push(label);
// After: Shared ownership with Arc
let new_path = {
let mut path = Vec::with_capacity(self.path.len() + 1);
path.extend_from_slice(&self.path);
path.push(label);
Arc::new(path)
};
Problem: Repeated allocation/deallocation of State objects in hot paths Solution: Object pool that reuses State instances
Impact:
Implementation: src/transducer/state_pool.rs
pub struct StatePool {
pool: RefCell<Vec<State>>,
}
impl StatePool {
pub fn acquire(&self, position: StatePosition) -> PooledState {
let mut pool = self.pool.borrow_mut();
let mut state = pool.pop().unwrap_or_else(State::default);
state.reset(position);
PooledState { state: Some(state), pool: self }
}
}
Problem: Frequent small vector allocations Solution: Stack-allocated vectors for small collections
Impact:
Implementation: Used throughout for edge lists, state collections
use smallvec::SmallVec;
// Stack-allocated for ≤8 elements, heap for larger
type EdgeList = SmallVec<[(u8, Node); 8]>;
Problem: Eager collection of all edges into vectors Solution: Zero-copy iterator over PathMap edges
Impact:
Implementation: src/dictionary/pathmap.rs
pub fn edges(&self) -> Box<dyn Iterator<Item = (u8, Self)> + '_> {
// Get child mask (cheap - just bit tests)
let edge_bytes: SmallVec<[u8; 8]> = self.with_zipper(|zipper| {
let mask = zipper.child_mask();
(0..=255u8).filter(|byte| mask.test_bit(*byte)).collect()
});
// Lazy iterator - creates nodes on-demand
Box::new(edge_bytes.into_iter().filter_map(move |byte| {
// ... create node only when iterated
}))
}
Problem: Function call overhead in hot paths
Solution: Strategic use of #[inline] and #[inline(always)]
Impact:
Functions Inlined:
is_final(), transition() - called millions of timesProblem: Cloning DAWG nodes during traversal Solution: Use node indices instead of cloning
Impact:
Implementation: src/dictionary/dawg_query.rs
pub struct DawgNode {
index: usize, // Reference by index, not clone
// ...
}
Feature: Optional gzip compression for serialized dictionaries
Results:
Usage:
use liblevenshtein::serialization::{BincodeSerializer, GzipSerializer};
// Save compressed
GzipSerializer::<BincodeSerializer>::serialize(&dict, file)?;
// Load compressed
let dict = GzipSerializer::<BincodeSerializer>::deserialize(file)?;
Formats Supported:
bincode-gz - Binary format with gzipprotobuf-gz - Protocol Buffers with gzip# All benchmarks
RUSTFLAGS="-C target-feature=+aes,+sse2" cargo bench
# Specific benchmark suite
RUSTFLAGS="-C target-feature=+aes,+sse2" cargo bench --bench dawg_benchmarks
# With profiling
RUSTFLAGS="-C target-feature=+aes,+sse2" cargo flamegraph --bench profiling_benchmark
query_ordered() has overhead for sortingDetailed historical performance analysis is available in the archived performance documentation:
See FUTURE_ENHANCEMENTS.md for planned improvements.
building.md for profiling instructionscargo bench to see current performanceexamples/ for real-world usage patternscontributing.md for optimization guidelinesCan 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 |