License
You're allowed to use the source code for education purposes, only.
- You can download it,
- You can review it,
- You can run it to learn how it works,
- You can run it to learn if it works,
- You can use it for learning how certain operations can be done, optimized etc.,
- You can use it to search and announce new perfect numbers, but please always include the information that you used my scanner. I have family and I need to bring money to the house.
What I'm asking you not to do, without license:
- You're not allowed to copy / paste or integrate my code and / or assemblies with your code.
- You're not allowed to replicate it 1 : 1 in your project. Please write your own implementation with the understanding how it works. We may end up with the same solution.
Please play fair and don't use it to claim your success in the area.
Any other use requires the payment of the licensing fee, which can be done using the links below. The payment is one-time payment and includes all future updates, if any.
In case of any concerns or questions, please feel free to reach out using my LinkedIn account.
-
Personal License - $10 https://buymeacoffee.com/mlemanczyk/e/455507
-
Earning / Commercial License - $100 https://buymeacoffee.com/mlemanczyk/e/455508
Thirdparty Components
The source code relays on selected functionality from the following packages:
- FluentAssertions
- ILGPU
- ILGPU.Algorithms
- LightningDB (LMDB-backed repositories for by-divisor state)
- Open.Numeric.Primes
- PeterO.Numbers
- xunit
Excluded p candidates by --mersenne=residue method.
Below you can find the filtering results coming from --mersenne=residue method, sorted and filtered to primes, only.
Excluded p candidates by --mersenne=bydivisor method.
The candidates below were excluded by the scanner. All calculated on AMD Ryzen 7 laptop. The divisors are randomly checked as sanity. The list is due complete validation when all candidates within each range are calculated.
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--mersenne=divisor--mersenne=bydivisor
Two-stage scanning flow
- Stage 1 -
--mersenne=residueacts as the fast, coarse filter for entire ranges of candidate exponents. It quickly rules out mostpvalues before any deep work, producing result files that record which primes survived. - Stage 2 -
--mersenne=bydivisorconsumes the survivors from stage 1 via--filter-p=<results>and performs the expensive divisor-by-divisor scan. It will not run without that filter. - Sample stage-1 output:
[sorted-primes-from-residue-method.zip](sorted-primes-from-residue-method.zip)contains primes that passed the residue sweep and can seed stage 2 directly. - Heuristic order/residue rules reused by stage 2 are documented in
docs/HeuristicIsPrimeSpecification.md; the by-divisor path reuses those wheel priorities, residue steppers, and cycle heuristics when walkingq = 2kp + 1.
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 in {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^Delta*r+(2^Delta-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.
--mersenne=bydivisor: divisor-first sweep for filtered primes
This is the second-stage scan that only runs after a residue pass. Feed it the stage-1 results (for example from sorted-primes-from-residue-method.zip) via --filter-p=<path> so it can focus solely on primes that already cleared the residue filter.
-
Prerequisites and setup:
--filter-pis mandatory (skips execution otherwise), candidates come from prior results filtered topassedAllTests=true, and the list is sorted, deduplicated, and optionally trimmed by--start-primeand--max-prime. No fresh prime sieve runs: the path assumes the filter already contains prime exponents.--min-kseeds the scan window and per-prime state is persisted underChecks/<p>.bin(CPU path only) everyByDivisorStateSaveIntervalto resume after restarts. -
Divisor generation and residue stepper:
- Divisors follow
q = 2kp + 1with monotonically increasing integerk, bounded byallowedMax = min(2^(p-1)-1, divisorLimitFromMaxPrime). The limit comes from the largest prime in the batch (ConfigureFromMaxPrime) and saturates at 2^256-1 for huge inputs. - Decimal masks come from my composite-divisor residue study: when
(p & 3) == 3the Mersenne number ends with 7, so only divisors with last digit{3,7,9}pass; otherwise{1,3,7,9}. All divisors must satisfyq % 8 in {1,7}. - Rolling remainders are maintained for each small modulus instead of recomputing divisions: CPU checks mod {3,7,11,13,17,19,8,10}, GPU checks mod {3,7,11,8,10}. The stride
step = 2pis pre-split into per-modulus deltas (step10,step8, ...) and residues advance via branch-freeAddMod*helpers, so the stepper cheaply skips entire residue classes that never yield valid divisors.
- Divisors follow
-
CPU scan path:
- Fast lane when
k/qfit in 64 bits:GpuUInt128holds stride, divisor, and limit; residues drive the wheel,MontgomeryDivisorDataPoolsupplies reduction constants, and every admissible divisor asksMersenneDivisorCycles.TryCalculateCycleLengthForExponent*for the order of 2 moduloq. If the order equalsp, the divisor is accepted immediately; otherwise the code falls back toCalculateCycleLength*(CPU/GPU/hybrid based on--order-device) and short-circuits on the same condition. The scan records progress (RecordState) every 10k steps so it can resume from the lastk. - When bounds overflow 64 bits, the loop switches to
BigInteger: the same residue masks apply, composites are rejected with a Miller-Rabin check before the expensiveBigInteger.ModPow(2, p, q), and hits still require2^p mod q == 1.divisorsExhaustedreports whether the configured window was fully covered (including resumes from--min-k).
- Fast lane when
-
GPU scan path:
- Requires
MinK <= ulong.MaxValue; allowedMax ismin(2^(p-1)-1, divisorLimitFromMaxPrime)with an extra 170M cap for tiny test primes.CheckDivisorschunks thekrange, applies the same decimal mask and small-modulus filters on CPU, and asksResolveDivisorCycle(GPU order solver with CPU fallback) for each admissible divisor. A cycle equal topends the scan immediately. - Survivors are batched into pooled buffers and pushed to
DivisorByDivisorKernels.CheckKernel. The kernel reusesExponentRemainderStepperGpuplus cycle remainders to avoid recomputing full powers for consecutive exponents (counts are 1 today but the stepper is shared). Hits are captured via an atomicfirstHitflag; accelerator-local bags reuse streams, buffers, and even divisor scan sessions. Unlike the CPU path, the GPU path does not persist state files; resuming relies solely on--min-k.
- Requires
-
Decision flags:
IsPrimereturns(isPrime, divisorsExhausted, divisor). The main loop treatsdivisorsExhausted=trueas a detailed check; a composite divisor setsisPrime=falseand stops the scan as soon as it is confirmed.
For the residue wheels, digit masks, and the heuristic order calculation that drive this stage, see docs/HeuristicIsPrimeSpecification.md (statistics from the composite study remain unchanged even as the code evolved).
--mersenne=divisor: cycle-gated GPU sweep
-
Where it plugs in: shares the same producer, residue locks (p % 6 is 1 or 5), and prefilters as the
--mersenne=residuedescription above. Once--mersenne=divisoris selected, the Mersenne stage delegates to_divisorTester.IsPrimeand skips Lucas/residue kernels. -
Cached small-divisor stage:
MersenneNumberDivisorGpuTester.BuildDivisorCandidateslifts all divisors <= 4,000,000 with known cycle lengths from thedivisor_cyclessnapshot (generated from my composite-divisor residue analysis). Each cached(divisor, cycle)is tried only whenp % cycle == 0, cutting out residue groups whose order of 2 provably cannot dividep. -
Generated divisor sweep:
- If a specific divisor is supplied (tests/manual runs), it is checked directly via
DivisorKernels.Kernel. - Otherwise the scanner walks
k = 1..divisorCyclesSearchLimit(--divisor-cycles-limit, default 64), buildingq = 2kp + 1and skipping the trivialq == M_pcase for smallp. For eachq,MersenneDivisorCycles.GetCycleGpucomputes the order of 2 (using the same heuristic factorization/cycle grouping as the snapshot); only whenp % cycle == 0do we launch the GPU divisibility kernel. The first hit stops the loop. divisorsExhaustedflips to true only if the configured limit covers all admissiblekup tofloor(UInt128.MaxValue / (2p)), so callers know whether the divisor window was fully searched.
- If a specific divisor is supplied (tests/manual runs), it is checked directly via
-
GPU resource handling: per-accelerator queues reuse the one-byte result buffers used by
DivisorKernels, and kernels run throughAcceleratorPoolwithout CPU fallbacks on this path (the exponent primality screen already ran on CPU/GPU earlier). Cycle data are loaded up front from--divisor-cyclesfor both the cached and generated phases. -
Early-rejection rules specific to this mode: Only divisors of shape
2kp+1whose order dividespever hit the GPU. The small-cycle snapshot already bakes in the decimal-digit and residue exclusions observed across millions of composite divisors, so the runtime sweep focuses exclusively on the most promising groups.
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.