using System; using System.Numerics; using System.Runtime.CompilerServices; using PerfectNumbers.Core.Gpu; namespace PerfectNumbers.Core; public static class ULongExtensions { private const int Pow2WindowSize = 8; private const ulong Pow2WindowFallbackThreshold = 32UL; private const int Pow2WindowOddPowerCount = 1 << (Pow2WindowSize - 1); private struct Pow2OddPowerTable { public ulong Element0; public ulong Element1; public ulong Element2; public ulong Element3; public ulong Element4; public ulong Element5; public ulong Element6; public ulong Element7; public ulong Element8; public ulong Element9; public ulong Element10; public ulong Element11; public ulong Element12; public ulong Element13; public ulong Element14; public ulong Element15; public ulong Element16; public ulong Element17; public ulong Element18; public ulong Element19; public ulong Element20; public ulong Element21; public ulong Element22; public ulong Element23; public ulong Element24; public ulong Element25; public ulong Element26; public ulong Element27; public ulong Element28; public ulong Element29; public ulong Element30; public ulong Element31; public ulong Element32; public ulong Element33; public ulong Element34; public ulong Element35; public ulong Element36; public ulong Element37; public ulong Element38; public ulong Element39; public ulong Element40; public ulong Element41; public ulong Element42; public ulong Element43; public ulong Element44; public ulong Element45; public ulong Element46; public ulong Element47; public ulong Element48; public ulong Element49; public ulong Element50; public ulong Element51; public ulong Element52; public ulong Element53; public ulong Element54; public ulong Element55; public ulong Element56; public ulong Element57; public ulong Element58; public ulong Element59; public ulong Element60; public ulong Element61; public ulong Element62; public ulong Element63; public ulong Element64; public ulong Element65; public ulong Element66; public ulong Element67; public ulong Element68; public ulong Element69; public ulong Element70; public ulong Element71; public ulong Element72; public ulong Element73; public ulong Element74; public ulong Element75; public ulong Element76; public ulong Element77; public ulong Element78; public ulong Element79; public ulong Element80; public ulong Element81; public ulong Element82; public ulong Element83; public ulong Element84; public ulong Element85; public ulong Element86; public ulong Element87; public ulong Element88; public ulong Element89; public ulong Element90; public ulong Element91; public ulong Element92; public ulong Element93; public ulong Element94; public ulong Element95; public ulong Element96; public ulong Element97; public ulong Element98; public ulong Element99; public ulong Element100; public ulong Element101; public ulong Element102; public ulong Element103; public ulong Element104; public ulong Element105; public ulong Element106; public ulong Element107; public ulong Element108; public ulong Element109; public ulong Element110; public ulong Element111; public ulong Element112; public ulong Element113; public ulong Element114; public ulong Element115; public ulong Element116; public ulong Element117; public ulong Element118; public ulong Element119; public ulong Element120; public ulong Element121; public ulong Element122; public ulong Element123; public ulong Element124; public ulong Element125; public ulong Element126; public ulong Element127; public ref ulong this[int index] { get { return ref GetElementRef(index); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref ulong GetElementRef(int index) { switch (index) { case 0: return ref Element0; case 1: return ref Element1; case 2: return ref Element2; case 3: return ref Element3; case 4: return ref Element4; case 5: return ref Element5; case 6: return ref Element6; case 7: return ref Element7; case 8: return ref Element8; case 9: return ref Element9; case 10: return ref Element10; case 11: return ref Element11; case 12: return ref Element12; case 13: return ref Element13; case 14: return ref Element14; case 15: return ref Element15; case 16: return ref Element16; case 17: return ref Element17; case 18: return ref Element18; case 19: return ref Element19; case 20: return ref Element20; case 21: return ref Element21; case 22: return ref Element22; case 23: return ref Element23; case 24: return ref Element24; case 25: return ref Element25; case 26: return ref Element26; case 27: return ref Element27; case 28: return ref Element28; case 29: return ref Element29; case 30: return ref Element30; case 31: return ref Element31; case 32: return ref Element32; case 33: return ref Element33; case 34: return ref Element34; case 35: return ref Element35; case 36: return ref Element36; case 37: return ref Element37; case 38: return ref Element38; case 39: return ref Element39; case 40: return ref Element40; case 41: return ref Element41; case 42: return ref Element42; case 43: return ref Element43; case 44: return ref Element44; case 45: return ref Element45; case 46: return ref Element46; case 47: return ref Element47; case 48: return ref Element48; case 49: return ref Element49; case 50: return ref Element50; case 51: return ref Element51; case 52: return ref Element52; case 53: return ref Element53; case 54: return ref Element54; case 55: return ref Element55; case 56: return ref Element56; case 57: return ref Element57; case 58: return ref Element58; case 59: return ref Element59; case 60: return ref Element60; case 61: return ref Element61; case 62: return ref Element62; case 63: return ref Element63; case 64: return ref Element64; case 65: return ref Element65; case 66: return ref Element66; case 67: return ref Element67; case 68: return ref Element68; case 69: return ref Element69; case 70: return ref Element70; case 71: return ref Element71; case 72: return ref Element72; case 73: return ref Element73; case 74: return ref Element74; case 75: return ref Element75; case 76: return ref Element76; case 77: return ref Element77; case 78: return ref Element78; case 79: return ref Element79; case 80: return ref Element80; case 81: return ref Element81; case 82: return ref Element82; case 83: return ref Element83; case 84: return ref Element84; case 85: return ref Element85; case 86: return ref Element86; case 87: return ref Element87; case 88: return ref Element88; case 89: return ref Element89; case 90: return ref Element90; case 91: return ref Element91; case 92: return ref Element92; case 93: return ref Element93; case 94: return ref Element94; case 95: return ref Element95; case 96: return ref Element96; case 97: return ref Element97; case 98: return ref Element98; case 99: return ref Element99; case 100: return ref Element100; case 101: return ref Element101; case 102: return ref Element102; case 103: return ref Element103; case 104: return ref Element104; case 105: return ref Element105; case 106: return ref Element106; case 107: return ref Element107; case 108: return ref Element108; case 109: return ref Element109; case 110: return ref Element110; case 111: return ref Element111; case 112: return ref Element112; case 113: return ref Element113; case 114: return ref Element114; case 115: return ref Element115; case 116: return ref Element116; case 117: return ref Element117; case 118: return ref Element118; case 119: return ref Element119; case 120: return ref Element120; case 121: return ref Element121; case 122: return ref Element122; case 123: return ref Element123; case 124: return ref Element124; case 125: return ref Element125; case 126: return ref Element126; case 127: return ref Element127; default: return ref Element0; } } } public static ulong CalculateOrder(this ulong q) { if (q <= 2UL) { return 0UL; } ulong order = q - 1UL, prime, temp; uint[] smallPrimes = PrimesGenerator.SmallPrimes; ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2; int i = 0, primesLength = smallPrimes.Length; UInt128 q128 = q, cycle = MersenneDivisorCycles.GetCycle(q128); // TODO: When the shared cycle snapshot cannot serve this divisor, trigger an on-demand // GPU computation (respecting the configured device) without promoting the result into // the cache so the order calculator still benefits from cycle stepping while keeping the // single-block memory plan intact. for (; i < primesLength; i++) { if (smallPrimesPow2[i] > order) { break; } prime = smallPrimes[i]; // TODO: Replace this `%` driven factor peeling with the divisor-cycle aware // factoring helper so large orders reuse the cached remainders highlighted in // the latest divisor-cycle benchmarks instead of recomputing slow modulo checks. while (order % prime == 0UL) { temp = order / prime; if (temp.PowModWithCycle(q128, cycle) == UInt128.One) { order = temp; } else { break; } } } return order; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetBitLength(this ulong value) { return 64 - BitOperations.LeadingZeroCount(value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong MultiplyShiftRight(ulong value, ulong multiplier, int shift) { UInt128 product = (UInt128)value * multiplier; return (ulong)(product >> shift); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong MultiplyShiftRightShiftFirst(ulong value, ulong multiplier, int shift) { ulong high = value >> shift; ulong mask = (1UL << shift) - 1UL; ulong low = value & mask; UInt128 highContribution = (UInt128)high * multiplier; UInt128 lowContribution = (UInt128)low * multiplier; UInt128 combined = highContribution + (lowContribution >> shift); return (ulong)combined; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong FastDiv64(this ulong value, ulong divisor, ulong mul) { ulong quotient = (ulong)(((UInt128)value * mul) >> 64); UInt128 remainder = (UInt128)value - ((UInt128)quotient * divisor); if (remainder >= divisor) { quotient++; } return quotient; } public const ulong WordBitMask = 0xFFFFUL; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsPrimeCandidate(this ulong n) { int i = 0; uint[] smallPrimes = PrimesGenerator.SmallPrimes; ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2; int len = smallPrimes.Length; ulong p; for (; i < len; i++) { if (smallPrimesPow2[i] > n) { break; } p = smallPrimes[i]; // TODO: Swap this modulo check for the shared small-prime cycle filter once the // divisor-cycle cache is mandatory, matching the PrimeTester improvements noted in // the CPU sieve benchmarks. if ((n % p) == 0UL) { return n == p; } } return true; } // Benchmarks (Mod5ULongBenchmarks) show the direct `% 5` is still cheaper (~0.26 ns vs 0.43 ns), so keep the modulo until a faster lookup is proven. // (Mod8/Mod10 stay masked because they win; Mod5 currently does not.) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong Mod10(this ulong value) => (value & 1UL) == 0UL ? (value % 5UL) switch { 0UL => 0UL, 1UL => 6UL, 2UL => 2UL, 3UL => 8UL, _ => 4UL, } : (value % 5UL) switch { 0UL => 5UL, 1UL => 1UL, 2UL => 7UL, 3UL => 3UL, _ => 9UL, }; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong Mod128(this ulong value) => value & 127UL; // Benchmarks confirm `%` beats our current Mod5/Mod3 helpers for 64-bit inputs, so leave these modulo operations in place until a superior lookup is available. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Mod10_8_5_3(this ulong value, out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3) { mod8 = value & 7UL; // Benchmarks show `%` remains faster for the Mod5/Mod3 pair on ulong, so we keep the modulo path here for now. mod5 = value % 5UL; mod3 = value % 3UL; mod10 = (mod8 & 1UL) == 0UL ? mod5 switch { 0UL => 0UL, 1UL => 6UL, 2UL => 2UL, 3UL => 8UL, _ => 4UL, } : mod5 switch { 0UL => 5UL, 1UL => 1UL, 2UL => 7UL, 3UL => 3UL, _ => 9UL, }; } // Mod5/Mod3 lookup tables are currently slower on 64-bit operands; keep the direct modulo until benchmarks flip. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Mod10_8_5_3Steps(this ulong value, out ulong step10, out ulong step8, out ulong step5, out ulong step3) { ulong mod8 = value & 7UL; // Same rationale: `%` wins in Mod5/Mod3 benches today, so avoid swapping until a faster lookup exists. ulong mod5 = value % 5UL; ulong mod3 = value % 3UL; ulong mod10 = (mod8 & 1UL) == 0UL ? mod5 switch { 0UL => 0UL, 1UL => 6UL, 2UL => 2UL, 3UL => 8UL, _ => 4UL, } : mod5 switch { 0UL => 5UL, 1UL => 1UL, 2UL => 7UL, 3UL => 3UL, _ => 9UL, }; step10 = mod10 + mod10; if (step10 >= 10UL) { step10 -= 10UL; } step8 = (mod8 + mod8) & 7UL; step5 = mod5 + mod5; if (step5 >= 5UL) { step5 -= 5UL; } step3 = mod3 + mod3; if (step3 >= 3UL) { step3 -= 3UL; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static UInt128 Mul64(this ulong a, ulong b) => ((UInt128)a.MulHigh(b) << 64) | (UInt128)(a * b); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong MulHigh(this ulong x, ulong y) { // TODO: Investigate replacing this manual decomposition with the UInt128-based implementation // for CPU callers; the latest benchmarks show the intrinsic path is an order of magnitude // faster, while GPU code can keep using GpuUInt128.MulHigh. ulong xLow = (uint)x; ulong xHigh = x >> 32; ulong yLow = (uint)y; ulong yHigh = y >> 32; ulong w1 = xLow * yHigh; ulong w2 = xHigh * yLow; ulong w3 = xLow * yLow; // Matching the layout used in GpuUInt128.MulHigh: introducing the // intermediate result looks like one extra store, but it lets RyuJIT keep // the accumulated high word entirely in registers. Without this explicit // local the JIT spills the partial sum, which is where the performance // regression in the benchmarks came from. ulong result = (xHigh * yHigh) + (w1 >> 32) + (w2 >> 32); result += ((w3 >> 32) + (uint)w1 + (uint)w2) >> 32; return result; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong MulHighGpuCompatible(this ulong x, ulong y) { GpuUInt128 product = new(x); product.Mul64(new GpuUInt128(y)); return product.High; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong ModPow64(this ulong value, ulong exponent, ulong modulus) { ulong result = 1UL; // TODO: Replace this `%` with the Montgomery folding helper highlighted in MulMod64Benchmarks so the // modular exponentiation avoids the slow integer division before the ladder even starts. value %= modulus; while (exponent != 0UL) { if ((exponent & 1UL) != 0UL) { result = MulMod64(result, value, modulus); } value = MulMod64(value, value, modulus); exponent >>= 1; } return result; } [MethodImpl(MethodImplOptions.AggressiveInlining)] // TODO: Replace this fallback with the UInt128 Montgomery helper measured fastest in // MulMod64Benchmarks so CPU callers stop paying for triple modulo operations. public static ulong MulMod64(this ulong a, ulong b, ulong modulus) => (ulong)(UInt128)(((a % modulus) * (b % modulus)) % modulus); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong MulMod64GpuCompatible(this ulong a, ulong b, ulong modulus) { // TODO: Remove this GPU-compatible shim from production once callers migrate to MulMod64, // which the benchmarks show is roughly 6-7× faster on dense 64-bit inputs. GpuUInt128 state = new(a % modulus); return state.MulMod(b, modulus); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong MulMod64GpuCompatibleDeferred(this ulong a, ulong b, ulong modulus) { // TODO: Move this deferred helper to the benchmark suite; the baseline MulMod64 avoids the // 5-40× slowdown seen across real-world operand distributions. GpuUInt128 state = new(a); return state.MulModWithNativeModulo(b, modulus); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong MontgomeryMultiply(this ulong a, ulong b, ulong modulus, ulong nPrime) { ulong tLow = unchecked(a * b); ulong m = unchecked(tLow * nPrime); ulong mTimesModulusLow = unchecked(m * modulus); ulong result = unchecked(a.MulHigh(b) + m.MulHigh(modulus) + (unchecked(tLow + mTimesModulusLow) < tLow ? 1UL : 0UL)); if (result >= modulus) { result -= modulus; } return result; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong Pow2MontgomeryModWindowed(this ulong exponent, in MontgomeryDivisorData divisor, bool keepMontgomery) { ulong modulus = divisor.Modulus; if (exponent == 0UL) { return keepMontgomery ? divisor.MontgomeryOne : 1UL % modulus; } if (exponent <= Pow2WindowFallbackThreshold) { return Pow2MontgomeryModSingleBit(exponent, divisor, keepMontgomery); } int bitLength = BitOperations.Log2(exponent) + 1; int windowSize = GetWindowSize(bitLength); int oddPowerCount = 1 << (windowSize - 1); ulong result = divisor.MontgomeryOne; ulong nPrime = divisor.NPrime; Pow2OddPowerTable oddPowers = default; InitializeMontgomeryOddPowers(divisor, modulus, nPrime, ref oddPowers, oddPowerCount); int index = bitLength - 1; while (index >= 0) { if (((exponent >> index) & 1UL) == 0UL) { result = result.MontgomeryMultiply(result, modulus, nPrime); index--; continue; } int windowStart = index - windowSize + 1; if (windowStart < 0) { windowStart = 0; } while (((exponent >> windowStart) & 1UL) == 0UL) { windowStart++; } int windowLength = index - windowStart + 1; for (int square = 0; square < windowLength; square++) { result = result.MontgomeryMultiply(result, modulus, nPrime); } ulong mask = (1UL << windowLength) - 1UL; ulong windowValue = (exponent >> windowStart) & mask; int tableIndex = (int)((windowValue - 1UL) >> 1); ulong multiplier = oddPowers[tableIndex]; result = result.MontgomeryMultiply(multiplier, modulus, nPrime); index = windowStart - 1; } if (keepMontgomery) { return result; } return result.MontgomeryMultiply(1UL, modulus, nPrime); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong Pow2MontgomeryModSingleBit(ulong exponent, in MontgomeryDivisorData divisor, bool keepMontgomery) { ulong modulus = divisor.Modulus; ulong nPrime = divisor.NPrime; ulong result = divisor.MontgomeryOne; ulong baseVal = divisor.MontgomeryTwo; ulong remainingExponent = exponent; while (remainingExponent != 0UL) { if ((remainingExponent & 1UL) != 0UL) { result = result.MontgomeryMultiply(baseVal, modulus, nPrime); } remainingExponent >>= 1; if (remainingExponent == 0UL) { break; } baseVal = baseVal.MontgomeryMultiply(baseVal, modulus, nPrime); } return keepMontgomery ? result : result.MontgomeryMultiply(1UL, modulus, nPrime); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int GetWindowSize(int bitLength) { if (bitLength <= 6) { return Math.Max(bitLength, 1); } if (bitLength <= 23) { return 4; } if (bitLength <= 79) { return 5; } if (bitLength <= 239) { return 6; } if (bitLength <= 671) { return 7; } return Pow2WindowSize; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void InitializeMontgomeryOddPowers(in MontgomeryDivisorData divisor, ulong modulus, ulong nPrime, ref Pow2OddPowerTable oddPowers, int oddPowerCount) { oddPowers[0] = divisor.MontgomeryTwo; if (oddPowerCount == 1) { return; } ulong square = divisor.MontgomeryTwoSquared; for (int i = 1; i < oddPowerCount; i++) { ulong previous = oddPowers[i - 1]; oddPowers[i] = previous.MontgomeryMultiply(square, modulus, nPrime); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong Pow2MontgomeryModWithCycle(this ulong exponent, ulong cycleLength, in MontgomeryDivisorData divisor) { ulong rotationCount = exponent % cycleLength; return Pow2MontgomeryModWindowed(rotationCount, divisor, keepMontgomery: false); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong Pow2MontgomeryModFromCycleRemainder(this ulong reducedExponent, in MontgomeryDivisorData divisor) { return Pow2MontgomeryModWindowed(reducedExponent, divisor, keepMontgomery: false); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static UInt128 PowMod(this ulong exponent, UInt128 modulus) { UInt128 result = UInt128.One; ulong exponentLoopIndex = 0UL; // TODO: Port this scalar PowMod fallback to the ProcessEightBitWindows helper so CPU callers get the // eight-bit window wins measured against the classic square-and-subtract implementation. // Return 1 because 2^0 = 1 if (exponent == 0UL) return result; // Any number mod 1 is 0 if (modulus == UInt128.One) return UInt128.Zero; // For small exponents, do classic method if (exponent < 64 || modulus < 4) { for (; exponentLoopIndex < exponent; exponentLoopIndex++) { result <<= 1; if (result >= modulus) result -= modulus; } return result; } // Special case: if modulus is a power of two, use bitmasking for efficiency and correctness if ((modulus & (modulus - 1)) == 0) { result = UInt128.One << (int)(exponent & 127); return result & (modulus - 1); } // Reusing exponentLoopIndex to iterate again over the exponent range for the general-case accumulation. exponentLoopIndex = 0UL; // Reusing result after resetting it for the general modulus accumulation phase. result = UInt128.One; for (; exponentLoopIndex < exponent; exponentLoopIndex++) { result <<= 1; if (result >= modulus) result -= modulus; } return result; } /// /// Computes 2^exponent mod modulus using a known cycle length. /// public static UInt128 PowModWithCycle(this ulong exponent, UInt128 modulus, ulong cycleLength) { UInt128 result = UInt128.One; ulong exponentLoopIndex = 0UL; // TODO: Wire this cycle-aware overload into the ProcessEightBitWindows helper so the reduced exponent path // inherits the faster windowed pow2 routine highlighted in the Pow2Montgomery benchmarks. // Return 1 because 2^0 = 1 if (exponent == 0UL) return result; // Any number mod 1 is 0 if (modulus == UInt128.One) return UInt128.Zero; // For small exponents, do classic method if (exponent < 64 || modulus < 4) { for (; exponentLoopIndex < exponent; exponentLoopIndex++) { result <<= 1; if (result >= modulus) result -= modulus; } return result; } // Special case: if modulus is a power of two, use bitmasking for efficiency and correctness if ((modulus & (modulus - 1)) == 0) { result = UInt128.One << (int)(exponent & 127); return result & (modulus - 1); } // Reusing exponentLoopIndex to iterate over the rotation count for the cycle-aware path. exponentLoopIndex = 0UL; // Reusing result after resetting it for the rotation-accumulation pass. result = UInt128.One; // TODO: Replace this modulo with the cached cycle remainder produced by the divisor-cycle cache so PowModWithCycle avoids // repeated `%` work, matching the ProcessEightBitWindows wins captured in Pow2MontgomeryModCycleComputationBenchmarks. ulong rotationCount = exponent % cycleLength; for (; exponentLoopIndex < rotationCount; exponentLoopIndex++) { result <<= 1; if (result >= modulus) result -= modulus; } return result; } /// /// Computes 2^exponent mod modulus using a known cycle length. /// public static UInt128 PowModWithCycle(this ulong exponent, UInt128 modulus, UInt128 cycleLength) { UInt128 result = UInt128.One; ulong exponentLoopIndex = 0UL; // TODO: Replace this UInt128-cycle overload with the ProcessEightBitWindows helper so large-exponent CPU scans // reuse the faster windowed pow2 ladder instead of the manual rotation loop measured to lag behind in benchmarks. // Return 1 because 2^0 = 1 if (exponent == UInt128.Zero) return result; // Any number mod 1 is 0 if (modulus == UInt128.One) return UInt128.Zero; // For small exponents, do classic method if (exponent < UInt128Numbers.SixtyFour || modulus < UInt128Numbers.Four) { for (; exponentLoopIndex < exponent; exponentLoopIndex++) { result <<= 1; if (result >= modulus) result -= modulus; } return result; } // Special case: if modulus is a power of two, use bitmasking for efficiency and correctness if ((modulus & (modulus - UInt128.One)) == UInt128.Zero) { result = UInt128.One << (int)(exponent & UInt128Numbers.OneHundredTwentySeven); return result & (modulus - 1); } // Reusing result after resetting it for the rotation-driven accumulation phase. result = UInt128.One; // TODO: Swap this modulo with the upcoming UInt128 cycle remainder helper so large-exponent scans reuse cached // reductions instead of recomputing `%` for every lookup, as demonstrated in Pow2MontgomeryModCycleComputationBenchmarks. UInt128 rotationCount = exponent % cycleLength; UInt128 rotationIndex = UInt128.Zero; while (rotationIndex < rotationCount) { result <<= 1; if (result >= modulus) result -= modulus; rotationIndex += UInt128.One; } return result; } /// /// Computes 2^exponent mod modulus using a known cycle length. /// public static UInt128 PowModWithCycle(this UInt128 exponent, UInt128 modulus, UInt128 cycleLength) { UInt128 one = UInt128.One, result = one, zero = UInt128.Zero; ulong exponentLoopIndex = 0UL; // TODO: Migrate this UInt128 exponent overload to ProcessEightBitWindows so the large-cycle reductions drop the // slow manual loop that underperforms the windowed pow2 helper in the Pow2 benchmark suite. // Return 1 because 2^0 = 1 if (exponent == zero) return result; // Any number mod 1 is 0 if (modulus == one) return zero; // For small exponents, do classic method if (exponent < UInt128Numbers.SixtyFour || modulus < UInt128Numbers.Four) { for (; exponentLoopIndex < exponent; exponentLoopIndex++) { result <<= 1; if (result >= modulus) result -= modulus; } return result; } // Special case: if modulus is a power of two, use bitmasking for efficiency and correctness if ((modulus & (modulus - one)) == zero) { result = one << (int)(exponent & UInt128Numbers.OneHundredTwentySeven); return result & (modulus - 1); } // Reusing result after resetting it for the rotation-driven accumulation phase. result = one; // TODO: Swap this modulo with the shared UInt128 cycle remainder helper once available so CRT powmods reuse cached // reductions in the windowed ladder, avoiding the `%` cost highlighted in Pow2MontgomeryModCycleComputationBenchmarks. UInt128 rotationCount = exponent % cycleLength; // We're reusing "zero" as rotation index for just a little better performance while (zero < rotationCount) { result <<= 1; if (result >= modulus) result -= modulus; zero += one; } return result; } /// /// Computes 2^exponent mod modulus using iterative CRT composition from mod 10 up to modulus. /// Only for modulus >= 10 and reasonable size. /// public static UInt128 PowModCrt(this ulong exponent, UInt128 modulus, MersenneDivisorCycles cycles) { if (modulus < 10) return PowMod(exponent, modulus); // fallback to classic // Use cycle length 4 for mod 10 UInt128 currentModulus = 10, cycle, modulusCandidate = 11, remainderForCandidate, result = PowModWithCycle(exponent, 10, 4), zero = UInt128.Zero; for (; modulusCandidate <= modulus; modulusCandidate++) { cycle = MersenneDivisorCycles.GetCycle(modulusCandidate); remainderForCandidate = cycle > zero ? PowModWithCycle(exponent, modulusCandidate, cycle) : PowMod(exponent, modulusCandidate); // Solve x ≡ result mod currentModulus // x ≡ remM mod m // Find x mod (currentModulus * m) // Since currentModulus and m are coprime, use CRT: // x = result + currentModulus * t, where t ≡ (remM - result) * inv(currentModulus, m) mod m // TODO: Replace this `% modulusCandidate` with the cached residue helper derived from Mod10_8_5_3Benchmarks so CRT // composition avoids repeated modulo divisions when combining residues for large divisor sets. result += currentModulus * ((remainderForCandidate + modulusCandidate - (result % modulusCandidate)) * ModInverse(currentModulus, modulusCandidate) % modulusCandidate); currentModulus *= modulusCandidate; if (currentModulus >= modulus) break; } // TODO: Swap this final `% modulus` with the pooled remainder cache so the CRT result write-back avoids one more division, // aligning with the optimizations captured in Mod10_8_5_3Benchmarks. return result % modulus; } // Helper: modular inverse (extended Euclidean algorithm) private static UInt128 ModInverse(UInt128 a, UInt128 m) { UInt128 m0 = m, originalA, originalM, temp, x0 = 0, x1 = 1; if (m == 1) { return 0; } while (a > 1) { originalA = a; originalM = m; m = originalA % originalM; a = originalM; temp = x0; x0 = x1 - (originalA / originalM) * x0; x1 = temp; } if (x1 < 0) { x1 += m0; } return x1; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool SharesFactorWithExponentMinusOne(this ulong exponent) { ulong prime, value = exponent - 1UL; value >>= BitOperations.TrailingZeroCount(value); uint[] smallPrimes = PrimesGenerator.SmallPrimes; ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2; int i = 1, smallPrimesLength = smallPrimes.Length; for (; i < smallPrimesLength && smallPrimesPow2[i] <= value; i++) { prime = smallPrimes[i]; if (value % prime != 0UL) { continue; } if (exponent % prime.CalculateOrder() == 0UL) { return true; } do { value /= prime; } while (value % prime == 0UL); } if (value > 1UL && exponent % value.CalculateOrder() == 0UL) { return true; } return false; } }