Skip to content

perf(align): memoize the junction-position scan inside stitchWindowAligns - #256

Open
BenjaminDEMAILLE wants to merge 1 commit into
scverse:mainfrom
BenjaminDEMAILLE:perf/junction-memo
Open

perf(align): memoize the junction-position scan inside stitchWindowAligns#256
BenjaminDEMAILLE wants to merge 1 commit into
scverse:mainfrom
BenjaminDEMAILLE:perf/junction-memo

Conversation

@BenjaminDEMAILLE

Copy link
Copy Markdown
Contributor

What

find_best_junction_position is the single hottest function in the aligner. On a 50k-pair yeast PE run (sample-based profile, single thread) it is ~60% of alignment time, inside stitch_seeds_core at ~76%.

The scan itself is not wasteful, but it is repeated. stitchWindowAligns' include/exclude recursion reaches the same (exon A end, seed B) pair through many different branch paths, and every path re-runs the identical scan.

The scan is a pure function of its arguments. Within one window read_seq, the genome, is_reverse and n_genome are fixed, so six coordinates identify a scan completely: exon A read end, exon A genome end, read gap, genome gap, previous exon length, next seed length.

This PR adds JunctionScanCache (a per-window FxHashMap on that key) and a find_best_junction_position_cached wrapper. The uncached function is untouched, so a cache hit returns exactly what a fresh scan would have returned. The cache is created per window in stitch_seeds_core and threaded down the recursion, which is what keeps the key complete: it never outlives the read, genome and strand it was filled for. An empty HashMap does not allocate, so windows that stitch nothing pay nothing.

Effect on the profile

find_best_junction_position inclusive samples drop from 6,855 to 2,941, a 57% cut in the function's total cost.

Benchmark

50k yeast read pairs, Apple M4 Max, quiet machine, best-of-6 interleaved rounds, --outSAMtype None:

threads before after change
1 19.51s 16.76s -14.1%
8 2.55s 2.18s -14.5%

Correctness

Output is byte-identical. Aligned.out.sam (records and header, modulo the @PG CL line naming the binary) and SJ.out.tab compare equal against the pre-change binary on the same input.

  • 592 tests pass
  • 0 clippy warnings (--all-targets --release)
  • cargo fmt --check clean

🤖 Generated with Claude Code

…igns

`find_best_junction_position` is the single hottest function in the
aligner: on a 50k-pair yeast PE run it accounts for ~60% of alignment
time. The scan itself is not wasteful, but it is repeated.

`stitchWindowAligns`' include/exclude recursion reaches the same
(exon A end, seed B) pair through many different branch paths, and each
path re-runs the identical scan. The scan is a pure function of its
arguments, and within one window `read_seq`, the genome, `is_reverse`
and `n_genome` are all fixed, so six coordinates identify a scan
completely: the exon A read end and genome end, the read and genome
gaps, the previous exon length and the next seed length.

Add `JunctionScanCache`, a per-window `FxHashMap` on that key, and a
`find_best_junction_position_cached` wrapper that consults it. The
uncached function is untouched, so a hit returns exactly what a fresh
scan would have. The cache is created per window in `stitch_seeds_core`
and threaded down the recursion, which is what keeps the key complete:
it never outlives the read, genome and strand it was filled for. An
empty `HashMap` does not allocate, so windows that stitch nothing pay
nothing.

Measured on 50k yeast read pairs (Apple M4 Max, quiet machine,
best-of-6 interleaved rounds, `--outSAMtype None`):

    threads   before   after    change
    1         19.51s   16.76s   -14.1%
    8          2.55s    2.18s   -14.5%

Output is byte-identical: `Aligned.out.sam` (records and header alike,
modulo the `@PG` CL line naming the binary) and `SJ.out.tab` compare
equal against the pre-change binary on the same input. 592 tests pass,
0 clippy warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant