perf(score): slide the splice-motif window across the junction scan (output-neutral) - #167
Open
BenjaminDEMAILLE wants to merge 3 commits into
Open
perf(score): slide the splice-motif window across the junction scan (output-neutral)#167BenjaminDEMAILLE wants to merge 3 commits into
BenjaminDEMAILLE wants to merge 3 commits into
Conversation
`detect_splice_motif` and `find_best_junction_position` were the two largest self-time entries in a sampled 2M-read run, together about 45% of the on-CPU samples. The scan moves the junction one base per iteration, so the four bases that decide the motif (the intron's first two and last two) overlap the previous position in two of four places. Carrying them in a `MotifWindow` and sliding turns four genome reads per position into two. Whether the motif branch runs at all is decided once before the loop rather than per iteration, since `del` does not change across the scan. An out-of-range position becomes a sentinel that no motif arm matches, which is what `get_base` returning `None` already meant. Output-neutral: SAM byte-identical to the parent commit on 200k real reads. Interleaved A/B at 16 threads on 2M reads, machine at 87-95% CPU idle: 22.94s median before, 22.29s after, the same direction in all four rounds. About 2.8%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four-base motif decision compiled to a chain of comparisons whose
outcome is data-dependent and close to unpredictable, and the junction
scan pays it at every position. A 256-entry table indexed by the four
bases packed two bits each replaces it with one load.
`examples/jrbench.rs` measures the scan in isolation: 8.5 ns per
iteration before, 5.2 ns after, a 38% cut, with identical results.
That example exists because the earlier attempts on this path were guesses
about whether the loop was memory-bound, and two of them were wrong. It
answers the question by construction, holding the iteration count and the
instruction mix fixed and changing only how far the acceptor sits from the
donor:
del ns/iter
64 8.42
1024 8.47
16384 8.52
262144 8.41
4194304 8.62
67108864 8.56
Flat from 64 bases to 67 million, so the acceptor distance costs nothing
and the loop is not stalled on that stream. Which is why skipping the
acceptor read behind a donor-pair test measured *slower*: it traded a
prefetchable access for an unpredictable branch. The branch was the cost
all along, and this removes it.
`the_motif_table_agrees_with_the_original_match_on_every_input` checks
the table against the match it replaces over every combination of
`A`/`C`/`G`/`T`, `N`, the chromosome-boundary byte and the out-of-range
sentinel, so the inputs that no match arm covered are covered explicitly.
Output-neutral: SAM byte-identical on 200k real reads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BenjaminDEMAILLE
force-pushed
the
bd/perf-align
branch
from
July 30, 2026 18:08
87f6779 to
56c1829
Compare
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Slide the four-base splice-motif window across the junction scan instead of re-reading it at every position. Output-neutral, about 2.8% off the wall clock.
Where the time actually goes
Sampled a 2M-read run at 16 threads with symbols, ranked by self time:
detect_splice_motiffind_best_junction_positioncluster_seedsextend_alignmentstitch_recursecompare_seq_to_genomeThe top two are together roughly 45% of the on-CPU samples. Worth noting for anyone planning work here: the seed search is not the bottleneck on this workload.
search_direction_sparseis 629 samples.What changed
find_best_junction_positionslides the junction one base per iteration and callsdetect_splice_motifat each position. That function reads four genome bases: the intron's first two and its last two.Because the junction moves by exactly one base, consecutive positions share two of those four. Moving right, the old
d2anda2are the newd1anda1; moving left, the oldd1anda1are the newd2anda2. Carrying them in aMotifWindowand sliding turns four genome reads per position into two.Two smaller things fell out of the same reading:
del, which does not change across the scan, so it is decided once before the loop instead of at every iteration.get_basereturningNonealready meant. That removes fourOptionunwraps from the inner comparison without changing which positions produceNonCanonical.detect_splice_motifkeeps its signature and behaviour; it is now a one-line call into the same window type, sogenomeGenerate's junction insertion and alignment scoring still share one truth table.Verification
Output-neutral. SAM byte-identical to the parent commit on 200 000 real reads (
ERR12389696, yeast), whole file including header.Timing. Interleaved A/B, order flipped on even rounds, 2M reads,
--runThreadN 16,--outSAMtype Noneso the measurement is alignment work and not the writer. Machine at 87-95% CPU idle, and the script refuses to run below 88%.Same direction in all four rounds, about 2.8%. I will not oversell it: the spread within the "before" column is 0.7 s, so a single pair would not have been worth reporting. Four out of four in the same direction is what makes it readable, and the mechanism is arithmetic rather than a scheduling effect.
Modest against 45% of the profile, because most of that self time is the branchy scoring logic around the fetches rather than the fetches alone. The remaining hotspots in the table above are the next targets, and they are separate changes.
Gate: 560 lib + 26 integration tests,
cargo clippy --all-targets -- -D warnings,cargo fmt --check, all green. No new dependency.Related
The same profiling run produced two findings that live elsewhere: #168 records that the align path does about 1 000 heap allocations per read, with the measurement and the conclusion that removing 7.6% of them changes nothing measurable, and #166 fixes
--runThreadN 1running on every core, which is what made the thread-scaling curve behind this work readable in the first place.