Liking cljdoc? Tell your friends :D

Index-Based Query Iterator Results

Executive Summary

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.


Implementation

What We Built

DawgQueryIterator - Specialized query iterator for DAWG:

  • Works directly with node indices (usize) instead of DawgDictionaryNode
  • Single Arc<Vec<DawgNode>> shared at iterator level
  • Eliminates Arc clones during edge traversal
  • Uses lightweight PathNode for parent chains

Code Changes:

  • src/dictionary/dawg_query.rs - New module (350 lines)
  • src/dictionary/dawg.rs - Added query_optimized() and query_with_distance_optimized() methods
  • src/dictionary/mod.rs - Added dawg_query module

Architecture

Before (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));
}

Benchmark Results

Real English Dictionary (89,545 words)

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% |


Analysis

Why Less Than Expected?

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

Arc Operations Breakdown

Before PathNode (Baseline):

  1. Parent chain Arc clones: ~25M per profiling run (dominant!)
  2. Edge traversal Arc clones: ~5M per profiling run (secondary)

After PathNode:

  1. Parent chain Arc clones: 0 (eliminated!)
  2. Edge traversal Arc clones: ~5M per profiling run (now only source)

After Index-Based:

  1. Parent chain Arc clones: 0
  2. Edge traversal Arc clones: 0

Impact: PathNode eliminated 83% of Arc overhead, leaving only 17% for index-based to address.

Distance-Dependent Behavior

Distance 1 (17% improvement):

  • Fewer paths explored = fewer intersections
  • Arc overhead more visible relative to other work
  • Best case for index-based optimization

Distance 2 (3% improvement):

  • More paths explored = more computational work
  • Arc overhead diluted by state transitions and distance calculations
  • Most common use case

Distance 3 (11% improvement):

  • Many paths explored = lots of work
  • But also many Arc operations avoided
  • Moderate improvement

Conclusion: Benefit diminishes as computational complexity increases.

Remaining Overhead

What's left?

  • State transitions (major component)
  • Distance calculations
  • PathNode cloning (lightweight but nonzero)
  • Memory allocation/deallocation

Arc elimination addressed: ~5M operations Total operations: ~100M+ (state transitions dominate)

Percentage: 5% of total → 3-17% improvement possible


Comparison with Previous Optimizations

OptimizationImpactArc EliminatedNotes
Arc-free contains()60-66%100% (contains only)Specialized method, huge win
PathNode44% queries~83% (parent chains)Lightweight path, major win
Index-based query3-17% queries~17% (edge traversal)Modest win, PathNode was better

Cumulative Query Performance:

  • Baseline: 4.71s (5k queries)
  • +PathNode: 2.18s (2.16x speedup)
  • +Index-based: ~2.11s (2.23x speedup from baseline)

Diminishing Returns: Each optimization has less impact as low-hanging fruit is eliminated.


Evaluation

Positives

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

Negatives

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

Trade-offs

Benefit:

  • 3-17% improvement (distance-dependent)
  • Eliminates all remaining Arc operations
  • Provides specialized API for DAWG

Cost:

  • 350 lines of additional code
  • Maintenance burden (two query implementations)
  • Complexity for users (which to use?)

Recommendations

For This Codebase

Decision:Keep the implementation but document limitations

Rationale:

  1. Improvement is real (3-17%), not noise
  2. Cost is reasonable (350 well-tested lines)
  3. Provides educational value (optimization techniques)
  4. Useful for performance-critical applications

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);
}

For Future Work

Low Priority:

  • ~~Index-based query~~ - Completed (3-17% improvement)
  • SIMD edge lookup - Potentially 5-10% for 10-16 edge nodes
  • State pooling optimization - Already implemented, further refinement possible

High Priority:

  • Focus on algorithmic improvements rather than micro-optimizations
  • Real-world validation with multiple languages - Confirm optimizations hold
  • Documentation and usability - Make performance easy to achieve

Lessons Learned

1. Measure Everything

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.

2. Pareto Principle Applies

80/20 rule:

  • PathNode: 44% improvement (one optimization)
  • Index-based: 3-17% improvement (second optimization)

Lesson: First optimization often has biggest impact. Subsequent ones have diminishing returns.

3. Context Matters

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.

4. Complexity vs Benefit

Index-based:

  • Added: 350 lines
  • Gained: 3-17% depending on usage

PathNode:

  • Added: 100 lines
  • Gained: 44% always

Lesson: Simple optimizations can be more cost-effective than complex ones.


Conclusion

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:

  • PathNode already eliminated 83% of Arc overhead
  • Remaining Arc operations are small fraction of total work
  • State transitions and distance calculations dominate at higher distances

Recommendation: Keep implementation for completeness and performance-critical use cases, but acknowledge that PathNode delivered the majority of query optimization potential.

Combined Achievements:

  • Contains: 3.76x speedup (Arc-free + threshold)
  • Query: 2.23x speedup (PathNode + index-based)
  • Production-ready with excellent real-world performance

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

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close