Files
even-perfect-numbers-scanner/docs/bydivisor_cpu_path.md
T

120 lines
6.0 KiB
Markdown

# CPU `--mersenne=bydivisor` pipeline notes
This document summarizes how the `EvenPerfectBitScanner` executable executes the
`--mersenne=bydivisor` mode on the CPU, and how the analytic observations we
derived for the quotients
\[
Q_k = \frac{N}{d_k}, \qquad d_k = 2pk + 1, \qquad N = 2^p - 1,
\]
can help us identify concrete optimization opportunities.
## High-level control flow
* `Program.Main` wires `_byDivisorTester` to a CPU implementation whenever the
CLI switches resolve to `--mersenne=bydivisor` and
`--mersenne-device=cpu`. The CLI also forces a block size of 1 so that the
pipeline pushes exponents to the tester one at a time.
(`Program.cs`, lines 459-520.)
* When the mode starts, `RunByDivisorMode` delegates to
`MersenneNumberDivisorByDivisorTester.Run`, passing the candidate list from
`--filter-p`, the CPU tester instance and the optional replay of previous
outcomes. (`Program.cs`, lines 809-826.)
* `MersenneNumberDivisorByDivisorTester.Run` sorts and deduplicates the
candidate exponents, drops the ones already confirmed in prior runs, validates
primality with the `Open.Numeric.Primes` enumerator and configures the tester
with the largest remaining exponent. (`MersenneNumberDivisorByDivisorTester.cs`,
lines 14-162.)
* For each confirmed prime exponent, the CPU tester computes the largest divisor
it needs to probe through `ComputeAllowedMaxDivisor`. If the range is non-empty
it enters the `CheckDivisors` loop. (`MersenneNumberDivisorByDivisorCpuTester.cs`,
lines 83-110 and 161-266.)
## Inner loop behaviour (`CheckDivisors`)
The hot path is the `while (divisor <= limit)` loop in
`MersenneNumberDivisorByDivisorCpuTester.CheckDivisors`.
1. The loop enumerates divisor candidates of the form `d_k = 2pk + 1` by
incrementing `divisor` by `step = 2p` every iteration. (`MersenneNumberDivisorByDivisorCpuTester.cs`,
lines 177-256.)
2. Small modulus filters track `d_k (mod 3·5·7·8·10·11)` to discard impossible
candidates without touching the divisor-cycle cache. (`MersenneNumberDivisorByDivisorCpuTester.cs`,
lines 195-263.)
3. For every admissible divisor the tester fetches its Montgomery data and the
cached cycle length. When the cache does not contain an entry the
implementation recomputes the cycle by factoring the divisor. (`MersenneNumberDivisorByDivisorCpuTester.cs`,
lines 223-254.)
4. A cycle equal to the tested exponent proves divisibility, so the loop aborts
as soon as one hit is found. (`MersenneNumberDivisorByDivisorCpuTester.cs`,
lines 241-247.)
`DivisorScanSession.CheckDivisor` follows the same `d_k` progression when a
batch API is used. It keeps a running Montgomery residue via
`ExponentRemainderStepper`, but the stepper currently recomputes the delta by
calling a full `Pow2MontgomeryModWindowed` whenever the exponent jumps.
(`MersenneNumberDivisorByDivisorCpuTester.cs`, lines 358-405 and
`ExponentRemainderStepper.cs`, lines 26-95.)
## Where the analytic model fits
The formulas we established for the quotient sequence
\[
\frac{Q_{k+1}}{Q_k} = \frac{2pk + 1}{2p(k+1) + 1}, \qquad
Q_k - Q_{k+1} = \frac{N·2p}{(2pk + 1)(2p(k+1) + 1)}
\]
tell us that the successive quotients and remainders shrink smoothly and that the
per-step update depends only on the previous `(Q_k, r_k)` state and the next
`d_{k+1}`. This lines up with several TODO comments that highlight missing
incremental updates:
* `ExponentRemainderStepper` already caches the previous exponent and a running
Montgomery residue, but it falls back to a full modular exponentiation to
compute the delta between consecutive divisors. (`ExponentRemainderStepper.cs`,
lines 49-72.)
* `CheckDivisors` rebuilds the Montgomery data for every hit candidate even
though the sequence of `d_k` values is strictly monotone.
The quotient identity gives us a way to compute `Δ_k = Q_k - Q_{k+1}` by reading
`Q_k` and the inexpensive division `(2pQ_k - r_k) / d_{k+1}`. Once we know `Δ_k`
we can update
\[
Q_{k+1} = Q_k - Δ_k, \qquad r_{k+1} = r_k - 2p·Q_k + Δ_k · d_{k+1},
\]
without ever evaluating `2^p mod d_{k+1}` from scratch. Incorporating this into
`ExponentRemainderStepper` would let the CPU path reuse a single Montgomery state
across the entire scan and swap the current delta exponentiation (which costs a
full modular ladder) for one multiply-high with a precomputed reciprocal of
`d_{k+1}`. The loop already maintains `d_k` and `step`, so the extra bookkeeping
fits naturally.
## Optimization ideas grounded in the code
1. **Delta-driven Montgomery updates.** Extend `DivisorScanSession` so it tracks
`(Q_k, r_k)` alongside the Montgomery residue and uses the analytic delta
formula to derive the next residue. This replaces the per-step powmod in
`ExponentRemainderStepper.ComputeNext` with a handful of 64-bit operations.
(`MersenneNumberDivisorByDivisorCpuTester.cs`, lines 358-405;
`ExponentRemainderStepper.cs`, lines 49-72.)
2. **Bit-range skipping.** Because `Q_{k+1} / Q_k` is exactly the rational
`(2pk + 1)/(2p(k+1) + 1)`, we can predict when `⌊log₂ Q_k⌋` loses a bit and
jump the loop counter to the first `k` that crosses a target quotient bound
instead of checking each divisor. This is compatible with the existing
`allowedMax` guard because it only modifies how we arrive at the stopping
divisor. (`MersenneNumberDivisorByDivisorCpuTester.cs`, lines 161-266.)
3. **Reuse Montgomery metadata.** The loop already calls `MontgomeryDivisorDataCache.Get`
for each divisor, but the cache lookup is repeated even though the next
divisor is a small increment away. Persisting the previous `MontgomeryDivisorData`
together with the `(Q_k, r_k)` tuple lets us skip redundant cache reads when
the divisor stays within the hot range. (`MersenneNumberDivisorByDivisorCpuTester.cs`,
lines 223-256.)
These adjustments directly leverage the quotient progression we analysed and
align with the existing TODO list, giving us a clear path to reduce the number of
modular exponentiations and cache lookups in the CPU by-divisor path.