PrimeOrderCalculatorAccelerator.Rent is now created outside of the loops and re-used for all calculations.

This commit is contained in:
mlemanczyk committed 2025-11-24 22:51:53 +01:00
1 parent 57a50e2a37
commit b9ca95bf8c
53 files changed
+4639 -4229

No files matched your search

@@ -1,11 +1,14 @@
using PerfectNumbers.Core.Gpu.Accelerators;
namespace PerfectNumbers.Core.Cpu;
internal sealed class MersenneCpuDivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
internal sealed class MersenneCpuDivisorScanSession(PrimeOrderCalculatorAccelerator gpu) : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
{
public void Reset()
public void Reset()
{
}
// TODO: Convert this to static method
public void CheckDivisor(
ulong divisor,
in MontgomeryDivisorData divisorData,
@@ -29,7 +32,7 @@ internal sealed class MersenneCpuDivisorScanSession : IMersenneNumberDivisorByDi
if (divisorCycle == 0UL)
{
divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
divisorCycle = DivisorCycleCache.Shared.GetCycleLength(gpu, divisor);
if (divisorCycle == 0UL)
{
// DivisorCycleCache guarantees a positive cycle for divisors greater than one.
@@ -4,7 +4,7 @@ using PerfectNumbers.Core.Gpu.Accelerators;
namespace PerfectNumbers.Core.Cpu;
public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDivisorByDivisorTester
public sealed class MersenneNumberDivisorByDivisorCpuTester() : IMersenneNumberDivisorByDivisorTester
{
private ulong _divisorLimit;
private int _batchSize = 1_024;
@@ -42,7 +42,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong GetAllowedMaxDivisor(ulong prime) => ComputeAllowedMaxDivisor(prime, _divisorLimit);
public bool IsPrime(ulong prime, out bool divisorsExhausted, out ulong divisor)
public bool IsPrime(PrimeOrderCalculatorAccelerator gpu, ulong prime, out bool divisorsExhausted, out ulong divisor)
{
ulong allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit);
@@ -58,6 +58,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
bool processedAll;
bool composite = CheckDivisors(
gpu,
prime,
allowedMax,
out processedAll,
@@ -84,7 +85,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
}
}
public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession()
public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession(PrimeOrderCalculatorAccelerator gpu)
{
MersenneCpuDivisorScanSession? session = ThreadStaticPools.RentMersenneCpuDivisorSession();
if (session is not null)
@@ -92,16 +93,16 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return session;
}
return new MersenneCpuDivisorScanSession();
return new MersenneCpuDivisorScanSession(gpu);
}
private bool CheckDivisors(
PrimeOrderCalculatorAccelerator gpu,
ulong prime,
ulong allowedMax,
out bool processedAll,
out ulong foundDivisor)
{
foundDivisor = 0UL;
// The EvenPerfectBitScanner feeds primes >= 138,000,000 here, so allowedMax >= 3 in production runs.
// Keeping the guard commented out documents the reasoning for benchmarks and tests.
// if (allowedMax < 3UL)
@@ -171,6 +172,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
remainder11 = (byte)(divisorLow % 11UL);
return CheckDivisors64(
gpu,
prime,
stepLow,
limit.Low,
@@ -203,7 +205,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
remainder7 = (byte)((((divisorHigh % 7UL) * 2UL) + (divisorLow % 7UL)) % 7UL);
remainder11 = (byte)((((divisorHigh % 11UL) * 5UL) + (divisorLow % 11UL)) % 11UL);
var gpu = PrimeOrderCalculatorAccelerator.Rent(1);
var divisorPool = MontgomeryDivisorDataPool.Shared;
while (divisor.CompareTo(limit) <= 0)
{
@@ -225,6 +226,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
// Divisors produced by 2 * k * p + 1 always exceed PerfectNumberConstants.MaxQForDivisorCycles
// for the exponents scanned here, so skip the unused cache fallback and compute directly.
divisorCycle = MersenneDivisorCycles.CalculateCycleLength(
gpu,
candidate,
divisorData,
skipPrimeOrderHeuristic: primeOrderFailed);
@@ -241,7 +243,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
// the corresponding Mersenne number because the order of 2 modulo the divisor is exactly p.
foundDivisor = candidate;
processedAll = true;
PrimeOrderCalculatorAccelerator.Return(gpu);
return true;
}
@@ -259,13 +260,13 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
remainder11 = AddMod11(remainder11, step11);
}
PrimeOrderCalculatorAccelerator.Return(gpu);
processedAll = true;
foundDivisor = 0UL;
return false;
}
private static bool CheckDivisors64(
PrimeOrderCalculatorAccelerator gpu,
ulong prime,
ulong step,
ulong limit,
@@ -285,7 +286,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
out ulong foundDivisor)
{
bool canAdvance = step <= limit;
var gpu = PrimeOrderCalculatorAccelerator.Rent(1);
Queue<MontgomeryDivisorData> divisorPool = MontgomeryDivisorDataPool.Shared;
if (!canAdvance)
{
@@ -304,6 +304,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
out bool primeOrderFailed) || computedCycle == 0UL)
{
divisorCycle = MersenneDivisorCycles.CalculateCycleLength(
gpu,
divisor,
divisorData,
skipPrimeOrderHeuristic: primeOrderFailed);
@@ -319,7 +320,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
{
foundDivisor = divisor;
processedAll = true;
PrimeOrderCalculatorAccelerator.Return(gpu);
return true;
}
@@ -332,7 +332,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
processedAll = true;
foundDivisor = 0UL;
PrimeOrderCalculatorAccelerator.Return(gpu);
return false;
}
@@ -340,7 +339,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
{
processedAll = true;
foundDivisor = 0UL;
PrimeOrderCalculatorAccelerator.Return(gpu);
return false;
}
@@ -361,6 +359,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
out bool primeOrderFailed) || computedCycle == 0UL)
{
divisorCycle = MersenneDivisorCycles.CalculateCycleLength(
gpu,
divisor,
divisorData,
skipPrimeOrderHeuristic: primeOrderFailed);
@@ -375,7 +374,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
{
foundDivisor = divisor;
processedAll = true;
PrimeOrderCalculatorAccelerator.Return(gpu);
return true;
}
@@ -390,7 +388,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
{
processedAll = true;
foundDivisor = 0UL;
PrimeOrderCalculatorAccelerator.Return(gpu);
return false;
}
@@ -1,90 +1,92 @@
using PerfectNumbers.Core.Gpu.Accelerators;
namespace PerfectNumbers.Core.Cpu;
public class MersenneNumberIncrementalCpuTester(GpuKernelType kernelType)
{
private static readonly MersenneDivisorCycles _cycles = MersenneDivisorCycles.Shared;
private readonly GpuKernelType _kernelType = kernelType;
private static readonly MersenneDivisorCycles _cycles = MersenneDivisorCycles.Shared;
private readonly GpuKernelType _kernelType = kernelType;
public void Scan(ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime)
{
UInt128 k = UInt128.One;
UInt128 one = k;
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
// Use residue automaton always on CPU
var auto = new MersenneResidueAutomaton(exponent);
public void Scan(PrimeOrderCalculatorAccelerator gpu, ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime)
{
UInt128 k = UInt128.One;
UInt128 one = k;
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
// Use residue automaton always on CPU
var auto = new MersenneResidueAutomaton(exponent);
bool reject;
bool shouldCheck;
UInt128 q = auto.CurrentQ();
UInt128 divPow;
UInt128 halfPow;
UInt128 phi;
UInt128 qCycle;
bool reject;
bool shouldCheck;
UInt128 q = auto.CurrentQ();
UInt128 divPow;
UInt128 halfPow;
UInt128 phi;
UInt128 qCycle;
ulong div;
ulong mod8;
ulong phi64;
ulong div;
ulong mod8;
ulong phi64;
while (k <= maxK && Volatile.Read(ref isPrime))
{
// TODO: Replace this direct GetCycle call with DivisorCycleCache.Lookup so we reuse the single snapshot block
// and compute missing cycles on the configured device without queuing additional block generation.
qCycle = MersenneDivisorCycles.GetCycle(q);
shouldCheck = auto.Mod10R != 5UL;
if (shouldCheck)
{
mod8 = auto.Mod8R;
if (mod8 != 1UL && mod8 != 7UL)
{
shouldCheck = false;
}
else if (auto.Mod3R == 0UL || auto.Mod5R == 0UL)
{
// TODO: Swap these Mod3/Mod5 zero checks to the cached residue tables once the automaton exposes
// the benchmarked lookup-based helpers so CPU scans avoid runtime modulo instructions.
shouldCheck = false;
}
}
while (k <= maxK && Volatile.Read(ref isPrime))
{
// TODO: Replace this direct GetCycle call with DivisorCycleCache.Lookup so we reuse the single snapshot block
// and compute missing cycles on the configured device without queuing additional block generation.
qCycle = MersenneDivisorCycles.GetCycle(gpu, q);
shouldCheck = auto.Mod10R != 5UL;
if (shouldCheck)
{
mod8 = auto.Mod8R;
if (mod8 != 1UL && mod8 != 7UL)
{
shouldCheck = false;
}
else if (auto.Mod3R == 0UL || auto.Mod5R == 0UL)
{
// TODO: Swap these Mod3/Mod5 zero checks to the cached residue tables once the automaton exposes
// the benchmarked lookup-based helpers so CPU scans avoid runtime modulo instructions.
shouldCheck = false;
}
}
if (shouldCheck)
{
if (_kernelType == GpuKernelType.Pow2Mod)
{
reject = exponent.PowModWithCycle(q, qCycle) == UInt128.One;
}
else
{
phi = q - one;
reject = false;
if (phi <= ulong.MaxValue)
{
phi64 = (ulong)phi;
if (phi64.PowModWithCycle(q, qCycle) == one)
{
halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - one;
if (halfPow.BinaryGcd(q) == one)
{
div = phi64.FastDiv64(exponent, divMul);
divPow = div.PowModWithCycle(q, qCycle) - one;
if (divPow.BinaryGcd(q) == one)
{
reject = true;
}
}
}
}
}
if (shouldCheck)
{
if (_kernelType == GpuKernelType.Pow2Mod)
{
reject = exponent.PowModWithCycle(q, qCycle) == UInt128.One;
}
else
{
phi = q - one;
reject = false;
if (phi <= ulong.MaxValue)
{
phi64 = (ulong)phi;
if (phi64.PowModWithCycle(q, qCycle) == one)
{
halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - one;
if (halfPow.BinaryGcd(q) == one)
{
div = phi64.FastDiv64(exponent, divMul);
divPow = div.PowModWithCycle(q, qCycle) - one;
if (divPow.BinaryGcd(q) == one)
{
reject = true;
}
}
}
}
}
if (reject && q.IsPrimeCandidate())
{
Volatile.Write(ref isPrime, false);
break;
}
}
if (reject && q.IsPrimeCandidate())
{
Volatile.Write(ref isPrime, false);
break;
}
}
k += 1UL;
auto.Next();
q = auto.CurrentQ();
}
}
k += 1UL;
auto.Next();
q = auto.CurrentQ();
}
}
}
@@ -1,44 +1,46 @@
using PerfectNumbers.Core.Gpu.Accelerators;
namespace PerfectNumbers.Core;
public class MersenneNumberLucasLehmerCpuTester
{
public bool IsPrime(ulong exponent)
{
// Early rejections aligned with incremental/order sieves, but safe for small p:
// - If 3 | p and p != 3, then 7 | M_p -> composite.
// - If p ≡ 1 (mod 4) and p shares a factor with (p-1), reject fast.
// - If p is divisible by divisors specific to numbers ending with 1 or 7, reject fast.
// TODO: Replace these `%` checks with Mod3/Mod5/Mod7/Mod11 helpers once Lucas–Lehmer CPU filtering
// shares the benchmarked bitmask implementations and avoids slow modulo instructions in the hot path.
if (
((exponent % 3UL) == 0UL && exponent != 3UL) ||
((exponent % 5UL) == 0UL && exponent != 5UL) ||
((exponent % 7UL) == 0UL && exponent != 7UL) ||
((exponent % 11UL) == 0UL && exponent != 11UL)
)
{
return false;
}
public bool IsPrime(PrimeOrderCalculatorAccelerator gpu, ulong exponent)
{
// Early rejections aligned with incremental/order sieves, but safe for small p:
// - If 3 | p and p != 3, then 7 | M_p -> composite.
// - If p ≡ 1 (mod 4) and p shares a factor with (p-1), reject fast.
// - If p is divisible by divisors specific to numbers ending with 1 or 7, reject fast.
// TODO: Replace these `%` checks with Mod3/Mod5/Mod7/Mod11 helpers once Lucas–Lehmer CPU filtering
// shares the benchmarked bitmask implementations and avoids slow modulo instructions in the hot path.
if (
((exponent % 3UL) == 0UL && exponent != 3UL) ||
((exponent % 5UL) == 0UL && exponent != 5UL) ||
((exponent % 7UL) == 0UL && exponent != 7UL) ||
((exponent % 11UL) == 0UL && exponent != 11UL)
)
{
return false;
}
if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOne())
{
return false;
}
if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOne(gpu))
{
return false;
}
UInt128 s = UInt128Numbers.Four, two = UInt128Numbers.Two,
m = (UInt128.One << (int)exponent) - UInt128.One;
UInt128 s = UInt128Numbers.Four, two = UInt128Numbers.Two,
m = (UInt128.One << (int)exponent) - UInt128.One;
UInt128 limit = exponent - UInt128Numbers.Two;
for (UInt128 i = UInt128.Zero; i < limit; i++)
{
// Lucas–Lehmer squares the evolving residue, so we stay on the Montgomery powmod path rather than the pow2-specific helper.
s = s.PowModWithCycle(two, MersenneDivisorCycles.GetCycle(m)) - two;
if (s < UInt128.Zero)
{
s += m;
}
}
UInt128 limit = exponent - UInt128Numbers.Two;
for (UInt128 i = UInt128.Zero; i < limit; i++)
{
// Lucas–Lehmer squares the evolving residue, so we stay on the Montgomery powmod path rather than the pow2-specific helper.
s = s.PowModWithCycle(two, MersenneDivisorCycles.GetCycle(gpu, m)) - two;
if (s < UInt128.Zero)
{
s += m;
}
}
return s == UInt128.Zero;
}
return s == UInt128.Zero;
}
}
@@ -1,10 +1,12 @@
using PerfectNumbers.Core.Gpu.Accelerators;
namespace PerfectNumbers.Core.Cpu;
public class MersenneNumberOrderCpuTester(GpuKernelType kernelType)
{
private readonly GpuKernelType _kernelType = kernelType;
public void Scan(ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime)
public void Scan(PrimeOrderCalculatorAccelerator gpu, ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime)
{
UInt128 k = 1UL;
var auto = new MersenneResidueAutomaton(exponent);
@@ -13,10 +15,10 @@ public class MersenneNumberOrderCpuTester(GpuKernelType kernelType)
while (k <= maxK && Volatile.Read(ref isPrime))
{
qCycle = MersenneDivisorCycles.GetCycle(q);
// TODO: When this lookup misses the snapshot, invoke the configured device to compute the
// single required cycle on demand without persisting it so the CPU order path retains the
// cycle-stepping speedups while honoring the no-extra-cache constraint for large divisors.
qCycle = MersenneDivisorCycles.GetCycle(gpu, q);
// TODO: When this lookup misses the snapshot, invoke the configured device to compute the
// single required cycle on demand without persisting it so the CPU order path retains the
// cycle-stepping speedups while honoring the no-extra-cache constraint for large divisors.
int allowMask2 = lastDigit == LastDigit.Seven ? CpuConstants.LastSevenMask10 : CpuConstants.LastOneMask10;
bool shouldCheck = ((allowMask2 >> (int)auto.Mod10R) & 1) != 0;
if (shouldCheck)
@@ -33,33 +35,33 @@ public class MersenneNumberOrderCpuTester(GpuKernelType kernelType)
}
if (shouldCheck)
{
if (_kernelType == GpuKernelType.Pow2Mod)
{
if (exponent.PowModWithCycle(q, qCycle) == 1UL)
{
Volatile.Write(ref isPrime, false);
break;
}
}
else
{
UInt128 phi = q - 1UL;
if (phi <= ulong.MaxValue)
{
ulong phi64 = (ulong)phi;
if (phi64.PowModWithCycle(q, qCycle) == 1UL)
{
UInt128 halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - 1UL;
if (halfPow.BinaryGcd(q) == 1UL)
{
ulong divMul = (ulong)((((UInt128)1 << 64) - 1UL) / exponent) + 1UL;
ulong div = phi64.FastDiv64(exponent, divMul);
UInt128 divPow = div.PowModWithCycle(q, qCycle) - 1UL;
if (divPow.BinaryGcd(q) == 1UL)
{
Volatile.Write(ref isPrime, false);
break;
}
if (_kernelType == GpuKernelType.Pow2Mod)
{
if (exponent.PowModWithCycle(q, qCycle) == 1UL)
{
Volatile.Write(ref isPrime, false);
break;
}
}
else
{
UInt128 phi = q - 1UL;
if (phi <= ulong.MaxValue)
{
ulong phi64 = (ulong)phi;
if (phi64.PowModWithCycle(q, qCycle) == 1UL)
{
UInt128 halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - 1UL;
if (halfPow.BinaryGcd(q) == 1UL)
{
ulong divMul = (ulong)((((UInt128)1 << 64) - 1UL) / exponent) + 1UL;
ulong div = phi64.FastDiv64(exponent, divMul);
UInt128 divPow = div.PowModWithCycle(q, qCycle) - 1UL;
if (divPow.BinaryGcd(q) == 1UL)
{
Volatile.Write(ref isPrime, false);
break;
}
}
}
}
@@ -70,7 +72,7 @@ public class MersenneNumberOrderCpuTester(GpuKernelType kernelType)
auto.Next();
q = auto.CurrentQ();
}
return;
}
@@ -1,3 +1,5 @@
using PerfectNumbers.Core.Gpu.Accelerators;
namespace PerfectNumbers.Core.Cpu;
public class MersenneNumberResidueCpuTester
@@ -5,7 +7,7 @@ public class MersenneNumberResidueCpuTester
private ModResidueTracker? _mersenneResidueTracker;
// CPU residue variant using tracker + unrolled residue updates (no method calls in the loop).
public void Scan(ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime)
public void Scan(PrimeOrderCalculatorAccelerator gpu, ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime)
{
// Initialize/update Mersenne residue tracker and start a merge walk for ascending q divisors
@@ -92,7 +94,7 @@ public class MersenneNumberResidueCpuTester
// (and below) so every qualifying q reuses precomputed lengths instead of recomputing via
// tracker.MergeOrAppend.
// cycle-based quick check for small q
if (localIsPrime && tracker.MergeOrAppend(exponent, q, out isDivider) && isDivider &&
if (localIsPrime && tracker.MergeOrAppend(gpu, exponent, q, out isDivider) && isDivider &&
(q <= ulong.MaxValue ? ((ulong)q).IsPrimeCandidate() : q.IsPrimeCandidate()))
{
localIsPrime = false;
@@ -105,7 +107,7 @@ public class MersenneNumberResidueCpuTester
((allowMask >> (int)r10_1) & 1) != 0 && (r8_1 == 1UL || r8_1 == 7UL) && r3_1 != 0UL && r5_1 != 0UL &&
localIsPrime)
{
if (localIsPrime && tracker.MergeOrAppend(exponent, qIncremental, out isDivider) && isDivider &&
if (localIsPrime && tracker.MergeOrAppend(gpu, exponent, qIncremental, out isDivider) && isDivider &&
(qIncremental <= ulong.MaxValue ? ((ulong)qIncremental).IsPrimeCandidate() : qIncremental.IsPrimeCandidate()))
{
localIsPrime = false;
@@ -118,7 +120,7 @@ public class MersenneNumberResidueCpuTester
((allowMask >> (int)r10_2) & 1) != 0 && (r8_2 == 1UL || r8_2 == 7UL) && r3_2 != 0UL && r5_2 != 0UL &&
localIsPrime)
{
if (localIsPrime && tracker.MergeOrAppend(exponent, qIncremental, out isDivider) && isDivider &&
if (localIsPrime && tracker.MergeOrAppend(gpu, exponent, qIncremental, out isDivider) && isDivider &&
(qIncremental <= ulong.MaxValue ? ((ulong)qIncremental).IsPrimeCandidate() : qIncremental.IsPrimeCandidate()))
{
localIsPrime = false;
@@ -131,7 +133,7 @@ public class MersenneNumberResidueCpuTester
((allowMask >> (int)r10_3) & 1) != 0 && (r8_3 == 1UL || r8_3 == 7UL) && r3_3 != 0UL && r5_3 != 0UL &&
localIsPrime)
{
if (localIsPrime && tracker.MergeOrAppend(exponent, qIncremental, out isDivider) && isDivider &&
if (localIsPrime && tracker.MergeOrAppend(gpu, exponent, qIncremental, out isDivider) && isDivider &&
(qIncremental <= ulong.MaxValue ? ((ulong)qIncremental).IsPrimeCandidate() : qIncremental.IsPrimeCandidate()))
{
localIsPrime = false;