Date: 2025-11-12 Purpose: Empirically determine optimal threshold for Hypothesis 3 (Hybrid Small/Large Strategy) Baseline: e5a32a0 (with H1 const array optimization)
Result: ✅ Crossover point identified at 5 pairs
rustc with -C target-cpu=nativeRUSTFLAGS="-C target-cpu=native" taskset -c 6 \
cargo bench --bench small_set_analysis --features rand
| Size | Hash (ns) | Linear (ns) | SmallVec8 (ns) | SmallVec16 (ns) | Winner | Speedup |
|---|---|---|---|---|---|---|
| 1 | 376.7 | 197.3 | 198.6 | 194.1 | Linear | 1.91× |
| 2 | 366.8 | 262.8 | 251.3 | 247.5 | SmallVec16 | 1.48× |
| 3 | 363.6 | 309.0 | 323.6 | 289.9 | Linear | 1.18× |
| 4 | 369.9 | 358.4 | 353.8 | 325.2 | SmallVec16 | 1.14× |
| 5 | 386.4 | 418.1 | 410.3 | 402.4 | Hash | 1.04× ⚡ CROSSOVER |
| 6 | 448.6 | 533.2 | 483.8 | 456.8 | Hash | 1.18× |
| 7 | 372.6 | 530.0 | 527.1 | 484.0 | Hash | 1.42× |
| 8 | 367.1 | 607.5 | 597.6 | 535.0 | Hash | 1.46× |
| 9 | 357.7 | 613.8 | 551.96 | 552.9 | Hash | 1.54× |
| 10 | 347.4 | 629.9 | 654.7 | 649.1 | Hash | 1.81× |
| 12 | 393.5 | 801.6 | 757.8 | 739.8 | Hash | 1.88× |
| 15 | 369.5 | 860.4 | 826.1 | 862.2 | Hash | 2.24× |
| 20 | 377.5 | 1159.4 | 1102.7 | 1077.7 | Hash | 2.85× |
Exact crossover at 5 pairs: Hash (386.4ns) edges out linear (418.1ns) by ~8%
Linear scan scaling: Time increases roughly linearly with set size
Hash lookup consistency: Hash maintains ~370ns regardless of size
SmallVec performance: Marginally better than Vec for small sizes
Test configuration: 5-pair set (near crossover point)
| Hit Rate | Hash (ns) | Linear (ns) | Winner | Notes |
|---|---|---|---|---|
| 10% | 370.2 | 362.1 | Linear | Mostly misses favor linear early exit |
| 50% | 386.4 | 418.1 | Hash | Baseline (50/50 mix) |
| 90% | 401.8 | 489.3 | Hash | High hit rate amplifies hash advantage |
Conclusion: Hit rate shifts crossover point slightly but doesn't change overall recommendation.
Comparing construction cost for small sets:
| Size | Hash Init (ns) | Linear Init (ns) | SmallVec8 Init (ns) | Winner |
|---|---|---|---|---|
| 1 | 12.4 | 4.2 | 4.8 | Linear (2.9× faster) |
| 3 | 31.7 | 11.8 | 13.2 | Linear (2.7× faster) |
| 5 | 49.3 | 19.4 | 21.1 | Linear (2.5× faster) |
| 10 | 94.1 | 38.2 | 42.7 | Linear (2.5× faster) |
| 20 | 187.9 | 76.8 | 85.3 | Linear (2.4× faster) |
Analysis: Linear initialization is consistently 2.4-2.9× faster, but:
Impact on H3: Initialization overhead is NOT a concern for hybrid strategy.
Measuring individual lookup latency (not amortized):
| Size | Hash Single (ns) | Linear Single (ns) | SmallVec Single (ns) | Winner |
|---|---|---|---|---|
| 1 | 5.18 | 1.97 | 1.98 | Linear (2.6× faster) |
| 3 | 5.19 | 3.09 | 3.24 | Linear (1.7× faster) |
| 5 | 5.21 | 4.18 | 4.10 | Linear (1.3× faster) |
| 10 | 5.17 | 6.30 | 6.55 | Hash (1.2× faster) |
Crossover for single lookup: Between 5-10 pairs (consistent with batch results)
Comparing memory usage per implementation:
| Size | Hash (bytes) | Linear (bytes) | SmallVec8 (bytes) | SmallVec16 (bytes) |
|---|---|---|---|---|
| 1 | 104 | 26 | 64 (inline) | 128 (inline) |
| 4 | 152 | 50 | 64 (inline) | 128 (inline) |
| 5 | 152 | 58 | 128 (heap) | 128 (inline) |
| 8 | 200 | 82 | 128 (heap) | 128 (inline) |
| 10 | 248 | 106 | 152 (heap) | 192 (heap) |
| 20 | 440 | 202 | 296 (heap) | 344 (heap) |
Calculations:
Conclusion: Linear uses 2-4× less memory for small sets (<10 pairs).
Hash cost: T_hash = T_hash_compute + T_probe ≈ 5.2ns
Linear cost: T_linear = N * T_compare ≈ N * 1.0ns
Crossover: 5.2ns = N * 1.0ns → N ≈ 5 pairs
Empirical validation: Crossover observed at exactly 5 pairs ✅
Rationale:
enum SubstitutionSetImpl {
Small(Vec<(u8, u8)>), // For ≤ 4 pairs
Large(FxHashSet<(u8, u8)>), // For > 4 pairs
}
impl SubstitutionSet {
const SMALL_SET_THRESHOLD: usize = 4;
pub fn new() -> Self {
Self { inner: Small(Vec::new()) }
}
pub fn allow_byte(&mut self, a: u8, b: u8) {
match &mut self.inner {
Small(vec) if vec.len() < SMALL_SET_THRESHOLD => {
vec.push((a, b));
}
Small(vec) => {
// Upgrade to hash set
let mut set = FxHashSet::default();
for &pair in vec.iter() {
set.insert(pair);
}
set.insert((a, b));
self.inner = Large(set);
}
Large(set) => {
set.insert((a, b));
}
}
}
#[inline]
pub fn contains(&self, dict_char: u8, query_char: u8) -> bool {
match &self.inner {
Small(vec) => vec.iter().any(|&(a, b)| a == dict_char && b == query_char),
Large(set) => set.contains(&(dict_char, query_char)),
}
}
}
Based on crossover analysis and preset distributions:
| Preset | Size | Current (Hash) | H3 (Hybrid) | Expected Improvement |
|---|---|---|---|---|
| Custom Small (avg 2-3 pairs) | 3 | 363.6ns | 309.0ns | 15% faster |
| Phonetic Basic | 14 | 369.5ns | 369.5ns | No change (>4) |
| Keyboard QWERTY | 68 | 377.5ns | 377.5ns | No change (>4) |
Overall impact:
# Crossover point analysis
RUSTFLAGS="-C target-cpu=native" taskset -c 6 \
cargo bench --bench small_set_analysis --features rand \
2>&1 | tee /tmp/small_set_crossover_final.txt
# Hit rate sensitivity
RUSTFLAGS="-C target-cpu=native" taskset -c 6 \
cargo bench --bench small_set_analysis --features rand \
-- --bench "hit_rate"
# Initialization overhead
RUSTFLAGS="-C target-cpu=native" taskset -c 6 \
cargo bench --bench small_set_analysis --features rand \
-- --bench "init"
# Single lookup performance
RUSTFLAGS="-C target-cpu=native" taskset -c 6 \
cargo bench --bench small_set_analysis --features rand \
-- --bench "single_lookup"
rustc --versionThe crossover analysis provides clear empirical evidence for H3 (Hybrid Small/Large Strategy):
Status: Ready for H3 implementation
Next: Implement hybrid SubstitutionSet with 4-pair threshold
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 |