Liking cljdoc? Tell your friends :D

Phase 7: Algorithmic Optimization Analysis (Position Skipping)

Date: 2025-11-18 Status: REJECTED FOR CURRENT RELEASE - Safety concerns identified Hypothesis: Position-based early termination can reduce O(n^1.5) complexity impact

Investigation Summary

After eliminating H1 (allocation overhead - 27% gain), H3 (cache - optimal), and H4 (slice copying - 3% overhead), the remaining performance is dominated by fundamental O(n^1.5) algorithmic complexity.

Remaining Breakdown (50-phone case):

  • Computation (pattern matching, context checking): ~97% (30,413 ns)
  • Slice copying: ~3% (933 ns)
  • Total: 31,346 ns

Proposed Optimization: Position Skipping

Concept

Track the position where the last rule was applied and start the next iteration's search from that position instead of position 0.

Rationale: After applying a rule at position last_pos, positions far before last_pos are unlikely to have new matches.

Implementation Approach

pub fn apply_rules_seq_optimized(rules: &[RewriteRule], s: &[Phone], fuel: usize) -> Option<Vec<Phone>> {
    let mut current = s.to_vec();
    let mut remaining_fuel = fuel;
    let mut last_pos = 0;  // Track last modification position

    loop {
        if remaining_fuel == 0 {
            return Some(current);
        }

        let mut applied = false;

        for rule in rules {
            // Start search from last_pos instead of 0
            if let Some(pos) = find_first_match_from(rule, &current, last_pos) {
                if let Some(new_s) = apply_rule_at(rule, &current, pos) {
                    last_pos = pos;
                    current = new_s;
                    remaining_fuel -= 1;
                    applied = true;
                    break;
                }
            }
        }

        if !applied {
            return Some(current);
        }
    }
}

Safety Analysis

Theoretical Concern

Question: After applying a rule at position last_pos, can a rule match at position p < last_pos that didn't match in the previous iteration?

Answer: YES, for Context::Final rules!

Counterexample:

String: "eye"
Rules:
  1. "e" → "" / Final  (remove final 'e')
  2. "y" → ""          (remove 'y')

WITH position skipping:
- Iteration 1: Rule 1 at pos 2: matches "e" at end → "ey" (last_pos=2)
- Iteration 2: Start from pos 2
  - Rule 1 at pos 2: out of bounds
  - Rule 2 at pos 2: out of bounds
  - Rule 2 at pos 1: "y" matches → "e" (last_pos=1)
- Iteration 3: Start from pos 1
  - All positions >= 1 checked, no matches
- Terminate with result: "e" ❌ WRONG!

WITHOUT position skipping:
- Iteration 1: Same as above → "ey" (last_pos=2)
- Iteration 2: Start from pos 0
  - Rule 1 at pos 1: "y" doesn't match
  - Rule 2 at pos 1: "y" matches → "e" (last_pos=1)
- Iteration 3: Start from pos 0
  - Rule 1 at pos 0: "e" matches AND is final! → "" (last_pos=0)
- Iteration 4: Empty string, no matches
- Terminate with result: "" ✅ CORRECT!

The issue: After applying a rule that shortens the string, an earlier position might become final, but we skip checking it!

Empirical Testing

Test Program: examples/position_skip_test.rs

Test Results:

✅ MATCH: phone
✅ MATCH: phonetics
✅ MATCH: phonograph
✅ MATCH: telephone
✅ MATCH: symphony

✅ All tests passed - optimization preserves correctness!

Full Test Suite: All 147 phonetic tests pass ✅

Interpretation:

  • The optimization produces correct results for all tested cases
  • However, the counterexample scenario might not be covered by existing tests
  • The orthography rules might not trigger the problematic interaction

Context-Specific Analysis

Contexts in Orthography Rules:

  • Context::Anywhere: Safe (no position dependencies)
  • Context::Initial: Safe (only depends on pos == 0, unchanged)
  • Context::BeforeVowel: Probably safe (looks ahead, but ahead positions unchanged if modification is behind)
  • Context::Final: UNSAFE (depends on string length, which can change)

Rules with Context::Final:

  • Rule 33: "e → ∅ / _#" (silent final e in orthography_rules())

Decision

Status: REJECTED FOR CURRENT RELEASE

Reasons:

  1. Safety concerns: Theoretical counterexample with Context::Final
  2. Test coverage: Existing tests may not cover the problematic scenario
  3. Complexity vs benefit: Significant implementation complexity for uncertain gain
  4. Current performance: Already achieved 27% improvement (H1), production-ready
  5. Risk management: Formal verification guarantees could be violated

Recommendation

For v0.8.0: Accept current performance (27% improvement from H1)

Acceptance criteria for any revived treatment:

  1. Add comprehensive tests specifically targeting Context::Final edge cases
  2. Prove safety formally or implement conservative variant:
    • Track minimum safe starting position based on context types
    • For Final context rules: always start from max(0, last_pos - max_pattern_len)
    • For other contexts: start from last_pos
  3. Implement as optional feature flag for A/B testing in production

Alternative Approaches (Lower Risk)

1. Rule-Specific Position Hints (SAFE)

Track per-rule last match position:

let mut rule_hints: Vec<usize> = vec![0; rules.len()];

for (rule_idx, rule) in rules.iter().enumerate() {
    let start_pos = rule_hints[rule_idx];
    if let Some(pos) = find_first_match_from(rule, &current, start_pos) {
        // Apply rule
        rule_hints[rule_idx] = pos;  // Remember for next iteration
        // Reset other rules that might be affected
        for hint in &mut rule_hints {
            *hint = 0;  // Conservative: reset all
        }
    }
}

Safety: Conservative reset ensures correctness

Complexity: Higher memory overhead (per-rule state)

2. Windowed Search (SAFE)

After applying a rule at pos, search in a window around pos:

let window_start = pos.saturating_sub(max_context_len);
let window_end = (pos + replacement.len() + max_context_len).min(current.len());

// First try the window
if let Some(pos) = find_first_match_in_range(rule, &current, window_start, window_end) {
    // Found match in window
} else {
    // Fallback: search entire string (rare)
    if let Some(pos) = find_first_match(rule, &current) {
        // Found match outside window
    }
}

Safety: Always correct (fallback to full search)

Performance: Optimizes common case, no slowdown in worst case

3. Accept O(n^1.5) as Expected Behavior (RECOMMENDED)

Rationale:

  • This is the fundamental complexity of sequential rewrite systems
  • Formal verification proves termination and correctness
  • Performance is production-ready:
    • 5 phones: 823 ns (< 1 µs)
    • 10 phones: 1,880 ns (< 2 µs)
    • 50 phones: 31,346 ns (< 32 µs)
  • 27% improvement already achieved

Documentation: Clearly document O(n^1.5) scaling in performance baseline


Hypotheses Summary (Complete Investigation)

HypothesisTestedOverheadStatusOptimization
H1 (Allocations in find_first_match)27%Fixed✅ v0.8.0
H2 (Algorithmic complexity)O(n^1.5)IdentifiedRejected for current release
H3 (Cache misses)<2%Optimal❌ None needed
H4 (Slice copying)2-3%Efficient❌ None needed
H5 (Iteration count)O(√n)ProvenN/A (fundamental)

Total Investigation Time: ~6 hours Optimization Achieved: 27-30% speedup (H1) Remaining Opportunity: ~3-5% (H3+H4 combined) Fundamental Limitation: O(n^1.5) algorithmic complexity (expected behavior)


Final Recommendation for v0.8.0

Accept Current Performance ✅

Justification:

  1. Significant improvement: 27-30% speedup from H1 optimization
  2. Excellent absolute performance: Sub-microsecond for typical inputs
  3. Cache optimal: 1.14-1.20% L1 miss rate, 3.65-4.94% LLC miss rate
  4. Memory efficient: Slice copying only 2-3% of total time
  5. Formally verified: All 87 tests passing, correctness maintained
  6. Production ready: Performance suitable for real-world use

Performance Summary (Post-H1 Optimization):

  • 5 phones: 823 ns (< 1 µs) ✅
  • 10 phones: 1,880 ns (< 2 µs) ✅
  • 20 phones: 6,247 ns (< 7 µs) ✅
  • 50 phones: 31,346 ns (< 32 µs) ✅

O(n^1.5) Scaling: This is expected behavior for sequential rewrite systems, NOT a bug!

Further optimization (targeting O(n^1.5)) rejected for the current release unless these conditions change:

  • Real-world usage data showing bottlenecks
  • Formal safety proofs for position-based optimizations
  • Comprehensive test coverage for Context::Final edge cases

Investigation Status: ✅ COMPLETE Optimization Implemented: ✅ H1 (27% speedup) Production Readiness: ✅ READY for v0.8.0 Future Work: 📋 Documented for v0.9.0+

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