Liking cljdoc? Tell your friends :D

Subsumption Optimization - Pre-Sorting for Early Termination

Question

Won't pre-sorting the positions reduce the number of comparisons that must be made during unsubsumption?

Answer

YES! Pre-sorting positions by (errors, offset) enables significant optimizations through early termination.

Implementation

Data Structure Change

Before: HashSet<UniversalPosition> (unordered) After: BTreeSet<UniversalPosition> (sorted by custom Ord)

Custom Ordering

Positions are sorted by (errors, offset):

impl<V: PositionVariant> Ord for UniversalPosition<V> {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        // Sort by (errors, offset) to enable early termination
        match (self, other) {
            (INonFinal { errors: e1, offset: o1, .. }, INonFinal { errors: e2, offset: o2, .. }) => {
                match e1.cmp(e2) {
                    Ordering::Equal => o1.cmp(o2),  // Secondary sort by offset
                    other => other,                  // Primary sort by errors
                }
            }
            // ... similar for MFinal
        }
    }
}

Subsumption Rule Reminder

Position i#e subsumes j#f if:

  1. f > e (subsumed position has MORE errors)
  2. $|j - i| \le f - e$ (offset distance is within error difference)

Optimization Benefits

Step 1: Removing Subsumed Positions

When adding position pos with errors=e, only positions with errors > e can be subsumed.

Before (HashSet):

self.positions.retain(|p| !subsumes(&pos, p, self.max_distance));
  • Complexity: $\mathcal{O}(n)$ - checks ALL n positions

After (BTreeSet with early termination):

self.positions.retain(|p| {
    if p.errors() <= pos_errors {
        true  // Keep - cannot be subsumed (early termination)
    } else {
        !subsumes(&pos, p, self.max_distance)  // Check full condition
    }
});
  • Complexity: $\mathcal{O}(k)$ where k = positions with errors > e
  • Early termination: Skips all positions with errors $\le e$

Step 2: Checking if New Position is Subsumed

When checking if pos is subsumed, only positions with errors < e can subsume it.

Before (HashSet):

if !self.positions.iter().any(|p| subsumes(p, &pos, self.max_distance)) {
    self.positions.insert(pos);
}
  • Complexity: $\mathcal{O}(n)$ - checks ALL n positions

After (BTreeSet with early termination):

let is_subsumed = self.positions.iter()
    .take_while(|p| p.errors() < pos_errors)  // Early termination!
    .any(|p| subsumes(p, &pos, self.max_distance));
  • Complexity: $\mathcal{O}(k)$ where k = positions with errors < e
  • Early termination: Stops at first position with errors $\ge e$

Performance Analysis

Typical State Sizes

From the thesis, states have $\mathcal{O}(n^{2})$ positions maximum, where n is max_distance.

  • n=1: $\le 1$ position
  • n=2: $\le 4$ positions
  • n=3: $\le 9$ positions

Error Distribution

Positions tend to have sparse error distributions:

  • Most transitions maintain or increase errors by 1
  • Matches keep errors constant
  • Error positions get subsumed by better (lower error) positions

Example state after processing "test" → "text":

  • Unsorted: {I+0#1, I-1#2, I-2#2} (random order in HashSet)
  • Sorted: [I+0#1, I-1#2, I-2#2] (ascending by errors in BTreeSet)

Complexity Comparison

OperationHashSet (unordered)BTreeSet (sorted)
Check Step 1$\mathcal{O}(n)$$\mathcal{O}(k_{1})$ where k₁ = positions with errors > e
Check Step 2$\mathcal{O}(n)$$\mathcal{O}(k_{2})$ where k₂ = positions with errors < e
Insert$\mathcal{O}(1)$ expected$\mathcal{O}(\log n)$
Total per add_position$\mathcal{O}(n)$$\mathcal{O}(k_{1} + k_{2} + \log n)$

Key insight: k₁ + k₂ << n in practice due to sparse error distribution!

Expected Speedup

For typical states with mixed error levels:

  • Before: Check all n positions twice → 2n comparisons
  • After: Check ~n/2 positions per step → ~n comparisons + $\mathcal{O}(\log n)$ insert
  • Speedup: ~2× fewer subsumption checks

For best case (inserting position with minimum errors):

  • Before: 2n comparisons
  • After: 0 comparisons (early termination) + $\mathcal{O}(\log n)$ insert
  • Speedup: Eliminates all subsumption checks!

Trade-offs

BTreeSet vs HashSet

Pros:

  • ✅ Enables early termination in subsumption checks
  • ✅ Reduces average number of comparisons by ~50%
  • ✅ Best case eliminates all comparisons
  • ✅ Maintains deterministic iteration order

Cons:

  • ⚠️ Insert is $\mathcal{O}(\log n)$ instead of $\mathcal{O}(1)$
  • ⚠️ Slightly higher memory overhead per node

Verdict: Benefits outweigh costs for typical small state sizes $(n \le 9$ positions).

Validation

Test Results

All tests pass with optimization:

  • ✅ Acceptance tests: 10/10 passing
  • ✅ Cross-validation tests: 3/3 passing
  • ✅ Subsumption tests: All passing

Correctness

The optimization is semantically transparent:

  • Does not change which positions are kept/discarded
  • Only reduces the number of comparisons needed
  • Maintains the anti-chain property

Conclusion

Yes, pre-sorting positions by (errors, offset) significantly reduces subsumption comparisons through early termination:

  1. Positions sorted by errors enable skipping positions that cannot participate in subsumption
  2. Early termination reduces average comparisons from $\mathcal{O}(n)$ to $\mathcal{O}(k)$ where k << n
  3. BTreeSet provides this ordering with minimal overhead ($\mathcal{O}(\log n)$ insert)
  4. All tests pass, confirming correctness

The optimization is a clear win for typical workloads with small, sparse error distributions.

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