Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .claude/PERFORMANCE_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -552,3 +552,45 @@ plain gap-affine: 56 → 45 ms.
Output identical to `main` on the interleaved set for every metric,
high/med/ultralow, score-only, corrected ends-free (affine and linear,
banded) and X-drop. Unit test `test_reap_cigar_busy_after_large_slab`.

## 2026-10-18: per-step overhead on narrow banded ends-free alignments (HPCBio/wfa2lib-rs#31, HPCBio/dada2-rs#51)

dada2-rs found begin-free gaps make each ends-free alignment ~3× slower
(#31 comment, 2026-10-18). Investigation on synthetic dada2-like pairs
(20 bases × 100 mutants, ~20% of differences indels; linear `-5,4,8` /
affine `-5,4,0,8`, corrected, banded-static ±band, free ends = band):

- Seeded begin-free diagonals do stay in the wavefront: 240 bp at 2%,
35 → 604 cells and 10 → 43 non-null steps per alignment; 1450 bp,
2,039 → 18,518 cells, mean width 50 of 65.
- Pruning them early does not help: an initial upper bound for corrected
mode's exact pruning (the better of two gapless alignments, O(n)) left
output identical but was 2–18% slower. With a match reward a cell's lower
bound grows only `|match|` per diagonal of distance from the end
diagonal, against 18 per difference, so the seeded diagonals can't be
excluded until the search is nearly done. Not merged.
- The time follows the step count, ~170 ns per extra non-null step (about
13 ns per extra cell, against < 1 ns of kernel work). Profile: corrected
extend ~40%, `compute_linear_step` ~33%, NULL fills (`memset_pattern16`)
~8%, slab allocation ~5%, banded cutoff ~6%. Corrected mode itself is only
7–10% over C mode here.

Two changes:
- `init_ends` fills of up to 8 offsets use (volatile) stores instead of
`fill`, which lowered to a `memset_pattern16` call per input per step.
- `apply_heuristic_cutoff` returns at once for banded-static inside the
band (compute already clamps to it).

M2, median of 5, ms, `main` (`bef359a`) → this change, output identical:

| data | linear, begin off | linear, begin on | affine, begin off | affine, begin on |
|---|---|---|---|---|
| 240 bp, 2% | 5.6 → 4.8 | 16.4 → 14.5 | 8.0 → 7.0 | 25.1 → 22.6 |
| 240 bp, 10% | 16.6 → 14.1 | 47.6 → 41.1 | 23.6 → 20.2 | 72.2 → 60.7 |
| 1450 bp, 2% | 58.8 → 52.9 | 190.3 → 172.8 | 79.6 → 70.3 | 257.5 → 226.8 |
| 1450 bp, 10% | 164.4 → 149.5 | 479.1 → 437.5 | 220.0 → 193.9 | 659.5 → 586.0 |

9–16% faster; utest/long gap-affine controls 0.99 / 0.96. The begin-free
3× remains: it is the step count of an exact WFA with seeded diagonals.
The rest of the per-step cost is spread thin (a call, a division, ~2–3%
each).
3 changes: 3 additions & 0 deletions .claude/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,6 @@ reports, inbound C ports) is tracked in HPCBio/wfa2lib-rs#19.
mixed long/short workloads in high memory 0.30× the time (C's benchmark:
1231 → 365 ms, C 421 ms); a dada2-like interleaved workload 735 → 209 ms.
Output identical.
- **2026-10-18:** begin-free ends-free cost (HPCBio/dada2-rs#51) investigated:
inherent step count, not prunable exactly; two per-step overhead cuts
(short NULL fills, banded cutoff fast path) give 9–16% on dada2-like pairs.
10 changes: 10 additions & 0 deletions src/aligner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1337,6 +1337,16 @@ impl<const N: usize> WavefrontAligner<N> {
/// Apply heuristic cutoff to the M-wavefront (and I/D wavefronts if affine).
/// Returns true if alignment should be terminated (z-drop).
fn apply_heuristic_cutoff(&mut self, score: i32, m_ptr: *mut crate::wavefront::Wavefront) -> bool {
// Banded-static inside its band (compute already clamps to it; only
// begin-free seeding can reach past it): the cutoff would change
// nothing, and it doesn't use `steps_wait`. Skip the general path,
// which runs every step (HPCBio/wfa2lib-rs#31).
if let HeuristicStrategy::BandedStatic { min_k, max_k } = self.heuristic {
let wf = unsafe { &*m_ptr };
if wf.lo >= min_k && wf.hi <= max_k {
return false;
}
}
let plen = self.sequences.pattern_length;
let tlen = self.sequences.text_length;

Expand Down
34 changes: 20 additions & 14 deletions src/wavefront.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,24 @@ impl Drop for Wavefront {
}
}

/// Fill `count` offsets at `ptr` with `OFFSET_NULL`. The per-step fills
/// widen an input's initialized range by a diagonal or two, and `fill`
/// lowers to a `memset_pattern16` call whose setup costs far more than the
/// stores (HPCBio/wfa2lib-rs#31). Short ones use plain stores, kept as
/// stores (volatile) so LLVM doesn't turn the loop back into that call.
#[inline(always)]
unsafe fn fill_null(ptr: *mut WfOffset, count: usize) {
unsafe {
if count <= 8 {
for i in 0..count {
std::ptr::write_volatile(ptr.add(i), OFFSET_NULL);
}
} else {
std::slice::from_raw_parts_mut(ptr, count).fill(OFFSET_NULL);
}
}
}

impl Wavefront {
/// Allocate a new wavefront with the given number of elements.
///
Expand Down Expand Up @@ -412,13 +430,7 @@ impl Wavefront {
// C's `for (k=min_lo;k<min_init;++k)` is a no-op in that case.
if start < min_init {
let count = (min_init - start) as usize;
unsafe {
let slice = std::slice::from_raw_parts_mut(
self.centered_offsets.offset(start as isize),
count,
);
slice.fill(OFFSET_NULL);
}
unsafe { fill_null(self.centered_offsets.offset(start as isize), count) };
self.zero_bt(start, count);
}
self.wf_elements_init_min = start;
Expand All @@ -438,13 +450,7 @@ impl Wavefront {
let start = max_init + 1;
if end > max_init {
let count = (end - max_init) as usize;
unsafe {
let slice = std::slice::from_raw_parts_mut(
self.centered_offsets.offset(start as isize),
count,
);
slice.fill(OFFSET_NULL);
}
unsafe { fill_null(self.centered_offsets.offset(start as isize), count) };
self.zero_bt(start, count);
}
self.wf_elements_init_max = end;
Expand Down
Loading