Investigation Start: 2025-11-18 Investigator: Claude Code Objective: Identify and resolve 4× performance degradation for large inputs (50+ phones)
Observation: Initial benchmarks showed unexpected performance degradation for large inputs:
Expected Behavior: Linear scaling (~220-260 ns/phone across all sizes) Actual Behavior: ~4× degradation at 50 phones (882 vs ~220 ns/phone)
Scientific Approach:
Hypotheses to Investigate:
Action: Running targeted benchmarks for input sizes: 5, 10, 20, 50, 100 phones
Method:
Benchmark Command:
RUSTFLAGS="-C target-cpu=native" taskset -c 0 cargo bench \
--bench phonetic_rules \
--features phonetic-rules \
-- throughput_by_input_size --output-format bencher
Status: Running...
Results: Confirmed 3.80× degradation at 50 phones
Hypotheses Formulated:
Document: 01-baseline-investigation.md created
Method: Manual code review of src/phonetic/application.rs
Critical Finding: find_first_match() allocates n+1 vectors per call
Root Cause:
for pos in 0..=s.len() {
if apply_rule_at(rule, s, pos).is_some() { // ⚠️ ALLOCATES EVERY TIME!
return Some(pos);
}
}
Allocation Count Analysis:
apply_rules_seq() callHypothesis Validation:
Document: 02-code-analysis.md created
Conclusion: No profiling needed - code analysis revealed the exact issue
Solution: Add can_apply_at() helper to avoid unnecessary allocations
Implementation:
can_apply_at() function - checks rule without allocatingfind_first_match() to use can_apply_at()Expected Impact: 3-4× speedup (reduce 42,997 ns → ~11,000 ns)
Status: Implementing...
Implementation: Added can_apply_at() helper to eliminate allocations in find_first_match()
Results:
Tests: ✅ All 87 tests passing (correctness maintained)
Unexpected Finding: Superlinear degradation persists unchanged
Analysis:
New Hypothesis H5: Iteration count increases superlinearly with input size
Document: 03-optimization-results.md created
Decision: Investigate H5 before proceeding
Method: Add instrumentation to apply_rules_seq() to count iterations
Goal: Determine if iteration count scales superlinearly with input size
Testable Prediction:
Status: Adding instrumentation...
Method: Instrumented apply_rules_seq() to count iterations for different input sizes
Results:
Hypothesis H5: ❌ REJECTED
Root Cause Identified:
Document: 04-iteration-analysis.md created
Final Findings:
Performance Summary (After Optimization):
Decision: ✅ Accept current performance for v0.8.0
Status: ✅ INVESTIGATION COMPLETE (EXTENDED)
Objective: Complete hypothesis testing and explore algorithmic optimizations
Phase 1 - H3 (Cache Inefficiency): ✅ REJECTED
05-h3-cache-analysis.mdPhase 2 - H4 (Slice Copying Overhead): ✅ REJECTED
apply_rule_at() with atomic counters06-h4-slice-analysis.mdPhase 3 - Algorithmic Optimization (Position Skipping): ⚠️ UNSAFE - NOT RETAINED
Context::Final rules break correctnessposition_skipping_proof.v (253 lines)docs/verification/phonetic/00-proof-summary.md07-algorithmic-optimization-analysis.mdTotal Time Invested: ~8 hours (baseline → optimization → H3 → H4 → algorithmic analysis) Optimization Achieved: 27-30% speedup across all input sizes (H1 only) Tests Passing: 87/87 (100% correctness maintained) Documentation: 8 analysis documents created (total ~2,000 lines)
Scientific Method Applied:
Complete Hypothesis Results: | Hypothesis | Tested | Result | Overhead | Action | |------------|--------|--------|----------|--------| | H1 - Vec allocations | ✅ | Confirmed | 27% | ✅ Fixed (v0.8.0) | | H2 - Algorithmic complexity | ✅ | O(n^1.5) | N/A | Expected behavior | | H3 - Cache misses | ✅ | Rejected | <2% | None needed | | H4 - Slice copying | ✅ | Rejected | 2-3% | None needed | | H5 - Iteration count | ✅ | O(√n) | N/A | Proven sublinear | | Position Skipping | ⚠️ | Unsafe | N/A | Not retained; v0.9.0+ research candidate |
Key Lessons:
Ready for v0.8.0: ✅
Production Performance (Post-H1 Optimization):
Objective: Complete the admitted proofs in position_skipping_proof.v to strengthen formal guarantees
Phase 1: Main Theorem 1 - find_first_match_from_equivalent_when_no_early_matches
find_first_match_some_implies_can_apply ✓ Provenfind_first_match_search_range ⚠️ Admitted (truncating subtraction complexity)find_first_match_is_first ⚠️ Admitted (complex context matching)find_first_match_finds_first_true ⚠️ Admitted (multiple admits)find_first_match_from_upper_bound ✓ Provenfind_first_match_from_is_first ✓ ProvenPhase 2: Main Theorem 2 - position_skip_safe_for_local_contexts
find_first_match and find_first_match_from (from 0) are equivalentlast_pos after each iterationPhase 3: Compilation and Verification
coqc -Q . PhoneticRewrites position_skipping_proof.vFinal Verification Status:
| Theorem/Lemma | Status | Notes |
|---|---|---|
find_first_match_from_lower_bound | ✓ Proven | Basic bounds property |
find_first_match_from_empty | ✓ Proven | Base case |
apply_rules_seq_opt_terminates | ✓ Proven | Termination guarantee |
find_first_match_some_implies_can_apply | ✓ Proven | Validity property |
find_first_match_from_upper_bound | ✓ Proven | Upper bound property |
find_first_match_from_is_first | ✓ Proven | First match property |
final_position_can_change | ✓ Proven | Unsafety counterexample |
position_skipping_conditionally_safe | ✓ Proven | Main safety theorem |
find_first_match_search_range | ⚠️ Admitted | Helper, truncating subtraction |
find_first_match_is_first | ⚠️ Admitted | Helper, context matching |
find_first_match_finds_first_true | ⚠️ Admitted | Helper, multiple admits |
find_first_match_from_equivalent_when_no_early_matches | ⚠️ Admitted | Main theorem 1 |
position_skip_safe_for_local_contexts | ⚠️ Admitted | Main theorem 2 |
Key Insights from Proof Attempt:
Decision: Accept current verification status for v0.8.0
Updated Documentation:
docs/verification/phonetic/00-proof-summary.md - Updated with detailed verification resultsdocs/verification/phonetic/position_skipping_proof.v - 350+ lines, compiles successfullyObjective: Complete remaining admitted proofs to achieve higher verification confidence
Phase 1: apply_rule_at_preserves_prefix (COMPLETED ✓)
Challenges Encountered:
injection H_apply as H_s' gave (firstn pos s) ++ ... = s' instead of s' = ...
rewrite <- H_s'.) instead of forward rewritenth_error_firstn returns if i <? pos then ... else None, not direct equality
(i <? pos) = true separately, then rewrite to simplify conditionalnth_error ((firstn pos s) ++ ...) i required careful tactic application
Proof Strategy (37 lines):
(* First direction: nth_error (firstn pos s) i = nth_error s i *)
assert (H_firstn_eq: ...).
{
rewrite nth_error_firstn.
assert (H_ltb: (i <? pos) = true) by (apply Nat.ltb_lt; exact H_lt).
rewrite H_ltb.
reflexivity.
}
(* Second direction: nth_error s' i = nth_error (firstn pos s) i *)
assert (H_s'_eq: ...).
{
rewrite <- H_s'. (* Backward rewrite! *)
rewrite nth_error_app1.
- reflexivity.
- (* Show i < length (firstn pos s) *)
rewrite firstn_length.
rewrite Nat.min_l by lia.
exact H_lt.
}
(* Combine *)
rewrite H_s'_eq.
rewrite H_firstn_eq.
reflexivity.
Results:
QedKey Lesson: When injection produces equality in unexpected direction, use backward rewrite (rewrite <-) rather than trying to reverse the hypothesis with symmetry.
Unretained proof candidate: 1 theorem (position_skip_safe_for_local_contexts) - not required for the accepted optimization set because position skipping was rejected as unsafe
Status: ✅ Track 1 COMPLETE - Significant progress toward full formal verification
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 |