Flame graph analysis reveals that queue_children() consumes 37.91% of total execution time, making it the primary optimization target. Key bottlenecks identified:
| Query Length | Exact (µs) | Prefix (µs) | Overhead |
|---|---|---|---|
| 3 chars | 14.0 | 41.1 | 2.9x |
| 5 chars | 17.4 | 51.9 | 3.0x |
| 7 chars | 15.1 | 50.1 | 3.3x |
| 10 chars | 15.3 | 48.9 | 3.2x |
Analysis: Prefix matching is ~3x slower than exact matching due to increased traversal (exploring longer paths).
| Distance | Time (µs) |
|---|---|
| 0 | 46.6 |
| 1 | 78.2 |
| 2 | 95.5 |
| 3 | 111.7 |
Analysis: Linear scaling with edit distance (expected). Each additional distance level adds ~16-18µs.
| Strategy | Time (µs) | Speedup |
|---|---|---|
| Post-filter | 79.0 | 1.0x (baseline) |
| Pre-filter (sub-trie) | 44.2 | 1.79x |
Analysis: Sub-trie construction provides 1.79x speedup for 33% filtering rate.
| Filter Type | Time (µs) |
|---|---|
| Simple (HashSet lookup) | 229.6 |
| Medium (string ops) | 131.6 |
| Complex (multiple conditions) | 226.2 |
Analysis: Counter-intuitive result - simple filter is slower! This is because:
| Operation | Time (µs) |
|---|---|
| Prefix only | 124.8 |
| Prefix + filter | 218.4 |
| Prefix + distance + filter | 218.1 |
| Prefix + distance + multi-filter | 224.8 |
Analysis: Filtering adds ~75% overhead. Multiple filters add minimal incremental cost.
| Dictionary Size | Time (µs) | Throughput (Melem/s) |
|---|---|---|
| 1,000 | 1.76 | 569 |
| 5,000 | 29.9 | 167 |
| 10,000 | 53.7 | 186 |
| 20,000 | 66.6 | 300 |
Analysis: Near-linear scaling up to 20K terms. Throughput improves at larger sizes (better amortization of fixed costs).
queue_children() - 37.91%Function: OrderedQueryIterator::queue_children
Breakdown:
Root Cause: This function is called for EVERY node visited during traversal. For a query with distance=1 on a 10K dictionary, this could be thousands of calls.
Code Path:
queue_children()
├─> node.edges() // 10.99% - PathMap lock + zipper iteration
│ └─> with_zipper() // 10.25% - RwLock acquisition + zipper setup
│ └─> SmallVec collect // 8.81% - collecting edges
│ └─> filter iteration // 4.91% - bit mask tests
├─> transition_state_pooled() // 3.61%
│ └─> epsilon_closure_into() // 2.72%
├─> Box::new() // 1.93% - heap allocation
└─> Arc drop // 1.74% - reference counting
Function: <PathMapNode as DictionaryNode>::edges
Issues:
Assembly observations:
lock cmpxchg instruction (0.07% just for atomic)Function: SmallVec collection and push operations
Issues:
Current inline capacity: Unknown (need to check source)
Function: epsilon_closure_into
Issues:
Note: Already optimized with pooling, but still hot.
Current: Every queue_children call acquires PathMap lock and creates zipper
Proposed: Cache edge results at node level (if PathMap supports it)
Potential: 10.99% → ~2-3% (7-8% improvement)
Complexity: Medium (requires PathMap changes or wrapper)
Current: VecDeque grows dynamically for each distance bucket Proposed: Pre-allocate based on typical traversal width Potential: 1.93% → ~0.5% (1.4% improvement) Complexity: Low
Current: Many small functions not inlined (visible in assembly)
Proposed: Add #[inline(always)] to hot path functions
Potential: 2-3% improvement (reduced call overhead)
Complexity: Low
Current: Unknown inline capacity Proposed: Tune inline capacity based on actual edge counts Potential: 2-3% improvement (fewer heap spills) Complexity: Low
Current: PathMapNode uses Arc, causing ref count overhead Proposed: Consider lifetime-based borrowing where possible Potential: 1.74% improvement Complexity: High (requires API changes)
Current: Computed for each transition Proposed: Batch compute for multiple transitions Potential: 1-2% improvement Complexity: Medium
Current: Filter iteration tests bits sequentially Proposed: Use BMI2 instructions (pdep/pext) if available Potential: 1-2% improvement Complexity: Medium (needs CPU feature detection)
#[inline(always)] to hot functionsExpected: 5-7% improvement, ~2 hours work
Expected: 3-5% improvement, ~1 day work
Expected: 10-15% improvement, ~1 week work
The prefix matching and filtering implementation is correct but has optimization opportunities:
queue_children function (37.91%)Priority: Implement Phase 1 optimizations immediately for 5-7% improvement with minimal risk.
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 |