Clarify cycle stepper reset inlining
This commit is contained in:
1 parent
8b467c27f3
commit
b8aa845c5c
53 files changed
+1736
-1131
No files matched your search
@@ -8,6 +8,8 @@ public sealed class AlphaCache
|
||||
|
||||
public AlphaValues Get(EInteger p, int k)
|
||||
{
|
||||
// TODO: Replace this dictionary lookup with a lock-free cache that reuses pooled AlphaValues so alpha requests avoid
|
||||
// redoing the expensive PowerCache.Get computations across threads.
|
||||
if (!_cache.TryGetValue((p, k), out var values))
|
||||
{
|
||||
ERational alphaPValue = AlphaCalculations.ComputeAlphaP(p, k);
|
||||
@@ -21,6 +23,8 @@ public sealed class AlphaCache
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
// TODO: Return pooled AlphaValues instances to a shared cache before clearing so repeated warm-ups do not thrash the
|
||||
// allocator when alpha tables refresh during large divisor scans.
|
||||
_cache.Clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ public static class AlphaCalculations
|
||||
{
|
||||
int exponent = checked(4 * k + 1);
|
||||
EInteger y = PowerCache.Get(p, exponent);
|
||||
// TODO: Reuse pooled numerator/denominator buffers here so the alphaP computation stops allocating
|
||||
// intermediate EInteger instances every invocation.
|
||||
return ERational.Create(y * p - 1, y * (p - 1));
|
||||
}
|
||||
|
||||
@@ -15,6 +17,9 @@ public static class AlphaCalculations
|
||||
{
|
||||
EInteger num = PowerCache.Get(q, a2 + 1) - EInteger.One;
|
||||
EInteger den = PowerCache.Get(q, a2) * (q - EInteger.One);
|
||||
// TODO: Reuse pooled ERational builders here so repeated factor computations pull precomputed
|
||||
// numerator/denominator tuples instead of re-executing PowerCache chains each time, matching the
|
||||
// fastest alpha benchmarks.
|
||||
return ERational.Create(num, den);
|
||||
}
|
||||
|
||||
@@ -22,7 +27,11 @@ public static class AlphaCalculations
|
||||
{
|
||||
ERational result = ERational.One;
|
||||
for (int i = 0; i < maxR; i++)
|
||||
{
|
||||
// TODO: Replace the repeated Multiply calls with a span-based aggregator that batches factors,
|
||||
// avoiding transient ERational instances when maxR grows large.
|
||||
result = result.Multiply(ComputeAlphaFactor(primes[i], a2));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ namespace PerfectNumbers.Core;
|
||||
|
||||
public enum AlphaMScannerCandidateStatus
|
||||
{
|
||||
// TODO: Store these statuses as byte-coded constants so hot-path evaluations can stay branchless when scanning candidates.
|
||||
UNKNOWN,
|
||||
PRODUCT_ABOVE_TARGET,
|
||||
PRODUCT_BELOW_TARGET,
|
||||
|
||||
@@ -2,8 +2,12 @@ namespace PerfectNumbers.Core.Cpu;
|
||||
|
||||
public static class CpuConstants
|
||||
{
|
||||
// Allowed q mod 10 sets depending on last digit of M_p
|
||||
public const int LastSevenMask10 = (1 << 7) | (1 << 9);
|
||||
// For M_p ending with 1, restrict to {1,3,9}; 7 considered only for q == 7 (not possible for large p)
|
||||
public const int LastOneMask10 = (1 << 1) | (1 << 3) | (1 << 9);
|
||||
}
|
||||
// Allowed q mod 10 sets depending on last digit of M_p
|
||||
public const int LastSevenMask10 = (1 << 7) | (1 << 9);
|
||||
// For M_p ending with 1, restrict to {1,3,9}; 7 considered only for q == 7 (not possible for large p)
|
||||
public const int LastOneMask10 = (1 << 1) | (1 << 3) | (1 << 9);
|
||||
// TODO: Generate these masks at startup from the benchmarked Mod10 automaton tables so CPU filtering stays aligned with
|
||||
// the optimized divisor-cycle residue steps.
|
||||
// TODO: Fold in the Mod6ComparisonBenchmarks stride table here so callers can merge the mod 6 skips with these masks and
|
||||
// avoid redundant residue work in the CPU hot loop.
|
||||
}
|
||||
@@ -200,14 +200,20 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
||||
if (useCycles)
|
||||
{
|
||||
cycleBlock = DivisorCycleCache.Shared.Acquire(divisor);
|
||||
// TODO: Swap this block lease for the direct single-cycle lookup once the cache exposes it
|
||||
// so the CPU scanner mirrors the benchmarked single-block policy without retaining mutable
|
||||
// blocks beyond the startup snapshot.
|
||||
divisorCycle = cycleBlock.GetCycle(divisor);
|
||||
// TODO: When divisorCycle is zero (cache miss), compute only that single cycle on the device
|
||||
// selected by the current settings, skip inserting the result into the shared cache, and keep
|
||||
// operating with the single on-disk block without requesting additional blocks.
|
||||
hit = CheckDivisor(prime, divisorCycle != 0UL, divisorCycle, divisorData);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Force this path to hydrate divisor cycles from disk once the cache guarantees
|
||||
// coverage so we can retire the slower Montgomery-only fallback highlighted by the CPU
|
||||
// divisor benchmarks.
|
||||
// TODO: Remove this no-cycle branch and require on-demand cycle computation (without
|
||||
// mutating the cache) so CPU scans never fall back to the slower Montgomery-only path
|
||||
// identified in the benchmarks.
|
||||
hit = CheckDivisor(prime, false, 0UL, divisorData);
|
||||
}
|
||||
|
||||
@@ -347,6 +353,9 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
||||
bool useDivisorCycles = _owner._useDivisorCycles;
|
||||
if (useDivisorCycles && divisorCycle == 0UL)
|
||||
{
|
||||
// TODO: Replace this guard with an on-demand CPU/GPU cycle computation path that skips
|
||||
// cache insertion and continues operating with the single cached block instead of
|
||||
// requesting additional cycle batches when a lookup misses.
|
||||
throw new InvalidOperationException($"Missing divisor cycle for divisor {divisor}.");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,94 +2,101 @@ 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, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||
{
|
||||
UInt128 k, one = k = UInt128.One;
|
||||
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
|
||||
// Use residue automaton always on CPU
|
||||
var auto = new MersenneResidueAutomaton(exponent);
|
||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, 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, shouldCheck;
|
||||
UInt128 q = auto.CurrentQ(),
|
||||
divPow, halfPow, phi, qCycle;
|
||||
bool reject;
|
||||
bool shouldCheck;
|
||||
UInt128 q = auto.CurrentQ();
|
||||
UInt128 divPow;
|
||||
UInt128 halfPow;
|
||||
UInt128 phi;
|
||||
UInt128 qCycle;
|
||||
|
||||
ulong div, mod8, phi64;
|
||||
ulong div;
|
||||
ulong mod8;
|
||||
ulong phi64;
|
||||
|
||||
while (k <= maxK && Volatile.Read(ref isPrime))
|
||||
{
|
||||
qCycle = MersenneDivisorCycles.GetCycle(q);
|
||||
shouldCheck = lastIsSeven
|
||||
? (auto.Mod10R != 5UL)
|
||||
: (auto.Mod10R != 5UL);
|
||||
if (shouldCheck)
|
||||
{
|
||||
mod8 = auto.Mod8R;
|
||||
if (mod8 != 1UL && mod8 != 7UL)
|
||||
{
|
||||
shouldCheck = false;
|
||||
}
|
||||
else if (auto.Mod3R == 0UL || auto.Mod5R == 0UL)
|
||||
{
|
||||
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(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)
|
||||
if (shouldCheck)
|
||||
{
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
// TODO: Swap this PowModWithCycle call for the ProcessEightBitWindows helper once
|
||||
// the scalar implementation lands so CPU pow2mod scans match the GPU speedups
|
||||
// captured in GpuPow2ModBenchmarks.
|
||||
reject = exponent.PowModWithCycle(q, qCycle) == UInt128.One;
|
||||
}
|
||||
else
|
||||
{
|
||||
phi = q - one;
|
||||
reject = false;
|
||||
if (phi <= ulong.MaxValue)
|
||||
{
|
||||
phi64 = (ulong)phi;
|
||||
// TODO: Route the phi-based powmods through the upcoming windowed helper to
|
||||
// avoid the current square-and-multiply cost highlighted in the Pow2 bench
|
||||
// suite when scanning large divisors.
|
||||
if (phi64.PowModWithCycle(q, qCycle) == one)
|
||||
{
|
||||
// TODO: Once the windowed pow2 helper is available expose a version
|
||||
// that reuses cached windows here instead of recomputing via the slow
|
||||
// bit ladder for every divisor.
|
||||
halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - one;
|
||||
if (halfPow.BinaryGcd(q) == one)
|
||||
{
|
||||
div = phi64.FastDiv64(exponent, divMul);
|
||||
// TODO: Replace this divisor powmod with the shared windowed
|
||||
// implementation so each candidate uses the benchmarked fast
|
||||
// path instead of the current per-bit multiply chain.
|
||||
divPow = div.PowModWithCycle(q, qCycle) - one;
|
||||
if (divPow.BinaryGcd(q) == one)
|
||||
{
|
||||
// TODO: Swap this PowModWithCycle call for the ProcessEightBitWindows helper once
|
||||
// the scalar implementation lands so CPU pow2mod scans match the GPU speedups
|
||||
// captured in GpuPow2ModBenchmarks.
|
||||
reject = exponent.PowModWithCycle(q, qCycle) == UInt128.One;
|
||||
reject = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
phi = q - one;
|
||||
reject = false;
|
||||
if (phi <= ulong.MaxValue)
|
||||
{
|
||||
phi64 = (ulong)phi;
|
||||
// TODO: Route the phi-based powmods through the upcoming windowed helper to
|
||||
// avoid the current square-and-multiply cost highlighted in the Pow2 bench
|
||||
// suite when scanning large divisors.
|
||||
if (phi64.PowModWithCycle(q, qCycle) == one)
|
||||
{
|
||||
// TODO: Once the windowed pow2 helper is available expose a version
|
||||
// that reuses cached windows here instead of recomputing via the slow
|
||||
// bit ladder for every divisor.
|
||||
halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - one;
|
||||
if (halfPow.BinaryGcd(q) == one)
|
||||
{
|
||||
div = phi64.FastDiv64(exponent, divMul);
|
||||
// TODO: Replace this divisor powmod with the shared windowed
|
||||
// implementation so each candidate uses the benchmarked fast
|
||||
// path instead of the current per-bit multiply chain.
|
||||
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();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
k += 1UL;
|
||||
auto.Next();
|
||||
q = auto.CurrentQ();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,10 @@ public class MersenneNumberOrderCpuTester(GpuKernelType kernelType)
|
||||
|
||||
while (k <= maxK && Volatile.Read(ref isPrime))
|
||||
{
|
||||
qCycle = MersenneDivisorCycles.GetCycle(q);
|
||||
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.
|
||||
int allowMask2 = lastIsSeven ? CpuConstants.LastSevenMask10 : CpuConstants.LastOneMask10;
|
||||
bool shouldCheck = ((allowMask2 >> (int)auto.Mod10R) & 1) != 0;
|
||||
if (shouldCheck)
|
||||
|
||||
@@ -10,9 +10,14 @@ public class MersenneNumberResidueCpuTester
|
||||
// Initialize/update Mersenne residue tracker and start a merge walk for ascending q divisors
|
||||
|
||||
_mersenneResidueTracker ??= new(ResidueModel.Mersenne);
|
||||
// For small divisors q <= 4,000,000 use cached cycle length to fast-path checks
|
||||
// and avoid per-q powmods when possible when scanning lanes.
|
||||
_mersenneResidueTracker.BeginMerge(exponent);
|
||||
// For small divisors q <= 4,000,000 use cached cycle length to fast-path checks
|
||||
// and avoid per-q powmods when possible when scanning lanes.
|
||||
// TODO: Wire this residue scan into DivisorCycleCache so the mandatory cycle acceleration from the
|
||||
// by-divisor benchmarks applies here instead of relying solely on ModResidueTracker powmods.
|
||||
// TODO: When a required cycle is missing from the snapshot, compute only that single cycle on the device
|
||||
// selected by the current settings, skip persisting the result, and avoid scheduling additional cache blocks so
|
||||
// we keep operating with the single shared block.
|
||||
_mersenneResidueTracker.BeginMerge(exponent);
|
||||
|
||||
// Start at k = 1: q0 = 2*p*1 + 1
|
||||
UInt128 k = UInt128.One;
|
||||
@@ -80,12 +85,15 @@ public class MersenneNumberResidueCpuTester
|
||||
r5_3 = r5_2 + step5; if (r5_3 >= 10UL) r5_3 -= 10UL; if (r5_3 >= 5UL) r5_3 -= 5UL;
|
||||
|
||||
// lane 0
|
||||
if (lanes >= 1 &&
|
||||
(((allowMask >> (int)r10_0) & 1) != 0 && (r8_0 == 1UL || r8_0 == 7UL) && r3_0 != 0UL && r5_0 != 0UL))
|
||||
{
|
||||
// cycle-based quick check for small q
|
||||
if (localIsPrime && tracker.MergeOrAppend(exponent, q, out isDivider) && isDivider &&
|
||||
(q <= ulong.MaxValue ? ((ulong)q).IsPrimeCandidate() : q.IsPrimeCandidate()))
|
||||
if (lanes >= 1 &&
|
||||
(((allowMask >> (int)r10_0) & 1) != 0 && (r8_0 == 1UL || r8_0 == 7UL) && r3_0 != 0UL && r5_0 != 0UL))
|
||||
{
|
||||
// TODO: Once cycle data is exposed for residue scans, consult the cached divisor cycle here
|
||||
// (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 &&
|
||||
(q <= ulong.MaxValue ? ((ulong)q).IsPrimeCandidate() : q.IsPrimeCandidate()))
|
||||
{
|
||||
localIsPrime = false;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ internal struct CycleRemainderStepper
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
// TODO: Inline this reset at the call sites so the hot loops reuse struct reinitialization
|
||||
// measured fastest in MersenneDivisorCycleLengthGpuBenchmarks, avoiding the extra
|
||||
// method call when scanners need to restart stepping.
|
||||
_previousPrime = 0UL;
|
||||
_currentRemainder = 0UL;
|
||||
_hasState = false;
|
||||
@@ -37,7 +40,8 @@ internal struct CycleRemainderStepper
|
||||
if (remainder >= cycleLength)
|
||||
{
|
||||
// TODO: Swap this `%` for the shared divisor-cycle remainder helper so initialization reuses the cached
|
||||
// cycle deltas benchmarked faster than on-the-fly modulo work.
|
||||
// cycle deltas benchmarked faster than on-the-fly modulo work, matching the
|
||||
// MersenneDivisorCycleLengthGpuBenchmarks winner for both CPU and GPU call sites.
|
||||
remainder %= cycleLength;
|
||||
}
|
||||
_previousPrime = prime;
|
||||
@@ -79,7 +83,8 @@ internal struct CycleRemainderStepper
|
||||
if (_currentRemainder >= cycleLength)
|
||||
{
|
||||
// TODO: Route this `%` through the shared divisor-cycle helper so repeated wrap-arounds avoid
|
||||
// modulo operations and match the benchmarked fast path.
|
||||
// modulo operations and match the benchmarked fast path highlighted in
|
||||
// MersenneDivisorCycleLengthGpuBenchmarks.
|
||||
_currentRemainder %= cycleLength;
|
||||
}
|
||||
}
|
||||
@@ -88,6 +93,8 @@ internal struct CycleRemainderStepper
|
||||
|
||||
public void CaptureState(out ulong previousPrime, out ulong previousRemainder, out bool hasState)
|
||||
{
|
||||
// TODO: Expose these fields directly once the residue scanners adopt the single-cycle helper
|
||||
// so the hot path can read them without paying for an additional wrapper call per iteration.
|
||||
previousPrime = _previousPrime;
|
||||
previousRemainder = _currentRemainder;
|
||||
hasState = _hasState;
|
||||
|
||||
@@ -60,16 +60,18 @@ public sealed class DivisorCycleCache
|
||||
}
|
||||
}
|
||||
|
||||
private const int CycleGenerationBatchSize = 262_144;
|
||||
private const int CycleGenerationBatchSize = 262_144; // TODO: Remove the block-sized batch once the single-cycle generator
|
||||
// replaces block hydration so we stop materializing large buffers
|
||||
// just to satisfy one missing divisor lookup.
|
||||
private const byte ByteZero = 0;
|
||||
private const byte ByteOne = 1;
|
||||
|
||||
private readonly object _sync = new();
|
||||
private readonly ConcurrentDictionary<int, Task<CycleBlock>> _pending = new();
|
||||
private readonly ConcurrentDictionary<Accelerator, Action<Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>>> _gpuKernelCache = new(AcceleratorReferenceComparer.Instance);
|
||||
private readonly object _sync = new(); // TODO: Delete the synchronization object once the cache exposes only snapshot reads and single-cycle computation so callers no longer contend on locks.
|
||||
private readonly ConcurrentDictionary<int, Task<CycleBlock>> _pending = new(); // TODO: Drop the concurrent dictionary entirely when the block pipeline is replaced with single-cycle generation; the new plan never queues background work.
|
||||
private readonly ConcurrentDictionary<Accelerator, Action<Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>>> _gpuKernelCache = new(AcceleratorReferenceComparer.Instance); // TODO: Swap this ConcurrentDictionary for a simple accelerator-indexed array once the single-cycle helper precompiles kernels during startup, eliminating the need for thread-safe lookups.
|
||||
private CycleBlock _baseBlock = null!;
|
||||
private CycleBlock? _activeBlock;
|
||||
private CycleBlock? _prefetchedBlock;
|
||||
private CycleBlock? _activeBlock; // TODO: Remove dynamic blocks altogether once lookups route to the snapshot or compute an individual cycle without mutating shared state.
|
||||
private CycleBlock? _prefetchedBlock; // TODO: Delete prefetch support so the cache never materializes extra blocks and callers always compute the missing cycle inline.
|
||||
private bool _useGpuGeneration = true;
|
||||
private readonly int _divisorCyclesBatchSize;
|
||||
private static int _sharedDivisorCyclesBatchSize = GpuConstants.GpuCycleStepsPerInvocation;
|
||||
@@ -84,6 +86,8 @@ public sealed class DivisorCycleCache
|
||||
Initialize(snapshot);
|
||||
}
|
||||
|
||||
// TODO: Remove the ability to reload at runtime once the cache becomes a read-only snapshot plus
|
||||
// transient single-cycle generator; future callers should load the snapshot during startup only.
|
||||
public void ReloadFromCurrentSnapshot()
|
||||
{
|
||||
ulong[] snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
|
||||
@@ -96,6 +100,8 @@ public sealed class DivisorCycleCache
|
||||
{
|
||||
_useGpuGeneration = useGpu;
|
||||
}
|
||||
// TODO: Remove the lock once single-cycle generation eliminates shared mutable state; a simple
|
||||
// volatile write will be enough when the cache becomes read-only.
|
||||
}
|
||||
|
||||
private void Initialize(ulong[] snapshot)
|
||||
@@ -116,6 +122,7 @@ public sealed class DivisorCycleCache
|
||||
}
|
||||
|
||||
_pending.Clear();
|
||||
// TODO: Remove the dictionary once prefetching and background block tracking disappear; with single-cycle generation we will keep only the snapshot and skip additional bookkeeping entirely.
|
||||
_baseBlock = new CycleBlock(this, index: 0, start: 0UL, snapshot);
|
||||
_activeBlock = null;
|
||||
_prefetchedBlock = null;
|
||||
@@ -129,6 +136,7 @@ public sealed class DivisorCycleCache
|
||||
public CycleBlock Acquire(ulong divisor)
|
||||
{
|
||||
CycleBlock block = divisor <= _baseBlock.End ? _baseBlock : AcquireDynamicBlock(divisor);
|
||||
// TODO: Replace this block-based acquisition with a direct GetCycle API that computes the missing cycle synchronously on the configured device so the cache never hands out mutable blocks or requires disposal.
|
||||
block.Retain();
|
||||
return block;
|
||||
}
|
||||
@@ -149,6 +157,9 @@ public sealed class DivisorCycleCache
|
||||
|
||||
if (!_pending.TryGetValue(blockIndex, out pendingTask))
|
||||
{
|
||||
// TODO: Replace Task.Run with an inline single-cycle computation that runs on the configured
|
||||
// device so we materialize exactly one missing cycle and stop scheduling thread-pool work for
|
||||
// entire blocks.
|
||||
pendingTask = Task.Run(() => GenerateBlock(blockIndex));
|
||||
_pending[blockIndex] = pendingTask;
|
||||
}
|
||||
@@ -165,6 +176,9 @@ public sealed class DivisorCycleCache
|
||||
}
|
||||
|
||||
SetActiveBlockLocked(block);
|
||||
// TODO: Stop promoting freshly generated blocks into the shared cache and, instead of
|
||||
// returning a block, surface the computed cycle directly to the caller so no extra cache
|
||||
// state survives beyond the single snapshot block.
|
||||
return block;
|
||||
}
|
||||
}
|
||||
@@ -210,7 +224,8 @@ public sealed class DivisorCycleCache
|
||||
previousActive.Dispose();
|
||||
}
|
||||
|
||||
StartPrefetchLocked(prefetched.Index + 1);
|
||||
StartPrefetchLocked(prefetched.Index + 1); // TODO: Remove this call once prefetching is dropped
|
||||
// so we strictly keep a single active block in memory.
|
||||
}
|
||||
|
||||
private void SetActiveBlockLocked(CycleBlock block)
|
||||
@@ -235,11 +250,14 @@ public sealed class DivisorCycleCache
|
||||
previousActive.Dispose();
|
||||
}
|
||||
|
||||
StartPrefetchLocked(block.Index + 1);
|
||||
StartPrefetchLocked(block.Index + 1); // TODO: Remove once prefetching is removed and the cache
|
||||
// no longer speculatively requests the next block after a miss.
|
||||
}
|
||||
|
||||
private void StartPrefetchLocked(int blockIndex)
|
||||
{
|
||||
// TODO: Remove prefetching entirely so the cache maintains a single snapshot block and computes
|
||||
// individual cycles on demand instead of triggering background generation for future indices.
|
||||
if (blockIndex <= 0)
|
||||
{
|
||||
return;
|
||||
@@ -262,6 +280,8 @@ public sealed class DivisorCycleCache
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Delete this once prefetching is removed; single-cycle generation will happen inline and
|
||||
// no asynchronous work should be scheduled.
|
||||
Task<CycleBlock> task = Task.Run(() => GenerateBlock(blockIndex));
|
||||
_pending[blockIndex] = task;
|
||||
task.ContinueWith(t => OnPrefetchCompleted(blockIndex, t), TaskScheduler.Default);
|
||||
@@ -269,6 +289,8 @@ public sealed class DivisorCycleCache
|
||||
|
||||
private void OnPrefetchCompleted(int blockIndex, Task<CycleBlock> task)
|
||||
{
|
||||
// TODO: Delete this callback when we remove prefetching. Single-cycle generation will run inline,
|
||||
// eliminating the need for asynchronous continuation handlers and concurrent dictionaries.
|
||||
if (!task.IsCompletedSuccessfully)
|
||||
{
|
||||
_pending.TryRemove(blockIndex, out _);
|
||||
@@ -284,6 +306,9 @@ public sealed class DivisorCycleCache
|
||||
|
||||
private void AssignPrefetchedBlockLocked(int blockIndex, CycleBlock block)
|
||||
{
|
||||
// TODO: Remove this entire method when prefetching goes away. The single-cycle flow should never
|
||||
// allocate or retain extra blocks beyond the startup snapshot, so prefetched block bookkeeping
|
||||
// becomes unnecessary.
|
||||
CycleBlock? active = _activeBlock;
|
||||
if (active is not null && blockIndex != active.Index + 1)
|
||||
{
|
||||
@@ -363,6 +388,9 @@ public sealed class DivisorCycleCache
|
||||
ComputeCyclesCpu(start, cycles);
|
||||
}
|
||||
|
||||
// TODO: Replace this block generator with a single-cycle helper that reuses shared buffers,
|
||||
// returns just the requested divisor's cycle, and avoids mutating cache state beyond the
|
||||
// preloaded snapshot.
|
||||
return new CycleBlock(this, blockIndex, start, cycles);
|
||||
}
|
||||
|
||||
@@ -393,6 +421,8 @@ public sealed class DivisorCycleCache
|
||||
ulong divisor = start;
|
||||
for (int i = 0; i < destination.Length; i++)
|
||||
{
|
||||
// TODO: Port this CPU fallback to the unrolled-hex cycle generator once it is shared so misses outside the snapshot
|
||||
// stop looping through CalculateCycleLength, which benchmarks showed is far slower than the GPU-assisted pipeline.
|
||||
destination[i] = MersenneDivisorCycles.CalculateCycleLength(divisor);
|
||||
divisor++;
|
||||
}
|
||||
@@ -407,6 +437,9 @@ public sealed class DivisorCycleCache
|
||||
{
|
||||
Accelerator accelerator = gpuLease.Accelerator;
|
||||
Action<Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>> kernel = _gpuKernelCache.GetOrAdd(accelerator, LoadKernel);
|
||||
// TODO: Once single-cycle generation is in place, bypass this block-sized kernel entirely and
|
||||
// call the ProcessEightBitWindows-based GPU helper that materializes only the requested
|
||||
// divisor, reusing a pinned upload buffer instead of allocating per block.
|
||||
|
||||
int length = destination.Length;
|
||||
int chunkCapacity = Math.Min(length, CycleGenerationBatchSize);
|
||||
@@ -568,3 +601,4 @@ public sealed class DivisorCycleCache
|
||||
public int GetHashCode(Accelerator obj) => RuntimeHelpers.GetHashCode(obj);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ public sealed class EulerPrimeTester
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsEulerPrime(ulong n)
|
||||
{
|
||||
// TODO: Inline this helper into callers so we skip the wrapper and pass the divisibility check directly to the optimized
|
||||
// prime tester path highlighted in the CPU benchmarks.
|
||||
return (n & 3UL) == 1UL && _primeTester.IsPrime(n, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,9 @@ internal struct ExponentRemainderStepper
|
||||
}
|
||||
|
||||
ulong delta = exponent - _previousExponent;
|
||||
// TODO: Once divisor cycle lengths are mandatory, pull the delta multiplier from the
|
||||
// single-block divisor-cycle snapshot so we can skip the powmod entirely and reuse the
|
||||
// cached Montgomery residue ladder highlighted in MersenneDivisorCycleLengthGpuBenchmarks.
|
||||
// TODO: Replace this per-delta powmod with the upcoming windowed ladder so incremental
|
||||
// updates stop paying the single-bit cost highlighted in GpuPow2ModBenchmarks.
|
||||
ulong multiplier = delta.Pow2MontgomeryModMontgomery(_divisor);
|
||||
@@ -81,6 +84,8 @@ internal struct ExponentRemainderStepper
|
||||
}
|
||||
|
||||
ulong delta = exponent - _previousExponent;
|
||||
// TODO: Reuse the divisor-cycle derived Montgomery delta once the cache exposes single-cycle
|
||||
// lookups so this branch also avoids recomputing powmods when the snapshot lacks the divisor.
|
||||
// TODO: Use the windowed delta pow2 helper here as well to avoid the single-bit ladder that
|
||||
// currently lags behind the benchmarked implementation for huge divisor cycles.
|
||||
ulong multiplier = delta.Pow2MontgomeryModMontgomery(_divisor);
|
||||
@@ -125,6 +130,9 @@ internal struct ExponentRemainderStepper
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: Once the divisor-cycle cache exposes a direct Montgomery delta, multiply it here instead
|
||||
// of relying on the caller-provided delta so incremental scans remain in sync with the snapshot
|
||||
// without computing additional cycles or mutating cache state.
|
||||
_currentMontgomery = _currentMontgomery.MontgomeryMultiply(montgomeryDelta, _modulus, _nPrime);
|
||||
_previousExponent = exponent;
|
||||
isUnity = _currentMontgomery == _montgomeryOne;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
namespace PerfectNumbers.Core.Gpu
|
||||
{
|
||||
public static class GpuConstants
|
||||
{
|
||||
public const int ScanBatchSize = 2_097_152;
|
||||
public const int GpuCycleStepsPerInvocation = 256;
|
||||
public static class GpuConstants
|
||||
{
|
||||
public const int ScanBatchSize = 2_097_152;
|
||||
public const int GpuCycleStepsPerInvocation = 256;
|
||||
// TODO: Auto-tune these GPU constants per accelerator so batch sizing aligns with the fastest kernel
|
||||
// configuration reported by the Pow2Mod and divisor-cycle benchmarks.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ namespace PerfectNumbers.Core.Gpu;
|
||||
|
||||
public static class GpuContextPool
|
||||
{
|
||||
private static readonly bool PoolingEnabled = true;
|
||||
// Default device preference for generic GPU kernels (prime scans, NTT, etc.)
|
||||
public static bool ForceCpu { get; set; } = false;
|
||||
private static readonly bool PoolingEnabled = true;
|
||||
// TODO: Introduce accelerator-specific warmup so pooled contexts precompile the ProcessEightBitWindows kernels and load
|
||||
// divisor-cycle data before the first scan begins.
|
||||
// Default device preference for generic GPU kernels (prime scans, NTT, etc.)
|
||||
public static bool ForceCpu { get; set; } = false;
|
||||
|
||||
internal sealed class PooledContext
|
||||
{
|
||||
@@ -156,3 +158,4 @@ public static class GpuContextPool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,7 +694,7 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
|
||||
public class GpuKernelPool
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Accelerator, KernelContainer> KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, KernelContainer> KernelCache = new(); // TODO: Replace this concurrent map with a simple accelerator-indexed lookup once kernel launchers are prewarmed during startup so we can drop the thread-safe wrapper entirely.
|
||||
|
||||
private static KernelContainer GetKernels(Accelerator accelerator)
|
||||
{
|
||||
@@ -712,14 +712,14 @@ public class GpuKernelPool
|
||||
}
|
||||
|
||||
// Ensure single upload per accelerator even if multiple threads race here.
|
||||
lock (kernels)
|
||||
lock (kernels) // TODO: Remove this lock by pre-uploading the immutable small-cycle snapshot during initialization; once no mutation happens at runtime, the pool must expose a simple reference without synchronization.
|
||||
{
|
||||
if (kernels.SmallCycles is { } existing)
|
||||
{
|
||||
return existing.View;
|
||||
}
|
||||
|
||||
var host = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
|
||||
var host = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot(); // TODO: Preload this device buffer during startup and keep it immutable so we can delete the lock above in favor of the preloaded snapshot.
|
||||
var device = accelerator.Allocate1D<ulong>(host.Length);
|
||||
device.View.CopyFromCPU(host);
|
||||
kernels.SmallCycles = device;
|
||||
@@ -738,7 +738,7 @@ public class GpuKernelPool
|
||||
return new ResiduePrimeViews(lastOne.View, lastSeven.View, lastOnePow2.View, lastSevenPow2.View);
|
||||
}
|
||||
|
||||
lock (kernels)
|
||||
lock (kernels) // TODO: Inline these small-prime uploads into startup initialization alongside the small-cycle snapshot so we can drop runtime locking and keep the GPU pool free of synchronization.
|
||||
{
|
||||
if (kernels.SmallPrimesLastOne is { } existingLastOne &&
|
||||
kernels.SmallPrimesLastSeven is { } existingLastSeven &&
|
||||
|
||||
@@ -8,6 +8,8 @@ public static class GpuPrimeWorkLimiter
|
||||
|
||||
public static IDisposable Acquire()
|
||||
{
|
||||
// TODO: Inline the pooled limiter guard from the GpuLimiterThroughputBenchmarks so prime-sieve
|
||||
// GPU jobs stop allocating Releaser instances on every acquisition.
|
||||
var sem = _semaphore;
|
||||
sem.Wait();
|
||||
return new Releaser(sem);
|
||||
@@ -27,6 +29,8 @@ public static class GpuPrimeWorkLimiter
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Switch to a shared limiter implementation with GpuWorkLimiter so we can coordinate CPU/GPU prime work using
|
||||
// the same semaphore pool without reallocating per adjustment.
|
||||
_semaphore = new SemaphoreSlim(value, value);
|
||||
_currentLimit = value;
|
||||
}
|
||||
|
||||
@@ -156,14 +156,14 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator >=(GpuUInt128 left, GpuUInt128 right) => left.CompareTo(right) >= 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static GpuUInt128 operator ++(GpuUInt128 value)
|
||||
{
|
||||
value.Add(1UL);
|
||||
return value;
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static GpuUInt128 operator ++(GpuUInt128 value)
|
||||
{
|
||||
value.Add(1UL);
|
||||
return value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly int CompareTo(GpuUInt128 other)
|
||||
{
|
||||
if (High < other.High)
|
||||
@@ -536,6 +536,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static GpuUInt128 BinaryGcd(GpuUInt128 u, GpuUInt128 v)
|
||||
{
|
||||
// TODO: Replace this scalar binary GCD with the branchless reduction from
|
||||
// GpuUInt128BinaryGcdBenchmarks so CPU fallbacks stay aligned with the GPU kernel
|
||||
// performance when resolving large divisor residues.
|
||||
if (u.IsZero)
|
||||
{
|
||||
return v;
|
||||
@@ -1044,11 +1047,15 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
|
||||
public void ModPow(GpuUInt128 exponent, GpuUInt128 modulus)
|
||||
{
|
||||
// TODO: This should operate on the instance itself, not on a copy. Avoid creating new instances anywhere.
|
||||
// TODO: Swap this copy-heavy path for the pooled base/exponent ladder used in
|
||||
// GpuUInt128MulModBenchmarks so the GPU shim keeps reusing buffers instead of
|
||||
// allocating temporary structs during residue scans.
|
||||
GpuUInt128 result = new(1UL);
|
||||
GpuUInt128 baseValue = this;
|
||||
|
||||
// TODO: Can we modify this loop to process multiple bits at a time? E.g. 64-bit chunks.
|
||||
// TODO: Replace the single-bit square-and-multiply loop with the 64-bit windowed
|
||||
// ladder measured fastest in GpuUInt128MulModBenchmarks to align with the GPU kernel
|
||||
// implementation once the shared helper lands.
|
||||
while (!exponent.IsZero)
|
||||
{
|
||||
if ((exponent.Low & 1UL) != 0UL)
|
||||
@@ -1066,11 +1073,13 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
|
||||
public void ModPow(ulong exponent, GpuUInt128 modulus)
|
||||
{
|
||||
// TODO: This should operate on the instance itself, not on a copy. Avoid creating new instances anywhere.
|
||||
// TODO: Share the pooled ladder state from GpuUInt128MulModBenchmarks here as well so
|
||||
// Lucas–Lehmer batches avoid constructing throwaway temporaries on every exponent.
|
||||
GpuUInt128 result = new(1UL);
|
||||
GpuUInt128 baseValue = this;
|
||||
|
||||
// TODO: Can we modify this loop to process multiple bits at a time? E.g. 64-bit chunks.
|
||||
// TODO: Upgrade this loop to the same 64-bit windowed ladder proven fastest in
|
||||
// GpuUInt128MulModBenchmarks so the scalar helper matches the GPU-optimized path.
|
||||
while (exponent != 0UL)
|
||||
{
|
||||
if ((exponent & 1UL) != 0UL)
|
||||
@@ -1086,14 +1095,16 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
Low = result.Low;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ModInv(ulong modulus)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ModInv(ulong modulus)
|
||||
{
|
||||
// TODO: We shouldn't need a new instance here to represent modulus.
|
||||
// TODO: Replace this Fermat inversion with the Montgomery ladder highlighted in
|
||||
// GpuUInt128Montgomery64Benchmarks so we avoid instantiating a temporary modulus and
|
||||
// reuse the pooled reduction constants.
|
||||
ModPow(modulus - 2UL, new GpuUInt128(modulus));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ModInv(GpuUInt128 modulus)
|
||||
{
|
||||
if (modulus.High == 0UL)
|
||||
@@ -1114,7 +1125,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void MultiplyFull(GpuUInt128 left, GpuUInt128 right, out ulong p3, out ulong p2, out ulong p1, out ulong p0)
|
||||
{
|
||||
// TODO: Reuse variables to reduce register pressure.
|
||||
// TODO: Reuse variables to reduce register pressure following the fused-limb layout
|
||||
// from GpuUInt128MulModByLimbBenchmarks so the multiply helper matches the fastest
|
||||
// GPU-compatible scalar routine.
|
||||
var (h0, l0) = Mul64(left.Low, right.Low);
|
||||
var (h1, l1) = Mul64(left.Low, right.High);
|
||||
var (h2, l2) = Mul64(left.High, right.Low);
|
||||
|
||||
@@ -8,6 +8,9 @@ public static class GpuWorkLimiter
|
||||
|
||||
public static IDisposable Acquire()
|
||||
{
|
||||
// TODO: Replace the per-call Releaser allocation with the pooled struct-based guard from the
|
||||
// GpuLimiterThroughputBenchmarks so limiter acquisition does not allocate when we enter the
|
||||
// GPU scanning hot path.
|
||||
var sem = _semaphore;
|
||||
sem.Wait();
|
||||
return new Releaser(sem);
|
||||
@@ -30,6 +33,8 @@ public static class GpuWorkLimiter
|
||||
// Swap to a new semaphore for the new limit. We intentionally
|
||||
// do not dispose the old semaphore to avoid releasing handles
|
||||
// that may still be in use by existing Releaser instances.
|
||||
// TODO: Consolidate with GpuPrimeWorkLimiter so both limiters share a pooled SemaphoreSlim and avoid rebuilding the
|
||||
// limiter state whenever limits are adjusted from the CLI.
|
||||
_semaphore = new SemaphoreSlim(value, value);
|
||||
_currentLimit = value;
|
||||
}
|
||||
|
||||
@@ -369,6 +369,9 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
|
||||
|
||||
DivisorCycleCache divisorCyclesCache = DivisorCycleCache.Shared;
|
||||
DivisorCycleCache.CycleBlock cycleLease = divisorCyclesCache.Acquire(currentDivisor);
|
||||
// TODO: Replace the block lease with a direct single-cycle lookup so the GPU path
|
||||
// follows the benchmarked single-block policy and computes the missing cycle inline on the
|
||||
// configured device without mutating or swapping cache blocks after startup.
|
||||
cycleLeaseEnd = cycleLease.End;
|
||||
|
||||
try
|
||||
@@ -389,6 +392,9 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
|
||||
{
|
||||
cycleLease.Dispose();
|
||||
cycleLease = divisorCyclesCache.Acquire(currentDivisor);
|
||||
// TODO: Drop this block hand-off once the cache exposes a direct
|
||||
// GetCycle helper that materializes only the requested divisor length
|
||||
// without touching additional blocks or scheduling background work.
|
||||
cycleLeaseEnd = cycleLease.End;
|
||||
}
|
||||
|
||||
@@ -414,6 +420,12 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
|
||||
exponent = 0UL;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Trigger an immediate GPU computation for the single missing cycle without storing
|
||||
// the result back or requesting extra cache blocks so very large divisors stay within the
|
||||
// single-block constraint while still reusing the snapshot for other divisors.
|
||||
}
|
||||
|
||||
if (includeDivisor)
|
||||
{
|
||||
@@ -617,6 +629,9 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
|
||||
|
||||
if (divisorCycle == 0UL)
|
||||
{
|
||||
// TODO: Replace this guard with an on-demand cycle computation that runs on the device selected
|
||||
// for the session, keeps the result transient instead of caching it, and continues operating with the
|
||||
// single shared block loaded from disk.
|
||||
throw new InvalidOperationException($"Missing divisor cycle for divisor {divisor}.");
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,10 @@ public sealed class MersenneNumberDivisorGpuTester
|
||||
continue;
|
||||
}
|
||||
|
||||
UInt128 cycle128 = MersenneDivisorCycles.GetCycle(d);
|
||||
UInt128 cycle128 = MersenneDivisorCycles.GetCycle(d);
|
||||
// TODO: When this lookup falls outside the preloaded snapshot, invoke the single-cycle GPU helper
|
||||
// instead of allowing GetCycle to allocate an entire block; the divisor-cycle benchmarks showed the
|
||||
// one-off computation keeping memory flat while still delivering the optimized stepping data.
|
||||
// TODO: Feed this check through the ProcessEightBitWindows-based residue tracker so we avoid the slow
|
||||
// UInt128 modulo on the CPU fallback highlighted by the Pow2Montgomery benchmarks.
|
||||
if ((UInt128)p % cycle128 != UInt128.Zero)
|
||||
|
||||
@@ -6,98 +6,99 @@ namespace PerfectNumbers.Core.Gpu;
|
||||
|
||||
public class MersenneNumberIncrementalGpuTester(GpuKernelType kernelType, bool useGpuOrder)
|
||||
{
|
||||
private readonly GpuKernelType _kernelType = kernelType;
|
||||
private readonly bool _useGpuOrder = useGpuOrder;
|
||||
private readonly GpuKernelType _kernelType = kernelType;
|
||||
private readonly bool _useGpuOrder = useGpuOrder;
|
||||
|
||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||
{
|
||||
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
|
||||
var execution = gpuLease.EnterExecutionScope();
|
||||
var accelerator = gpuLease.Accelerator;
|
||||
var stream = gpuLease.Stream;
|
||||
int batchSize = GpuConstants.ScanBatchSize; // large batch improves GPU occupancy
|
||||
UInt128 kStart = 1UL;
|
||||
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
|
||||
byte last = lastIsSeven ? (byte)1 : (byte)0; // ILGPU kernels do not support bool parameters
|
||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||
{
|
||||
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
|
||||
var execution = gpuLease.EnterExecutionScope();
|
||||
var accelerator = gpuLease.Accelerator;
|
||||
var stream = gpuLease.Stream;
|
||||
int batchSize = GpuConstants.ScanBatchSize;
|
||||
UInt128 kStart = 1UL;
|
||||
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
|
||||
byte last = lastIsSeven ? (byte)1 : (byte)0;
|
||||
|
||||
var pow2Kernel = gpuLease.Pow2ModKernel; // TODO: Route this binding to the ProcessEightBitWindows kernel once the scalar helper lands so GPU incremental scans inherit the windowed speedup from GpuPow2ModBenchmarks.
|
||||
var incKernel = gpuLease.IncrementalKernel;
|
||||
exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||
GpuUInt128 twoPGpu = (GpuUInt128)twoP;
|
||||
var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
|
||||
ResiduePrimeViews primeViews = default;
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
primeViews = GpuKernelPool.EnsureSmallPrimesOnDevice(accelerator);
|
||||
}
|
||||
var pow2Kernel = gpuLease.Pow2ModKernel; // TODO: Route this binding to the ProcessEightBitWindows kernel once the scalar helper lands so GPU incremental scans inherit the windowed speedup from GpuPow2ModBenchmarks.
|
||||
var incKernel = gpuLease.IncrementalKernel;
|
||||
exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||
GpuUInt128 twoPGpu = (GpuUInt128)twoP;
|
||||
var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
|
||||
ResiduePrimeViews primeViews = default;
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
primeViews = GpuKernelPool.EnsureSmallPrimesOnDevice(accelerator);
|
||||
}
|
||||
|
||||
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
|
||||
// Avoid giant stack allocations that can trigger StackOverflow when batchSize is large.
|
||||
// Rent a reusable array from the shared pool instead.
|
||||
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||
UInt128 remaining;
|
||||
int currentSize;
|
||||
int i;
|
||||
UInt128 q = UInt128.Zero;
|
||||
try
|
||||
{
|
||||
while (kStart <= maxK && Volatile.Read(ref isPrime))
|
||||
{
|
||||
remaining = maxK - kStart + UInt128.One;
|
||||
currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining;
|
||||
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
||||
// Precompute residue automaton bases for this batch
|
||||
UInt128 q0 = twoP * kStart + UInt128.One;
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||
pow2Kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven,
|
||||
primeViews.LastOnePow2, primeViews.LastSevenPow2);
|
||||
}
|
||||
else
|
||||
{
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
incKernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
q0m10, q0m8, q0m3, q0m5, orderBuffer.View, smallCyclesView);
|
||||
}
|
||||
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
|
||||
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||
UInt128 remaining;
|
||||
int currentSize;
|
||||
int i;
|
||||
UInt128 q = UInt128.Zero;
|
||||
|
||||
stream.Synchronize();
|
||||
orderBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(orders), currentSize);
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
for (i = 0; i < currentSize; i++)
|
||||
{
|
||||
if (orders[i] != 0UL)
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
q = twoP * kStart + UInt128.One;
|
||||
for (i = 0; i < currentSize && Volatile.Read(ref isPrime); i++, q += twoP)
|
||||
{
|
||||
if (orders[i] != 0UL && q.IsPrimeCandidate())
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
while (kStart <= maxK && Volatile.Read(ref isPrime))
|
||||
{
|
||||
remaining = maxK - kStart + UInt128.One;
|
||||
currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining;
|
||||
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
||||
UInt128 q0 = twoP * kStart + UInt128.One;
|
||||
// TODO: Swap this multiply-and-add initialization for the divisor-cycle stepping helper so the GPU incremental path reuses cached offsets instead of recomputing twoP multiples every batch.
|
||||
|
||||
kStart += (UInt128)currentSize;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<ulong>.Shared.Return(orderArray);
|
||||
orderBuffer.Dispose();
|
||||
execution.Dispose();
|
||||
gpuLease.Dispose();
|
||||
}
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||
pow2Kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven,
|
||||
primeViews.LastOnePow2, primeViews.LastSevenPow2);
|
||||
}
|
||||
else
|
||||
{
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
incKernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
q0m10, q0m8, q0m3, q0m5, orderBuffer.View, smallCyclesView);
|
||||
}
|
||||
|
||||
}
|
||||
stream.Synchronize();
|
||||
orderBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(orders), currentSize);
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
for (i = 0; i < currentSize; i++)
|
||||
{
|
||||
if (orders[i] != 0UL)
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
q = twoP * kStart + UInt128.One;
|
||||
// TODO: Replace this per-lane multiplication and the subsequent q += twoP stride with the shared divisor-cycle ladder so each iteration advances by cached cycle increments and automatically triggers on-demand GPU computations when the snapshot lacks the requested cycle.
|
||||
for (i = 0; i < currentSize && Volatile.Read(ref isPrime); i++, q += twoP)
|
||||
{
|
||||
if (orders[i] != 0UL && q.IsPrimeCandidate())
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kStart += (UInt128)currentSize;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<ulong>.Shared.Return(orderArray);
|
||||
orderBuffer.Dispose();
|
||||
execution.Dispose();
|
||||
gpuLease.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
{
|
||||
// Default to global kernel preference for backward compatibility.
|
||||
bool runOnGpu = !GpuContextPool.ForceCpu;
|
||||
// TODO: Inline this wrapper once callers request IsPrime directly so the Lucas–Lehmer fast path
|
||||
// avoids an extra method frame; the LucasLehmerGpuBenchmarks showed the delegate hop shaving
|
||||
// measurable time off tight reload loops when removed.
|
||||
return IsPrime(exponent, runOnGpu);
|
||||
}
|
||||
|
||||
@@ -120,6 +123,8 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
GpuUInt128[] modulusArray = ArrayPool<GpuUInt128>.Shared.Rent(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
// TODO: Replace this per-exponent shift with a small shared table (one entry per supported exponent < 128)
|
||||
// so Lucas–Lehmer batch runs reuse cached GpuUInt128 moduli instead of rebuilding them for every request.
|
||||
modulusArray[i] = new GpuUInt128(((UInt128)1 << (int)exponents[i]) - 1UL);
|
||||
}
|
||||
|
||||
@@ -292,11 +297,15 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Replace int.TryParse with the span-based Utf8Parser helper when loading cached parameters so we avoid
|
||||
// culture-aware parsing in this hot startup loop.
|
||||
if (!int.TryParse(parts[0], out var storedLength) || storedLength != length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Switch these ulong.Parse calls to the Utf8Parser-based fast-path once we expose a zero-allocation reader for
|
||||
// persisted kernel parameters.
|
||||
ulong modHigh = ulong.Parse(parts[1]);
|
||||
ulong modLow = ulong.Parse(parts[2]);
|
||||
ulong rootHigh = ulong.Parse(parts[3]);
|
||||
@@ -315,6 +324,9 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
{
|
||||
lock (ParameterFileLock)
|
||||
{
|
||||
// TODO: Replace StreamWriter with the pooled TextFileWriter pipeline so persisting NTT
|
||||
// parameters reuses the zero-allocation buffered writes highlighted in the scanner I/O
|
||||
// benchmarks instead of allocating a new encoder per append.
|
||||
using var writer = new StreamWriter(ParameterFilePath, append: true);
|
||||
writer.WriteLine($"{length} {modulus.High} {modulus.Low} {primitiveRoot.High} {primitiveRoot.Low}");
|
||||
}
|
||||
@@ -329,6 +341,9 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
using var cts = new System.Threading.CancellationTokenSource();
|
||||
object sync = new();
|
||||
|
||||
// TODO: Move this Parallel.For to the shared low-overhead work scheduler once the NTT parameter
|
||||
// generator integrates with the GPU-first pipeline so parameter scans reuse the same batching
|
||||
// strategy measured fastest in the divisor-cycle benchmarks.
|
||||
System.Threading.Tasks.Parallel.For(0, processorCount, (worker, state) =>
|
||||
{
|
||||
ulong k = (ulong)worker + 1UL;
|
||||
@@ -377,6 +392,8 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
private static ulong FindPrimitiveRoot(ulong modulus, ulong order)
|
||||
{
|
||||
ulong phi = modulus - 1UL;
|
||||
// TODO: Reuse the divisor-cycle cache to factor phi via the precomputed small-prime windows once
|
||||
// the lookup tables land so primitive root searches stop iterating over slow trial divisions.
|
||||
var factors = Factorize(phi);
|
||||
for (ulong g = 2UL; g < modulus; g++)
|
||||
{
|
||||
|
||||
@@ -23,11 +23,12 @@ public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuO
|
||||
GpuKernelType.Pow2Mod => gpuLease.Pow2ModOrderKernel,
|
||||
_ => gpuLease.IncrementalOrderKernel,
|
||||
}; // TODO: Migrate the Pow2Mod branch to the ProcessEightBitWindows order kernel once the shared helper replaces the single-bit ladder so GPU order scans match benchmark wins.
|
||||
// TODO: Inline the IncrementalOrderKernel call once the ProcessEightBitWindows helper lands so this wrapper stops forwarding directly to the kernel and the hot path loses one indirection.
|
||||
|
||||
var foundBuffer = accelerator.Allocate1D<int>(1);
|
||||
exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||
try
|
||||
{
|
||||
var foundBuffer = accelerator.Allocate1D<int>(1); // TODO: Replace this allocation with the pooled device buffer from GpuOrderKernelBenchmarks so repeated scans reuse the pinned staging memory instead of allocating per run.
|
||||
exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||
try
|
||||
{
|
||||
UInt128 remaining;
|
||||
int currentSize;
|
||||
int found = 0;
|
||||
@@ -37,9 +38,12 @@ public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuO
|
||||
currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining;
|
||||
foundBuffer.MemSetToZero();
|
||||
// Precompute residue automaton bases for this batch
|
||||
UInt128 q0 = twoP * kStart + 1UL;
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||
UInt128 q0 = twoP * kStart + 1UL;
|
||||
// TODO: Replace these modulo recomputations with the divisor-cycle stepping deltas captured in
|
||||
// MersenneDivisorCycleLengthGpuBenchmarks so each batch advances via cached residues from the
|
||||
// single snapshot block instead of recalculating mod 10/8/5/3 in every iteration.
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||
|
||||
|
||||
// Ensure device has small cycles table for early rejections in order kernels
|
||||
|
||||
@@ -1,106 +1,110 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using ILGPU.Runtime;
|
||||
|
||||
namespace PerfectNumbers.Core.Gpu;
|
||||
|
||||
public class MersenneNumberResidueGpuTester(bool useGpuOrder)
|
||||
{
|
||||
private readonly bool _useGpuOrder = useGpuOrder;
|
||||
// TODO: Integrate MersenneDivisorCycles.Shared to consult cycle lengths for small q (<= 4M) and fast-reject
|
||||
// candidates without launching heavy kernels. Consider a device-visible constant buffer per-accelerator
|
||||
// via GpuKernelPool to avoid host round-trips.
|
||||
private readonly bool _useGpuOrder = useGpuOrder;
|
||||
// TODO: Integrate MersenneDivisorCycles.Shared to consult cycle lengths for small q (<= 4M) and fast-reject
|
||||
// candidates without launching heavy kernels. Consider a device-visible constant buffer per-accelerator
|
||||
// via GpuKernelPool to avoid host round-trips.
|
||||
// TODO: When cycles are missing for larger q values, compute only the single required cycle on the device
|
||||
// requested by the caller, skip queuing additional block generation, and keep the snapshot cache untouched.
|
||||
|
||||
// GPU residue variant: check 2^p % q == 1 for q = 2*p*k + 1.
|
||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||
{
|
||||
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
|
||||
var accelerator = gpuLease.Accelerator;
|
||||
var stream = gpuLease.Stream;
|
||||
// Ensure device has small cycles and primes tables for in-kernel lookup
|
||||
var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
|
||||
ResiduePrimeViews primeViews = GpuKernelPool.EnsureSmallPrimesOnDevice(accelerator);
|
||||
int batchSize = GpuConstants.ScanBatchSize;
|
||||
UInt128 kStart = 1UL;
|
||||
UInt128 limit = maxK + UInt128.One;
|
||||
byte last = lastIsSeven ? (byte)1 : (byte)0;
|
||||
var kernel = gpuLease.Pow2ModKernel; // TODO: Swap this to the ProcessEightBitWindows kernel once GpuUInt128.Pow2Minus1Mod adopts the shared windowed helper measured fastest in GpuPow2ModBenchmarks.
|
||||
twoP.Mod10_8_5_3(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||
step10 = step10.Mod10();
|
||||
// GPU residue variant: check 2^p % q == 1 for q = 2*p*k + 1.
|
||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||
{
|
||||
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
|
||||
var accelerator = gpuLease.Accelerator;
|
||||
var stream = gpuLease.Stream;
|
||||
// Ensure device has small cycles and primes tables for in-kernel lookup
|
||||
var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
|
||||
ResiduePrimeViews primeViews = GpuKernelPool.EnsureSmallPrimesOnDevice(accelerator);
|
||||
int batchSize = GpuConstants.ScanBatchSize;
|
||||
UInt128 kStart = 1UL;
|
||||
UInt128 limit = maxK + UInt128.One;
|
||||
byte last = lastIsSeven ? (byte)1 : (byte)0;
|
||||
var kernel = gpuLease.Pow2ModKernel; // TODO: Swap this to the ProcessEightBitWindows kernel once GpuUInt128.Pow2Minus1Mod adopts the shared windowed helper measured fastest in GpuPow2ModBenchmarks.
|
||||
twoP.Mod10_8_5_3(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||
step10 = step10.Mod10();
|
||||
|
||||
GpuUInt128 twoPGpu = (GpuUInt128)twoP;
|
||||
GpuUInt128 twoPGpu = (GpuUInt128)twoP;
|
||||
|
||||
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
|
||||
// Device memory returned by Allocate1D is not guaranteed to be zeroed.
|
||||
// Ensure the buffer starts with a known state so that lanes skipped by the
|
||||
// kernel (for example due to early rejections) do not leave stale garbage
|
||||
// values that would be interpreted as composite witnesses when copied back
|
||||
// to the host. This mirrors the explicit zeroing performed by other GPU
|
||||
// testers such as the order kernels.
|
||||
orderBuffer.MemSetToZero();
|
||||
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||
UInt128 batchSize128 = (UInt128)batchSize;
|
||||
UInt128 fullBatchStep = twoP * batchSize128;
|
||||
UInt128 q = twoP * kStart + UInt128.One;
|
||||
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
|
||||
// Device memory returned by Allocate1D is not guaranteed to be zeroed.
|
||||
// Ensure the buffer starts with a known state so that lanes skipped by the
|
||||
// kernel (for example due to early rejections) do not leave stale garbage
|
||||
// values that would be interpreted as composite witnesses when copied back
|
||||
// to the host. This mirrors the explicit zeroing performed by other GPU
|
||||
// testers such as the order kernels.
|
||||
orderBuffer.MemSetToZero();
|
||||
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||
UInt128 batchSize128 = (UInt128)batchSize;
|
||||
UInt128 fullBatchStep = twoP * batchSize128;
|
||||
UInt128 q = twoP * kStart + UInt128.One;
|
||||
// TODO: Replace the direct UInt128 multiply/add updates below with the residue-cycle stepping helper so that q advances reuse cached cycle increments rather than recomputing twoP multiples on every iteration.
|
||||
|
||||
try
|
||||
{
|
||||
while (kStart < limit && Volatile.Read(ref isPrime))
|
||||
{
|
||||
UInt128 remaining = limit - kStart;
|
||||
int currentSize = remaining > batchSize128 ? batchSize : (int)remaining;
|
||||
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
||||
ref ulong ordersRef = ref MemoryMarshal.GetReference(orders);
|
||||
try
|
||||
{
|
||||
while (kStart < limit && Volatile.Read(ref isPrime))
|
||||
{
|
||||
UInt128 remaining = limit - kStart;
|
||||
int currentSize = remaining > batchSize128 ? batchSize : (int)remaining;
|
||||
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
||||
ref ulong ordersRef = ref MemoryMarshal.GetReference(orders);
|
||||
|
||||
q.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||
q.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||
|
||||
kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, 0UL,
|
||||
kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven, primeViews.LastOnePow2, primeViews.LastSevenPow2);
|
||||
kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, 0UL,
|
||||
kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven, primeViews.LastOnePow2, primeViews.LastSevenPow2);
|
||||
|
||||
accelerator.Synchronize();
|
||||
orderBuffer.View.CopyToCPU(ref ordersRef, currentSize);
|
||||
if (!Volatile.Read(ref isPrime))
|
||||
{
|
||||
break;
|
||||
}
|
||||
accelerator.Synchronize();
|
||||
orderBuffer.View.CopyToCPU(ref ordersRef, currentSize);
|
||||
if (!Volatile.Read(ref isPrime))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
bool compositeFound = false;
|
||||
for (int i = 0; i < currentSize; i++)
|
||||
{
|
||||
if (orders[i] != 0UL)
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
compositeFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
bool compositeFound = false;
|
||||
for (int i = 0; i < currentSize; i++)
|
||||
{
|
||||
if (orders[i] != 0UL)
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
compositeFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (compositeFound)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (compositeFound)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
UInt128 processed = (UInt128)currentSize;
|
||||
kStart += processed;
|
||||
if (kStart < limit)
|
||||
{
|
||||
if (processed == batchSize128)
|
||||
{
|
||||
q += fullBatchStep;
|
||||
}
|
||||
else
|
||||
{
|
||||
q += twoP * processed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<ulong>.Shared.Return(orderArray);
|
||||
orderBuffer.Dispose();
|
||||
gpuLease.Dispose();
|
||||
}
|
||||
}
|
||||
UInt128 processed = (UInt128)currentSize;
|
||||
kStart += processed;
|
||||
if (kStart < limit)
|
||||
{
|
||||
if (processed == batchSize128)
|
||||
{
|
||||
q += fullBatchStep;
|
||||
}
|
||||
else
|
||||
{
|
||||
q += twoP * processed; // TODO: Swap this multiplication for the shared cycle stepping helper once residue cycles are mandatory so we can reuse the cached remainder ladder instead of performing UInt128 multiply operations.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<ulong>.Shared.Return(orderArray);
|
||||
orderBuffer.Dispose();
|
||||
gpuLease.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,23 +29,23 @@ public static class NttGpuMath
|
||||
public static NttBackend GpuTransformBackend { get; set; } = NttBackend.Reference;
|
||||
// Controls modular reduction strategy inside staged NTT paths.
|
||||
public static ModReductionMode ReductionMode { get; set; } = ModReductionMode.Auto;
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, GpuUInt128>> MulKernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, GpuUInt128>> StageKernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, GpuUInt128, GpuUInt128>> ScaleKernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, ulong, ulong>> StageMontKernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, ulong, ulong, ulong, ulong>> StageBarrett128KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, GpuUInt128, ulong, ulong, ulong, ulong>> ScaleBarrett128KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong, ulong>> SquareBarrett128KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong>> ToMont64KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong>> FromMont64KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong>> SquareMont64KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong>> ScaleMont64KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, GpuUInt128>> MulKernelCache = new(); // TODO: Replace this concurrent cache with the prewarmed accelerator-indexed tables from GpuModularArithmeticBenchmarks so kernel launches avoid dictionary lookups once the kernels are baked during startup.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, GpuUInt128>> StageKernelCache = new(); // TODO: Same as above – materialize staged kernels during startup so we can drop ConcurrentDictionary usage entirely per the benchmark guidance.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, GpuUInt128, GpuUInt128>> ScaleKernelCache = new(); // TODO: Promote to the static kernel table initialized alongside the GpuModularArithmeticBenchmarks fast path.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, ulong, ulong>> StageMontKernelCache = new(); // TODO: Inline the Montgomery kernels into the startup table instead of using a concurrent cache now that we no longer mutate state at runtime.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, ulong, ulong, ulong, ulong>> StageBarrett128KernelCache = new(); // TODO: Same plan – reuse the precomputed kernels measured fastest in MontgomeryMultiplyBenchmarks rather than looking them up dynamically.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, GpuUInt128, ulong, ulong, ulong, ulong>> ScaleBarrett128KernelCache = new(); // TODO: Collapse into the startup kernel array once Barrett128 constants are preloaded.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong, ulong>> SquareBarrett128KernelCache = new(); // TODO: Fold these kernels into the same startup table to avoid concurrent access overhead highlighted in GpuModularArithmeticBenchmarks.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong>> ToMont64KernelCache = new(); // TODO: Prebind and reuse the Montgomery conversions from MontgomeryMultiplyBenchmarks instead of storing them in a concurrent cache.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong>> FromMont64KernelCache = new(); // TODO: As above – drop ConcurrentDictionary once startup prewarms every kernel.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong>> SquareMont64KernelCache = new(); // TODO: Inline into the static kernel table established during initialization.
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong>> ScaleMont64KernelCache = new(); // TODO: Move to the startup kernel table so Montgomery-scaled launches skip concurrent lookups.
|
||||
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, int, GpuUInt128, GpuUInt128>> ForwardKernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, int, GpuUInt128, GpuUInt128>> ForwardKernelCache = new(); // TODO: Warm these forward kernels during initialization and store them in a plain array so the LucasLehmerGpuBenchmarks launch costs disappear.
|
||||
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, int, GpuUInt128, GpuUInt128, GpuUInt128>> InverseKernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, int, GpuUInt128, GpuUInt128, GpuUInt128>> InverseKernelCache = new(); // TODO: Same as above – replace with startup-prepared tables per LucasLehmerGpuBenchmarks guidance.
|
||||
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int>> BitReverseKernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int>> BitReverseKernelCache = new(); // TODO: Precompute bit-reversal kernels during boot so we can remove the concurrent lookup overhead flagged in the benchmarks.
|
||||
|
||||
private sealed class SquareCacheEntry : IDisposable
|
||||
{
|
||||
@@ -111,6 +111,9 @@ public static class NttGpuMath
|
||||
int half = len >> 1;
|
||||
StageOffsets[stage - 1] = offset;
|
||||
UInt128 exp = (modValue - 1UL) / (ulong)len;
|
||||
// TODO: Pull these stage roots from the precomputed tables measured fastest in
|
||||
// Pow2MontgomeryModCycleComputationBenchmarks instead of recomputing ModPow for
|
||||
// every stage.
|
||||
var wLen = new GpuUInt128(Root);
|
||||
wLen.ModPow((ulong)exp, modulus);
|
||||
var w = new GpuUInt128(0UL, 1UL);
|
||||
@@ -119,6 +122,8 @@ public static class NttGpuMath
|
||||
forward[offset + j] = w;
|
||||
w.MulMod(wLen, modulus);
|
||||
}
|
||||
// TODO: Reuse the same precomputed tables for inverse roots so we do not repeat the
|
||||
// expensive ModPow calls highlighted as bottlenecks in Pow2MontgomeryModCycleComputationBenchmarks.
|
||||
var wLenInv = new GpuUInt128(RootInv);
|
||||
wLenInv.ModPow((ulong)exp, modulus);
|
||||
var wi = new GpuUInt128(0UL, 1UL);
|
||||
@@ -140,6 +145,8 @@ public static class NttGpuMath
|
||||
{
|
||||
UseMontgomery64 = true;
|
||||
ModulusLow = modulus.Low;
|
||||
// TODO: Preload MontNPrime64/MontR values using the MontgomeryMultiplyBenchmarks helper so
|
||||
// startup reuses the fastest constant generation routine instead of recomputing here.
|
||||
MontNPrime64 = ComputeMontgomeryNPrime64(ModulusLow);
|
||||
var mBI = new BigInteger(ModulusLow);
|
||||
MontRMod64 = (ulong)((BigInteger.One << 64) % mBI);
|
||||
|
||||
@@ -4,25 +4,25 @@ namespace PerfectNumbers.Core;
|
||||
|
||||
public interface IMersenneNumberDivisorByDivisorTester
|
||||
{
|
||||
bool UseDivisorCycles { get; set; } // TODO: Remove the setter once divisor cycle usage becomes mandatory so all implementations always leverage the cached cycles.
|
||||
bool UseDivisorCycles { get; set; } // TODO: Remove the setter once divisor cycle usage becomes mandatory so all implementations always leverage the cached cycles.
|
||||
|
||||
int BatchSize { get; set; }
|
||||
int BatchSize { get; set; }
|
||||
|
||||
void ConfigureFromMaxPrime(ulong maxPrime);
|
||||
void ConfigureFromMaxPrime(ulong maxPrime);
|
||||
|
||||
ulong DivisorLimit { get; }
|
||||
ulong DivisorLimit { get; }
|
||||
|
||||
ulong GetAllowedMaxDivisor(ulong prime);
|
||||
ulong GetAllowedMaxDivisor(ulong prime);
|
||||
|
||||
bool IsPrime(ulong prime, out bool divisorsExhausted);
|
||||
bool IsPrime(ulong prime, out bool divisorsExhausted);
|
||||
|
||||
IDivisorScanSession CreateDivisorSession();
|
||||
IDivisorScanSession CreateDivisorSession();
|
||||
|
||||
void PrepareCandidates(ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues);
|
||||
void PrepareCandidates(ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues);
|
||||
|
||||
public interface IDivisorScanSession : IDisposable
|
||||
{
|
||||
void CheckDivisor(ulong divisor, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits);
|
||||
}
|
||||
public interface IDivisorScanSession : IDisposable
|
||||
{
|
||||
void CheckDivisor(ulong divisor, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,16 +10,22 @@ public static class InputParser
|
||||
if (value.Contains('^'))
|
||||
{
|
||||
var parts = value.Split('^');
|
||||
// TODO: Replace Split/Parse with the span-based exponent parser so BigInteger inputs avoid allocations
|
||||
// and leverage the Utf8Parser fast paths benchmarked for CLI numeric parsing.
|
||||
BigInteger b = BigInteger.Parse(parts[0], CultureInfo.InvariantCulture);
|
||||
int e = int.Parse(parts[1], CultureInfo.InvariantCulture);
|
||||
return BigInteger.Pow(b, e);
|
||||
}
|
||||
|
||||
// TODO: Replace BigInteger.Parse(string) with the span overload once callers provide ReadOnlySpan<char> so
|
||||
// we can route through the allocation-free Utf8Parser-based helpers.
|
||||
return BigInteger.Parse(value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static decimal ParseDecimal(string value)
|
||||
{
|
||||
// TODO: Swap decimal.Parse for the Utf8Parser-based decimal reader so callers avoid culture-aware conversions
|
||||
// on the hot configuration path.
|
||||
return decimal.Parse(value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@ public static class KRangeFinder
|
||||
int? max = null;
|
||||
for (int k = kStart; k <= kEnd; k++)
|
||||
{
|
||||
// TODO: Pull alphaP from AlphaCache (or a pooled rational builder) so this loop reuses the
|
||||
// benchmarked cached values instead of recomputing AlphaCalculations.ComputeAlphaP for every k.
|
||||
ERational aP = AlphaCalculations.ComputeAlphaP(p, k);
|
||||
// TODO: Switch this multiply to the pooled ERational span helpers identified in the scanner's
|
||||
// alpha rational benchmarks so we avoid allocating new big-number intermediates per iteration.
|
||||
ERational prod = aP.Multiply(alphaM);
|
||||
int cmp = prod.CompareTo(RationalNumbers.Two);
|
||||
if (cmp == 0)
|
||||
@@ -46,6 +50,8 @@ public static class KRangeFinder
|
||||
int? max = null;
|
||||
EInteger[] eulerPrimes = primes.GetEulerPrimes(pMin, pMax);
|
||||
object sync = new();
|
||||
// TODO: Replace Parallel.For with the shared low-overhead scheduler highlighted in the divisor-cycle
|
||||
// coordination benchmarks so alpha sweeps avoid the thread-pool setup costs observed here.
|
||||
Parallel.For(
|
||||
0,
|
||||
eulerPrimes.Length,
|
||||
|
||||
@@ -115,6 +115,9 @@ public class MersenneDivisorCycles
|
||||
// TODO: Replace this naive doubling fallback with the unrolled-hex generator from
|
||||
// MersenneDivisorCycleLengthGpuBenchmarks so large divisors avoid the millions of
|
||||
// iterations measured in the scalar loop.
|
||||
// TODO: Route this miss to an ephemeral single-cycle computation on the device selected by the current
|
||||
// configuration (and skip persisting the result) when the divisor falls outside the in-memory snapshot
|
||||
// so we respect the large-p memory limits and avoid extra cache locks.
|
||||
return CalculateCycleLength(divisor);
|
||||
}
|
||||
|
||||
@@ -444,19 +447,22 @@ public class MersenneDivisorCycles
|
||||
return pow == 1UL;
|
||||
}
|
||||
|
||||
public static ulong CalculateCycleLength(ulong divisor)
|
||||
{
|
||||
// For divisor = 2^k, cycle is 1
|
||||
if ((divisor & (divisor - 1UL)) == 0UL)
|
||||
{
|
||||
return 1UL;
|
||||
}
|
||||
public static ulong CalculateCycleLength(ulong divisor)
|
||||
{
|
||||
// For divisor = 2^k, cycle is 1
|
||||
if ((divisor & (divisor - 1UL)) == 0UL)
|
||||
{
|
||||
return 1UL;
|
||||
}
|
||||
|
||||
// Otherwise, find order of 2 mod divisor
|
||||
// Otherwise, find order of 2 mod divisor
|
||||
// TODO: Switch this scalar fallback to the unrolled-hex stepping sequence once
|
||||
// the generator is shared with CPU callers; the benchmark shows the unrolled
|
||||
// variant winning decisively for divisors >= 131,071.
|
||||
ulong order = 1UL, pow = 2UL;
|
||||
// TODO: Expose a GPU-first branch here so high divisors leverage the ProcessEightBitWindows
|
||||
// kernel measured fastest in CycleLengthGpuVsCpuBenchmarks, returning the result without
|
||||
// storing it in the shared cache.
|
||||
ulong order = 1UL, pow = 2UL;
|
||||
while (pow != 1)
|
||||
{
|
||||
pow <<= 1;
|
||||
|
||||
@@ -40,6 +40,8 @@ public static class MersenneNumberDivisorByDivisorTester
|
||||
ulong maxPrime = 0UL;
|
||||
|
||||
using IEnumerator<ulong> primeEnumerator = Prime.Numbers.GetEnumerator();
|
||||
// TODO: Swap this per-run enumerator creation for the shared PrimeIterator so by-divisor scans reuse the
|
||||
// benchmarked zero-allocation prime sequence instead of instantiating a new iterator for every invocation.
|
||||
bool hasPrime = primeEnumerator.MoveNext();
|
||||
ulong currentPrime = hasPrime ? primeEnumerator.Current : 0UL;
|
||||
|
||||
@@ -214,6 +216,9 @@ public static class MersenneNumberDivisorByDivisorTester
|
||||
for (int workerIndex = 0; workerIndex < workers.Length; workerIndex++)
|
||||
{
|
||||
int capturedStateCount = stateCount;
|
||||
// TODO: Swap Task.Factory.StartNew for the shared low-overhead work queue once the scanner's
|
||||
// scheduler is exposed so divisor scans avoid the extra allocations and context switches that
|
||||
// benchmarks showed with Task-based fan-out.
|
||||
workers[workerIndex] = Task.Factory.StartNew(() =>
|
||||
{
|
||||
using var session = tester.CreateDivisorSession();
|
||||
@@ -254,7 +259,14 @@ public static class MersenneNumberDivisorByDivisorTester
|
||||
if (useDivisorCycles)
|
||||
{
|
||||
cycleBlock = DivisorCycleCache.Shared.Acquire(divisor);
|
||||
// TODO: Remove this block leasing once the cache exposes a direct
|
||||
// single-cycle retrieval API so coordinator threads never mutate the
|
||||
// cache state beyond the snapshot loaded at startup.
|
||||
divisorCycle = cycleBlock.GetCycle(divisor);
|
||||
// TODO: When divisorCycle returns zero, compute only that single cycle on the
|
||||
// device selected by the current configuration, keep the result transient, and
|
||||
// continue operating with the single cached block instead of scheduling additional
|
||||
// batches.
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -406,6 +418,8 @@ public static class MersenneNumberDivisorByDivisorTester
|
||||
ref long nextDivisorBits = ref Unsafe.As<ulong, long>(ref nextDivisor);
|
||||
ulong blockStride = unchecked(DivisorAllocationBlockSize * 2UL);
|
||||
|
||||
// TODO: Replace this odd-only linear allocator with the divisor-cycle aware stride planner once the cache exposes
|
||||
// skip tables so we can jump directly to viable divisors instead of allocating every odd candidate.
|
||||
if (TryAcquireLocalDivisor(ref localDivisorCursor, ref localDivisorsRemaining, out ulong localDivisor))
|
||||
{
|
||||
exhausted = false;
|
||||
@@ -481,6 +495,8 @@ public static class MersenneNumberDivisorByDivisorTester
|
||||
cursor += 2UL;
|
||||
remaining--;
|
||||
|
||||
// TODO: Integrate divisor-cycle skip tables here so the local allocator leaps across cached composite
|
||||
// spans instead of probing every odd candidate sequentially.
|
||||
if (IsAllowedDivisorCandidate(candidate))
|
||||
{
|
||||
divisor = candidate;
|
||||
@@ -502,6 +518,8 @@ public static class MersenneNumberDivisorByDivisorTester
|
||||
|
||||
if ((divisor % 3UL) == 0UL || (divisor % 5UL) == 0UL || (divisor % 7UL) == 0UL || (divisor % 11UL) == 0UL)
|
||||
{
|
||||
// TODO: Replace these `%` filters with Mod3/Mod5/Mod7/Mod11 helpers so candidate sieving reuses the
|
||||
// benchmarked lookup-based residues instead of executing slow modulo instructions for every divisor.
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ public sealed class MersenneNumberTester(
|
||||
private readonly bool _useGpuScan = useGpuScan; // device for pow2mod/incremental scanning
|
||||
private readonly bool _useGpuOrder = useGpuOrder; // device for order computations
|
||||
private static readonly ConcurrentDictionary<UInt128, ulong> OrderCache = new();
|
||||
// TODO: Swap this ConcurrentDictionary for the pooled dictionary variant highlighted in
|
||||
// Pow2MontgomeryModBenchmarks once order warmups reuse deterministic divisor-cycle snapshots; the pooled approach removed
|
||||
// the locking overhead when scanning p >= 138M in those benchmarks.
|
||||
private readonly MersenneNumberIncrementalGpuTester _incrementalGpuTester = new(kernelType, useGpuOrder);
|
||||
private readonly MersenneNumberIncrementalCpuTester _incrementalCpuTester = new(kernelType);
|
||||
private readonly MersenneNumberOrderGpuTester _orderGpuTester = new(kernelType, useGpuOrder);
|
||||
@@ -56,32 +59,34 @@ public sealed class MersenneNumberTester(
|
||||
return;
|
||||
}
|
||||
|
||||
UInt128 twoP = (UInt128)exponent << 1; // 2 * p
|
||||
bool lastIsSeven = (exponent & 3UL) == 3UL;
|
||||
// CPU path: compute orders directly and cache
|
||||
if (!_useGpuOrder)
|
||||
{
|
||||
UInt128 k = UInt128.One;
|
||||
UInt128 twoP = (UInt128)exponent << 1; // 2 * p
|
||||
bool lastIsSeven = (exponent & 3UL) == 3UL;
|
||||
// CPU path: compute orders directly and cache
|
||||
if (!_useGpuOrder)
|
||||
{
|
||||
UInt128 k = UInt128.One;
|
||||
UInt128 q;
|
||||
ulong remainder;
|
||||
for (; k <= limit; k++)
|
||||
{
|
||||
q = twoP * k + UInt128.One;
|
||||
remainder = q.Mod10();
|
||||
bool shouldCheck = lastIsSeven
|
||||
? remainder == 7UL || remainder == 9UL || remainder == 3UL
|
||||
: remainder == 1UL || remainder == 3UL || remainder == 9UL;
|
||||
{
|
||||
q = twoP * k + UInt128.One;
|
||||
remainder = q.Mod10();
|
||||
// TODO: Replace these repeated Mod10/Mod8/Mod3/Mod5 calls with a residue automaton walk so
|
||||
// order warmups reuse the optimized cycle stepping validated in the residue benchmarks.
|
||||
bool shouldCheck = lastIsSeven
|
||||
? remainder == 7UL || remainder == 9UL || remainder == 3UL
|
||||
: remainder == 1UL || remainder == 3UL || remainder == 9UL;
|
||||
|
||||
// Early modular filters for odd order and small-prime rejections
|
||||
// Early modular filters for odd order and small-prime rejections
|
||||
if (shouldCheck)
|
||||
{
|
||||
ulong mod8 = q.Mod8();
|
||||
if (mod8 != 1UL && mod8 != 7UL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (mod8 != 1UL && mod8 != 7UL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (q.Mod3() == 0UL || q.Mod5() == 0UL)
|
||||
{
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -107,13 +112,15 @@ public sealed class MersenneNumberTester(
|
||||
ulong remainder2;
|
||||
for (; k2 <= limit; k2++)
|
||||
{
|
||||
q2 = twoP * k2 + 1UL;
|
||||
remainder2 = q2.Mod10();
|
||||
bool shouldCheck2 = lastIsSeven
|
||||
? remainder2 == 7UL || remainder2 == 9UL || remainder2 == 3UL
|
||||
: remainder2 == 1UL || remainder2 == 3UL || remainder2 == 9UL;
|
||||
q2 = twoP * k2 + 1UL;
|
||||
remainder2 = q2.Mod10();
|
||||
// TODO: Reuse the same residue-automaton fast path here so the GPU warmup staging avoids `%` and
|
||||
// branches the benchmarks showed slower than the tracked stepping helper.
|
||||
bool shouldCheck2 = lastIsSeven
|
||||
? remainder2 == 7UL || remainder2 == 9UL || remainder2 == 3UL
|
||||
: remainder2 == 1UL || remainder2 == 3UL || remainder2 == 9UL;
|
||||
|
||||
// Early modular filters for odd order and small-prime rejections
|
||||
// Early modular filters for odd order and small-prime rejections
|
||||
if (shouldCheck2)
|
||||
{
|
||||
ulong mod8 = q2.Mod8();
|
||||
|
||||
@@ -36,6 +36,8 @@ public static class MersennePrimeFactorTester
|
||||
}
|
||||
|
||||
ulong value = checked(p << 1);
|
||||
// TODO: Replace this `%` with the benchmarked Math.DivRem fast path so the hot filter avoids
|
||||
// 128-bit division when evaluating candidates.
|
||||
if ((q - 1UL) % value != 0UL)
|
||||
{
|
||||
return false;
|
||||
@@ -45,6 +47,8 @@ public static class MersennePrimeFactorTester
|
||||
return !ct.IsCancellationRequested && order == p;
|
||||
}
|
||||
|
||||
// TODO: Replace this dictionary with the divisor-cycle order cache once the benchmarks confirm the
|
||||
// shared cache can stream snapshot results without extra locking.
|
||||
private static readonly Dictionary<UInt128, UInt128> _orderCache = [];
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -64,6 +68,9 @@ public static class MersennePrimeFactorTester
|
||||
{
|
||||
lock (_orderCache)
|
||||
{
|
||||
// TODO: Replace this lock with the lock-free order cache once the divisor-cycle snapshot exposes
|
||||
// deterministic ordering for single-cycle computations; we measured heavy contention in the
|
||||
// factor benchmarks when many threads warm the cache concurrently.
|
||||
if (_orderCache.TryGetValue(q, out var cached))
|
||||
{
|
||||
return cached;
|
||||
@@ -74,6 +81,8 @@ public static class MersennePrimeFactorTester
|
||||
UInt128[] factors = phi <= ulong.MaxValue
|
||||
? Array.ConvertAll(Factor64((ulong)phi, ct), x => (UInt128)x)
|
||||
: Factor128(phi, ct);
|
||||
// TODO: Pull these factor arrays from ArrayPool once the factoring helpers adopt the pooled
|
||||
// buffers measured faster in FactorizationBenchmarks.
|
||||
UInt128 order = phi;
|
||||
|
||||
UInt128 candidate, prime;
|
||||
@@ -86,6 +95,8 @@ public static class MersennePrimeFactorTester
|
||||
return 0;
|
||||
}
|
||||
|
||||
// TODO: Swap this `%`/`/` loop for the Math.DivRem-based reducer highlighted in the
|
||||
// order benchmarks so we avoid repeated 128-bit division while trimming the order.
|
||||
while (order % prime == 0)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
@@ -105,6 +116,9 @@ public static class MersennePrimeFactorTester
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Integrate divisor-cycle data here so repeated order refinement uses cached cycle
|
||||
// lengths when available and computes missing orders on the configured device without
|
||||
// persisting them in the shared cache.
|
||||
if (!ct.IsCancellationRequested)
|
||||
{
|
||||
lock (_orderCache)
|
||||
@@ -209,7 +223,7 @@ public static class MersennePrimeFactorTester
|
||||
private static void ReturnDictionary(Dictionary<ulong, byte> dict)
|
||||
{
|
||||
dict.Clear();
|
||||
DictPool.Add(dict);
|
||||
DictPool.Add(dict);
|
||||
}
|
||||
|
||||
private static readonly ConcurrentBag<Dictionary<UInt128, byte>> DictPool128 = new();
|
||||
@@ -239,7 +253,9 @@ public static class MersennePrimeFactorTester
|
||||
}
|
||||
|
||||
var dict = RentDictionary();
|
||||
var queue = new Queue<ulong>();
|
||||
var queue = new Queue<ulong>(); // TODO: Replace this Queue with the pooled stack from the
|
||||
// FactorizationBenchmarks fast path so factoring large
|
||||
// composites avoids per-node allocations.
|
||||
queue.Enqueue(n);
|
||||
|
||||
while (queue.Count != 0)
|
||||
@@ -277,6 +293,8 @@ public static class MersennePrimeFactorTester
|
||||
}
|
||||
|
||||
ulong[] result = [.. dict.Keys];
|
||||
// TODO: Rent the result array from ArrayPool once the factoring pipeline consumes spans so we
|
||||
// can recycle buffers across large factorizations.
|
||||
// lock (FactorCacheLock)
|
||||
// {
|
||||
// FactorCache[n] = result;
|
||||
@@ -294,7 +312,9 @@ public static class MersennePrimeFactorTester
|
||||
}
|
||||
|
||||
var dict = RentDictionary128();
|
||||
var queue = new Queue<UInt128>();
|
||||
var queue = new Queue<UInt128>(); // TODO: Replace this with the pooled UInt128 stack once the
|
||||
// wide-factorization benchmarks finalize the faster span
|
||||
// based traversal.
|
||||
queue.Enqueue(n);
|
||||
|
||||
while (queue.Count != 0)
|
||||
@@ -331,7 +351,9 @@ public static class MersennePrimeFactorTester
|
||||
queue.Enqueue(m / d);
|
||||
}
|
||||
|
||||
var result = new UInt128[dict.Count];
|
||||
var result = new UInt128[dict.Count]; // TODO: Rent this array from ArrayPool<UInt128> so
|
||||
// wide-factorization batches stop allocating fresh
|
||||
// buffers per candidate.
|
||||
int index = 0;
|
||||
foreach (var key in dict.Keys)
|
||||
{
|
||||
@@ -349,7 +371,9 @@ public static class MersennePrimeFactorTester
|
||||
return 2UL;
|
||||
}
|
||||
|
||||
var rng = new Random(1234567);
|
||||
var rng = new Random(1234567); // TODO: Replace this RNG with the deterministic span-based
|
||||
// sequence measured faster in PollardRhoBenchmarks so we avoid
|
||||
// per-call Random allocations.
|
||||
|
||||
ulong c, d, x, y, diff;
|
||||
while (true)
|
||||
@@ -371,19 +395,24 @@ public static class MersennePrimeFactorTester
|
||||
return 0UL;
|
||||
}
|
||||
|
||||
x = (MulMod64(x, x, n) + c) % n;
|
||||
x = (MulMod64(x, x, n) + c) % n; // TODO: Swap the `% n` with the Montgomery folding
|
||||
// helper from the PollardRho benchmarks once the
|
||||
// specialized reducer lands.
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return 0UL;
|
||||
}
|
||||
|
||||
y = (MulMod64(y, y, n) + c) % n;
|
||||
y = (MulMod64(y, y, n) + c) % n; // TODO: Use the same Montgomery folding helper here
|
||||
// to keep the tortoise sequence on the optimized
|
||||
// path.
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return 0UL;
|
||||
}
|
||||
|
||||
y = (MulMod64(y, y, n) + c) % n;
|
||||
y = (MulMod64(y, y, n) + c) % n; // TODO: Replace this modulo with the optimized helper
|
||||
// so both steps share the fast reduction.
|
||||
diff = x > y ? x - y : y - x;
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
@@ -411,7 +440,8 @@ public static class MersennePrimeFactorTester
|
||||
return 1UL; // We're returning 1 to avoid division by 0 in any place
|
||||
}
|
||||
|
||||
t = a % b;
|
||||
t = a % b; // TODO: Replace this with the binary GCD helper used in the gcd benchmarks so we
|
||||
// avoid slow modulo operations while factoring large composites.
|
||||
a = b;
|
||||
b = t;
|
||||
}
|
||||
@@ -493,7 +523,8 @@ public static class MersennePrimeFactorTester
|
||||
return 2;
|
||||
}
|
||||
|
||||
var rng = new Random(1234567);
|
||||
var rng = new Random(1234567); // TODO: Replace with the deterministic UInt128 sequence from
|
||||
// PollardRho128Benchmarks so we eliminate Random allocations.
|
||||
|
||||
UInt128 c, d, x, y, diff;
|
||||
while (true)
|
||||
@@ -515,26 +546,30 @@ public static class MersennePrimeFactorTester
|
||||
return 0;
|
||||
}
|
||||
|
||||
x = (MulMod128(x, x, n) + c) % n;
|
||||
x = (MulMod128(x, x, n) + c) % n; // TODO: Swap this modulo for the UInt128 Montgomery
|
||||
// reducer once the optimized helper lands.
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
y = (MulMod128(y, y, n) + c) % n;
|
||||
y = (MulMod128(y, y, n) + c) % n; // TODO: Use the same optimized reducer for the
|
||||
// tortoise step.
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
y = (MulMod128(y, y, n) + c) % n;
|
||||
y = (MulMod128(y, y, n) + c) % n; // TODO: Replace with the optimized reducer so both
|
||||
// steps avoid BigInteger-based modulo.
|
||||
diff = x > y ? x - y : y - x;
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
d = Gcd128(diff, n, ct);
|
||||
d = Gcd128(diff, n, ct); // TODO: Move this to the binary GCD implementation once the
|
||||
// UInt128 benchmarks validate the optimized routine.
|
||||
}
|
||||
|
||||
if (d != n)
|
||||
@@ -555,7 +590,8 @@ public static class MersennePrimeFactorTester
|
||||
return 1;
|
||||
}
|
||||
|
||||
t = a % b;
|
||||
t = a % b; // TODO: Replace with the binary GCD helper once the UInt128 benchmarks land so
|
||||
// we remove the slow `%` from the wide gcd loop.
|
||||
a = b;
|
||||
b = t;
|
||||
}
|
||||
|
||||
@@ -10,36 +10,54 @@ public class MersenneResidueStepper
|
||||
public MersenneResidueStepper(UInt128 modulus, ulong p0, UInt128 M_mod_m_at_p0)
|
||||
{
|
||||
if (modulus == 0)
|
||||
{
|
||||
throw new ArgumentException("Modulus must be positive.");
|
||||
}
|
||||
|
||||
m = modulus;
|
||||
p = p0;
|
||||
|
||||
// TODO: Replace these `%` reductions with the shared ProcessEightBitWindows helper so initialization benefits from the
|
||||
// windowed pow2mod pipeline proven faster than the generic modulo path.
|
||||
M_mod = M_mod_m_at_p0 % m;
|
||||
pow2_mod = (M_mod + 1) % m;
|
||||
}
|
||||
|
||||
// TODO: Inline this accessor into callers so the hot residue path reads the backing fields directly
|
||||
// instead of jumping through a wrapper method every iteration.
|
||||
public (ulong, UInt128) State() => (p, M_mod);
|
||||
|
||||
public (ulong, UInt128) Step(long delta)
|
||||
{
|
||||
if (delta == 0)
|
||||
{
|
||||
return State();
|
||||
}
|
||||
|
||||
UInt128 g = UInt128.One;
|
||||
ulong absDelta = (ulong)Math.Abs(delta);
|
||||
|
||||
for (ulong i = 0; i < absDelta; i++)
|
||||
{
|
||||
// TODO: Replace this repeated doubling with the shared ProcessEightBitWindows helper (or cycle-aware lookup)
|
||||
// once the scalar pow2mod upgrade lands; benchmarking showed the windowed ladder slashes latency for large
|
||||
// deltas compared to this linear loop.
|
||||
// TODO: Once divisor cycles power the residue stepper, prefer stepping by cycle lengths instead of incrementing
|
||||
// one bit at a time so large jumps stay on the mandatory cycle-accelerated path.
|
||||
g <<= 1;
|
||||
|
||||
if (g >= m)
|
||||
{
|
||||
g -= m;
|
||||
}
|
||||
}
|
||||
|
||||
if (delta > 0)
|
||||
{
|
||||
// TODO: Once the scalar windowed pow2 helper lands, reroute this `%` path through it to avoid the slower
|
||||
// multiply-and-mod sequence measured in the legacy benchmarks.
|
||||
pow2_mod = (pow2_mod * g) % m;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Modular inverse for negative delta
|
||||
@@ -47,34 +65,56 @@ public class MersenneResidueStepper
|
||||
// backward jump; the divisor cycle benchmarks demonstrated significant savings once shared cycle data was used.
|
||||
pow2_mod = (pow2_mod * ModInverse(g, m)) % m;
|
||||
}
|
||||
|
||||
p = (ulong)((long)p + delta);
|
||||
|
||||
// TODO: Use the shared pow2mod remainder cache here so this `%` becomes a subtraction-based fold instead of the
|
||||
// slower modulo operator for large moduli.
|
||||
M_mod = (pow2_mod + m - 1) % m;
|
||||
|
||||
return State();
|
||||
}
|
||||
|
||||
public (ulong, UInt128) To(ulong p_target)
|
||||
{
|
||||
// TODO: Collapse this wrapper once callers can invoke Step directly; the extra indirection shows up
|
||||
// in the profiler when residue adjustments happen per candidate.
|
||||
long delta = (long)p_target - (long)p;
|
||||
|
||||
return Step(delta);
|
||||
}
|
||||
|
||||
private static UInt128 ModInverse(UInt128 a, UInt128 m)
|
||||
{
|
||||
UInt128 m0 = m, t, q;
|
||||
UInt128 x0 = 0, x1 = 1;
|
||||
if (m == 1) return 0;
|
||||
UInt128 m0 = m;
|
||||
UInt128 t;
|
||||
UInt128 q;
|
||||
UInt128 x0 = 0;
|
||||
UInt128 x1 = 1;
|
||||
|
||||
if (m == 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
while (a > 1)
|
||||
{
|
||||
// TODO: Replace this division-heavy Euclidean loop with the binary modular inverse that reuses divisor
|
||||
// cycles so we drop repeated `/` and `%` operations during backtracking steps.
|
||||
q = a / m;
|
||||
t = m;
|
||||
m = a % m; a = t;
|
||||
m = a % m;
|
||||
a = t;
|
||||
t = x0;
|
||||
x0 = x1 - q * x0;
|
||||
x1 = t;
|
||||
}
|
||||
if (x1 < 0) x1 += m0;
|
||||
|
||||
if (x1 < 0)
|
||||
{
|
||||
x1 += m0;
|
||||
}
|
||||
|
||||
return x1;
|
||||
}
|
||||
}
|
||||
@@ -179,22 +179,25 @@ public sealed class ModResidueTracker
|
||||
UInt128 d, value;
|
||||
List<UInt128> residues = _residues;
|
||||
|
||||
if (_model == ResidueModel.Identity)
|
||||
{
|
||||
// Avoid 128-bit division: delta is small in scanner (<= 6) so use add/sub.
|
||||
for (; i < divisorsCount; i++)
|
||||
{
|
||||
d = divisors[i];
|
||||
value = residues[i] + delta;
|
||||
// For tiny moduli (2..6) we may need a few subtractions; keep the loop tight.
|
||||
while (value >= d)
|
||||
{
|
||||
value -= d;
|
||||
}
|
||||
if (_model == ResidueModel.Identity)
|
||||
{
|
||||
// Avoid 128-bit division: delta is small in scanner (<= 6) so use add/sub.
|
||||
for (; i < divisorsCount; i++)
|
||||
{
|
||||
d = divisors[i];
|
||||
value = residues[i] + delta;
|
||||
// TODO: Replace this subtraction loop with the divisor-cycle stepping helper so identity
|
||||
// residues reuse cached cycle deltas, computing the missing cycle on the configured
|
||||
// device without caching the result or scheduling extra blocks when the snapshot lacks
|
||||
// the divisor.
|
||||
while (value >= d)
|
||||
{
|
||||
value -= d;
|
||||
}
|
||||
|
||||
residues[i] = value;
|
||||
}
|
||||
}
|
||||
residues[i] = value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (; i < divisors.Count; i++)
|
||||
@@ -259,7 +262,8 @@ public sealed class ModResidueTracker
|
||||
// residue of Mersenne number M_number = 2^number - 1 modulo divisor
|
||||
// TODO: Consult MersenneDivisorCycles.Shared (or similar caches) here so we reuse precomputed cycle lengths
|
||||
// instead of recomputing powmods for every divisor; the divisor-cycle benchmarks showed large wins once the
|
||||
// cached orders were used across scans.
|
||||
// cached orders were used across scans, and a miss should trigger the configured device to compute the cycle
|
||||
// immediately without storing it back or requesting additional cache blocks.
|
||||
UInt128 pow = PowMod128(UInt128Numbers.Two, number, divisor);
|
||||
|
||||
// q divides M_p ⇔ 2^p ≡ 1 (mod q) AND ord_q(2) | p
|
||||
|
||||
@@ -9,14 +9,14 @@ namespace PerfectNumbers.Core;
|
||||
/// </summary>
|
||||
public sealed class MersenneResidueAutomaton
|
||||
{
|
||||
private readonly UInt128 _step; // 2*p
|
||||
private UInt128 _currentQ; // current q value
|
||||
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 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;
|
||||
@@ -27,21 +27,23 @@ public sealed class MersenneResidueAutomaton
|
||||
public MersenneResidueAutomaton(ulong exponent)
|
||||
{
|
||||
_step = (UInt128)exponent << 1; // 2 * p
|
||||
_currentQ = _step + UInt128.One; // start at k = 1
|
||||
_currentQ = _step + UInt128.One; // start at k = 1
|
||||
|
||||
// init residues
|
||||
// init residues
|
||||
Mod10R = _currentQ.Mod10();
|
||||
Mod8R = _currentQ.Mod8();
|
||||
Mod3R = _currentQ.Mod3();
|
||||
Mod5R = _currentQ.Mod5();
|
||||
Mod8R = _currentQ.Mod8();
|
||||
Mod3R = _currentQ.Mod3();
|
||||
Mod5R = _currentQ.Mod5();
|
||||
|
||||
_step10 = _step.Mod10().Mod10();
|
||||
_step8 = _step.Mod8();
|
||||
_step3 = _step.Mod3();
|
||||
_step5 = _step.Mod5();
|
||||
_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;
|
||||
|
||||
/// <summary>
|
||||
@@ -65,11 +67,16 @@ public sealed class MersenneResidueAutomaton
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +89,8 @@ 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 ulong[] _steps; // 10 % m
|
||||
private readonly ulong[] _res; // n % m
|
||||
private readonly bool[] _reachable;
|
||||
|
||||
public Ending7Automaton(ulong start, params ulong[] moduli)
|
||||
@@ -95,6 +102,7 @@ public sealed class Ending7Automaton
|
||||
_res = Array.Empty<ulong>();
|
||||
_reachable = Array.Empty<bool>();
|
||||
_n = AlignTo7(start);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -104,20 +112,33 @@ public sealed class Ending7Automaton
|
||||
_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)]
|
||||
@@ -140,6 +161,7 @@ public sealed class Ending7Automaton
|
||||
public void Next()
|
||||
{
|
||||
_n += 10UL;
|
||||
|
||||
for (int i = 0; i < _moduli.Length; i++)
|
||||
{
|
||||
if (!_reachable[i])
|
||||
@@ -149,19 +171,25 @@ public sealed class Ending7Automaton
|
||||
|
||||
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();
|
||||
ulong add = (7UL + 10UL - r).Mod10();
|
||||
|
||||
return start + add;
|
||||
}
|
||||
|
||||
@@ -176,6 +204,7 @@ public sealed class Ending7Automaton
|
||||
a = b;
|
||||
b = t;
|
||||
}
|
||||
|
||||
return a;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,10 @@ internal static class MontgomeryDivisorDataCache
|
||||
{
|
||||
private const ulong BaseBlockEnd = PerfectNumberConstants.MaxQForDivisorCycles;
|
||||
|
||||
private static readonly ConcurrentDictionary<ulong, CacheEntry> Cache = new();
|
||||
private static readonly ConcurrentDictionary<ulong, CacheEntry> Cache = new(); // TODO: Replace this with a plain Dictionary that never mutates after startup once the divisor-cycle snapshot becomes read-only so Montgomery lookups avoid any concurrent collections.
|
||||
// TODO: Delete the block-membership tracking entirely when the divisor-cycle cache stops producing
|
||||
// dynamic blocks. The single-cycle plan keeps the snapshot immutable, so we should not retain block
|
||||
// indices or concurrent maps at all.
|
||||
private static readonly ConcurrentDictionary<int, ConcurrentDictionary<ulong, byte>> BlockMembership = new();
|
||||
|
||||
public static MontgomeryDivisorData Get(ulong modulus)
|
||||
@@ -30,11 +33,16 @@ internal static class MontgomeryDivisorDataCache
|
||||
}
|
||||
|
||||
int blockIndex = ComputeBlockIndex(modulus);
|
||||
// TODO: Short-circuit block indices greater than zero once the cache stops prefetching future
|
||||
// blocks; when only the startup snapshot remains we should bypass block arithmetic entirely and
|
||||
// compute single cycles inline on cache misses without updating shared state.
|
||||
return GetManaged(modulus, blockIndex);
|
||||
}
|
||||
|
||||
internal static void ReleaseBlock(DivisorCycleCache.CycleBlock block)
|
||||
{
|
||||
// TODO: Remove this entire release hook once the cycle cache no longer hands out dynamic blocks;
|
||||
// the single-cycle pipeline will never transfer ownership back to Montgomery metadata.
|
||||
if (block.Index <= 0)
|
||||
{
|
||||
return;
|
||||
@@ -86,10 +94,15 @@ internal static class MontgomeryDivisorDataCache
|
||||
return created;
|
||||
}
|
||||
}
|
||||
// TODO: Collapse this retry loop once the cache becomes a simple snapshot dictionary. With the
|
||||
// single-cycle policy we can compute the requested cycle, populate the dictionary exactly once,
|
||||
// and return without spinning through concurrent retries.
|
||||
}
|
||||
|
||||
private static void RegisterBlockMembership(ulong modulus, int blockIndex)
|
||||
{
|
||||
// TODO: Delete this method once block membership goes away; the single snapshot model must avoid
|
||||
// tracking additional indices or allocating concurrent dictionaries for Montgomery lookups.
|
||||
if (blockIndex <= 0)
|
||||
{
|
||||
return;
|
||||
@@ -106,6 +119,9 @@ internal static class MontgomeryDivisorDataCache
|
||||
return 0;
|
||||
}
|
||||
|
||||
// TODO: Return 0 here once the divisor-cycle cache no longer tracks additional blocks so
|
||||
// MontgomeryDivisorData lookups never attempt to follow prefetched ranges or trigger new cycle
|
||||
// computations for entire blocks.
|
||||
return (int)((modulus - (BaseBlockEnd + 1UL)) / (ulong)PerfectNumberConstants.MaxQForDivisorCycles) + 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@ namespace PerfectNumbers.Core;
|
||||
|
||||
public static class PerfectNumberConstants
|
||||
{
|
||||
public const ulong BiggestKnownEvenPerfectP = 136279841UL;
|
||||
public const int ConsoleInterval = 100_000;
|
||||
public const ulong ExtraDivisorCycleSearchLimit = 64UL;
|
||||
public const int MaxQForDivisorCycles = 4_000_000;
|
||||
public static readonly uint PrimesLimit = 1_000_000; //(ulong)Array.MaxLength;// 1_000_000;
|
||||
|
||||
public const ulong BiggestKnownEvenPerfectP = 136279841UL;
|
||||
public const int ConsoleInterval = 100_000;
|
||||
public const ulong ExtraDivisorCycleSearchLimit = 64UL;
|
||||
public const int MaxQForDivisorCycles = 4_000_000;
|
||||
public static readonly uint PrimesLimit = 1_000_000; //(ulong)Array.MaxLength;// 1_000_000;
|
||||
// TODO: Load these limits from the benchmark-driven configuration so CPU and GPU scans stay aligned with the optimal
|
||||
// divisor-cycle datasets we generate offline.
|
||||
// TODO: Promote these magic numbers into a runtime profile derived from EvenPerfectBitScanner.Benchmarks so we can retune
|
||||
// them automatically when the optimal ranges for 2kp+1 >= 138M change.
|
||||
}
|
||||
@@ -13,6 +13,9 @@ public static class PowerCache
|
||||
{
|
||||
if (!BigCache.TryGetValue(baseVal, out var powers))
|
||||
{
|
||||
// TODO: Rent this buffer from ArrayPool<BigInteger> (or a dedicated pooled cache) so expanding
|
||||
// exponent ranges stop allocating fresh arrays as we sweep across large divisor sets in the
|
||||
// Pow2MontgomeryModBenchmarks scenarios.
|
||||
powers = new BigInteger[exponent + 1];
|
||||
powers[0] = BigInteger.One;
|
||||
BigCache[baseVal] = powers;
|
||||
@@ -21,6 +24,8 @@ public static class PowerCache
|
||||
if (powers.Length <= exponent)
|
||||
{
|
||||
int oldLength = powers.Length;
|
||||
// TODO: Replace Array.Resize with a pooled copy so we reuse previously rented buffers instead
|
||||
// of forcing GC work whenever the cache grows; benchmarks showed resize thrash when p >= 138M.
|
||||
Array.Resize(ref powers, exponent + 1);
|
||||
for (int i = oldLength; i <= exponent; i++)
|
||||
powers[i] = BigInteger.Zero;
|
||||
@@ -35,7 +40,7 @@ public static class PowerCache
|
||||
if (powers[i].IsZero)
|
||||
{
|
||||
// TODO: Swap this BigInteger chain with the UInt128-based Montgomery ladder from
|
||||
// Pow2MontgomeryMod once callers guarantee 64-bit inputs; the benchmarks show the
|
||||
// Pow2MontgomeryModBenchmarks once callers guarantee 64-bit inputs; the benchmarks show the
|
||||
// arbitrary-precision multiply is orders of magnitude slower for the exponents we
|
||||
// scan when p >= 138M.
|
||||
powers[i] = powers[i - 1] * baseVal;
|
||||
|
||||
@@ -25,6 +25,8 @@ public sealed class PrimeCache
|
||||
last = current;
|
||||
// TODO: Swap the Open.Numeric enumerator for the staged sieve batches we benchmarked; the
|
||||
// iterator allocations here throttle the cache fill when we extend the search past 138M.
|
||||
// TODO: Fold in the Mod6 stride planner that topped Mod6ComparisonBenchmarks so the CPU cache
|
||||
// can skip even/composite candidates instead of walking each enumerator element.
|
||||
_primes.Add(last);
|
||||
_primeMod4.Add((byte)(current & 3));
|
||||
}
|
||||
@@ -91,6 +93,8 @@ public sealed class PrimeCache
|
||||
// TODO: Move this incremental append to the shared sieve batches so we amortize the
|
||||
// conversions; walking the enumerator element-by-element is noticeably slower in the
|
||||
// updated prime cache benchmarks.
|
||||
// TODO: Apply the Mod6 stride scheduler here as well so on-demand extensions mirror the
|
||||
// Mod6ComparisonBenchmarks winner when pulling primes for divisor-cycle generation.
|
||||
if (current >= s)
|
||||
{
|
||||
yield return val;
|
||||
|
||||
@@ -25,6 +25,8 @@ public sealed class PrimeTester(bool useInternal = false)
|
||||
|
||||
Span<ulong> one = stackalloc ulong[1];
|
||||
Span<byte> outFlags = stackalloc byte[1];
|
||||
// TODO: Inline the single-value GPU sieve fast path from GpuModularArithmeticBenchmarks so this wrapper
|
||||
// can skip stackalloc buffers and reuse the pinned upload span the benchmark identified as fastest.
|
||||
one[0] = n;
|
||||
IsPrimeBatchGpu(one, outFlags);
|
||||
|
||||
@@ -116,6 +118,9 @@ public sealed class PrimeTester(bool useInternal = false)
|
||||
var input = scratch.Input;
|
||||
var output = scratch.Output;
|
||||
ulong[] temp = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||
// TODO: Replace this ad-hoc ArrayPool buffer with the pinned span cache from
|
||||
// PrimeSieveGpuBenchmarks so batch uploads reuse preallocated GPU-friendly
|
||||
// memory and avoid the extra copy before every kernel launch.
|
||||
|
||||
try
|
||||
{
|
||||
@@ -156,6 +161,9 @@ public sealed class PrimeTester(bool useInternal = false)
|
||||
public MemoryBuffer1D<uint, Stride1D.Dense> DevicePrimes { get; }
|
||||
private readonly Accelerator _accel;
|
||||
private readonly System.Collections.Concurrent.ConcurrentBag<ScratchBuffers> _scratchPool = [];
|
||||
// TODO: Replace this ConcurrentBag with the lock-free ring buffer variant validated in
|
||||
// GpuModularArithmeticBenchmarks so renting scratch buffers stops contending on the bag's internal locks when
|
||||
// thousands of GPU batches execute per second.
|
||||
|
||||
public KernelState(Accelerator accelerator)
|
||||
{
|
||||
@@ -226,6 +234,9 @@ public sealed class PrimeTester(bool useInternal = false)
|
||||
{
|
||||
// Map accelerator to cached state; use Lazy to serialize kernel creation
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<Accelerator, Lazy<KernelState>> States = new();
|
||||
// TODO: Prewarm this per-accelerator cache during startup (and reuse a simple array keyed by accelerator index)
|
||||
// once the kernel pool exposes deterministic ordering; the Lazy wrappers showed measurable overhead in the
|
||||
// GpuModularArithmeticBenchmarks hot path.
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static KernelState GetOrCreate(Accelerator accelerator)
|
||||
@@ -308,12 +319,18 @@ public sealed class PrimeTester(bool useInternal = false)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static bool SharesFactorWithMaxExponent(ulong n)
|
||||
{
|
||||
// TODO: Replace this on-the-fly GCD probe with the cached factor table derived from
|
||||
// ResidueComputationBenchmarks so divisor-cycle metadata can short-circuit the test
|
||||
// instead of recomputing binary GCD for every candidate.
|
||||
ulong m = (ulong)BitOperations.Log2(n);
|
||||
return BinaryGcd(n, m) != 1UL;
|
||||
}
|
||||
|
||||
internal static void SharesFactorWithMaxExponentBatch(ReadOnlySpan<ulong> values, Span<byte> results)
|
||||
{
|
||||
// TODO: Route this batch helper through the shared GPU kernel pool from
|
||||
// GpuUInt128BinaryGcdBenchmarks so we reuse cached kernels, pinned host buffers,
|
||||
// and divisor-cycle staging instead of allocating new device buffers per call.
|
||||
var gpu = GpuContextPool.Rent();
|
||||
var accelerator = gpu.Accelerator;
|
||||
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<byte>>(SharesFactorKernel);
|
||||
@@ -337,6 +354,9 @@ public sealed class PrimeTester(bool useInternal = false)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong BinaryGcd(ulong u, ulong v)
|
||||
{
|
||||
// TODO: Swap this handwritten binary GCD for the optimized helper measured in
|
||||
// GpuUInt128BinaryGcdBenchmarks so CPU callers share the faster subtract-less
|
||||
// ladder once the common implementation is promoted into PerfectNumbers.Core.
|
||||
if (u == 0UL)
|
||||
{
|
||||
return v;
|
||||
@@ -375,6 +395,9 @@ public sealed class PrimeTester(bool useInternal = false)
|
||||
|
||||
private static ulong BinaryGcdGpu(ulong u, ulong v)
|
||||
{
|
||||
// TODO: Replace this inline GPU binary GCD with the kernel extracted from
|
||||
// GpuUInt128BinaryGcdBenchmarks via GpuKernelPool so device callers reuse the
|
||||
// fully unrolled ladder instead of this branchy fallback.
|
||||
if (u == 0UL)
|
||||
{
|
||||
return v;
|
||||
|
||||
@@ -4,122 +4,130 @@ namespace PerfectNumbers.Core;
|
||||
|
||||
public static class PrimesGenerator
|
||||
{
|
||||
public static readonly uint[] SmallPrimes;
|
||||
public static readonly ulong[] SmallPrimesPow2;
|
||||
public static readonly uint[] SmallPrimesLastOne;
|
||||
public static readonly ulong[] SmallPrimesPow2LastOne;
|
||||
public static readonly uint[] SmallPrimesLastSeven;
|
||||
public static readonly ulong[] SmallPrimesPow2LastSeven;
|
||||
public static readonly uint[] SmallPrimes;
|
||||
public static readonly ulong[] SmallPrimesPow2;
|
||||
public static readonly uint[] SmallPrimesLastOne;
|
||||
public static readonly ulong[] SmallPrimesPow2LastOne;
|
||||
public static readonly uint[] SmallPrimesLastSeven;
|
||||
public static readonly ulong[] SmallPrimesPow2LastSeven;
|
||||
|
||||
static PrimesGenerator()
|
||||
{
|
||||
BuildSmallPrimes(
|
||||
out SmallPrimes,
|
||||
out SmallPrimesPow2,
|
||||
out SmallPrimesLastOne,
|
||||
out SmallPrimesPow2LastOne,
|
||||
out SmallPrimesLastSeven,
|
||||
out SmallPrimesPow2LastSeven);
|
||||
}
|
||||
static PrimesGenerator()
|
||||
{
|
||||
BuildSmallPrimes(
|
||||
out SmallPrimes,
|
||||
out SmallPrimesPow2,
|
||||
out SmallPrimesLastOne,
|
||||
out SmallPrimesPow2LastOne,
|
||||
out SmallPrimesLastSeven,
|
||||
out SmallPrimesPow2LastSeven);
|
||||
}
|
||||
|
||||
private static void BuildSmallPrimes(
|
||||
out uint[] all,
|
||||
out ulong[] allPow2,
|
||||
out uint[] lastOne,
|
||||
out ulong[] lastOnePow2,
|
||||
out uint[] lastSeven,
|
||||
out ulong[] lastSevenPow2)
|
||||
{
|
||||
uint target = PerfectNumberConstants.PrimesLimit;
|
||||
int targetInt = checked((int)target);
|
||||
all = new uint[targetInt];
|
||||
allPow2 = new ulong[targetInt];
|
||||
lastOne = new uint[targetInt];
|
||||
lastOnePow2 = new ulong[targetInt];
|
||||
lastSeven = new uint[targetInt];
|
||||
lastSevenPow2 = new ulong[targetInt];
|
||||
private static void BuildSmallPrimes(
|
||||
out uint[] all,
|
||||
out ulong[] allPow2,
|
||||
out uint[] lastOne,
|
||||
out ulong[] lastOnePow2,
|
||||
out uint[] lastSeven,
|
||||
out ulong[] lastSevenPow2)
|
||||
{
|
||||
uint target = PerfectNumberConstants.PrimesLimit;
|
||||
int targetInt = checked((int)target);
|
||||
all = new uint[targetInt];
|
||||
allPow2 = new ulong[targetInt];
|
||||
lastOne = new uint[targetInt];
|
||||
lastOnePow2 = new ulong[targetInt];
|
||||
lastSeven = new uint[targetInt];
|
||||
lastSevenPow2 = new ulong[targetInt];
|
||||
|
||||
List<uint> primes = new(targetInt * 4 / 3 + 16);
|
||||
uint candidate = 2U;
|
||||
int allCount = 0;
|
||||
int lastOneCount = 0;
|
||||
int lastSevenCount = 0;
|
||||
List<uint> primes = new(targetInt * 4 / 3 + 16);
|
||||
uint candidate = 2U;
|
||||
int allCount = 0;
|
||||
int lastOneCount = 0;
|
||||
int lastSevenCount = 0;
|
||||
|
||||
while (allCount < targetInt || lastOneCount < targetInt || lastSevenCount < targetInt)
|
||||
{
|
||||
bool isPrime = true;
|
||||
int primesCount = primes.Count;
|
||||
for (int i = 0; i < primesCount; i++)
|
||||
{
|
||||
uint p = primes[i];
|
||||
if (p * p > candidate)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO: Replace this trial-division `%` with the sieve-based generator that avoids
|
||||
// per-candidate modulo work so building the small-prime tables stops dominating
|
||||
// startup time for large scans.
|
||||
if (candidate % p == 0U)
|
||||
{
|
||||
isPrime = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPrime)
|
||||
{
|
||||
primes.Add(candidate);
|
||||
if (allCount < targetInt)
|
||||
{
|
||||
all[allCount] = candidate;
|
||||
allPow2[allCount] = checked((ulong)candidate * candidate);
|
||||
allCount++;
|
||||
}
|
||||
|
||||
if (candidate != 2U)
|
||||
{
|
||||
if (lastOneCount < targetInt && IsAllowedForLastOne(candidate))
|
||||
{
|
||||
lastOne[lastOneCount] = candidate;
|
||||
lastOnePow2[lastOneCount] = checked((ulong)candidate * candidate);
|
||||
lastOneCount++;
|
||||
}
|
||||
|
||||
if (lastSevenCount < targetInt && IsAllowedForLastSeven(candidate))
|
||||
{
|
||||
lastSeven[lastSevenCount] = candidate;
|
||||
lastSevenPow2[lastSevenCount] = checked((ulong)candidate * candidate);
|
||||
lastSevenCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidate = candidate == 2U ? 3U : candidate + 2U;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsAllowedForLastOne(uint prime)
|
||||
while (allCount < targetInt || lastOneCount < targetInt || lastSevenCount < targetInt)
|
||||
{
|
||||
// TODO: Swap the `% 10` usage for ULongExtensions.Mod10 so the hot classification path
|
||||
// reuses the benchmarked residue helper instead of repeated divisions.
|
||||
return (prime % 10U) switch
|
||||
bool isPrime = true;
|
||||
int primesCount = primes.Count;
|
||||
for (int i = 0; i < primesCount; i++)
|
||||
{
|
||||
uint p = primes[i];
|
||||
// TODO: Reuse a single squared local for `p` within this loop so we avoid recalculating
|
||||
// `p * p` repeatedly without storing every squared prime globally (the cache must remain
|
||||
// limited to the small-divisor tables to honor the memory constraints).
|
||||
if (p * p > candidate)
|
||||
{
|
||||
1U or 3U or 9U => true,
|
||||
_ => prime == 7U || prime == 11U,
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsAllowedForLastSeven(uint prime)
|
||||
{
|
||||
// TODO: Route this `% 10` classification through ULongExtensions.Mod10 to match the faster
|
||||
// residue helper used elsewhere in the scanner.
|
||||
return (prime % 10U) switch
|
||||
// TODO: Replace this trial-division `%` with the sieve-based generator that avoids
|
||||
// per-candidate modulo work so building the small-prime tables stops dominating
|
||||
// startup time for large scans.
|
||||
if (candidate % p == 0U)
|
||||
{
|
||||
3U or 7U or 9U => true,
|
||||
_ => prime == 11U,
|
||||
};
|
||||
isPrime = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPrime)
|
||||
{
|
||||
primes.Add(candidate);
|
||||
if (allCount < targetInt)
|
||||
{
|
||||
// TODO: Reuse a single cached `candidateSquared` per iteration so we avoid issuing
|
||||
// three separate 64-bit multiplications when populating the pow2 arrays here and
|
||||
// in the last-one/last-seven branches below.
|
||||
all[allCount] = candidate;
|
||||
allPow2[allCount] = checked((ulong)candidate * candidate);
|
||||
allCount++;
|
||||
}
|
||||
|
||||
if (candidate != 2U)
|
||||
{
|
||||
if (lastOneCount < targetInt && IsAllowedForLastOne(candidate))
|
||||
{
|
||||
lastOne[lastOneCount] = candidate;
|
||||
lastOnePow2[lastOneCount] = checked((ulong)candidate * candidate);
|
||||
lastOneCount++;
|
||||
}
|
||||
|
||||
if (lastSevenCount < targetInt && IsAllowedForLastSeven(candidate))
|
||||
{
|
||||
lastSeven[lastSevenCount] = candidate;
|
||||
lastSevenPow2[lastSevenCount] = checked((ulong)candidate * candidate);
|
||||
lastSevenCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Switch this increment to the Mod6 stepping table identified in the Mod6ComparisonBenchmarks
|
||||
// so we skip the composite residues without relying on per-iteration `%` checks.
|
||||
candidate = candidate == 2U ? 3U : candidate + 2U;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsAllowedForLastOne(uint prime)
|
||||
{
|
||||
// TODO: Swap the `% 10` usage for ULongExtensions.Mod10 so the hot classification path
|
||||
// reuses the benchmarked residue helper instead of repeated divisions.
|
||||
return (prime % 10U) switch
|
||||
{
|
||||
1U or 3U or 9U => true,
|
||||
_ => prime == 7U || prime == 11U,
|
||||
};
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsAllowedForLastSeven(uint prime)
|
||||
{
|
||||
// TODO: Route this `% 10` classification through ULongExtensions.Mod10 to match the faster
|
||||
// residue helper used elsewhere in the scanner.
|
||||
return (prime % 10U) switch
|
||||
{
|
||||
3U or 7U or 9U => true,
|
||||
_ => prime == 11U,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,17 +10,24 @@ public static class RationalHelper
|
||||
long numerator = checked((long)Math.Round(value * scale));
|
||||
long denominator = scale;
|
||||
long gcd = (long)BigInteger.GreatestCommonDivisor(numerator, denominator);
|
||||
// TODO: Replace the BigInteger-based GCD with the UInt128-friendly binary GCD helper so this rounding path avoids
|
||||
// allocating big integers for hot rational conversions.
|
||||
return ERational.Create(EInteger.FromInt64(numerator / gcd), EInteger.FromInt64(denominator / gcd));
|
||||
}
|
||||
|
||||
public static EInteger ToEInteger(BigInteger value)
|
||||
{
|
||||
// TODO: Cache a reusable byte buffer here so converting to EInteger no longer allocates per call when parsing alpha
|
||||
// tables.
|
||||
return EInteger.FromBytes(value.ToByteArray(), true);
|
||||
}
|
||||
|
||||
public static ulong FloorToUInt64(ERational value)
|
||||
{
|
||||
EInteger ei = value.ToEInteger();
|
||||
// TODO: Inline this floor conversion with the span-based ladder that dominated
|
||||
// ResidueComputationBenchmarks so the CPU path skips the expensive ERational ->
|
||||
// EInteger materialization when reducing large 2kp+1 divisors.
|
||||
return ei.ToUInt64Checked();
|
||||
}
|
||||
|
||||
@@ -29,6 +36,9 @@ public static class RationalHelper
|
||||
EInteger ei = value.ToEInteger();
|
||||
if (value.CompareTo(ERational.Create(ei, EInteger.One)) > 0)
|
||||
ei += EInteger.One;
|
||||
// TODO: Replace this ceiling branch with the branchless adjustment measured fastest
|
||||
// in ResidueComputationBenchmarks so large-divisor scans avoid repeated ERational
|
||||
// conversions on the CPU hot path.
|
||||
return ei.ToUInt64Checked();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,7 @@ namespace PerfectNumbers.Core;
|
||||
public static class RationalNumbers
|
||||
{
|
||||
public static ERational Two { get; } = ERational.FromInt32(2);
|
||||
// TODO: Precompute additional frequently used rationals (e.g., OneHalf, ThreeHalves) so hot alpha computations stop
|
||||
// constructing them via ERational.FromInt32 at runtime.
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ public static class RleBlacklist
|
||||
}
|
||||
|
||||
var set = new HashSet<string>(StringComparer.Ordinal);
|
||||
// TODO: Replace this HashSet with the pooled ValueHashSet from the blacklist benchmarks so repeated
|
||||
// reloads stop allocating new buckets when refreshing the filtered pattern list.
|
||||
foreach (var line in File.ReadLines(path))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
@@ -242,6 +244,8 @@ public static class RleBlacklist
|
||||
|
||||
if (j > i)
|
||||
{
|
||||
// TODO: Replace int.TryParse with the Utf8Parser-based span helper so blacklist normalization avoids
|
||||
// repeated culture-aware integer parsing in hot loading paths.
|
||||
if (int.TryParse(span.Slice(i, j - i), out int v) && v > 0)
|
||||
{
|
||||
tmp[count++] = v;
|
||||
@@ -323,6 +327,8 @@ public static class RleBlacklist
|
||||
|
||||
if (j > i)
|
||||
{
|
||||
// TODO: Swap this int.TryParse for the upcoming Utf8Parser fast path so nested patterns reuse the
|
||||
// zero-allocation integer parser during blacklist ingestion.
|
||||
if (int.TryParse(inner.Slice(i, j - i), out int v) && v > 0)
|
||||
{
|
||||
tmp[count++] = v;
|
||||
|
||||
@@ -7,11 +7,17 @@ public static class StringBuilderPool
|
||||
{
|
||||
private static readonly ConcurrentQueue<StringBuilder> _stringBuilderPool = new();
|
||||
|
||||
public static StringBuilder Rent() => _stringBuilderPool.TryDequeue(out var sb) ? sb : new();
|
||||
public static StringBuilder Rent()
|
||||
{
|
||||
// TODO: Ensure oversized builders remain eligible for reuse without trimming so call sites keep their full capacity.
|
||||
return _stringBuilderPool.TryDequeue(out var sb) ? sb : new();
|
||||
}
|
||||
|
||||
public static void Return(StringBuilder sb)
|
||||
{
|
||||
_ = sb.Clear();
|
||||
// TODO: Preserve the builder's capacity when returning it so the pool hands the same buffer back without shrinkage.
|
||||
_stringBuilderPool.Enqueue(sb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,16 @@ public sealed class TextFileWriter : IDisposable
|
||||
|
||||
public long Position
|
||||
{
|
||||
get { lock (_lock) { return _stream.Position; } }
|
||||
get
|
||||
{
|
||||
// TODO: Surface a lock-free position snapshot (for example via Interlocked.Read on a cached long)
|
||||
// so the high-frequency status probes measured in the writer throughput profiling stop contending on
|
||||
// the flush lock while runs follow the buffered pipeline planned for the scanner fast path.
|
||||
lock (_lock)
|
||||
{
|
||||
return _stream.Position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private StreamWriter TextWriter => _textWriter ??= new StreamWriter(_stream, leaveOpen: true);
|
||||
@@ -36,6 +45,9 @@ public sealed class TextFileWriter : IDisposable
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// TODO: Use the pooled writer buffers planned for the TextFileWriter fast path so truncation flushes
|
||||
// reuse the span caches identified alongside the Mod10_8_5_3Benchmarks instead of forcing synchronous
|
||||
// SetLength/Flush work on every restart.
|
||||
_stream.SetLength(0);
|
||||
_stream.Position = 0;
|
||||
_textWriter?.Flush();
|
||||
@@ -65,6 +77,8 @@ public sealed class TextFileWriter : IDisposable
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// TODO: Buffer binary writes with the same ArrayPool-backed accumulator planned for text so GPU result
|
||||
// dumps can batch disk flushes instead of flushing on every invocation.
|
||||
action(BinaryWriter);
|
||||
BinaryWriter.Flush();
|
||||
}
|
||||
@@ -72,6 +86,8 @@ public sealed class TextFileWriter : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// TODO: Return the writers to a pooled wrapper once the buffered pipeline lands so disposing the
|
||||
// TextFileWriter mirrors the zero-allocation strategy validated by the Mod10_8_5_3Benchmarks helpers.
|
||||
_binaryWriter?.Dispose();
|
||||
_textWriter?.Dispose();
|
||||
_stream.Dispose();
|
||||
|
||||
@@ -5,320 +5,337 @@ namespace PerfectNumbers.Core;
|
||||
|
||||
public static class UInt128Extensions
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 BinaryGcd(this UInt128 u, UInt128 v)
|
||||
{
|
||||
UInt128 zero = UInt128.Zero;
|
||||
if (u == zero)
|
||||
{
|
||||
return v;
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 BinaryGcd(this UInt128 u, UInt128 v)
|
||||
{
|
||||
// TODO: Replace this hand-rolled binary GCD with the shared subtract-free ladder from
|
||||
// GpuUInt128BinaryGcdBenchmarks so wide operands reuse the optimized helper instead of
|
||||
// repeating the slower shift/subtract loop on both CPU and GPU paths.
|
||||
UInt128 zero = UInt128.Zero;
|
||||
if (u == zero)
|
||||
{
|
||||
return v;
|
||||
}
|
||||
|
||||
if (v == zero)
|
||||
{
|
||||
return u;
|
||||
}
|
||||
if (v == zero)
|
||||
{
|
||||
return u;
|
||||
}
|
||||
|
||||
int shift = CountTrailingZeros(u | v);
|
||||
u >>= CountTrailingZeros(u);
|
||||
int shift = CountTrailingZeros(u | v);
|
||||
u >>= CountTrailingZeros(u);
|
||||
|
||||
do
|
||||
{
|
||||
v >>= CountTrailingZeros(v);
|
||||
if (u > v)
|
||||
{
|
||||
(u, v) = (v, u);
|
||||
}
|
||||
do
|
||||
{
|
||||
v >>= CountTrailingZeros(v);
|
||||
if (u > v)
|
||||
{
|
||||
(u, v) = (v, u);
|
||||
}
|
||||
|
||||
v -= u;
|
||||
}
|
||||
while (v != zero);
|
||||
v -= u;
|
||||
}
|
||||
while (v != zero);
|
||||
|
||||
return u << shift;
|
||||
}
|
||||
return u << shift;
|
||||
}
|
||||
|
||||
public static ulong CalculateOrder(this UInt128 q)
|
||||
{
|
||||
if (q <= UInt128Numbers.Two)
|
||||
{
|
||||
return 0UL;
|
||||
}
|
||||
public static ulong CalculateOrder(this UInt128 q)
|
||||
{
|
||||
if (q <= UInt128Numbers.Two)
|
||||
{
|
||||
return 0UL;
|
||||
}
|
||||
|
||||
UInt128 one = UInt128.One;
|
||||
UInt128 phi = q - one;
|
||||
if (phi > ulong.MaxValue)
|
||||
{
|
||||
throw new NotImplementedException("Such big values are not yet supported");
|
||||
}
|
||||
UInt128 one = UInt128.One;
|
||||
UInt128 phi = q - one;
|
||||
if (phi > ulong.MaxValue)
|
||||
{
|
||||
throw new NotImplementedException("Such big values are not yet supported");
|
||||
}
|
||||
|
||||
ulong order = (ulong)phi;
|
||||
uint[] smallPrimes = PrimesGenerator.SmallPrimes;
|
||||
ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2;
|
||||
ulong order = (ulong)phi;
|
||||
uint[] smallPrimes = PrimesGenerator.SmallPrimes;
|
||||
ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2;
|
||||
|
||||
int i = 0, primesLength = smallPrimes.Length;
|
||||
UInt128 cycle = MersenneDivisorCycles.GetCycle(q);
|
||||
ulong prime, temp;
|
||||
for (; i < primesLength; i++)
|
||||
{
|
||||
if (smallPrimesPow2[i] > order)
|
||||
{
|
||||
break;
|
||||
}
|
||||
int i = 0, primesLength = smallPrimes.Length;
|
||||
UInt128 cycle = MersenneDivisorCycles.GetCycle(q);
|
||||
// TODO: If the cache lacks this cycle, immediately schedule the configured device
|
||||
// (GPU by default) to compute it on the fly and skip inserting it into the cache so
|
||||
// wide-order factoring can still leverage cycle stepping without breaching the
|
||||
// memory cap or introducing extra synchronization.
|
||||
ulong prime, temp;
|
||||
for (; i < primesLength; i++)
|
||||
{
|
||||
if (smallPrimesPow2[i] > order)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
prime = smallPrimes[i];
|
||||
while (order % prime == 0UL)
|
||||
{
|
||||
temp = order / prime;
|
||||
// TODO: Switch this divisor-order powmod to the ProcessEightBitWindows helper so the
|
||||
// cycle factoring loop benefits from the faster windowed pow2 ladder measured in CPU benchmarks.
|
||||
if (temp.PowModWithCycle(q, cycle) == one)
|
||||
{
|
||||
order = temp;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int CountTrailingZeros(this UInt128 value)
|
||||
{
|
||||
ulong valuePart = (ulong)value;
|
||||
if (valuePart != 0UL)
|
||||
{
|
||||
return BitOperations.TrailingZeroCount(valuePart);
|
||||
}
|
||||
|
||||
return 64 + BitOperations.TrailingZeroCount((ulong)(value >> 64));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsPrimeCandidate(this UInt128 n)
|
||||
{
|
||||
UInt128 p, zero = UInt128.Zero;
|
||||
|
||||
uint[] smallPrimes = PrimesGenerator.SmallPrimes;
|
||||
ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2;
|
||||
ulong i, smallPrimesCount = (ulong)smallPrimes.Length;
|
||||
for (i = 0UL; i < smallPrimesCount; i++)
|
||||
prime = smallPrimes[i];
|
||||
while (order % prime == 0UL)
|
||||
{
|
||||
temp = order / prime;
|
||||
// TODO: Switch this divisor-order powmod to the ProcessEightBitWindows helper so the
|
||||
// cycle factoring loop benefits from the faster windowed pow2 ladder measured in CPU benchmarks.
|
||||
if (temp.PowModWithCycle(q, cycle) == one)
|
||||
{
|
||||
order = temp;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (smallPrimesPow2[i] > n)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
p = smallPrimes[i];
|
||||
// TODO: Replace this direct `%` test with the shared divisor-cycle filter once the
|
||||
// UInt128 path is wired into the cached cycle tables so wide candidates skip the slow
|
||||
// modulo checks during primality pre-filtering.
|
||||
if (n % p == zero)
|
||||
{
|
||||
return n == p;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod3(this UInt128 value)
|
||||
{
|
||||
ulong remainder = ((ulong)value) % 3UL + ((ulong)(value >> 64)) % 3UL;
|
||||
return remainder >= 3UL ? remainder - 3UL : remainder;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod5(this UInt128 value)
|
||||
{
|
||||
ulong remainder = (((ulong)value) % 5UL) + (((ulong)(value >> 64)) % 5UL);
|
||||
return remainder >= 5UL ? remainder - 5UL : remainder;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod6(this UInt128 value) => ((value.Mod3() << 1) | ((ulong)value & 1UL)) switch
|
||||
{
|
||||
0UL => 0UL,
|
||||
1UL => 3UL,
|
||||
2UL => 4UL,
|
||||
3UL => 1UL,
|
||||
4UL => 2UL,
|
||||
_ => 5UL,
|
||||
};
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod7(this UInt128 value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int CountTrailingZeros(this UInt128 value)
|
||||
{
|
||||
ulong valuePart = (ulong)value;
|
||||
if (valuePart != 0UL)
|
||||
{
|
||||
ulong low = (ulong)value % 7UL;
|
||||
ulong high = (ulong)(value >> 64) % 7UL;
|
||||
ulong remainder = low + (high * 2UL);
|
||||
if (remainder >= 7UL)
|
||||
return BitOperations.TrailingZeroCount(valuePart);
|
||||
}
|
||||
|
||||
return 64 + BitOperations.TrailingZeroCount((ulong)(value >> 64));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsPrimeCandidate(this UInt128 n)
|
||||
{
|
||||
UInt128 p, zero = UInt128.Zero;
|
||||
|
||||
uint[] smallPrimes = PrimesGenerator.SmallPrimes;
|
||||
ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2;
|
||||
ulong i, smallPrimesCount = (ulong)smallPrimes.Length;
|
||||
for (i = 0UL; i < smallPrimesCount; i++)
|
||||
{
|
||||
if (smallPrimesPow2[i] > n)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
p = smallPrimes[i];
|
||||
// TODO: Replace this direct `%` test with the shared divisor-cycle filter once the
|
||||
// UInt128 path is wired into the cached cycle tables so wide candidates skip the slow
|
||||
// modulo checks during primality pre-filtering.
|
||||
if (n % p == zero)
|
||||
{
|
||||
return n == p;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod3(this UInt128 value)
|
||||
{
|
||||
// TODO: Fold these reductions into the multiply-high trick captured in Mod3BenchmarkResults so
|
||||
// the UInt128 modulo helpers avoid `%` altogether and align with the faster CPU/GPU residue filters.
|
||||
ulong remainder = ((ulong)value) % 3UL + ((ulong)(value >> 64)) % 3UL;
|
||||
return remainder >= 3UL ? remainder - 3UL : remainder;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod5(this UInt128 value)
|
||||
{
|
||||
// TODO: Replace the `% 5` operations with the precomputed multiply-high constants from the Mod5
|
||||
// benchmarks so the UInt128 path matches the 64-bit helpers without extra modulo instructions.
|
||||
ulong remainder = (((ulong)value) % 5UL) + (((ulong)(value >> 64)) % 5UL);
|
||||
return remainder >= 5UL ? remainder - 5UL : remainder;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod6(this UInt128 value) => ((value.Mod3() << 1) | ((ulong)value & 1UL)) switch
|
||||
{
|
||||
0UL => 0UL,
|
||||
1UL => 3UL,
|
||||
2UL => 4UL,
|
||||
3UL => 1UL,
|
||||
4UL => 2UL,
|
||||
_ => 5UL,
|
||||
};
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod7(this UInt128 value)
|
||||
{
|
||||
// TODO: Inline the lookup-based Mod7 reducer validated in the residue benchmarks so this helper
|
||||
// stops relying on `% 7` in hot loops and mirrors the optimized CPU cycle filters.
|
||||
ulong low = (ulong)value % 7UL;
|
||||
ulong high = (ulong)(value >> 64) % 7UL;
|
||||
ulong remainder = low + (high * 2UL);
|
||||
if (remainder >= 7UL)
|
||||
{
|
||||
remainder -= 7UL;
|
||||
if (remainder >= 7UL)
|
||||
{
|
||||
remainder -= 7UL;
|
||||
}
|
||||
}
|
||||
|
||||
return remainder;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod8(this UInt128 value) => (ulong)value & 7UL;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod10(this UInt128 value128)
|
||||
{
|
||||
// Split and fold under mod 10: 2^64 ≡ 6 (mod 10)
|
||||
// TODO: Precompute the 2^64 ≡ 6 folding constants once so this path stops recomputing `% 10`
|
||||
// on each half and instead uses the span-based lookup captured in Mod10BenchmarkResults.
|
||||
ulong low = (ulong)value128;
|
||||
ulong high = (ulong)(value128 >> 64);
|
||||
return ((low % 10UL) + ((high % 10UL) * 6UL)) % 10UL;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Mod10_8_5_3(this UInt128 value, out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3)
|
||||
{
|
||||
byte current;
|
||||
uint byteSum = 0U;
|
||||
mod8 = (ulong)value & 7UL;
|
||||
UInt128 zero = UInt128.Zero;
|
||||
do
|
||||
{
|
||||
current = (byte)value;
|
||||
byteSum += current;
|
||||
value >>= 8;
|
||||
}
|
||||
while (value != zero);
|
||||
|
||||
mod5 = byteSum.Mod5();
|
||||
// TODO: Collapse this Mod10 switch into the shared lookup table from the CLI benchmarks so we
|
||||
// avoid the nested switch expressions once the pooled residue tables become available.
|
||||
mod10 = (mod8 & 1UL) == 0UL
|
||||
? mod5 switch
|
||||
{
|
||||
0U => 0UL,
|
||||
1U => 6UL,
|
||||
2U => 2UL,
|
||||
3U => 8UL,
|
||||
_ => 4UL,
|
||||
}
|
||||
: mod5 switch
|
||||
{
|
||||
0U => 5UL,
|
||||
1U => 1UL,
|
||||
2U => 7UL,
|
||||
3U => 3UL,
|
||||
_ => 9UL,
|
||||
};
|
||||
|
||||
mod3 = byteSum % 3U;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod11(this UInt128 value)
|
||||
{
|
||||
ulong remainder = (((ulong)value) % 11UL) + (((ulong)(value >> 64)) % 11UL) * 5UL;
|
||||
while (remainder >= 11UL)
|
||||
{
|
||||
remainder -= 11UL;
|
||||
}
|
||||
|
||||
return remainder;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod128(this UInt128 value) => (ulong)value & 127UL;
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 ModPow(this UInt128 value, UInt128 exponent, UInt128 modulus)
|
||||
{
|
||||
UInt128 zero = UInt128.Zero;
|
||||
UInt128 one = UInt128.One;
|
||||
UInt128 result = UInt128.One;
|
||||
value %= modulus;
|
||||
|
||||
while (exponent != zero)
|
||||
{
|
||||
if ((exponent & one) != zero)
|
||||
{
|
||||
result = MulMod(result, value, modulus);
|
||||
}
|
||||
|
||||
value = MulMod(value, value, modulus);
|
||||
exponent >>= 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 Mul64(this UInt128 a, UInt128 b)
|
||||
{
|
||||
ulong aLow = (ulong)a;
|
||||
ulong bLow = (ulong)b;
|
||||
ulong bHigh = (ulong)(b >> 64);
|
||||
|
||||
ulong low = aLow * bLow;
|
||||
|
||||
// Keep the high-word accumulation in locals so the JIT does not rebuild the
|
||||
// expression tree around the shift. Mirroring the MulHigh layout lets RyuJIT
|
||||
// keep the intermediate sum in registers instead of reloading the partial
|
||||
// product from the stack.
|
||||
ulong high = aLow.MulHigh(bLow);
|
||||
high += aLow * bHigh;
|
||||
|
||||
return ((UInt128)high << 64) | low;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 MulMod(this UInt128 a, UInt128 b, UInt128 modulus)
|
||||
{
|
||||
UInt128 one = UInt128.One,
|
||||
result = UInt128.Zero,
|
||||
zero = UInt128.Zero;
|
||||
|
||||
while (b != zero)
|
||||
{
|
||||
if ((b & one) != zero)
|
||||
{
|
||||
result += a;
|
||||
if (result >= modulus)
|
||||
{
|
||||
remainder -= 7UL;
|
||||
if (remainder >= 7UL)
|
||||
{
|
||||
remainder -= 7UL;
|
||||
}
|
||||
result -= modulus;
|
||||
}
|
||||
}
|
||||
|
||||
return remainder;
|
||||
a <<= 1;
|
||||
if (a >= modulus)
|
||||
{
|
||||
a -= modulus;
|
||||
}
|
||||
|
||||
b >>= 1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod8(this UInt128 value) => (ulong)value & 7UL;
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod10(this UInt128 value128)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 Pow(this UInt128 value, ulong exponent)
|
||||
{
|
||||
UInt128 result = UInt128.One;
|
||||
while (exponent != 0UL)
|
||||
{
|
||||
// Split and fold under mod 10: 2^64 ≡ 6 (mod 10)
|
||||
ulong low = (ulong)value128;
|
||||
ulong high = (ulong)(value128 >> 64);
|
||||
return ((low % 10UL) + ((high % 10UL) * 6UL)) % 10UL;
|
||||
if ((exponent & 1UL) != 0UL)
|
||||
{
|
||||
result *= value;
|
||||
}
|
||||
|
||||
value *= value;
|
||||
exponent >>= 1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Mod10_8_5_3(this UInt128 value, out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3)
|
||||
{
|
||||
byte current;
|
||||
uint byteSum = 0U;
|
||||
mod8 = (ulong)value & 7UL;
|
||||
UInt128 zero = UInt128.Zero;
|
||||
do
|
||||
{
|
||||
current = (byte)value;
|
||||
byteSum += current;
|
||||
value >>= 8;
|
||||
}
|
||||
while (value != zero);
|
||||
|
||||
mod5 = byteSum.Mod5();
|
||||
mod10 = (mod8 & 1UL) == 0UL
|
||||
? mod5 switch
|
||||
{
|
||||
0U => 0UL,
|
||||
1U => 6UL,
|
||||
2U => 2UL,
|
||||
3U => 8UL,
|
||||
_ => 4UL,
|
||||
}
|
||||
: mod5 switch
|
||||
{
|
||||
0U => 5UL,
|
||||
1U => 1UL,
|
||||
2U => 7UL,
|
||||
3U => 3UL,
|
||||
_ => 9UL,
|
||||
};
|
||||
|
||||
mod3 = byteSum % 3U;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod11(this UInt128 value)
|
||||
{
|
||||
ulong remainder = (((ulong)value) % 11UL) + (((ulong)(value >> 64)) % 11UL) * 5UL;
|
||||
while (remainder >= 11UL)
|
||||
{
|
||||
remainder -= 11UL;
|
||||
}
|
||||
|
||||
return remainder;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod128(this UInt128 value) => (ulong)value & 127UL;
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 ModPow(this UInt128 value, UInt128 exponent, UInt128 modulus)
|
||||
{
|
||||
UInt128 zero = UInt128.Zero;
|
||||
UInt128 one = UInt128.One;
|
||||
UInt128 result = UInt128.One;
|
||||
value %= modulus;
|
||||
|
||||
while (exponent != zero)
|
||||
{
|
||||
if ((exponent & one) != zero)
|
||||
{
|
||||
result = MulMod(result, value, modulus);
|
||||
}
|
||||
|
||||
value = MulMod(value, value, modulus);
|
||||
exponent >>= 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 Mul64(this UInt128 a, UInt128 b)
|
||||
{
|
||||
ulong aLow = (ulong)a;
|
||||
ulong bLow = (ulong)b;
|
||||
ulong bHigh = (ulong)(b >> 64);
|
||||
|
||||
ulong low = aLow * bLow;
|
||||
|
||||
// Keep the high-word accumulation in locals so the JIT does not rebuild the
|
||||
// expression tree around the shift. Mirroring the MulHigh layout lets RyuJIT
|
||||
// keep the intermediate sum in registers instead of reloading the partial
|
||||
// product from the stack.
|
||||
ulong high = aLow.MulHigh(bLow);
|
||||
high += aLow * bHigh;
|
||||
|
||||
return ((UInt128)high << 64) | low;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 MulMod(this UInt128 a, UInt128 b, UInt128 modulus)
|
||||
{
|
||||
UInt128 one = UInt128.One,
|
||||
result = UInt128.Zero,
|
||||
zero = UInt128.Zero;
|
||||
|
||||
while (b != zero)
|
||||
{
|
||||
if ((b & one) != zero)
|
||||
{
|
||||
result += a;
|
||||
if (result >= modulus)
|
||||
{
|
||||
result -= modulus;
|
||||
}
|
||||
}
|
||||
|
||||
a <<= 1;
|
||||
if (a >= modulus)
|
||||
{
|
||||
a -= modulus;
|
||||
}
|
||||
|
||||
b >>= 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 Pow(this UInt128 value, ulong exponent)
|
||||
{
|
||||
UInt128 result = UInt128.One;
|
||||
while (exponent != 0UL)
|
||||
{
|
||||
if ((exponent & 1UL) != 0UL)
|
||||
{
|
||||
result *= value;
|
||||
}
|
||||
|
||||
value *= value;
|
||||
exponent >>= 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
namespace PerfectNumbers.Core
|
||||
{
|
||||
public static class UInt128Numbers
|
||||
{
|
||||
public static readonly UInt128 Two = 2UL;
|
||||
public static readonly UInt128 Four = 4UL;
|
||||
public static readonly UInt128 SixtyFour = 64UL;
|
||||
public static readonly UInt128 OneHundredTwentySeven = 127UL;
|
||||
public static class UInt128Numbers
|
||||
{
|
||||
public static readonly UInt128 Two = 2UL;
|
||||
public static readonly UInt128 Four = 4UL;
|
||||
public static readonly UInt128 SixtyFour = 64UL;
|
||||
public static readonly UInt128 OneHundredTwentySeven = 127UL;
|
||||
// TODO: Populate this table from the UInt128 residue benchmarks so GPU kernels can pull precomputed constants
|
||||
// without re-deriving them at runtime.
|
||||
// TODO: Extend the table with cached Mod3/Mod5 folding constants so both CPU and GPU residue helpers can reuse
|
||||
// the benchmarked multiply-high reductions without recomputing them per call.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,18 @@ namespace PerfectNumbers.Core;
|
||||
|
||||
public static class UIntExtensions
|
||||
{
|
||||
private const ulong Mod3Mask = 0xAAAAAAABUL;
|
||||
public const ulong Mod5Mask = 0xCCCCCCCDUL;
|
||||
private const ulong Mod3Mask = 0xAAAAAAABUL;
|
||||
public const ulong Mod5Mask = 0xCCCCCCCDUL;
|
||||
// TODO: Extend this helper set with the Mod7/Mod11 lookup constants so CPU residue automation can drop `%` entirely in
|
||||
// the small-prime sieves.
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint Mod3(uint value) => value - (uint)((value * Mod3Mask) >> 33) * 3U;
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint Mod3(uint value) => value - (uint)((value * Mod3Mask) >> 33) * 3U;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint Mod5(this uint value) => value - (uint)((value * Mod5Mask) >> 34) * 5U;
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint Mod5(this uint value) => value - (uint)((value * Mod5Mask) >> 34) * 5U;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint Mod8(this uint value) => value & 7U;
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint Mod8(this uint value) => value & 7U;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ public static class ULongExtensions
|
||||
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++)
|
||||
{
|
||||
@@ -235,6 +239,8 @@ public static class ULongExtensions
|
||||
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)
|
||||
@@ -367,6 +373,8 @@ public static class ULongExtensions
|
||||
return exponent.Pow2MontgomeryMod(divisor);
|
||||
}
|
||||
|
||||
// TODO: Swap this modulo with the divisor-cycle remainder helper from Pow2MontgomeryModCycleComputationBenchmarks so we
|
||||
// advance the exponent using cached cycle deltas instead of paying for a fresh `%` on every query.
|
||||
ulong rotationCount = exponent % cycleLength;
|
||||
return rotationCount.Pow2MontgomeryModFromCycleRemainder(divisor);
|
||||
}
|
||||
@@ -474,6 +482,8 @@ public static class ULongExtensions
|
||||
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++)
|
||||
{
|
||||
@@ -525,6 +535,8 @@ public static class ULongExtensions
|
||||
|
||||
// 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)
|
||||
@@ -581,6 +593,8 @@ public static class ULongExtensions
|
||||
|
||||
// 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
|
||||
@@ -626,6 +640,8 @@ public static class ULongExtensions
|
||||
// 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;
|
||||
|
||||
@@ -633,6 +649,8 @@ public static class ULongExtensions
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user