Liking cljdoc? Tell your friends :D

Arc Reference Counting Optimization Results

Executive Summary

After profiling identified Arc overhead as 41% of execution time, we implemented targeted optimizations that delivered:

  • contains() operations: 57-66% faster (2-3x speedup)
  • Edge iteration: 20-26% faster
  • End-to-end realistic workload: 57% faster lookups, 14% faster queries

These optimizations far exceed PGO's 1-4% gains and represent the highest-impact performance improvement for liblevenshtein-rust.


Background: Profiling Revealed Arc as #1 Bottleneck

Flame Graph Analysis

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)
  • Total atomic operations: 232 million

Other operations for comparison:

  • Binary search: 27%
  • Vector operations: 11%
  • Hash operations: 8%

Conclusion: Arc overhead is 1.5x larger than binary search and 10x more impactful than PGO's potential gains.


Optimization 1: Arc-Free contains() Method

Problem

The 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:

  • 1 Arc clone in root()
  • N Arc clones in transition() (one per character)
  • Total: N+1 Arc clones per lookup

With 1M contains() calls in profiling, this resulted in hundreds of millions of atomic operations.

Solution

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.

Benchmark Results: contains()

Dictionary SizeBeforeAfterImprovement
100 terms9.30 µs3.13 µs-66.3% (3.0x faster)
500 terms9.60 µs3.22 µs-66.4% (2.98x faster)
1000 terms9.61 µs3.84 µs-60.1% (2.50x faster)
5000 terms9.71 µs3.86 µs-60.2% (2.52x faster)

Average improvement: 60-66% faster (2.5-3x speedup)


Optimization 2: Reduced Arc Clones in edges() Iterator

Problem

The 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:

  • 1 upfront clone to capture in closure
  • N clones inside the map (one per edge returned)

Solution

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.

Benchmark Results: edges() Iterator

Dictionary SizeBeforeAfterImprovement
100 terms2.12 µs1.57 µs-26.0% (1.35x faster)
500 terms1.95 µs1.56 µs-19.9% (1.25x faster)
1000 terms2.00 µs1.57 µs-21.3% (1.27x faster)
5000 terms1.97 µs1.55 µs-21.7% (1.28x faster)

Average improvement: 20-26% faster


End-to-End Impact: Realistic Workload

Profiling benchmark simulates real-world usage:

  • 10,000-word dictionary
  • 5,000 fuzzy queries (edit distance 1-2)
  • 1,000,000 contains() calls

Results

OperationBeforeAfterImprovement
1M contains() calls203.48 ms87.18 ms-57.2% (2.3x faster)
5k fuzzy queries4.71 s (942 µs/query)4.06 s (812 µs/query)-13.8% faster
DAWG construction3.24 ms2.44 ms-24.6% faster

Key takeaway: Contains() operations are 2.3x faster, with significant improvements to query performance as well.


Comparison to Other Optimizations

OptimizationImpactEffortROI
Arc optimization57-66% contains(), 14% queriesMediumHighest
Adaptive edge lookup3-14% (mixed results)LowMedium
Binary insertion13% insertion, -2% lookupLowMedium
PGO1-4% lookups, -2-6% constructionHighLow

Verdict: Arc optimization provides 10-15x more value than PGO and is the single highest-impact optimization for liblevenshtein-rust.


Technical Details

Why contains() Can Be Arc-Free

The 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:

  1. We have direct access to the internal nodes array
  2. DAWG traversal can use indices instead of DictionaryNode instances
  3. No trait requirements prevent this optimization

This is an example of specialization: providing a more efficient implementation for a specific type while maintaining trait compatibility.

Why edges() Can't Be Fully Arc-Free

The 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.

Remaining Arc Overhead

Arc clones are still required in:

  1. transition() method: Returns owned DawgDictionaryNode (trait requirement)
  2. edges() iterator: Returns N owned nodes (trait requirement)
  3. Query operations: Stores DawgDictionaryNode instances in state

These could be optimized by:

  • Creating Arc-free query traversal (advanced optimization)
  • Using node indices internally in transducer (requires API changes)
  • Caching Arc references (complexity vs benefit tradeoff)

Code Changes Summary

File: src/dictionary/dawg.rs

1. Arc-free contains() override (lines 266-299)

  • Added to impl Dictionary for DawgDictionary
  • Works with node indices, zero Arc operations
  • Reuses adaptive search logic from transition()

2. Optimized edges() iterator (lines 343-360)

  • Removed upfront Arc clone
  • Captures self by reference instead of cloned Arc
  • Reduces Arc clones from N+1 to N

Performance Impact by Use Case

High-Value Scenarios

  • Dictionary lookup services: 2-3x faster contains()
  • Spell checkers: 57% faster dictionary operations
  • Fuzzy search systems: 14% faster query processing

Lower-Value Scenarios

  • Construction-heavy workloads: Minimal impact (construction unchanged)
  • Edge iteration workloads: 20-26% improvement (if edges() is hot path)

Recommendations

Production Deployments

Apply Arc optimizations for all production builds

  • 2-3x faster lookups with no downsides
  • Compatible with PGO (combine for 60-70% total improvement)

Development

Use Arc-optimized code in development

  • No build-time overhead (unlike PGO)
  • Improves test execution speed

Future Optimizations

Consider for v2.0:

  1. Arc-free query traversal: Eliminate Arc in transducer hot path
  2. Index-based API: Alternative trait using node indices
  3. Cached Arc references: For workloads with repeated traversals

Conclusion

The Arc optimization delivers game-changing performance improvements:

Micro-benchmarks:

  • contains(): 2-3x faster (60-66% reduction)
  • Edge iteration: 1.2-1.35x faster (20-26% reduction)

Real-world workload:

  • Dictionary lookups: 2.3x faster
  • Fuzzy queries: 14% faster

Compared to alternatives:

  • 10-15x more value than PGO
  • Higher ROI than any other optimization

Next priority: If further optimization needed, consider Arc-free query traversal or index-based transducer API.


Files

  • Benchmark results:

    • dawg_contains_arc_optimized.txt - Contains() micro-benchmarks
    • dawg_edge_iteration_optimized.txt - Edge iteration benchmarks
    • profiling_benchmark_arc_optimized.txt - End-to-end realistic workload
  • Code changes:

    • src/dictionary/dawg.rs:266-299 - Arc-free contains() method
    • src/dictionary/dawg.rs:343-360 - Optimized edges() iterator
  • Related 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

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