Performance profiling has identified a critical bottleneck in the DAT edges() implementation that severely impacts Levenshtein automaton composition performance.
Key Finding: The current edges() implementation causes 7-36% performance regression in distance matching operations.
| Operation | Time | Status |
|---|---|---|
| Construction (10k words) | 2.99 ms | ✅ Good (7% improvement) |
| Exact matching | 6.59 µs | ✅ Excellent |
| Contains operation | 232 ns | ✅ Excellent (30x faster than DAWG) |
| Memory construction | 2.63 ms | ✅ Good (7% improvement) |
| Operation | Time | Status | Change |
|---|---|---|---|
| Distance 1 matching | 13.86 µs | ⚠️ Regressed | +7.1% slower |
| Distance 2 matching | 22.40 µs | ❌ Regressed | +35.4% slower |
Root Cause: Inefficient edges() implementation in DAT node traversal.
edges() Implementationfn edges(&self) -> Box<dyn Iterator<Item = (u8, Self)> + '_> {
let state = self.state;
let base = if state < self.base.len() {
self.base[state]
} else {
-1
};
if base < 0 {
return Box::new(std::iter::empty());
}
// ❌ BOTTLENECK: Iterates through ALL 256 possible bytes
let edges: Vec<(u8, Self)> = (0u8..=255)
.filter_map(|byte| {
let next = (base as usize).wrapping_add(byte as usize);
if next < self.check.len() && self.check[next] == state as i32 {
// ❌ BOTTLENECK: 3 Arc clones per valid edge
Some((
byte,
DoubleArrayTrieNode {
state: next,
base: Arc::clone(&self.base),
check: Arc::clone(&self.check),
is_final: Arc::clone(&self.is_final),
},
))
} else {
None
}
})
.collect(); // ❌ BOTTLENECK: Collects into Vec
Box::new(edges.into_iter())
}
Exhaustive Byte Iteration: Checks ALL 256 possible byte values
Excessive Arc Cloning: 3 Arc clones × number of edges
Unnecessary Vec Collection: Pre-collects all edges
Called Frequently: Levenshtein automaton composition
edges() multiple times during traversalThe Levenshtein automaton works by composing with the dictionary:
For each Levenshtein state:
For each dictionary edge: ← edges() called here
Compute next Levenshtein state
Recurse
Impact Calculation (distance 2, query "test"):
edges() on current dictionary nodeStore actual edges during construction:
struct DoubleArrayTrieNode {
state: usize,
edges: Arc<Vec<u8>>, // NEW: Actual edges for this state
base: Arc<Vec<i32>>,
check: Arc<Vec<i32>>,
is_final: Arc<Vec<bool>>,
}
fn edges(&self) -> Box<dyn Iterator<Item = (u8, Self)> + '_> {
let base = self.base[self.state];
Box::new(self.edges[self.state].iter().map(move |&byte| {
let next = (base as usize) + (byte as usize);
(byte, DoubleArrayTrieNode {
state: next,
edges: Arc::clone(&self.edges),
base: Arc::clone(&self.base),
check: Arc::clone(&self.check),
is_final: Arc::clone(&self.is_final),
})
}))
}
Pros:
Cons:
Expected Improvement: 30-50% faster Levenshtein matching
Return an iterator that checks on-demand:
struct DATEdgeIterator {
byte: u8,
base: Arc<Vec<i32>>,
check: Arc<Vec<i32>>,
is_final: Arc<Vec<bool>>,
state: usize,
base_value: i32,
}
impl Iterator for DATEdgeIterator {
type Item = (u8, DoubleArrayTrieNode);
fn next(&mut self) -> Option<Self::Item> {
while self.byte < 255 {
let byte = self.byte;
self.byte += 1;
let next = (self.base_value as usize) + (byte as usize);
if next < self.check.len() && self.check[next] == self.state as i32 {
return Some((byte, DoubleArrayTrieNode { ... }));
}
}
None
}
}
Pros:
Cons:
Expected Improvement: 10-20% faster
Share Arc references across all nodes from same DAT:
#[derive(Clone)]
struct DATSharedData {
base: Arc<Vec<i32>>,
check: Arc<Vec<i32>>,
is_final: Arc<Vec<bool>>,
edges: Arc<Vec<Vec<u8>>>, // Edge lists per state
}
struct DoubleArrayTrieNode {
state: usize,
shared: DATSharedData, // Single clone for all 3-4 Arcs
}
Pros:
Cons:
Expected Improvement: 15-25% faster
Combine edge list storage with shared Arc structure:
Expected Total Improvement: 40-60% faster Levenshtein matching
# Before optimization
cargo bench --bench dat_levenshtein_profiling
# After optimization
cargo bench --bench dat_levenshtein_profiling
# Compare
cargo bench --bench backend_comparison -- DoubleArrayTrie
| Metric | Before | After | Improvement |
|---|---|---|---|
| Distance 1 | 13.86 µs | ~9-10 µs | 30-40% |
| Distance 2 | 22.40 µs | ~14-16 µs | 30-40% |
| Edge iteration | 256 checks | 3-5 checks | 50x |
| Arc clones/edge | 3 | 1 | 3x |
The DAT has excellent baseline performance as a dictionary (6.59 µs exact matching, 232 ns contains).
However, the edges() implementation creates a severe bottleneck for Levenshtein automaton composition, causing 7-36% performance regression.
Recommended Action: Implement Option 1 (edge list storage) immediately. This is the critical path for the project's core value proposition: fast approximate string matching with Levenshtein automata.
Analysis Date: 2025-10-28 Profiling Tools: cargo bench, flamegraph Test Dataset: 10,000 words from /usr/share/dict/words
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 |