The fuzzy map benchmarks successfully executed, revealing critical performance characteristics:
-
Memory Overhead: Adding values to PathMapDictionary incurs modest overhead
- u32 values: ~14% slower construction than no values
- Vec values: ~53% slower construction than no values
-
Value-Filtered vs Post-Filtered Queries: Post-filtering is actually faster
- This contradicts the initial hypothesis of 10-100x speedup
- Value-filtered: 43.2μs
- Post-filtered: 42.8μs (~1% faster)
- Unfiltered: 42.4μs (baseline)
-
Filter Selectivity Impact: Value-filtering overhead increases with selectivity
- High selectivity (50%+): Post-filtering becomes clearly faster
- Low selectivity (1-10%): Performance parity
-
Dictionary Operations: Value access is extremely fast
get_value(u32): 59nsget_value(Vec<u32>): 70nsinsert_with_value(u32): 269nsinsert_with_value(Vec<u32>): 401ns
-
Query Performance by Value Type: Minimal impact
- No values: 39.3μs (baseline)
- u32 values: 40.4μs (+2.8%)
- Vec values: 41.6μs (+5.9%)
Construction time for different dictionary sizes and value types:
| Size | No Values | u32 Values | Vec Values |
|---|
| 100 | 20.8μs | 23.8μs | 31.8μs |
| 500 | 121μs | 131μs | 158μs |
| 1000 | 248μs | 263μs | 329μs |
Analysis:
- u32 overhead: 14-15% slower
- Vec overhead: 30-53% slower
- Overhead scales linearly with dictionary size
- Overhead primarily from value storage allocation, not insertion logic
On 1000-term dictionary with 10 scopes (10% selectivity) at distance 2:
| Approach | Time | vs Baseline |
|---|
| Unfiltered | 42.4μs | 0% |
| Value-filtered | 43.2μs | +2.1% |
| Post-filtered | 42.8μs | +0.9% |
Analysis:
- Value-filtering during traversal is slower, not faster
- The overhead of predicate evaluation during graph traversal outweighs savings
- Post-filtering benefits from better CPU pipelining and cache locality
Testing different match rates (1%, 10%, 50%, 100%) with distance 2:
| Selectivity | Value-Filtered | Post-Filtered | Winner |
|---|
| 1% | 43.4μs | 42.7μs | Post (-1.6%) |
| 10% | 42.8μs | 42.7μs | Tie (~0%) |
| 50% | 44.3μs | 45.4μs | Value (-2.4%) |
| 100% | 43.6μs | 42.5μs | Post (-2.5%) |
Analysis:
- At 50% selectivity, value-filtering finally shows benefit (2.4% faster)
- At all other selectivities, post-filtering equals or beats value-filtering
- Conclusion: Value-filtering only helps when ~50% of terms match filter
Testing different HashSet sizes for set membership checks:
| Set Size | Time | vs Single Value |
|---|
| 1 | 41.9μs | 0% |
| 5 | 41.7μs | -0.5% |
| 10 | 41.9μs | 0% |
| 20 | 41.9μs | 0% |
Analysis:
- HashSet lookups are extremely efficient (O(1) with low constant)
- No measurable overhead for larger value sets
- Excellent for hierarchical scope queries (current scope + parent scopes)
Microbenchmarks for value operations:
| Operation | Time |
|---|
insert_with_value(u32) | 269ns |
insert_with_value(Vec) | 401ns |
get_value(u32) | 59ns |
get_value(Vec<u32>) | 70ns |
contains() with values | 1.3μs |
Analysis:
- Value retrieval is extremely fast (59-70ns)
- Insertion overhead is modest (269-401ns)
- Contains check is slower due to path traversal
Distance 2 queries on 1000-term dictionaries:
| Value Type | Query Time | vs Baseline |
|---|
| None (()) | 39.3μs | 0% |
| u32 | 40.4μs | +2.8% |
| Vec | 41.6μs | +5.9% |
Analysis:
- Small overhead from storing values in memory (cache effects)
- Vec adds ~1.2μs vs u32 (larger memory footprint)
- Overall impact is minimal (<6%)
Value-filtered queries at distance 2 across different dictionary sizes:
| Dict Size | Time | Throughput |
|---|
| 100 | 8.0μs | 12.5 Melem/s |
| 500 | 23.1μs | 21.7 Melem/s |
| 1000 | 42.1μs | 23.8 Melem/s |
| 5000 | 206μs | 24.3 Melem/s |
Analysis:
- Throughput improves with dictionary size (better cache utilization)
- Scales well from 100 to 5000 terms
- Peak throughput: 24.3 million elements/second
Value-filtered queries on 1000-term dictionary:
| Distance | Time | vs d=0 |
|---|
| 0 | 3.9μs | 0% |
| 1 | 6.9μs | +77% |
| 2 | 41.6μs | +967% |
| 3 | 433μs | +10,985% |
Analysis:
- Exponential growth as expected (Levenshtein automata property)
- Distance 3 is ~10x slower than distance 2
- Typical use cases (distance 1-2) are still very fast
10,000 identifiers across 100 scopes, querying with 5 visible scopes, distance 2:
| Metric | Value |
|---|
| Time | 407μs |
| Top 10 results | Fast enough for interactive use |
Analysis:
- Sub-millisecond performance for realistic codebase size
- Suitable for interactive IDE code completion
- Hierarchical scope filtering works efficiently
The initial hypothesis was that filtering during graph traversal would be 10-100x faster by pruning the search space early. However, benchmarks show:
- Traversal is already highly optimized: The Levenshtein automata limits exploration efficiently
- Predicate overhead: Evaluating predicates during traversal adds overhead to every node visit
- Pipeline stalls: Conditional logic (if predicate matches) hurts CPU pipelining
- Cache locality: Post-filtering benefits from better memory access patterns
Value-filtering shows benefit only when:
- High selectivity (~50%): Many terms match the filter
- Complex predicates: The predicate itself is cheap, but result processing is expensive
For most use cases: Use post-filtering with query() + .filter()
transducer.query("term", 2)
.filter(|term| dict.get_value(term) == target_scope)
Only use value-filtering when:
- Selectivity is known to be ~50%
- You need set-based filtering (multiple values)
- You want to limit result set size early
Original claim: "10-100x speedup via value-filtering"
Reality:
- Value-filtering: 2.1% slower than unfiltered
- Post-filtering: 0.9% slower than unfiltered
- Neither approach provides significant speedup
Root cause of discrepancy:
- The original claim likely assumed a naive post-filtering approach that processed all results
- Modern iterator chains with lazy evaluation make post-filtering nearly free
- The Levenshtein automata already prunes the search space efficiently
- Keep both APIs - Both
query_filtered() and query() + .filter() are useful - Document performance characteristics - Be honest about when each approach helps
- Default to post-filtering in examples - Simpler and faster for most cases
- Optimize value-filtering path - Reduce predicate evaluation overhead
- Early termination - Add
.take(n) optimization for value-filtered queries - Batch filtering - Evaluate predicates in batches for better CPU utilization
- Specialize for common cases - Create fast paths for single-value and set-based filters
- Profile-guided optimization - Use PGO to optimize the actual hot paths
- SIMD filtering - Explore vectorized predicate evaluation
benches/fuzzy_map_benchmarks.rs - Comprehensive benchmark suitefuzzy_map_results.txt - Raw Criterion outputFUZZY_MAP_BENCHMARK_RESULTS.md - This analysis document
Now that we have benchmark data showing value-filtering underperforms, Phase 3 should focus on:
- Generate flame graphs for value-filtered queries to identify bottlenecks
- Profile predicate evaluation overhead during graph traversal
- Compare against post-filtered flame graphs to understand pipeline differences
- Identify optimization opportunities to make value-filtering actually faster
The goal: Either optimize value-filtering to live up to its promise, or deprecate it in favor of post-filtering.