Clarify cycle stepper reset inlining

This commit is contained in:
Marek Lemańczyk committed 2025-10-02 13:30:56 +02:00
1 parent 8b467c27f3
commit b8aa845c5c
53 files changed
+1736 -1131

No files matched your search

+4
View File
@@ -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();
}
+9
View File
@@ -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,
+9 -5
View File
@@ -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;
}
+9 -2
View File
@@ -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;
+42 -8
View File
@@ -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);
}
}
+3
View File
@@ -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;
+8 -5
View File
@@ -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 -3
View File
@@ -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
}
}
}
+4 -4
View File
@@ -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;
}
+29 -16
View File
@@ -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();
}
}
}
+21 -14
View File
@@ -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);
}
}
+6
View File
@@ -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);
}
}
+6
View File
@@ -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,
+15 -9
View File
@@ -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;
}
+31 -24
View File
@@ -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;
}
+45 -5
View File
@@ -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;
}
}
+20 -16
View File
@@ -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
+47 -18
View File
@@ -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;
}
}
+17 -1
View File
@@ -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.
}
+6 -1
View File
@@ -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;
+4
View File
@@ -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;
+23
View File
@@ -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;
+117 -109
View File
@@ -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
View File
@@ -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();
}
}
+3
View File
@@ -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.
}
+6
View File
@@ -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;
+8 -2
View File
@@ -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);
}
}
}
+17 -1
View File
@@ -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();
+309 -292
View File
@@ -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;
}
}
+12 -7
View File
@@ -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.
}
}
}
+10 -8
View File
@@ -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;
}
+18
View File
@@ -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;
}