Implemented DAWG-specific query iterator (DawgQueryIterator) that works with node indices instead of DawgDictionaryNode to eliminate remaining Arc operations.
Result: 3.2% overall improvement (less than predicted 10-15%) because PathNode optimization already eliminated the majority of Arc overhead.
Key Finding: PathNode was highly effective - remaining Arc clones from edge traversal have minimal impact.
DawgQueryIterator - Specialized query iterator for DAWG:
usize) instead of DawgDictionaryNodeArc<Vec<DawgNode>> shared at iterator levelPathNode for parent chainsCode Changes:
src/dictionary/dawg_query.rs - New module (350 lines)src/dictionary/dawg.rs - Added query_optimized() and query_with_distance_optimized() methodssrc/dictionary/mod.rs - Added dawg_query moduleBefore (Generic QueryIterator):
for (label, child_node) in intersection.node.edges() { // Arc::clone per edge!
// child_node is DawgDictionaryNode with Arc<Vec<DawgNode>>
let child = Box::new(Intersection::new(label, child_node, state, parent));
}
After (DawgQueryIterator):
for &(label, child_idx) in &node.edges { // No Arc clone!
// child_idx is just usize
let child = Box::new(DawgIntersection::new(label, child_idx, state, parent));
}
Overall Performance (Distance=2, 1000 queries): | Metric | Generic | Optimized | Improvement | |--------|---------|-----------|-------------| | Time | 15.54 ms | 15.04 ms | 3.22% | | µs/query | 15.54 | 15.04 | 0.50 µs faster | | Results | 4,857 | 4,857 | ✅ Identical |
By Distance (100 queries): | Distance | Generic | Optimized | Speedup | Improvement | |----------|---------|-----------|---------|-------------| | 1 | 5.62 µs | 4.78 µs | 1.17x | 17% | | 2 | 27.27 µs | 27.91 µs | 0.98x | -2% (variance) | | 3 | 383.23 µs | 346.65 µs | 1.11x | 11% |
Original Prediction: 10-15% improvement from eliminating Arc clones
Reality: 3.2% improvement overall (17% at distance=1, 11% at distance=3)
Reason: PathNode already eliminated the dominant Arc source
Before PathNode (Baseline):
After PathNode:
After Index-Based:
Impact: PathNode eliminated 83% of Arc overhead, leaving only 17% for index-based to address.
Distance 1 (17% improvement):
Distance 2 (3% improvement):
Distance 3 (11% improvement):
Conclusion: Benefit diminishes as computational complexity increases.
What's left?
Arc elimination addressed: ~5M operations Total operations: ~100M+ (state transitions dominate)
Percentage: 5% of total → 3-17% improvement possible
| Optimization | Impact | Arc Eliminated | Notes |
|---|---|---|---|
| Arc-free contains() | 60-66% | 100% (contains only) | Specialized method, huge win |
| PathNode | 44% queries | ~83% (parent chains) | Lightweight path, major win |
| Index-based query | 3-17% queries | ~17% (edge traversal) | Modest win, PathNode was better |
Cumulative Query Performance:
Diminishing Returns: Each optimization has less impact as low-hanging fruit is eliminated.
✅ Works correctly - All tests pass, identical results
✅ No regressions - Never slower (within variance)
✅ Consistent improvement at distance 1 - 17% is solid
✅ Clean API - dawg.query_optimized() is simple to use
✅ Validates PathNode - Shows PathNode was highly effective
❌ Less than predicted - 3.2% vs 10-15% expected ❌ Minimal at distance 2 - Most common use case sees little benefit ❌ Adds complexity - 350 lines of code for modest gains ❌ DAWG-specific - Not generic, requires specialized implementation
Benefit:
Cost:
Decision: ✅ Keep the implementation but document limitations
Rationale:
Usage Guidance:
// Default: Use generic Transducer (works for all dictionaries)
let transducer = Transducer::new(dictionary, Algorithm::Standard);
for term in transducer.query("query", 2) {
println!("{}", term);
}
// Performance-critical: Use optimized DAWG query (3-17% faster)
let dawg = DawgDictionary::from_iter(words);
for term in dawg.query_optimized("query", 2, Algorithm::Standard) {
println!("{}", term);
}
Low Priority:
High Priority:
Expectation: 10-15% improvement from Arc elimination Reality: 3-17% depending on distance
Why Different? PathNode already did the heavy lifting.
Lesson: Always benchmark before assuming impact.
80/20 rule:
Lesson: First optimization often has biggest impact. Subsequent ones have diminishing returns.
Distance 1: 17% improvement (Arc overhead visible) Distance 2: 3% improvement (Arc overhead diluted) Distance 3: 11% improvement (medium complexity)
Lesson: Optimization impact depends on workload characteristics.
Index-based:
PathNode:
Lesson: Simple optimizations can be more cost-effective than complex ones.
Index-based query iterator provides 3.2-17% improvement depending on distance, with best results at distance=1 (17%) and modest results at distance=2 (3%).
Why less than expected:
Recommendation: Keep implementation for completeness and performance-critical use cases, but acknowledge that PathNode delivered the majority of query optimization potential.
Combined Achievements:
Next steps: Focus on algorithmic improvements and usability rather than further micro-optimizations.
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 |