perf: prefetch the next candidates' text in the merge loop (independent) - #12
Conversation
Phase 4 had become the largest ext-mem cost, and its merge CPU (19.2 s
on 80 MB of DNA) exceeded the entire CPU of the in-memory path. The
merge is latency-bound: the tied branch dereferences the text at two
random addresses, and the address for step i+1 is not known until step i
retires, so there is no memory-level parallelism and the hardware
prefetcher cannot see the pattern.
The candidate positions themselves live in the two index arrays, which
are sequential and already in cache, so the addresses several steps
ahead are known even though the dependent loads are not. Issue them as
prefetches, offset by the current boundary LCP `m`, which estimates
where the next scans start.
This is not the prefetch recorded as a negative result in `lcp.rs`. That
one sat inside the strided scan loop, which the hardware prefetcher
already covers. This one targets the random access, which it cannot.
Apple M4 Max, 12 threads, ext-mem, output verified identical to the
in-memory suffix array on both inputs:
chr21.0123, 80 MB phase4 merge CPU 19.20 s -> 12.42 s
phase4 wall 2.17 s -> 1.46 s
total 3.55 s -> 2.90 s CPU 33.8 -> 27.0 s
chr21.fa, 47.5 MB phase4 merge CPU 7.99 s -> 6.76 s
total 2.56 s -> 1.99 s CPU 23.3 -> 18.0 s
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Accepted and merged into I tested PR head Full production-shaped benchmarkSame fixture and protocol used for the other open-PR decisions:
One candidate warm-up was followed by an immediately adjacent baseline/candidate pair:
The candidate warm-up was almost identical: 292.905 s total, 106.547 s phase 1, 181.146 s phase 4, and 4,683.203 merge CPU-seconds. The two candidate totals differ by only 0.647 s. Nearby baseline totals on the same machine were 373.521, 373.800, and 374.029 s. The improvement is therefore decisively above noise. The measured peak-RSS difference is not accompanied by any allocation change in this PR and was not reproduced by the warm-up (candidate warm-up RSS was 9,869,584 KiB, below the paired baseline). I treat that counter as run-to-run residency variation rather than a memory cost of the prefetch instructions. Temporary-I/O volume is unchanged in practical terms. Every full run emitted 6,176,694,310 positions with stream hash Why it helps hereUnlike run skipping, this optimization does not require suffixes to begin inside a special periodic region. The LCP-enhanced merge eventually needs text at suffix positions that are effectively random across the 6.56-billion-symbol backing text. The upcoming positions are already available sequentially in the two index streams, so issuing cache hints eight candidates ahead creates memory-level parallelism that the hardware prefetcher cannot infer from the eventual random text accesses. The data localize the effect exactly where expected: phase 1 pays about one additional second, while phase 4 saves 81.5 seconds and its aggregate merge CPU falls 34.2%. This AMD EPYC x86_64 result also independently confirms that the benefit is not specific to the PR author's Apple M4/Arm64 measurements. Correctness and validation
This is a clear accept on the ruSTAR workload. Thanks for extracting it into an independent change; that made the attribution straightforward. |
Independent of every other open PR. Branches from
main, one commit, ~30 lines.Extracted from #5 so it is separately bisectable, as @rob-p asked in #7.
Why
The merge is latency-bound. Its tied branch dereferences the text at two random addresses, and the address for step
i + 1is not known until stepiretires, so there is no memory-level parallelism to exploit and the hardware prefetcher cannot see the pattern either.But the candidate positions themselves live in the two index arrays, which are sequential and already in cache. The addresses several steps ahead are known, even though the dependent loads are not. Issuing them as prefetches, offset by the current boundary LCP
m— which estimates where the next scans start — costs two instructions per step.This is not the prefetch recorded as a negative result in
lcp.rs. That one sat inside the strided scan loop, which the hardware prefetcher already covers. This one targets the random access, which it cannot.Measured
chr21 forward ++ revcomp (80 MB), external memory, 12 threads, interleaved against
main, three pairs, prefetch winning every pair:Output byte-identical to
main.Portability
_mm_prefetchon x86_64. On aarch64core::arch::aarch64::_prefetchis still unstable, so the instruction is emitted directly withasm!;prfmnever faults. A no-op elsewhere. The address is clamped and only ever used as a prefetch operand, never dereferenced.🤖 Generated with Claude Code