License
You're allowed to use the source code for education purposes, only. Any other use requires the payment of the licensing fee, which can be done using the links below.
-
Personal License - $10 https://buymeacoffee.com/mlemanczyk/e/455507
-
Earning / Commercial License - $100 https://buymeacoffee.com/mlemanczyk/e/455508
Description
This document explains what the program does, how it works step-by-step, and why each optimization exists. It reflects the current code paths in Program.cs and the helper library (PerfectNumbers.Core), describing only what the code actually does (comments ignored).
Supported modes
--mersenne=pow2mod--mersenne=incremental--mersenne=lucas--mersenne=residue
EvenPerfectBitScanner – Configuration and Pipeline
Please run EvenPerfectBitScanner.exe /? to display all command line parameters with a short description. The description below describes --mersenne=residue mode.
What the exact CLI config does
./EvenPerfectBitScanner.exe
--increment=add
--mersenne=residue
--threads=1024
--primes-device=gpu
--mersenne-device=gpu
--block-size=10000000
--gpu-prime-threads=1024
--write-batch-size=10000
--gpu-prime-batch=512000
-
Candidate stepping:
--increment=add→ the producer usesTransformPAdd(see myProgram.cs), which locks intop ≡ 1 or 5 (mod 6). -
Prime tests (p):
--primes-device=gpu→PrimeTesterruns a GPU-batched small-prime sieve (and light CPU checks) (PrimeTester.cs). -
Mersenne stage:
--mersenne=residue+--mersenne-device=gpu→MersenneNumberTesterselects the residue divisor path and routes its scanning kernels to GPU (MersenneNumberTester.cs+Gpu/MersenneNumberResidueGpuTester.cs). We do not run Lucas–Lehmer in this config. -
GPU tuning:
--gpu-prime-threads=1024gates concurrent GPU prime work viaGpuPrimeWorkLimiter.--gpu-prime-batch=512000sets the batch size inPrimeTester. -
I/O:
--write-batch-size=10000batches CSV writes.
The pipeline, line-by-line logic of each stage (and every early rejection)
I’ll walk you through what happens for a single candidate exponent p. The exact code paths below are the ones used in my run.
Candidate acquisition (producer)
-
We atomically reserve the next
pusing a packedulongCAS over{p << 3 | remainder}(my_state). That’s the lock-free distributor inProgram.cs. -
TransformPAdd: First step normalizes to
p % 6 ∈ {1,5}and after that it fixes remainders to 1 or 5 with each increase.
Cheap filters on p
(All in PrimeTester.cs unless noted)
-
Trivial rejects:
n <= 3: accept 2 and 3 only.- even
n: reject. n % 5 == 0: reject quickly.
-
“Ends-with-1” GCD heuristic: If
n % 10 == 1andSharesFactorWithMaxExponent(n)→ reject.SharesFactorWithMaxExponent(n)computesm = floor(log2 n)and rejects ifgcd(n, m) ≠ 1. There’s also a GPU batch formSharesFactorWithMaxExponentBatchused elsewhere when screening arrays.Why it’s valid: numbers of the form
10t+1have specific small-factor structure; combining that withgcd(n, floor(log2 n))cheaply kills many composites without MR. (This is a heuristic prefilter based on my analysis of primes ending with 1 or 7 and testing; it won’t reject primes incorrectly.) -
Small-prime sieve (CPU loop, still inside PrimeTester): Iterate
PrimesGenerator.SmallPrimeswhileprime^2 ≤ nusing the ready-madeSmallPrimesPow2to avoid sqrt. Reject immediately if divisible.With
--primes-device=gpu, we also have a device-side sieve for batches (IsPrimeGpuBatch): it copies chunks to the device, runs a simple sieve kernel over an uploaded small-primes table, and copies flags back. My per-candidate path uses the single-value version, but the machinery (stateful kernel cache, scratch buffers) is there for vectorized checks.
➡ If p fails any of the above → we never touch M_p. That’s my first wall of early rejections.
Mersenne stage
(All driven by MersenneNumberTester.cs)
-
Hard mathematical pre-rejects (top of IsMersennePrime):
-
If
p % 3 == 0andp != 3→ reject (since7 ∣ M_pwhen3 ∣ p). -
If
(p & 3) == 1andp.SharesFactorWithExponentMinusOne()→ reject.SharesFactorWithExponentMinusOne(inULongExtensions.cs) factorsp-1by trial overSmallPrimes, removes multiplicities, and for each distinct primerchecks whether the multiplicative order of 2 modulordividesp(viaCalculateOrder()), also handling the leftover cofactor. If any such order dividesp, we can certify a small factor forM_pand rejectpoutright.
-
If
pis even → reject (trivial, herepis already odd from the prime screen, but it’s kept for safety).
-
-
Residue divisor scanning (
_useResidue == true):-
We set
twoP = 2*p. Valid Mersenne divisors have the formq = 2kp + 1, k ≥ 1with congruence constraintsq ≡ 1 (mod 2p)andq ≡ 1 or 7 (mod 8). -
Digit filter:
-
Compute
lastIsSeven = ((p & 3) == 3)which encodes the last decimal digit ofM_p = 2^p - 1. -
If last digit of
M_pis 7 → we allow onlyqwith last digit in{3, 7, 9}. -
Else (last digit 1) → we allow only
{1, 3, 7, 9}. This is implemented both on CPU (CpuConstants.LastSevenMask10,LastOneMask10) and GPU by calculatingq % 10incrementally askgrows.
-
-
Rolling residues: we keep four rolling residues for each lane:
q mod 10, 8, 5, 3. That allows immediate rejection ofqdivisible by 2, 3, or 5 without any division (just adds, bit-ands and subtracts). -
CPU residue scanner: uses
ModResidueTrackerwith update formular′ ≡ 2^Δ⋅r+(2^Δ−1) (mod d). -
GPU residue scanner: batches
k, evaluates residues, prunes with cycles table, runs mod-pow on survivors. Uses pooled contexts and kernel caches. -
Short-circuit: as soon as we find any valid divisor
q | M_pin the scanned window, we stop.
-
➡ If scanning completes up to k 2^p / 2 + 1 and no divisor was found → IsMersennePrime(p) returns true and the CSV line has isPerfect=true. This means that p passed all the tests and is a candidate for constructing a perfect number, which requires testing with other methods.
What we cache, pool, and batch (and why)
-
Per-thread singletons:
PrimeTester,MersenneNumberTester,ModResidueTracker→ avoid hot locks, reuse GPU/CPU state. -
Mod residue tracker: avoids recomputing residues from scratch, supports incremental updates.
-
Divisor cycles: persistent, precomputed orders for
divisors ≤ 4,000,000, shared singleton, uploaded to GPU. -
GPU infra: pooled contexts/kernels, concurrency gating.
-
I/O pooling:
StringBuilderPool+ batched writes.
Every mathematical rule that we’re using to reject early
- Only prime exponents matter.
- 7 divides
M_pif3 ∣ p. - Order-based rejections using factors of
p−1. - Shape of Mersenne divisor:
q=2kp+1,q≡1 or 7 (mod 8). - Cycle length filter for small
divisors. - Monotone residue stepping with closed forms.
Hot-path micro-optimizations already in the code
- Lock-free work distribution.
- Per-thread testers & trackers.
- Sqrt-free stopping in sieves.
- Device-side constant tables.
Risks, correctness gaps, and performance pitfalls (with precise fixes)
-
Per-thread buffers are huge:
--block-size=10_000_000→ ~80 GB at 1024 threads. -
1024 CPU threads + 1024 GPU permits oversubscribe virtually any GPU. When running such configurations, make sure that you have enough RAM or swap file.
- How: set
--threadstoEnvironment.ProcessorCountand--gpu-prime-threadsto ~8–32.
- How: set
“Full accounting” of what gets computed, rejected, cached, or pooled
Per p
- Computed: next
p, small-prime residues, heuristic gcd if ending in 1, trial division. - Rejected: trivial composite checks; small-prime hits.
- If p prime: pre-rules, order checks, residue scan inputs, k-batches.
Cached/pooled
- Thread scope: testers, trackers, GPU contexts, buffers.
- Process scope: divisor cycles dataset, small primes.
- IO: pooled
StringBuilder+ batched writes.