Phase 3 focused on micro-optimizations with fast paths and additional inlining, achieving 6-19% improvements on top of Phases 1 and 2.
Combined Phase 1 + Phase 2 + Phase 3: 25-38% total improvement from original baseline.
infer_distance()Added early return for single-position states (common case):
#[inline]
pub fn infer_distance(&self, query_length: usize) -> Option<usize> {
// Fast path: single position (common case)
if self.positions.len() == 1 {
let p = &self.positions[0];
let remaining = query_length.saturating_sub(p.term_index);
return Some(p.num_errors + remaining);
}
// General case: find minimum across all positions
self.positions
.iter()
.map(|p| {
let remaining = query_length.saturating_sub(p.term_index);
p.num_errors + remaining
})
.min()
}
infer_prefix_distance()Added early return for single-position states:
#[inline]
pub fn infer_prefix_distance(&self, query_length: usize) -> Option<usize> {
// Fast path: single position
if self.positions.len() == 1 {
let p = &self.positions[0];
return if p.term_index >= query_length {
Some(p.num_errors)
} else {
None
};
}
// General case: find minimum among positions that consumed the full query
self.positions
.iter()
.filter(|p| p.term_index >= query_length)
.map(|p| p.num_errors)
.min()
}
Added #[inline(always)] to frequently-called methods:
Intersection::is_final() - called in hot loop for every intersectionIntersection::min_distance() - called for bucketingPathNode::new() - called for every child queuedIntersection::with_parent() - called for every child intersection| Benchmark | Phase 2 | Phase 3 | Phase 3 Improvement | Total from Baseline |
|---|---|---|---|---|
| prefix_distances/distance=1 | 75.2µs | 61.7µs | -18.9% ⚡⚡⚡ | -27.4% from 85µs |
| prefix_distances/distance=2 | 83.9µs | 68.5µs | -18.7% ⚡⚡⚡ | -30.4% from 98µs |
| combined_ops/prefix_distance_filter | 251.5µs | 211.4µs | -16.0% ⚡⚡⚡ | -25.2% from 282µs |
| Benchmark | Phase 2 | Phase 3 | Phase 3 Improvement | Total from Baseline |
|---|---|---|---|---|
| prefix_vs_exact/exact/10 | 16.9µs | 14.9µs | -11.9% ⚡⚡ | -23.1% from 19.4µs |
| prefix_vs_exact/prefix/7 | 51.7µs | 45.8µs | -11.4% ⚡⚡ | -19.8% from 57.1µs |
| iteration_limits/collect_all | 7.9µs | 7.1µs | -10.3% ⚡⚡ | -21.8% from 9.0µs |
| prefix_distances/distance=0 | 49.6µs | 44.8µs | -9.9% ⚡⚡ | -28.3% from 62.5µs |
| Benchmark | Phase 2 | Phase 3 | Phase 3 Improvement |
|---|---|---|---|
| filtering_strategies/pre_filter | 45.6µs | 41.4µs | -9.2% ⚡ |
| filter_complexity/complex_filter | 240.6µs | 218.3µs | -9.3% ⚡ |
| prefix_vs_exact/exact/5 | 16.8µs | 15.5µs | -8.0% ⚡ |
| filter_complexity/simple_filter | 224.4µs | 209.2µs | -6.8% ⚡ |
| combined_operations/prefix_only | 132.2µs | 123.7µs | -6.5% ⚡ |
| Benchmark | Baseline | After Phase 3 | Total Improvement |
|---|---|---|---|
| prefix_distances/distance=1 | 85.0µs | 61.7µs | -27.4% ⚡⚡⚡ |
| prefix_distances/distance=2 | 98.0µs | 68.5µs | -30.1% ⚡⚡⚡ |
| prefix_distances/distance=0 | 62.5µs | 44.8µs | -28.3% ⚡⚡⚡ |
| combined_ops/prefix_distance_filter | 282.0µs | 211.4µs | -25.0% ⚡⚡⚡ |
| exact/10 | 19.4µs | 14.9µs | -23.2% ⚡⚡⚡ |
| prefix/7 | 57.1µs | 45.8µs | -19.8% ⚡⚡ |
Impact: Major - Saved iterator/min operations for most common case
Rationale:
Result: 10-19% improvement on distance=1,2 queries
Impact: Major - Eliminated call overhead in nested loops
Rationale:
is_final() called for every intersection processed (thousands of times)PathNode::new() and Intersection::with_parent() called in hot loop (queue_children)Result: 6-12% improvement across most benchmarks
The 18-19% improvements for distance=1,2 are remarkable because:
infer_distance() and infer_prefix_distance() calls per nodeBefore (General Case):
self.positions.iter() // Create iterator
.map(|p| ...) // Map transformation
.min() // Find minimum (branch-heavy)
After (Fast Path):
if self.positions.len() == 1 {
let p = &self.positions[0];
return Some(p.num_errors + remaining);
}
Functions like is_final() and PathNode::new():
iteration_limits/take_while_distance: +6.4%
This is likely measurement noise or slight code bloat from inlining. The absolute time increase is ~30µs on a 500µs benchmark. All other benchmarks improved significantly, so this is acceptable.
All 94 tests passing - no regressions:
test result: ok. 94 passed; 0 failed
| Metric | Baseline | Phase 1 | Phase 2 | Phase 3 | Total Improvement |
|---|---|---|---|---|---|
| Distance=1 | 85.0µs | 69.7µs | 75.2µs | 61.7µs | -27.4% ⚡⚡⚡ |
| Distance=2 | 98.0µs | 87.5µs | 83.9µs | 68.5µs | -30.1% ⚡⚡⚡ |
| Distance=0 | 62.5µs | 56.1µs | 49.6µs | 44.8µs | -28.3% ⚡⚡⚡ |
Average improvement across critical workloads: ~25-30%
The flame graph still shows:
These are Phase 4 opportunities but require more invasive changes:
Recommendation: Stop here and commit Phase 3.
Rationale:
When to revisit:
Optimization changes:
src/transducer/state.rs - Fast paths for infer_distance(), infer_prefix_distance()src/transducer/intersection.rs - Aggressive inlining of hot methodsNo API changes - All modifications are internal optimizations.
✅ All 94 tests passing ✅ Significant performance improvements (6-19%) ✅ No regressions (except minor noise in 1 benchmark) ✅ Documentation complete
Ready to commit Phase 3 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 |