After profiling identified Arc overhead as 41% of execution time, we implemented targeted optimizations that delivered:
These optimizations far exceed PGO's 1-4% gains and represent the highest-impact performance improvement for liblevenshtein-rust.
Profiling with a realistic workload (10k words, 5k queries, 1M contains() calls) revealed:
Arc overhead: 41% of total execution time
Arc::clone (atomic increment): 20.77% (117M samples)Arc::drop (atomic decrement): 20.65% (116M samples)Other operations for comparison:
Conclusion: Arc overhead is 1.5x larger than binary search and 10x more impactful than PGO's potential gains.
contains() MethodThe default contains() implementation from the Dictionary trait works through the DictionaryNode API:
// Default implementation (trait default)
fn contains(&self, term: &str) -> bool {
let mut node = self.root(); // Arc::clone here
for byte in term.as_bytes() {
match node.transition(*byte) { // Arc::clone on each transition
Some(next) => node = next,
None => return false,
}
}
node.is_final()
}
For a term with N characters, this performs:
root()transition() (one per character)With 1M contains() calls in profiling, this resulted in hundreds of millions of atomic operations.
Override contains() in DawgDictionary to work directly with node indices:
// Optimized implementation (src/dictionary/dawg.rs:271-299)
fn contains(&self, term: &str) -> bool {
let mut node_idx = 0; // Start at root (no Arc clone)
for &byte in term.as_bytes() {
let edges = &self.nodes[node_idx].edges;
// Use adaptive search strategy (same as transition())
let next_idx = if edges.len() < 8 {
// Linear search for small edge counts
edges.iter().find(|(l, _)| *l == byte).map(|(_, idx)| *idx)
} else {
// Binary search for large edge counts
edges
.binary_search_by_key(&byte, |(l, _)| *l)
.ok()
.map(|pos| edges[pos].1)
};
match next_idx {
Some(idx) => node_idx = idx,
None => return false,
}
}
self.nodes[node_idx].is_final
}
Key improvement: Works with usize indices instead of DawgDictionaryNode, completely eliminating all Arc operations.
contains()| Dictionary Size | Before | After | Improvement |
|---|---|---|---|
| 100 terms | 9.30 µs | 3.13 µs | -66.3% (3.0x faster) |
| 500 terms | 9.60 µs | 3.22 µs | -66.4% (2.98x faster) |
| 1000 terms | 9.61 µs | 3.84 µs | -60.1% (2.50x faster) |
| 5000 terms | 9.71 µs | 3.86 µs | -60.2% (2.52x faster) |
Average improvement: 60-66% faster (2.5-3x speedup)
edges() IteratorThe original edges() iterator implementation cloned Arc unnecessarily:
// Original implementation
fn edges(&self) -> Box<dyn Iterator<Item = (u8, Self)> + '_> {
let nodes = Arc::clone(&self.nodes); // Clone 1 (upfront)
let iter = self.nodes[self.node_idx]
.edges
.iter()
.map(move |(label, idx)| { // 'move' captures nodes
(
*label,
DawgDictionaryNode {
nodes: Arc::clone(&nodes), // Clone 2, 3, 4... (per edge)
node_idx: *idx,
},
)
});
Box::new(iter)
}
For a node with N edges, this performed N+1 Arc clones:
Capture self by reference instead of cloning Arc upfront:
// Optimized implementation (src/dictionary/dawg.rs:343-360)
fn edges(&self) -> Box<dyn Iterator<Item = (u8, Self)> + '_> {
// Optimized: capture self by reference instead of cloning Arc upfront.
// This reduces Arc clones from N+1 to N (one per edge returned).
Box::new(
self.nodes[self.node_idx]
.edges
.iter()
.map(|(label, idx)| {
(
*label,
DawgDictionaryNode {
nodes: Arc::clone(&self.nodes), // Clone only when edge is consumed
node_idx: *idx,
},
)
}),
)
}
Key improvement: Reduced from N+1 to N Arc clones by eliminating the upfront clone.
edges() Iterator| Dictionary Size | Before | After | Improvement |
|---|---|---|---|
| 100 terms | 2.12 µs | 1.57 µs | -26.0% (1.35x faster) |
| 500 terms | 1.95 µs | 1.56 µs | -19.9% (1.25x faster) |
| 1000 terms | 2.00 µs | 1.57 µs | -21.3% (1.27x faster) |
| 5000 terms | 1.97 µs | 1.55 µs | -21.7% (1.28x faster) |
Average improvement: 20-26% faster
Profiling benchmark simulates real-world usage:
| Operation | Before | After | Improvement |
|---|---|---|---|
| 1M contains() calls | 203.48 ms | 87.18 ms | -57.2% (2.3x faster) |
| 5k fuzzy queries | 4.71 s (942 µs/query) | 4.06 s (812 µs/query) | -13.8% faster |
| DAWG construction | 3.24 ms | 2.44 ms | -24.6% faster |
Key takeaway: Contains() operations are 2.3x faster, with significant improvements to query performance as well.
| Optimization | Impact | Effort | ROI |
|---|---|---|---|
| Arc optimization | 57-66% contains(), 14% queries | Medium | Highest |
| Adaptive edge lookup | 3-14% (mixed results) | Low | Medium |
| Binary insertion | 13% insertion, -2% lookup | Low | Medium |
| PGO | 1-4% lookups, -2-6% construction | High | Low |
Verdict: Arc optimization provides 10-15x more value than PGO and is the single highest-impact optimization for liblevenshtein-rust.
contains() Can Be Arc-FreeThe Dictionary trait defines a default contains() that works through the DictionaryNode API. This is necessary for generic implementations but creates Arc overhead.
For DawgDictionary specifically, we can override contains() because:
This is an example of specialization: providing a more efficient implementation for a specific type while maintaining trait compatibility.
edges() Can't Be Fully Arc-FreeThe DictionaryNode::edges() trait method must return owned DawgDictionaryNode instances:
fn edges(&self) -> Box<dyn Iterator<Item = (u8, Self)> + '_>;
Each returned node must own an Arc, so we can't eliminate Arc clones entirely. However, we reduced clones from N+1 to N by avoiding the upfront clone.
Arc clones are still required in:
transition() method: Returns owned DawgDictionaryNode (trait requirement)edges() iterator: Returns N owned nodes (trait requirement)These could be optimized by:
src/dictionary/dawg.rs1. Arc-free contains() override (lines 266-299)
impl Dictionary for DawgDictionarytransition()2. Optimized edges() iterator (lines 343-360)
self by reference instead of cloned Arc✅ Apply Arc optimizations for all production builds
✅ Use Arc-optimized code in development
Consider for v2.0:
The Arc optimization delivers game-changing performance improvements:
Micro-benchmarks:
Real-world workload:
Compared to alternatives:
Next priority: If further optimization needed, consider Arc-free query traversal or index-based transducer API.
Benchmark results:
dawg_contains_arc_optimized.txt - Contains() micro-benchmarksdawg_edge_iteration_optimized.txt - Edge iteration benchmarksprofiling_benchmark_arc_optimized.txt - End-to-end realistic workloadCode changes:
src/dictionary/dawg.rs:266-299 - Arc-free contains() methodsrc/dictionary/dawg.rs:343-360 - Optimized edges() iteratorRelated analysis:
docs/PROFILING_AND_PGO_RESULTS.md - Flame graph analysis (identified 41% Arc overhead)docs/PGO_IMPACT_ANALYSIS.md - PGO comparison (1-4% vs 60% from Arc optimization)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 |