Profiled FuzzyMultiMap using flame graphs, identified bottlenecks, implemented optimizations targeting:
| Benchmark | Baseline (µs) | Optimized (µs) | Change | Result |
|---|---|---|---|---|
fuzzy_multimap_query | 38.55 | 39.98 | +3.7% | ⚠️ Slight regression |
fuzzy_multimap_high_aggregation | 394.96 | 406.55 | +2.9% | ⚠️ Slight regression |
fuzzy_multimap_vec_concat | 38.13 | 38.73 | +1.6% | ≈ Neutral |
transducer_query_baseline | 46.69 | 39.57 | -15.2% | ✅ Major improvement |
dict_get_value_hashset | 1.20 | 1.17 | -2.5% | ✅ Minor improvement |
hashset_aggregation | 11.53 | 10.86 | -5.8% | ✅ Good improvement |
fuzzy_multimap_complete | 211.45 | 216.29 | +2.3% | ⚠️ Slight regression |
The optimizations showed mixed results:
✅ Improvements:
hashset_aggregation: -5.8% (11.53 µs → 10.86 µs)
transducer_query_baseline: -15.2% (46.69 µs → 39.57 µs)
⚠️ Regressions:
fuzzy_multimap_query: +3.7% (38.55 µs → 39.98 µs)fuzzy_multimap_high_aggregation: +2.9% (394.96 µs → 406.55 µs)The regressions are likely due to:
Peekable Iterator Overhead
.peekable() to check for empty results adds a state machine wrapperCapacity Estimation Heuristic
2x first set size heuristic may over-allocate in some casesAdditional Capacity Checks
if acc.len() + set.len() > acc.capacity() check adds branching overheadThe transducer baseline improved by 15.2% because:
flamegraph_fuzzy_multimap.svg:Transducer query dominates (largest stack in flame graph)
Transducer::query()HashSet::extend() shows up but is not the bottleneck
String allocations visible in flamegraph
Reasoning:
.peekable() and extra capacity checks outweighs benefitsTrade-off:
Add size-based heuristics:
pub fn query(&self, query_term: &str, max_distance: usize) -> Option<C> {
let candidates: Vec<_> = self.transducer
.query(query_term, max_distance)
.collect();
if candidates.is_empty() {
return None;
}
// Optimization: Only use pre-allocation for large result sets
let values: Vec<C> = if candidates.len() > 20 {
// Use pre-allocated capacity for large sets
let mut values = Vec::with_capacity(candidates.len());
for term in candidates {
if let Some(value) = self.dictionary.get_value(&term) {
values.push(value);
}
}
values
} else {
// Use simple collection for small sets
candidates
.into_iter()
.filter_map(|term| self.dictionary.get_value(&term))
.collect()
};
if values.is_empty() {
return None;
}
Some(C::aggregate(values.into_iter()))
}
Revert the query() method changes but keep the HashSet/Vec capacity optimizations:
hashset_aggregation showed -5.8% improvement.peekable() overhead from query()CollectionAggregate trait implementationsImplement Option 3: Keep aggregation optimizations, revert query changes.
Justification:
.peekable() adds state machinesrc/cache/multimap.rs:258-273 - query() method (revert recommended)src/cache/multimap.rs:87-111 - HashSet aggregation (keep)src/cache/multimap.rs:134-156 - Vec aggregation (keep)Expected results after selective revert:
| Benchmark | Baseline | After Revert | Net Change |
|---|---|---|---|
fuzzy_multimap_query | 38.55 µs | ~38.5 µs | ≈0% |
hashset_aggregation | 11.53 µs | ~10.9 µs | -5.5% ✅ |
fuzzy_multimap_high_aggregation | 394.96 µs | ~390 µs | -1.2% ✅ |
Net improvement: ~1-2% overall with 5.8% aggregation improvement.
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 |