Liking cljdoc? Tell your friends :D

SubstitutionSet Baseline Performance Results

Date: 2025-11-12 Hardware: Intel Xeon E5-2699 v3 @ 2.30GHz (CPU Core 0 & 1, performance governor) Rust: 1.91.0 Commit: e5a32a0

Executive Summary

Baseline measurements show that FxHashSet-based SubstitutionSet implementation performs excellently:

  • Single lookup: ~5.0ns (hash lookup is extremely fast)
  • Batch operations: ~4.0ns per lookup with overhead
  • Policy overhead: +10-20% for restricted policies (acceptable)
  • Preset initialization: 200ns-1µs (not a bottleneck)

Optimization Priority: Focus on preset initialization (perfect hashing) as primary optimization target.


Micro-Benchmark Results

1. Contains() Performance by Set Size

Testing 100 lookups (50% hit rate) across varying set sizes:

Set SizeTime (ns)Throughput (Melem/s)Per-Lookup (ns)
1407.38245.474.07
5411.14243.234.11
10413.05242.104.13
20430.16232.474.30
50431.69231.654.32
100402.83248.244.03
200421.07237.494.21
500448.15223.144.48

Analysis:

  • Performance relatively flat across sizes (4.0-4.5ns per lookup)
  • Slight degradation at size=500 (448ns vs 407ns for size=1) = +10%
  • Excellent hash distribution (no major collisions)
  • FxHashSet scales well for this use case

2. Hit Rate Impact (1000 lookups, set size=50)

Hit RateTime (µs)Throughput (Melem/s)Per-Lookup (ns)
10%4.025248.444.025
50%4.525220.974.525
90%4.787208.884.787

Analysis:

  • Hit rate affects performance: 10% → 90% hit = +19% latency
  • Cache effects: More hits = more data access = slightly slower
  • Difference is modest (0.76ns per lookup)

3. Insertion Performance

Byte-level (SubstitutionSet):

PairsTime (µs)Throughput (Melem/s)Per-Insert (ns)
100.26937.1526.9
501.02448.8220.5
1001.90152.6019.0
5007.70764.8815.4

Analysis:

  • Insertion gets more efficient with larger batches (cache warming)
  • 500 inserts: 15.4ns per insert (excellent)

Char-level (SubstitutionSetChar):

PairsTime (µs)Throughput (Melem/s)Per-Insert (ns)
100.32730.5532.7
501.12744.3822.5
1002.05048.7820.5
50011.28244.3222.6

Analysis:

  • Char operations ~20-30% slower than byte operations (expected)
  • Unicode handling adds overhead but still performant

4. Preset Initialization

Byte-level Presets:

PresetTime (ns)Description
phonetic_basic209.66Common phonetic substitutions
leet_speak286.131337 speak mappings
ocr_friendly267.40OCR confusion pairs
keyboard_qwerty645.64QWERTY adjacent key typos

Analysis:

  • Keyboard preset is 3x slower (larger set: ~80 pairs vs ~20-30)
  • All presets initialize in <1µs (fast)
  • Optimization opportunity: Perfect hashing could eliminate hash computation

Char-level Presets:

PresetTime (µs)Description
greek_case_insensitive0.493Greek letter mappings
japanese_hiragana_katakana0.369Japanese kana mappings
cyrillic_case_insensitive0.636Cyrillic letter mappings
diacritics_latin1.024Diacritic removal

Analysis:

  • Char presets 2-5x slower than byte presets (Unicode overhead)
  • Still sub-microsecond performance
  • Diacritics preset largest (1µs)

5. Single Lookup Profiling

OperationTime (ns)Notes
Hit5.054Lookup found in set
Miss5.178Lookup not found (early exit)

Analysis:

  • Absolute minimum latency: ~5ns per lookup
  • Miss slightly slower (+2.5%) due to full scan confirmation
  • This represents raw FxHashSet performance

Integration Benchmark Results

1. Unrestricted Query Performance (Baseline)

Testing real fuzzy queries with varying edit distances:

QueryDistanceTime (µs)Description
aple110.45Simple 1-edit query
appl111.171-edit, multiple results
banan116.86Longer term, 1-edit
famly116.801-edit query
computr121.11Long term, 1-edit
aplpy258.002-edit query
beutiful279.602-edit, long term
buisness265.672-edit, medium term
govrment255.222-edit, no results
intresting3178.893-edit, long term

Analysis:

  • Distance scaling: d=1→d=2 ≈ 3-4x slower, d=2→d=3 ≈ 2x slower
  • Query length impacts performance significantly
  • Exponential growth in state space with distance

2. Policy Overhead Comparison

Query: "computer" (exact match in dictionary)

Distance = 1:

PolicyTime (µs)Overhead vs Baseline
Unrestricted20.93-
Phonetic22.94+9.6%
Custom (small)22.91+9.5%

Distance = 2:

PolicyTime (µs)Overhead vs Baseline
Unrestricted86.15-
Phonetic93.27+8.3%
Custom (small)85.82-0.4%

Distance = 3:

PolicyTime (µs)Overhead vs Baseline
Unrestricted186.77-
Phonetic223.75+19.8%
Custom (small)214.40+14.8%

Analysis:

  • Policy overhead increases with edit distance
  • At d=3, phonetic adds ~37µs overhead (~20%)
  • Small custom set performs slightly better than phonetic (fewer checks)
  • Overhead is acceptable for most use cases

3. Preset Policy Performance

Phonetic Preset Queries:

QueryDistanceTime (µs)Notes
aple125.16vs 10.45µs unrestricted
senter2133.61c/s substitution
kollege293.55c/k substitution
foto283.35f/ph substitution (no match)
nite268.14ight/ite (no match)
kwick273.54qu/k (no match)

Analysis:

  • Phonetic preset adds 10-40µs overhead depending on query
  • Overhead acceptable for phonetic search use cases

Keyboard Preset Queries:

QueryDistanceTime (µs)Notes
aoole2111.70p/o adjacent key
bannna290.99a/n adjacent key
vook145.97b/v adjacent key
cimputer2136.96o/i adjacent key
familh128.44y/h adjacent key

Analysis:

  • Keyboard preset overhead similar to phonetic
  • Useful for typo correction scenarios

Bottleneck Analysis

Current Performance Characteristics:

  1. Single lookup: ~5ns - Extremely fast, hard to optimize further
  2. Preset initialization: 200-640ns - Could benefit from perfect hashing
  3. Policy overhead: +10-20% - Acceptable, but could be reduced
  4. Hash computation: ~2-3ns - Part of the 5ns total

Optimization Opportunities (Ranked):

  1. HIGH: Perfect hash for presets - Eliminate 200-640ns initialization cost
  2. MEDIUM: Bitmap for ASCII - Potentially faster than hash for byte-level (5ns → 3ns?)
  3. MEDIUM: Hybrid small/large - Linear scan for <10 pairs might be faster
  4. LOW: Custom hasher - Minimal gains (~1-2%)

What NOT to Optimize:

  • Single lookup performance (5ns) - Already excellent
  • Insertion performance - Not a hot path
  • Policy overhead at d=1 - Minimal impact

Reproducibility

Environment:

OS: Linux 6.17.7-arch1-1
Rust: 1.91.0 (f8297e351 2025-10-28)
Cargo: 1.91.0 (ea2d97820 2025-10-10)
CPU Governor: performance
CPU Affinity: Core 0 (micro), Core 1 (integration)
RUSTFLAGS: -C target-cpu=native

Commands:

# Micro-benchmarks
RUSTFLAGS="-C target-cpu=native" taskset -c 0 \
  cargo bench --bench substitution_set_microbench --features rand

# Integration benchmarks
RUSTFLAGS="-C target-cpu=native" taskset -c 1 \
  cargo bench --bench substitution_integration_bench

Raw Output:

  • Micro-benchmarks: /tmp/substitution_set_baseline.txt
  • Integration: /tmp/substitution_integration_baseline.txt

Next Steps

  1. Generate flamegraphs - Identify exact hotspots in contains() and preset init
  2. Run perf stat - Analyze cache misses, branch mispredictions
  3. Implement Hypothesis 1 - Perfect hash for presets (HIGH priority)
  4. Measure improvements - Compare against this baseline

Generated: 2025-11-12 Baseline Commit: e5a32a0

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close