Conversation
trimBySequence's insertion and deletion loops step pos through the read but passed rdata (not rdata + pos) to Matcher::matchWithOneInsertion, so every iteration compared the start of the read with the adapter. An adapter with a one-base indel was only ever found at position 0. Pass rdata + pos, as the exact-match loop above already does. Adds AdapterTrimmer::test cases with a TruSeq adapter at position 40 carrying a one-base insertion and a one-base deletion; both fail without this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The one-gap adapter search calls matchWithOneInsertion at every read position for every read without an exact adapter match (most reads), and it was the largest single function in worker profiles. It built full prefix and suffix mismatch arrays and then scanned them, including an O(cmplen) fill of sentinels on every call. The new version scans the prefix forward and the suffix backward and stops each scan as soon as it can no longer be part of a match within diffLimit. It then checks only the splits both scans reached. On unrelated sequence both scans stop after a few bases. Results are identical to the previous implementation, which is kept as matchWithOneInsertionReference (used for cmplen > 512 and by the new Matcher::test, which compares the two on 200k random, near-match and exact-match cases). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The one-gap loops compared the read start at every position, so once the compared length got short a read beginning with adapter-like sequence matched and had its tail cut (upstream cuts this 81 bp test read to 72 bp). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hardware-counter data on where the fix's extra CPU goes, in case it helps review. Setup. GCE
Reading. The PR executes fewer instructions than the old search but takes about 10% more cycles. My interpretation, not something I tested: the old loop compared the same bytes at every position, so its branches were almost always predicted; the corrected search has data-dependent exits that mispredict. That fits the +13–15% wall/CPU in the table above. It also means instruction counts are a poor proxy here: cycles or CPU time are what to compare. If someone wants to bring the remaining cost down, the mispredictions are the target rather than instruction count. In the PR build, Raw counter files and the profiling scripts are in dougnukem#9 ( 🤖 Generated with Claude Code |
Fixes #728.
trimBySequence's one-gap search compared the adapter with the start of the read at every position, instead of at the position being tested. So it missed adapters with a one-base indel, and it trimmed the tail of reads that only start like the adapter (~1.8% of reads on a public WGBS run). Details and a 5-read reproduction are in #728.Changes
rdata + posto bothMatcher::matchWithOneInsertioncalls inAdapterTrimmer::trimBySequence, the same offset the exact-match loop already uses.Matcher::matchWithOneInsertion.perfprofile of SRR10007843).diffLimit, then checks only the splits both scans reached. On unrelated sequence both stop within a few bases.matchWithOneInsertionReference. It's used forcmplen > 512and by the newMatcher::test.AdapterTrimmer::testgains a TruSeq adapter at position 40 with a one-base insertion and a one-base deletion (both fail on master), and a read that starts like the adapter (master cuts it from 81 to 72 bp).Matcher::testcompares the new and referencematchWithOneInsertionon 200k random, near-match and exact cases.Verification
fastp testpasses. Each new test fails without the change it covers.Matcher::testcatches deliberate off-by-one mutations of the new function (4 of 4 tried).Performance
Complete public runs,
fastp -w 16(PE with--detect_adapter_for_pe), GCE n2d-highmem-48, inputs in page cache. Mean of 2 runs in alternating build order.A separate benchmark on GitHub-hosted 4-CPU runners, which projects 300K–500K-pair subsets to a 50M-read run (synthetic PE/SE 150 bp and SRR891268 ATAC PE 50,
-w 1and-w 4), gave:Peak RSS is unchanged.
So the fix makes trimming correct at roughly the old cost. It isn't a speedup, since the search now does the work it was meant to do.
matchWithOneInsertionis still the largest worker function after this change. A bit-parallel or SIMD version of the one-gap search could take it further, but would change results in edge cases, so it's left out of this PR.Output changes users will see
🤖 Generated with Claude Code