Created: 2025-12-28 Purpose: Track empirical results for Persistent ARTrie (u8 and u32 variants) optimizations with statistical rigor
taskset -c 0 (pinned to core 0)Date: 2025-12-28
Branch: master
Purpose: Establish performance baseline before optimizations
Status: COMPLETE
taskset -c 0Status: COMPLETE
| Dict Size | Mean Time | 95% CI Lower | 95% CI Upper | Throughput |
|---|---|---|---|---|
| 100 | 4.4586 µs | 4.4213 µs | 4.5044 µs | 22.429 Melem/s |
| 500 | 44.657 µs | 44.217 µs | 45.079 µs | 11.196 Melem/s |
| 1000 | 63.775 µs | 63.351 µs | 64.246 µs | 15.680 Melem/s |
| 5000 | 279.14 µs | 276.97 µs | 281.34 µs | 17.912 Melem/s |
| Dict Size | Mean Time | 95% CI Lower | 95% CI Upper | Throughput |
|---|---|---|---|---|
| 100 | 4.6794 µs | 4.6413 µs | 4.7157 µs | 21.370 Melem/s |
| 1000 | 6.4569 µs | 6.3422 µs | 6.5698 µs | 15.487 Melem/s |
| 5000 | 7.0879 µs | 7.0289 µs | 7.1456 µs | 14.109 Melem/s |
| Dict Size | Mean Time | 95% CI Lower | 95% CI Upper | Throughput |
|---|---|---|---|---|
| 100 | 5.7938 µs | 5.7431 µs | 5.8487 µs | 17.260 Melem/s |
| 1000 | 2.1033 µs | 2.0857 µs | 2.1211 µs | 475.44 Melem/s |
| 5000 | 464.83 ns | 460.75 ns | 468.53 ns | 10.757 Gelem/s |
| Dict Size | Mean Time | 95% CI Lower | 95% CI Upper | Throughput |
|---|---|---|---|---|
| 100 | 24.731 µs | 24.583 µs | 24.889 µs | 4.0435 Melem/s |
| 1000 | 11.617 µs | 11.541 µs | 11.699 µs | 8.6083 Melem/s |
| 5000 | 10.189 µs | 10.127 µs | 10.252 µs | 9.8146 Melem/s |
| Dict Size | Operation | Mean Time | 95% CI Lower | 95% CI Upper |
|---|---|---|---|---|
| 100 | create_insert_sync | 81.735 µs | 80.403 µs | 83.760 µs |
| 100 | recovery | 118.39 µs | 116.63 µs | 119.53 µs |
| 100 | checkpoint | 643.14 µs | 636.25 µs | 652.41 µs |
| 500 | create_insert_sync | 243.60 µs | 240.37 µs | 246.86 µs |
| 500 | recovery | 443.53 µs | 439.58 µs | 448.18 µs |
| 500 | checkpoint | 5.2551 ms | 5.2002 ms | 5.3398 ms |
| 1000 | create_insert_sync | 342.68 µs | 339.80 µs | 346.11 µs |
| 1000 | recovery | 657.30 µs | 651.13 µs | 667.03 µs |
| 1000 | checkpoint | 5.2327 ms | 5.1856 ms | 5.3337 ms |
Status: COMPLETE
| Dict Size | Mean Time | 95% CI Lower | 95% CI Upper | Throughput |
|---|---|---|---|---|
| 100 | 153.54 µs | 151.54 µs | 155.59 µs | 651.29 Kelem/s |
| 500 | 948.09 µs | 935.84 µs | 955.72 µs | 527.38 Kelem/s |
| 1000 | 2.0792 ms | 2.0467 ms | 2.1179 ms | 480.96 Kelem/s |
| 5000 | 10.281 ms | 10.121 ms | 10.446 ms | 486.35 Kelem/s |
| Dict Size | Mean Time | 95% CI Lower | 95% CI Upper | Throughput |
|---|---|---|---|---|
| 100 | 206.78 µs | 204.15 µs | 209.11 µs | 483.61 Kelem/s |
| 500 | 1.0480 ms | 1.0383 ms | 1.0574 ms | 477.09 Kelem/s |
| 1000 | 2.1316 ms | 2.1165 ms | 2.1497 ms | 469.13 Kelem/s |
| 5000 | 11.387 ms | 11.342 ms | 11.422 ms | 439.11 Kelem/s |
| Dict Size | Mean Time | 95% CI Lower | 95% CI Upper | Throughput |
|---|---|---|---|---|
| 100 | 15.269 µs | 15.144 µs | 15.417 µs | 6.5494 Melem/s |
| 1000 | 18.397 µs | 18.286 µs | 18.503 µs | 5.4356 Melem/s |
| 5000 | 20.109 µs | 19.948 µs | 20.285 µs | 4.9730 Melem/s |
| Benchmark | Mean Time | 95% CI Lower | 95% CI Upper | Throughput |
|---|---|---|---|---|
| cjk_lookup | 11.089 µs | 10.989 µs | 11.185 µs | 9.0176 Melem/s |
| Dict Size | Mean Time | 95% CI Lower | 95% CI Upper | Throughput |
|---|---|---|---|---|
| 100 | 12.654 µs | 12.531 µs | 12.794 µs | 7.9024 Melem/s |
| 1000 | 88.141 µs | 87.497 µs | 88.809 µs | 11.345 Melem/s |
| 5000 | 336.50 µs | 334.13 µs | 338.81 µs | 14.859 Melem/s |
| Dict Size | Mean Time | 95% CI Lower | 95% CI Upper | Throughput |
|---|---|---|---|---|
| 100 | 13.659 µs | 13.565 µs | 13.766 µs | 7.3210 Melem/s |
| 1000 | 17.310 µs | 17.150 µs | 17.458 µs | 5.7772 Melem/s |
| 5000 | 19.361 µs | 19.217 µs | 19.519 µs | 5.1650 Melem/s |
| Benchmark | Mean Time | 95% CI Lower | 95% CI Upper | Throughput |
|---|---|---|---|---|
| emoji_transitions | 4.6050 µs | 4.5610 µs | 4.6534 µs | 10.858 Melem/s |
| Dict Size | Mean Time | 95% CI Lower | 95% CI Upper | Throughput |
|---|---|---|---|---|
| 100 | 21.936 µs | 21.763 µs | 22.134 µs | 4.5587 Melem/s |
| 500 | 78.178 µs | 77.273 µs | 79.396 µs | 6.3957 Melem/s |
| 1000 | 161.03 µs | 159.54 µs | 162.69 µs | 6.2101 Melem/s |
| Dict Size | Operation | Mean Time | 95% CI Lower | 95% CI Upper |
|---|---|---|---|---|
| 100 | create_insert_sync | 124.74 µs | 123.43 µs | 125.81 µs |
| 100 | recovery | 148.75 µs | 146.89 µs | 150.74 µs |
| 100 | checkpoint | 83.323 ms | 82.389 ms | 85.285 ms |
| 500 | create_insert_sync | 416.93 µs | 410.59 µs | 424.91 µs |
| 500 | recovery | 589.32 µs | 583.16 µs | 595.38 µs |
| 500 | checkpoint | 322.93 ms | 320.80 ms | 325.18 ms |
| 1000 | create_insert_sync | 791.22 µs | 786.82 µs | 796.66 µs |
| 1000 | recovery | 1.0952 ms | 1.0876 ms | 1.1099 ms |
| 1000 | checkpoint | 582.45 ms | 578.52 ms | 586.04 ms |
Date: 2025-12-28
Branch: master
Purpose: Identify performance bottlenecks using perf profiling
Status: COMPLETE
| Rank | Symbol | Overhead | Analysis |
|---|---|---|---|
| 1 | StringBucket::search | 15.06% | String storage layer - linear search in bucket |
| 2 | exp (libm) | 11.42% | Criterion statistics overhead |
| 3 | StringBucket::insert_impl | 9.49% | String storage layer - insertion |
| 4 | rayon::bridge_producer_consumer::helper | 8.67% | Parallel iteration overhead |
| 5 | PersistentARTrieInner::insert_impl_core | 6.38% | Actual trie insertion logic |
| 6 | PersistentARTrie::insert | 2.91% | Top-level insert wrapper |
| 7 | libc (memcpy) | 2.85% | Memory operations |
| 8 | bucket_to_art_node | 1.68% | Node type promotion |
Key Finding: StringBucket operations dominate at 24.55% combined. Actual trie insertion (insert_impl_core) is only 6.38%.
| Rank | Symbol | Overhead | Analysis |
|---|---|---|---|
| 1 | exp (libm) | 15.76% | Criterion statistics overhead |
| 2 | Bencher::iter | 13.09% | Criterion benchmark loop |
| 3 | rayon::bridge_producer_consumer::helper | 12.08% | Parallel iteration overhead |
| 4 | StringBucket::search | 6.25% | String storage layer |
| 5 | rayon::slice::sort::recurse | 1.25% | Result sorting |
| 6 | rayon::slice::sort::insertion_sort | 1.14% | Result sorting |
Key Finding: Criterion framework overhead dominates at 28.85%. Actual trie lookup operations are NOT visible in top hotspots - they are highly optimized and complete quickly.
| Metric | Value | Rate |
|---|---|---|
| Cycles | 343.94 B | - |
| Instructions | 453.47 B | - |
| IPC | 1.32 | - |
| Cache References | 3.83 B | - |
| Cache Misses | 301.36 M | - |
| Cache Miss Rate | - | 7.87% |
| Branch Instructions | 87.51 B | - |
| Branch Misses | 2.71 B | - |
| Branch Miss Rate | - | 3.09% |
| Rank | Symbol | Overhead | Analysis |
|---|---|---|---|
| 1 | PersistentARTrieChar::insert_with_value | 11.40% | Actual trie insertion |
| 2 | exp (libm) | 9.92% | Criterion statistics overhead |
| 3 | BTreeMap::IntoIter::dying_next | 7.93% | BTreeMap iteration/destruction |
| 4 | rayon::bridge_producer_consumer::helper | 7.45% | Parallel iteration overhead |
| 5 | BTreeMap clone_subtree | 5.27% | BTreeMap cloning (persistent data) |
| 6 | malloc | 4.67% | Memory allocation |
| 7 | cfree | 4.33% | Memory deallocation |
| 8 | BTreeMap::drop | 4.19% | BTreeMap destruction |
| 9 | Arc::drop_slow | 1.23% | Reference counting cleanup |
| 10 | realloc | 1.17% | Memory reallocation |
Key Finding: BTreeMap operations + memory allocation dominate at 22.39% combined. Memory management (malloc/cfree/realloc) accounts for 10.17%.
| Rank | Symbol | Overhead | Analysis |
|---|---|---|---|
| 1 | Bencher::iter | 44.95% | Criterion benchmark loop - lookups complete inside this |
| 2 | exp (libm) | 12.21% | Criterion statistics overhead |
| 3 | rayon::bridge_producer_consumer::helper | 9.21% | Parallel iteration overhead |
Key Finding: Criterion overhead is 57.16%! The actual trie lookup operations are so fast they don't appear as separate hotspots - they're completing within Bencher::iter. This indicates the trie lookup path is already highly optimized.
| Metric | Value | Rate |
|---|---|---|
| Cycles | 410.71 B | - |
| Instructions | 434.86 B | - |
| IPC | 1.06 | - |
| Cache References | 3.73 B | - |
| Cache Misses | 258.19 M | - |
| Cache Miss Rate | - | 6.92% |
| Branch Instructions | 82.14 B | - |
| Branch Misses | 2.60 B | - |
| Branch Miss Rate | - | 3.16% |
⚠️ Major Discovery: Original hypotheses target the wrong bottlenecks!
The perf data reveals that the node-level operations (find_child(), find_key_index_simd(), SIMD prefix matching) are NOT the performance bottlenecks. They don't even appear in the top 10 hotspots.
Actual Bottleneck Distribution:
| Category | u8 Construction | u8 Lookup | u32 Construction | u32 Lookup |
|---|---|---|---|---|
| Storage Layer (StringBucket/BTreeMap) | 24.55% | 6.25% | 17.39% | <1% |
| Memory Management (malloc/free/realloc) | ~2.85% | <1% | 10.17% | <1% |
| Criterion/Benchmark Overhead | 11.42% | 28.85% | 9.92% | 57.16% |
| Parallelism Overhead (Rayon) | 8.67% | 12.08% | 7.45% | 9.21% |
| Actual Trie Operations | ~11% | <5% | ~11% | <5% |
Implications for Hypothesis Prioritization:
Node-level optimizations (S4, U8-1, U8-2, U32-1, U32-3, etc.) will have LIMITED IMPACT because the code they target is only ~10% of execution time at most.
Storage layer optimization would yield highest ROI:
StringBucket (24.55% of construction time)BTreeMap usage (17.39% + 10.17% memory overhead)Memory allocation optimization could help u32 variant:
Benchmark overhead is unavoidable but informative:
feat/artrie-opt-S4 (deleted after rejection)#[inline(always)] provides no improvementImplementation Details:
Added #[inline(always)] to all hot path functions:
find_child() in Node4, Node16, Node48, Node256 (u8)find_key_index(), find_key_index_simd(), find_key_index_linear() (u8)find_child() in CharNode4, CharNode16, CharNode48, CharBucket (u32)find_key_index(), find_key_index_simd(), find_key_index_linear(), find_key_index_binary() (u32)Benchmark Results - u8 Variant:
| Benchmark | Baseline | With S4 | Change | Significance |
|---|---|---|---|---|
| construct_100 | 4.4586 µs | 4.7440 µs | +6.4% | REGRESSION |
| construct_500 | 44.657 µs | 47.055 µs | +5.4% | REGRESSION |
| construct_1000 | 63.775 µs | 68.152 µs | +6.9% | REGRESSION |
| construct_5000 | 279.14 µs | 340.36 µs | +21.9% | REGRESSION |
| lookup_100 | 4.6794 µs | 4.6953 µs | +0.3% | No change |
| lookup_1000 | 6.4569 µs | 6.4850 µs | +0.4% | No change |
| lookup_5000 | 7.0879 µs | 7.1174 µs | +0.4% | No change |
Benchmark Results - u32 Variant:
| Benchmark | Baseline | With S4 | Change | Significance |
|---|---|---|---|---|
| char_construct_100 | 153.54 µs | 159.87 µs | +4.1% | REGRESSION |
| char_construct_500 | 948.09 µs | 987.23 µs | +4.1% | REGRESSION |
| char_construct_1000 | 2.0792 ms | 2.1456 ms | +3.2% | REGRESSION |
| char_lookup_100 | 15.269 µs | 15.134 µs | -0.9% | No change |
| char_lookup_1000 | 18.397 µs | 18.512 µs | +0.6% | No change |
Analysis:
Decision: REJECTED
match_key() by >10%src/dictionary/persistent_artrie/nodes/mod.rsinsert_impl_core is only 6.38% of construction time. Even 10% improvement would yield <0.64% overall gain.src/dictionary/persistent_artrie_char/nodes/node16_char.rsmatch_key() by >10%src/dictionary/persistent_artrie_char/nodes/mod.rsDecision: SKIPPED
match_key() doesn't appear in perf hotspotsfeat/artrie-opt-S2 (deleted after rejection)Implementation Details:
Changed from current alignment to #[repr(C, align(64))] in 5 files:
node4.rs: #[repr(C)] → #[repr(C, align(64))]node16.rs: #[repr(C, align(16))] → #[repr(C, align(64))]node4_char.rs: #[repr(C, align(8))] → #[repr(C, align(64))]node16_char.rs: #[repr(C, align(32))] → #[repr(C, align(64))]node48_char.rs: #[repr(C)] → #[repr(C, align(64))]Benchmark Results - u8 Variant (SEVERE REGRESSIONS ✗):
| Benchmark | Baseline | With S2 | Change | Status |
|---|---|---|---|---|
| construct/100 | 4.4586 µs | 4.5200 µs | +1.4% | Marginal |
| construct/500 | 44.657 µs | 50.248 µs | +12.5% | ✗ SEVERE REGRESSION |
| construct/1000 | 63.775 µs | 66.065 µs | +3.6% | ✗ REGRESSION |
| construct/5000 | 279.14 µs | 272.92 µs | -2.2% | ✓ Improved |
| lookup/100 | 4.6794 µs | 4.5016 µs | -3.8% | ✓ Improved |
| lookup/1000 | 6.4569 µs | 6.7163 µs | +4.0% | ✗ REGRESSION |
| lookup/5000 | 7.0879 µs | 8.0231 µs | +13.2% | ✗ SEVERE REGRESSION |
| edge_traversal/100 | 5.7938 µs | 5.7597 µs | -0.6% | No change |
| edge_traversal/1000 | 2.1033 µs | 3.1005 µs | +47.4% | ✗ CATASTROPHIC REGRESSION |
| edge_traversal/5000 | 464.83 ns | 509.00 ns | +9.5% | ✗ REGRESSION |
Analysis:
The cache line alignment optimization showed catastrophic regressions, particularly:
Root Cause:
Key Insight: Cache line alignment is a micro-optimization that only helps when structures are frequently split across cache lines during hot paths. The ART node structures are already well-aligned (16/32 bytes for SIMD operations). Adding 64-byte alignment causes memory overhead that outweighs any potential cache line split benefits.
Decision: REJECTED
src/dictionary/persistent_artrie/nodes/node16.rsDecision: SKIPPED
src/dictionary/persistent_artrie_char/nodes/bucket_char.rssrc/dictionary/persistent_artrie/nodes/node48.rsDecision: SKIPPED
Decision: SKIPPED
unlikely() hints reduce misprediction by >3%Analysis:
This hypothesis was not tested based on perf-driven analysis:
Low impact ceiling: Perf data shows node-level find_child() operations are NOT in the top 10 hotspots. They represent <11% of total execution time combined.
Dependency requirement: Testing requires either:
likely_stable crate dependency (adds maintenance burden)core::intrinsics::likely/unlikely (not compatible with rust-version = "1.70")Branch misprediction is not a bottleneck: Hardware counters show only 3.09% branch miss rate (u8) and 3.16% (u32). Modern CPUs handle these efficiently via dynamic branch prediction.
Cost/benefit analysis:
Decision: SKIPPED
src/dictionary/persistent_artrie/nodes/node16.rsDecision: SKIPPED
insert_impl_core is only 6.38% of construction timesrc/dictionary/persistent_artrie_char/nodes/node48_char.rsDecision: SKIPPED
The following hypotheses target the actual bottlenecks identified through perf profiling.
StringBucket::search is 15.06% of construction timesrc/dictionary/persistent_artrie/bucket.rsRoot Cause Analysis: StringBucket already uses binary search (O(log n)). The overhead comes from:
self.header() is called on every search(), insert_impl(), and get_entry() call, reading 32 bytes each timeget_entry() re-reads header to validate boundsProposed Changes:
entry_count_fast() to read entry count directly (2 bytes) instead of parsing full 32-byte headerget_entry_unchecked() to bypass bounds checking in binary search (caller guarantees index < count)search() to use these optimized methodslen() and is_empty() to use entry_count_fast()#[inline(always)] to new helper methodsImplementation Details:
/// Read entry count directly from raw data (faster than parsing full header)
#[inline(always)]
fn entry_count_fast(&self) -> usize {
u16::from_le_bytes([self.data[12], self.data[13]]) as usize
}
/// Get directory entry without bounds checking
#[inline(always)]
fn get_entry_unchecked(&self, index: usize) -> StringEntry {
let offset = HEADER_SIZE + (index * ENTRY_SIZE);
let bytes: [u8; ENTRY_SIZE] = self.data[offset..offset + ENTRY_SIZE]
.try_into()
.expect("slice length matches ENTRY_SIZE");
StringEntry::from_bytes(&bytes)
}
Benchmark Results - Construction (REGRESSED ✗):
| Benchmark | Baseline | With NEW-U8-1 | Change | p-value | Status |
|---|---|---|---|---|---|
| construct/100 | 4.4586 µs | 4.4923 µs | -0.05% | p = 0.95 | No change |
| construct/500 | 44.657 µs | 51.461 µs | +14.49% | p < 0.05 | ✗ REGRESSION |
| construct/1000 | 63.775 µs | 66.478 µs | +3.53% | p < 0.05 | ✗ REGRESSION |
| construct/5000 | 279.14 µs | 301.52 µs | +8.69% | p < 0.05 | ✗ REGRESSION |
Benchmark Results - Lookup (REGRESSED ✗):
| Benchmark | Baseline | With NEW-U8-1 | Change | p-value | Status |
|---|---|---|---|---|---|
| lookup/100 | 4.6794 µs | 4.6834 µs | +0.77% | p = 0.15 | No change |
| lookup/1000 | 6.4569 µs | 6.6958 µs | +4.03% | p < 0.05 | ✗ REGRESSION |
| lookup/5000 | 7.0879 µs | 7.9183 µs | +10.93% | p < 0.05 | ✗ REGRESSION |
Benchmark Results - Edge Traversal (IMPROVED ✓):
| Benchmark | Baseline | With NEW-U8-1 | Change | p-value | Status |
|---|---|---|---|---|---|
| edge_traversal/100 | 5.7938 µs | 5.4285 µs | -4.65% | p < 0.05 | ✓ Improved |
| edge_traversal/1000 | 2.1033 µs | 1.9920 µs | -6.49% | p < 0.05 | ✓ Improved |
| edge_traversal/5000 | 464.83 ns | 437.83 ns | -5.01% | p < 0.05 | ✓ Improved |
Benchmark Results - Transitions (IMPROVED ✓):
| Benchmark | Baseline | With NEW-U8-1 | Change | p-value | Status |
|---|---|---|---|---|---|
| transitions/100 | 24.731 µs | 23.346 µs | -5.55% | p < 0.05 | ✓ Improved |
| transitions/1000 | 11.617 µs | 10.793 µs | -7.20% | p < 0.05 | ✓ Improved |
| transitions/5000 | 10.189 µs | 10.104 µs | -0.48% | p = 0.40 | No change |
Benchmark Results - Disk I/O (MIXED):
| Benchmark | Baseline | With NEW-U8-1 | Change | p-value | Status |
|---|---|---|---|---|---|
| create_insert/100 | 81.735 µs | 87.647 µs | +4.54% | p < 0.05 | ✗ Regression |
| recovery/100 | 118.39 µs | 125.82 µs | +7.76% | p < 0.05 | ✗ Regression |
| create_insert/1000 | 342.68 µs | 330.22 µs | -3.65% | p < 0.05 | ✓ Improved |
Analysis:
The optimization showed an unexpected pattern:
Root Cause of Unexpected Results:
The #[inline(always)] attributes on the new helper functions likely caused instruction cache pressure, similar to the S4 hypothesis failure. The overhead of inlining the header-caching code into every call site outweighed the benefit of avoiding header parsing.
Additionally, the binary search in search() may be calling get_entry_unchecked() more frequently than the original get_entry() was called, since the original implementation may have had better branch prediction due to the bounds check.
Decision: REJECTED
src/dictionary/persistent_artrie_char/mod.rs (CharTrieNode)Root Cause Analysis:
The u32 variant uses BTreeMap<char, Arc<CharTrieNode<V>>> for children. Perf breakdown:
BTreeMap::IntoIter::dying_next - iterating/destroying during cloneBTreeMap clone_subtree - cloning entire subtrees for persistenceBTreeMap::drop - destructor overheadWhy BTreeMap is slow here:
Arc<CharTrieNode> requires cloning the BTreeMap on modificationsProposed Changes:
Option A: Replace BTreeMap<char, Arc<...>> with Vec<(char, Arc<...>)> kept sorted
Option B: Use SmallVec<[(char, Arc<...>); 8]> to inline small children counts
Most nodes have <8 children
Avoids allocation for common case
Falls back to heap for larger nodes
Expected Impact: Up to 17.39% × 30% = ~5% overall construction improvement
Acceptance: p < 0.05, >20% improvement in construction, no regression in lookups
Implementation Details:
Replaced BTreeMap<char, Arc<CharTrieNode<V>>> with SmallVec<[(char, Arc<CharTrieNode<V>>); 8]> and added helper methods (get_child(), get_child_mut(), entry_or_insert(), insert_child()) that use binary search to maintain sorted order.
Benchmark Results - Construction (ALL IMPROVED ✓):
| Benchmark | Baseline | With NEW-U32-1 | Change | p-value | Status |
|---|---|---|---|---|---|
| char_construct/100 | 153.54 µs | 131.34 µs | -14.15% | p < 0.05 | ✓ Improved |
| char_construct/500 | 948.09 µs | 743.77 µs | -19.11% | p < 0.05 | ✓ Improved |
| char_construct/1000 | 2.0792 ms | 1.6407 ms | -21.77% | p < 0.05 | ✓ Improved |
| char_construct/5000 | 10.281 ms | 8.1599 ms | -21.80% | p < 0.05 | ✓ Improved |
| char_construct_ascii/100 | 206.78 µs | 176.18 µs | -13.46% | p < 0.05 | ✓ Improved |
| char_construct_ascii/500 | 1.0480 ms | 910.89 µs | -13.64% | p < 0.05 | ✓ Improved |
| char_construct_ascii/1000 | 2.1316 ms | 1.8598 ms | -13.59% | p < 0.05 | ✓ Improved |
| char_construct_ascii/5000 | 11.387 ms | 9.5838 ms | -15.67% | p < 0.05 | ✓ Improved |
Benchmark Results - Lookup (IMPROVED ✓):
| Benchmark | Baseline | With NEW-U32-1 | Change | p-value | Status |
|---|---|---|---|---|---|
| char_lookup/100 | 15.269 µs | 14.911 µs | -3.79% | p < 0.05 | ✓ Improved |
| char_lookup/1000 | 18.397 µs | 17.909 µs | -2.94% | p < 0.05 | ✓ Improved |
| char_lookup/5000 | 20.109 µs | 18.867 µs | -7.21% | p < 0.05 | ✓ Improved |
| cjk_lookup | 11.089 µs | 10.328 µs | -6.06% | p < 0.05 | ✓ Improved |
Benchmark Results - Memory Efficiency (IMPROVED ✓):
| Benchmark | Baseline | With NEW-U32-1 | Change | p-value | Status |
|---|---|---|---|---|---|
| memory_size/1000 | 1.8217 ms | 1.6110 ms | -11.29% | p < 0.05 | ✓ Improved |
| memory_size/5000 | 9.7272 ms | 7.5649 ms | -22.25% | p < 0.05 | ✓ Improved |
| memory_size/10000 | 19.361 ms | 14.238 ms | -26.44% | p < 0.05 | ✓ Improved |
Benchmark Results - Transitions (REGRESSED ✗):
| Benchmark | Baseline | With NEW-U32-1 | Change | p-value | Status |
|---|---|---|---|---|---|
| char_transitions/100 | 13.659 µs | 15.632 µs | +11.61% | p < 0.05 | ✗ REGRESSION |
| char_transitions/1000 | 17.310 µs | 19.264 µs | +12.59% | p < 0.05 | ✗ REGRESSION |
| char_transitions/5000 | 19.361 µs | 23.277 µs | +20.10% | p < 0.05 | ✗ REGRESSION |
| emoji_transitions | 4.6050 µs | 5.2655 µs | +14.37% | p < 0.05 | ✗ REGRESSION |
Benchmark Results - Edge Traversal (MIXED):
| Benchmark | Baseline | With NEW-U32-1 | Change | p-value | Status |
|---|---|---|---|---|---|
| edge_traversal/100 | 12.654 µs | 11.914 µs | -6.64% | p < 0.05 | ✓ Improved |
| edge_traversal/1000 | 88.141 µs | 93.706 µs | +5.85% | p < 0.05 | ✗ REGRESSION |
| edge_traversal/5000 | 336.50 µs | 359.13 µs | +6.00% | p < 0.05 | ✗ REGRESSION |
Benchmark Results - Iteration (REGRESSED):
| Benchmark | Baseline | With NEW-U32-1 | Change | p-value | Status |
|---|---|---|---|---|---|
| iter/100 | 21.936 µs | 23.098 µs | +4.93% | p < 0.05 | ✗ Regression |
| iter/500 | 78.178 µs | 84.815 µs | +6.19% | p < 0.05 | ✗ Regression |
| iter/1000 | 161.03 µs | 159.29 µs | -0.87% | p = 0.22 | No change |
Benchmark Results - Disk I/O (IMPROVED ✓):
| Benchmark | Baseline | With NEW-U32-1 | Change | p-value | Status |
|---|---|---|---|---|---|
| create_insert/100 | 124.74 µs | 118.67 µs | -4.71% | p < 0.05 | ✓ Improved |
| create_insert/1000 | 791.22 µs | 738.78 µs | -7.59% | p < 0.05 | ✓ Improved |
| recovery/500 | 589.32 µs | 561.37 µs | -4.68% | p < 0.05 | ✓ Improved |
| recovery/1000 | 1.0952 ms | 1.0432 ms | -5.61% | p < 0.05 | ✓ Improved |
| checkpoint/500 | 322.93 ms | 309.32 ms | -4.22% | p < 0.05 | ✓ Improved |
| checkpoint/1000 | 582.45 ms | 557.72 ms | -4.25% | p < 0.05 | ✓ Improved |
Analysis:
The SmallVec replacement successfully addressed the BTreeMap bottleneck, achieving:
However, critical regressions occurred in:
Root Cause of Regressions: The transition benchmark performs single-character lookups repeatedly. With BTreeMap, the lookup path was optimized for tree traversal. With SmallVec + binary search:
Decision: REJECTED
src/dictionary/persistent_artrie_char/dict_impl.rsStringBucket::insert_impl is 9.49% of construction timesrc/dictionary/persistent_artrie/bucket.rs| Hypothesis | Status | Change | p-value | Decision | Notes |
|---|---|---|---|---|---|
| S4 | REJECTED | +3-22% regression | N/A | NO | Forced inlining caused icache pressure |
| U8-2 | DEPRIORITIZED | - | - | - | <6% impact ceiling per perf data |
| U32-1 | DEPRIORITIZED | - | - | - | Not a measurable bottleneck |
| U32-3 | SKIPPED | - | - | - | Prefix matching not in hotspots |
| S2 | REJECTED | +47% edge traversal regression | N/A | NO | Memory bloat from 64-byte alignment |
| U8-1 | SKIPPED | - | - | - | <1.1% max impact; SSE4.1 sufficient |
| U32-2 | PENDING | - | - | - | Could target storage layer |
| U8-3 | SKIPPED | - | - | - | O(1) index lookup already optimal |
| S1 | SKIPPED | - | - | - | <7% cache miss, HW prefetch sufficient |
| S3 | SKIPPED | - | - | - | <0.4% max impact; 3% branch miss already low |
| U8-4 | SKIPPED | - | - | - | <0.2% max impact; LLVM optimizes loops |
| U32-4 | SKIPPED | - | - | - | <1.1% max impact; 6 comparisons trivial |
| NEW-U8-1 | REJECTED | +3-14% construct, +4-11% lookup | p < 0.05 | NO | Inline cache pressure; edge traversal improved but core ops regressed |
| NEW-U32-1 | REJECTED | -14-22% construct, +10-20% transition | p < 0.05 | NO | Transition regressions exceed 2% threshold |
| NEW-U32-2 | PENDING | - | - | - | MEDIUM PRIORITY - targets 10% hotspot |
| NEW-U8-2 | PENDING | - | - | - | MEDIUM PRIORITY - targets 9% hotspot |
The trie node operations are already highly optimized. They don't appear in the top hotspots because they complete so quickly. The original hypotheses (S4, U8-1, U8-2, U32-1, etc.) targeting these functions have limited optimization potential (<11% of total time).
The actual bottlenecks are in the storage/support layers:
The lookup path is already excellent. Criterion benchmark overhead dominates (28-57%), meaning the actual trie lookups complete in a tiny fraction of the measured time.
| Category | Tested | Rejected | Skipped | Accepted |
|---|---|---|---|---|
| Original Hypotheses (S1-S4, U8-1-4, U32-1-4) | 2 | 2 | 10 | 0 |
| Data-Driven Hypotheses (NEW-*) | 2 | 2 | 0 | 0 |
| Total | 4 | 4 | 10 | 0 |
All 10 skipped hypotheses target node-level operations that perf data shows are NOT bottlenecks:
Maximum impact ceiling for any node-level optimization: <1.5% overall improvement.
The ARTrie implementation is already well-optimized:
The remaining bottlenecks are:
NEW-U32-2 (Arena Allocation): Could reduce memory allocation overhead (10.17% of u32 construction). However, this requires careful integration with Arc-based persistence.
NEW-U8-2 (Batch Insertion): Could optimize StringBucket insertion (9.49% of construction) by deferring sorting. However, this would add complexity.
Relaxed Regression Constraints: If the 2% regression threshold were relaxed to allow 10-15% regressions in rarely-used operations (transitions), NEW-U32-1 would provide significant overall improvement.
Different Data Structure: For write-heavy workloads, consider non-persistent alternatives that don't require copy-on-write semantics.
The Persistent ARTrie implementations are production-ready and well-optimized. Further optimization would require either:
The scientific approach of perf-driven hypothesis testing successfully prevented wasted effort on ineffective optimizations and identified the true performance characteristics of the system.
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 |