diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index d02630b..28b8260 100644 --- a/PerfectNumbers.Core/MersenneDivisorCycles.cs +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -527,20 +527,31 @@ public class MersenneDivisorCycles return 2UL; } + PollardRhoMontgomeryReducer reducer = PollardRhoMontgomeryReducer.Create(n); + ulong diff = 0UL; + ulong d = 0UL; + while (true) { ulong c = (DeterministicRandom.NextUInt64() % (n - 1UL)) + 1UL; - ulong x = (DeterministicRandom.NextUInt64() % (n - 2UL)) + 2UL; - ulong y = x; - ulong d = 1UL; + ulong xStandard = (DeterministicRandom.NextUInt64() % (n - 2UL)) + 2UL; + ulong yStandard = xStandard; + ulong cMontgomery = reducer.ConvertToMontgomery(c); + ulong xMontgomery = reducer.ConvertToMontgomery(xStandard); + ulong yMontgomery = xMontgomery; + d = 1UL; while (d == 1UL) { - x = AdvancePolynomial(x, c, n); - y = AdvancePolynomial(y, c, n); - y = AdvancePolynomial(y, c, n); + xMontgomery = reducer.Advance(xMontgomery, cMontgomery); + xStandard = reducer.AdvanceStandard(xStandard, c); - ulong diff = x > y ? x - y : y - x; + yMontgomery = reducer.Advance(yMontgomery, cMontgomery); + yStandard = reducer.AdvanceStandard(yStandard, c); + yMontgomery = reducer.Advance(yMontgomery, cMontgomery); + yStandard = reducer.AdvanceStandard(yStandard, c); + + diff = xStandard > yStandard ? xStandard - yStandard : yStandard - xStandard; d = BinaryGcd(diff, n); } @@ -551,13 +562,6 @@ public class MersenneDivisorCycles } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong AdvancePolynomial(ulong x, ulong c, ulong modulus) - { - UInt128 value = (UInt128)x * x + c; - return (ulong)(value % modulus); - } - private static ulong BinaryGcd(ulong a, ulong b) { if (a == 0UL) diff --git a/PerfectNumbers.Core/PollardRhoMontgomeryReducer.cs b/PerfectNumbers.Core/PollardRhoMontgomeryReducer.cs new file mode 100644 index 0000000..78459aa --- /dev/null +++ b/PerfectNumbers.Core/PollardRhoMontgomeryReducer.cs @@ -0,0 +1,119 @@ +using System; +using System.Runtime.CompilerServices; + +namespace PerfectNumbers.Core; + +internal readonly struct PollardRhoMontgomeryReducer +{ + private readonly MontgomeryDivisorData _divisorData; + private readonly ulong _reciprocal; + + [ThreadStatic] + private static bool s_hasCachedReciprocal; + + [ThreadStatic] + private static ulong s_cachedReciprocal; + + public PollardRhoMontgomeryReducer(in MontgomeryDivisorData divisorData, ulong reciprocal) + { + _divisorData = divisorData; + _reciprocal = reciprocal == 0UL || reciprocal == ulong.MaxValue + ? ReciprocalMath.ComputeReciprocalEstimate(divisorData.Modulus) + : reciprocal; + } + + public ulong Modulus => _divisorData.Modulus; + private ulong NPrime => _divisorData.NPrime; + private ulong MontgomeryTwoSquared => _divisorData.MontgomeryTwoSquared; + private ulong MontgomeryOne => _divisorData.MontgomeryOne; + private ulong MontgomeryTwo => _divisorData.MontgomeryTwo; + public ulong Reciprocal => _reciprocal; + + public static PollardRhoMontgomeryReducer Create(ulong modulus) + { + MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(modulus); + ulong reciprocal; + if (s_hasCachedReciprocal) + { + reciprocal = ReciprocalMath.RefineReciprocalEstimate(s_cachedReciprocal, modulus); + if (reciprocal == 0UL || reciprocal == ulong.MaxValue) + { + reciprocal = ReciprocalMath.ComputeReciprocalEstimate(modulus); + } + } + else + { + reciprocal = ReciprocalMath.ComputeReciprocalEstimate(modulus); + } + + s_cachedReciprocal = reciprocal; + s_hasCachedReciprocal = true; + return new PollardRhoMontgomeryReducer(divisorData, reciprocal); + } + + public static PollardRhoMontgomeryReducer Create(in MontgomeryDivisorData divisorData, ulong previousReciprocal) + { + ulong reciprocal = ReciprocalMath.RefineReciprocalEstimate(previousReciprocal, divisorData.Modulus); + if (reciprocal == 0UL || reciprocal == ulong.MaxValue) + { + reciprocal = ReciprocalMath.ComputeReciprocalEstimate(divisorData.Modulus); + } + + s_cachedReciprocal = reciprocal; + s_hasCachedReciprocal = true; + return new PollardRhoMontgomeryReducer(divisorData, reciprocal); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong ConvertToMontgomery(ulong value) + { + if (value == 0UL) + { + return 0UL; + } + + if (value == 1UL) + { + return MontgomeryOne; + } + + if (value == 2UL) + { + return MontgomeryTwo; + } + + return value.MontgomeryMultiply(MontgomeryTwoSquared, Modulus, NPrime); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong ReduceFromMontgomery(ulong montgomeryValue) + { + return montgomeryValue.MontgomeryMultiply(1UL, Modulus, NPrime); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong Advance(ulong montgomeryValue, ulong cMontgomery) + { + ulong squared = montgomeryValue.MontgomeryMultiply(montgomeryValue, Modulus, NPrime); + return MontgomeryAdd(squared, cMontgomery, Modulus); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong AdvanceStandard(ulong standardValue, ulong c) + { + UInt128 polynomial = (UInt128)standardValue * standardValue + c; + return ReciprocalMath.Reduce(polynomial, Modulus, _reciprocal); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong MontgomeryAdd(ulong a, ulong b, ulong modulus) + { + UInt128 total = (UInt128)a + b; + if (total >= modulus) + { + total -= modulus; + } + + return (ulong)total; + } +} diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.cs b/PerfectNumbers.Core/PrimeOrderCalculator.cs index df8e4a2..5596adc 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.cs @@ -1164,11 +1164,9 @@ internal static partial class PrimeOrderCalculator return true; } - ulong c; - ulong x; - ulong y; - ulong d; + PollardRhoMontgomeryReducer reducer = PollardRhoMontgomeryReducer.Create(n); ulong diff = 0UL; + ulong d = 0UL; while (true) { @@ -1177,9 +1175,12 @@ internal static partial class PrimeOrderCalculator return false; } - c = (DeterministicRandom.NextUInt64() % (n - 1UL)) + 1UL; - x = (DeterministicRandom.NextUInt64() % (n - 2UL)) + 2UL; - y = x; + ulong c = (DeterministicRandom.NextUInt64() % (n - 1UL)) + 1UL; + ulong xStandard = (DeterministicRandom.NextUInt64() % (n - 2UL)) + 2UL; + ulong yStandard = xStandard; + ulong cMontgomery = reducer.ConvertToMontgomery(c); + ulong xMontgomery = reducer.ConvertToMontgomery(xStandard); + ulong yMontgomery = xMontgomery; d = 1UL; while (d == 1UL) @@ -1189,10 +1190,15 @@ internal static partial class PrimeOrderCalculator return false; } - x = AdvancePolynomial(x, c, n); - y = AdvancePolynomial(y, c, n); - y = AdvancePolynomial(y, c, n); - diff = x > y ? x - y : y - x; + xMontgomery = reducer.Advance(xMontgomery, cMontgomery); + xStandard = reducer.AdvanceStandard(xStandard, c); + + yMontgomery = reducer.Advance(yMontgomery, cMontgomery); + yStandard = reducer.AdvanceStandard(yStandard, c); + yMontgomery = reducer.Advance(yMontgomery, cMontgomery); + yStandard = reducer.AdvanceStandard(yStandard, c); + + diff = xStandard > yStandard ? xStandard - yStandard : yStandard - xStandard; d = BinaryGcd(diff, n); } @@ -1204,13 +1210,6 @@ internal static partial class PrimeOrderCalculator } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong AdvancePolynomial(ulong x, ulong c, ulong modulus) - { - UInt128 value = (UInt128)x * x + c; - return (ulong)(value % modulus); - } - private static ulong BinaryGcd(ulong a, ulong b) { if (a == 0UL) @@ -1397,25 +1396,31 @@ internal static partial class PrimeOrderCalculator return 2UL; } - ulong c; - ulong x; - ulong y; - ulong d; + PollardRhoMontgomeryReducer reducer = PollardRhoMontgomeryReducer.Create(n); ulong diff = 0UL; + ulong d = 0UL; while (true) { - c = (DeterministicRandom.NextUInt64() % (n - 1UL)) + 1UL; - x = (DeterministicRandom.NextUInt64() % (n - 2UL)) + 2UL; - y = x; + ulong c = (DeterministicRandom.NextUInt64() % (n - 1UL)) + 1UL; + ulong xStandard = (DeterministicRandom.NextUInt64() % (n - 2UL)) + 2UL; + ulong yStandard = xStandard; + ulong cMontgomery = reducer.ConvertToMontgomery(c); + ulong xMontgomery = reducer.ConvertToMontgomery(xStandard); + ulong yMontgomery = xMontgomery; d = 1UL; while (d == 1UL) { - x = AdvancePolynomial(x, c, n); - y = AdvancePolynomial(y, c, n); - y = AdvancePolynomial(y, c, n); - diff = x > y ? x - y : y - x; + xMontgomery = reducer.Advance(xMontgomery, cMontgomery); + xStandard = reducer.AdvanceStandard(xStandard, c); + + yMontgomery = reducer.Advance(yMontgomery, cMontgomery); + yStandard = reducer.AdvanceStandard(yStandard, c); + yMontgomery = reducer.Advance(yMontgomery, cMontgomery); + yStandard = reducer.AdvanceStandard(yStandard, c); + + diff = xStandard > yStandard ? xStandard - yStandard : yStandard - xStandard; d = BinaryGcd(diff, n); } diff --git a/PerfectNumbers.Core/ReciprocalMath.cs b/PerfectNumbers.Core/ReciprocalMath.cs new file mode 100644 index 0000000..20f9fcd --- /dev/null +++ b/PerfectNumbers.Core/ReciprocalMath.cs @@ -0,0 +1,125 @@ +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace PerfectNumbers.Core; + +internal static class ReciprocalMath +{ + private const int ReciprocalShift = 64; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong ComputeReciprocalEstimate(ulong divisor) + { + if (divisor <= 1UL) + { + return ulong.MaxValue; + } + + UInt128 scaledOne = UInt128.One << ReciprocalShift; + UInt128 estimate = scaledOne / divisor; + if ((scaledOne % divisor) != UInt128.Zero) + { + estimate += UInt128.One; + } + + if (estimate == UInt128.Zero) + { + return 1UL; + } + + if (estimate > ulong.MaxValue) + { + return ulong.MaxValue; + } + + return (ulong)estimate; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong RefineReciprocalEstimate(ulong reciprocal, ulong divisor) + { + if (divisor <= 1UL) + { + return ulong.MaxValue; + } + + if (reciprocal == 0UL || reciprocal == ulong.MaxValue) + { + return ComputeReciprocalEstimate(divisor); + } + + UInt128 reciprocal128 = reciprocal; + UInt128 product = (UInt128)divisor * reciprocal128; + UInt128 scaled = product >> ReciprocalShift; + UInt128 twoScaled = UInt128.One << (ReciprocalShift + 1); + if (scaled >= twoScaled) + { + return ComputeReciprocalEstimate(divisor); + } + + UInt128 correction = twoScaled - scaled; + UInt128 refined = (reciprocal128 * correction) >> ReciprocalShift; + if (refined == UInt128.Zero) + { + return 1UL; + } + + return refined > ulong.MaxValue ? ulong.MaxValue : (ulong)refined; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Reduce(UInt128 numerator, ulong divisor, ulong reciprocal) + { + if (divisor <= 1UL) + { + return 0UL; + } + + if (numerator == UInt128.Zero) + { + return 0UL; + } + + if (reciprocal == 0UL || reciprocal == ulong.MaxValue) + { + return (ulong)(numerator % divisor); + } + + ulong high = (ulong)(numerator >> ReciprocalShift); + ulong low = (ulong)numerator; + + if (high >= divisor) + { + return (ulong)(numerator % divisor); + } + + UInt128 highProduct = (UInt128)high * reciprocal; + UInt128 lowProduct = (UInt128)low * reciprocal; + UInt128 estimate = highProduct + (lowProduct >> ReciprocalShift); + + if (estimate == UInt128.Zero) + { + return (ulong)(numerator % divisor); + } + + UInt128 product = estimate * divisor; + while (product > numerator) + { + if (estimate == UInt128.Zero) + { + return (ulong)(numerator % divisor); + } + + estimate--; + product -= divisor; + } + + UInt128 remainder = numerator - product; + while (remainder >= divisor) + { + remainder -= divisor; + } + + return (ulong)remainder; + } +} diff --git a/docs/bydivisor_cpu_path.md b/docs/bydivisor_cpu_path.md index fa28523..d3962a2 100644 --- a/docs/bydivisor_cpu_path.md +++ b/docs/bydivisor_cpu_path.md @@ -138,3 +138,59 @@ modular exponentiations and cache lookups in the CPU by-divisor path. * **Reuse Montgomery metadata:** Implemented. Each prime state now persists the divisor's `n'`, `R`, and `R^2` factors so resynchronization seeds the exponent stepper directly, and divisor sessions rebuild the Montgomery tuple from the cached state before touching the shared cache, meaning cache lookups only occur when no stored snapshot matches the new divisor. * **Reciprocal-based delta evaluation:** Implemented. The analytic updater now retries with a direct 128-bit division when the reciprocal corrections exhaust their tolerance, refreshes the cached reciprocal from that exact quotient, and keeps the progression on the analytic path without rebuilding the state. * **Carry the Montgomery residue forward:** Implemented. Each analytic update now normalizes the refreshed remainder and multiplies it by the cached `R^2 mod d` factor immediately, so the exponent stepper always has a Montgomery residue ready without recomputing modular reductions. + + +## Prime-order factoring bottleneck and planned fixes + +The Pollard rho fallback in `PrimeOrderCalculator` still performs every quadratic +polynomial advance by issuing a full-width division. The tight loop in +`AdvancePolynomial` squares the iterate and takes a `% modulus`, which triggers +hardware division for each step and dominates the runtime once we test large +Mersenne divisors.【F:PerfectNumbers.Core/PrimeOrderCalculator.cs†L1180-L1211】 +The by-divisor CPU path consequently spends a measurable portion of its wall +clock time inside the factorization helper whenever it needs to refresh a cycle +or validate that a divisor candidate is prime. + +To bring this code in line with the Montgomery-first approach we use elsewhere +we should: + +* **Introduce a Montgomery-backed polynomial step.** `PrimeDivisorState` already + carries the full set of Montgomery constants (`n'`, `R`, `R^2`) for each + divisor we touch in the by-divisor scan, and `DivisorScanSession` rebuilds a + `MontgomeryDivisorData` snapshot whenever it seeds the analytic stepper or the + exponent synchronizer.【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L645-L779】【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L1246-L1398】 + We can surface the same data to the Pollard rho helper and rewrite + `AdvancePolynomial` so it accepts a precomputed reducer `(modulus, n', R^2)`. + The update then becomes `x = MontgomerySquareAdd(x, cMontgomery)`—a multiply + followed by one Montgomery reduction instead of a general-purpose remainder. + This eliminates the `% modulus` call entirely and lets the CPU factorizer reuse + the same fast path as the analytic delta updates. +* **Reuse reciprocal estimates between neighbouring divisors.** The analytic path + already stores a reciprocal for each divisor and refines it with Newton steps + to divide by `d_k` without hitting the 128-bit fallback.【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L876-L939】 + Pollard rho can share the very same multiplier. We can stash the `FastDiv64` + multiplier inside the Pollard rho state and update it when `q` grows by `2p` + using a single Newton refinement, mirroring `DivideRoundUpUsingReciprocal`. + Once the reciprocal is in place, the `(x * x + c) mod q` step reduces to a + pair of `UInt128` products and one multiply-high to estimate the quotient—a + drop-in use of `ULongExtensions.FastDiv64`.【F:PerfectNumbers.Core/ULongExtensions.cs†L69-L101】 +* **Warm-start the Montgomery context from the monotone `q` stream.** Each scan + step increments the divisor by the fixed stride `2p`, so the cached Montgomery + tuple only changes slowly. We can therefore hand the next divisor’s + `MontgomeryDivisorData` directly to the Pollard rho routine instead of forcing + it to recompute the reducer. This is already how `DivisorScanSession` avoids + redundant cache lookups; exporting the cached tuple keeps the factorizer in + lock-step with the rest of the pipeline.【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L645-L779】 + +With these changes the divisor-refresh path would no longer spend most of its +cycles on integer divisions, and the monotone growth of `q = 2kp + 1` for +`p ≥ 138,000,000` would guarantee that the reciprocal refinements stay stable +across thousands of consecutive candidates. + +### Pollard rho factoring implementation status + +* **Montgomery-backed polynomial step:** Completed. `PollardRhoMontgomeryReducer` now squares and adds in Montgomery space, + and the Pollard rho loops in `PrimeOrderCalculator` and `MersenneDivisorCycles` reuse the shared reducer instead of issuing `%` operations. +* **Reciprocal reuse:** Completed. The reducer tracks a cached FastDiv64 reciprocal, refines it for each monotone modulus, and exposes `AdvanceStandard` so the standard-domain iterates reuse the cached multiplier without `%` or `UInt128` division. +* **Warm-started Montgomery context:** Completed. The reducer accepts precomputed `MontgomeryDivisorData` snapshots, letting callers seed Pollard rho directly from the cached divisor state when the by-divisor pipeline advances to the next `q`. +