using System.Runtime.CompilerServices; using UInt128 = System.UInt128; namespace PerfectNumbers.Core; /// /// Tracks residues for a fixed-step arithmetic progression without divisions. /// Optimized variant for the Mersenne scan q = 2*p*k + 1 that needs mod 10, 8, 3 and 5. /// public sealed class MersenneResidueAutomaton { private readonly UInt128 _step; // 2*p private UInt128 _currentQ; // current q value // Cached residues for current q public ulong Mod10R; public ulong Mod8R; public ulong Mod3R; public ulong Mod5R; // Cached residues for step (2*p mod m) private readonly ulong _step10; private readonly ulong _step8; private readonly ulong _step3; private readonly ulong _step5; public MersenneResidueAutomaton(ulong exponent) { _step = (UInt128)exponent << 1; // 2 * p _currentQ = _step + UInt128.One; // start at k = 1 // init residues Mod10R = _currentQ.Mod10(); Mod8R = _currentQ.Mod8(); Mod3R = _currentQ.Mod3(); Mod5R = _currentQ.Mod5(); _step10 = _step.Mod10().Mod10(); _step8 = _step.Mod8(); _step3 = _step.Mod3(); _step5 = _step.Mod5(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] // TODO: Remove this wrapper and expose the field directly once callers switch to struct-based residue // tracking so we can trim another layer from the tight scanning loop. public UInt128 CurrentQ() => _currentQ; /// /// Advance to next k (q += 2*p), updating residues branchlessly. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Next() { _currentQ += _step; ulong r10 = Mod10R + _step10; r10 -= (r10 >= 10UL) ? 10UL : 0UL; Mod10R = r10; ulong r8 = Mod8R + _step8; r8 &= 7UL; // % 8 Mod8R = r8; ulong r3 = Mod3R + _step3; r3 -= (r3 >= 3UL) ? 3UL : 0UL; Mod3R = r3; ulong r5 = Mod5R + _step5; if (r5 >= 5UL) { r5 -= 5UL; } Mod5R = r5; // TODO: Once divisor cycle lookups feed residue scanning, reuse the cached cycle length here to skip directly to the // next admissible q instead of iterating through every residue update. } } /// /// Generic automaton for scanning integers ending with digit 7: n = 10*t + 7. /// Maintains residues r_m without divisions while advancing by +10 each step. /// public sealed class Ending7Automaton { private ulong _n; // current candidate n, ends with 7 private readonly ulong[] _moduli; private readonly ulong[] _steps; // 10 % m private readonly ulong[] _res; // n % m private readonly bool[] _reachable; public Ending7Automaton(ulong start, params ulong[] moduli) { if (moduli == null || moduli.Length == 0) { _moduli = Array.Empty(); _steps = Array.Empty(); _res = Array.Empty(); _reachable = Array.Empty(); _n = AlignTo7(start); return; } _n = AlignTo7(start); int len = moduli.Length; _moduli = new ulong[len]; _steps = new ulong[len]; _res = new ulong[len]; _reachable = new bool[len]; for (int i = 0; i < len; i++) { ulong m = moduli[i]; _moduli[i] = m; // TODO: Replace the `% 10` computation with the Mod10 helper once Ending7Automaton plugs into the // shared residue tables so we avoid generic divisions while initializing the step cache. ulong step = 10UL % m; _steps[i] = step; // TODO: Precompute the initial residues via the divisor-cycle cache so this setup phase reuses the // GPU-generated cycle data instead of issuing on-the-fly modulo operations. _res[i] = _n % m; // one-time division at construction // Zero is reachable iff gcd(step, m) | res ulong g = Gcd(step, m); // TODO: Swap this `%` check for the binary-gcd aware reducer once the automaton wiring shares the // branchless Mod helpers benchmarked faster than repeated modulo instructions. _reachable[i] = (g == 0UL) ? false : (_res[i] % g == 0UL); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] // TODO: Inline this getter into the automaton consumers so residue scans can read the backing field // without paying the delegate call overhead highlighted in the profiler snapshots. public ulong Current() => _n; [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool DivisibleBy(ulong modulus) { for (int i = 0; i < _moduli.Length; i++) { if (_moduli[i] == modulus) { return _reachable[i] && _res[i] == 0UL; } } // TODO: Pre-register every modulus routed through the automaton so the fallback `%` path disappears; // the benchmarked residue trackers beat this division-heavy branch when scanning millions of values. return (_n % modulus) == 0UL; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Next() { _n += 10UL; for (int i = 0; i < _moduli.Length; i++) { if (!_reachable[i]) { continue; } ulong m = _moduli[i]; ulong r = _res[i] + _steps[i]; if (r >= m) { r -= m; } _res[i] = r; } // TODO: Once divisor cycles are mandatory, reuse the cached cycle offset per modulus to skip directly to valid // candidates so these per-step additions disappear from the hot path. } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong AlignTo7(ulong start) { ulong r = start.Mod10(); ulong add = (7UL + 10UL - r).Mod10(); return start + add; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong Gcd(ulong a, ulong b) { while (b != 0UL) { // TODO: Switch this to the binary GCD helper used in GpuUInt128BinaryGcdBenchmarks so we avoid `%` // inside the residue automaton setup. ulong t = a % b; a = b; b = t; } return a; } }