Add TODOs for divisor metadata caching

This commit is contained in:
Marek Lemańczyk committed 2025-10-01 22:52:21 +02:00
1 parent 513fc7acb0
commit 36d316d19c
53 files changed
+929 -393

No files matched your search

@@ -18,7 +18,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
public bool UseDivisorCycles
{
get => _useDivisorCycles;
set => _useDivisorCycles = value;
set => _useDivisorCycles = value; // TODO: Delete the toggle once divisor cycle data is always consulted so CPU scans never fall back to the slower pure-Montgomery path.
}
public int BatchSize
@@ -191,6 +191,9 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
lastProcessed = divisor;
MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(divisor);
// TODO: Hoist MontgomeryDivisorData acquisition into the divisor-cycle cache so we reuse the
// staged ProcessEightBitWindows-ready metadata instead of reloading the slower Montgomery
// structs for every divisor.
byte hit;
ulong divisorCycle = 0UL;
DivisorCycleCache.CycleBlock? cycleBlock = null;
@@ -202,6 +205,9 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
}
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.
hit = CheckDivisor(prime, false, 0UL, divisorData);
}
@@ -225,6 +231,9 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
}
divisor += 2UL;
// TODO: Replace this linear increment with the batched divisor-cycle walker validated in the
// CPU by-divisor benchmarks so we advance directly to the next cached divisor candidate
// instead of testing every odd integer.
}
if (!processedAll)
@@ -244,7 +253,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
ulong residue = useCycles && divisorCycle != 0UL
? prime.Pow2MontgomeryModWithCycle(divisorCycle, divisorData)
: prime.Pow2MontgomeryMod(divisorData);
: prime.Pow2MontgomeryMod(divisorData); // TODO: Switch to the upcoming ProcessEightBitWindows helper once the
// scalar Pow2Minus1Mod adopts it so CPU scans match the GPU speedups.
return residue == 1UL ? (byte)1 : (byte)0;
}
@@ -264,6 +274,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
}
ulong total = _lastStatusDivisor + processedCount;
// TODO: Replace this modulo with the ring-buffer style counter (subtract loop) used in the fast CLI
// status benchmarks so we avoid `%` in this hot loop while still wrapping progress correctly.
_lastStatusDivisor = total % interval;
}
@@ -372,6 +384,9 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return;
}
// TODO: Remove this no-cycle fallback once divisor cycles are mandatory; the per-prime pow2 ladder here
// keeps the slower Montgomery stepping alive even though the benchmarks showed the cached cycle path is far
// faster for large divisor sets.
for (int i = 0; i < length; i++)
{
hits[i] = exponentStepper.ComputeNextIsUnity(primes[i]) ? (byte)1 : (byte)0;
@@ -39,29 +39,41 @@ public class MersenneNumberIncrementalCpuTester(GpuKernelType kernelType)
if (shouldCheck)
{
if (_kernelType == GpuKernelType.Pow2Mod)
{
reject = exponent.PowModWithCycle(q, qCycle) == UInt128.One;
}
else
{
phi = q - one;
reject = false;
if (phi <= ulong.MaxValue)
{
phi64 = (ulong)phi;
if (phi64.PowModWithCycle(q, qCycle) == one)
{
halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - one;
if (halfPow.BinaryGcd(q) == one)
{
div = phi64.FastDiv64(exponent, divMul);
divPow = div.PowModWithCycle(q, qCycle) - one;
if (divPow.BinaryGcd(q) == one)
{
reject = true;
}
}
if (_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)
{
reject = true;
}
}
}
}
}
@@ -3,39 +3,43 @@ namespace PerfectNumbers.Core;
public class MersenneNumberLucasLehmerCpuTester
{
public bool IsPrime(ulong exponent)
{
{
// Early rejections aligned with incremental/order sieves, but safe for small p:
// - If 3 | p and p != 3, then 7 | M_p -> composite.
// - If p ≡ 1 (mod 4) and p shares a factor with (p-1), reject fast.
// - If p is divisible by divisors specific to numbers ending with 1 or 7, reject fast.
// TODO: Replace these `%` checks with Mod3/Mod5/Mod7/Mod11 helpers once Lucas–Lehmer CPU filtering
// shares the benchmarked bitmask implementations and avoids slow modulo instructions in the hot path.
if (
((exponent % 3UL) == 0UL && exponent != 3UL) ||
((exponent % 5UL) == 0UL && exponent != 5UL) ||
((exponent % 7UL) == 0UL && exponent != 7UL) ||
((exponent % 11UL) == 0UL && exponent != 11UL)
)
{
return false;
}
{
return false;
}
if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOne())
{
return false;
}
if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOne())
{
return false;
}
UInt128 s = UInt128Numbers.Four, two = UInt128Numbers.Two,
m = (UInt128.One << (int)exponent) - UInt128.One;
UInt128 s = UInt128Numbers.Four, two = UInt128Numbers.Two,
m = (UInt128.One << (int)exponent) - UInt128.One;
UInt128 limit = exponent - UInt128Numbers.Two;
for (UInt128 i = UInt128.Zero; i < limit; i++)
{
s = s.PowModWithCycle(two, MersenneDivisorCycles.GetCycle(m)) - two;
if (s < UInt128.Zero)
{
s += m;
}
}
UInt128 limit = exponent - UInt128Numbers.Two;
for (UInt128 i = UInt128.Zero; i < limit; i++)
{
// TODO: Port this Lucas–Lehmer powmod to the ProcessEightBitWindows helper so the CPU
// fallback benefits from the same windowed ladder that halves runtime in the Pow2 benchmarks.
s = s.PowModWithCycle(two, MersenneDivisorCycles.GetCycle(m)) - two;
if (s < UInt128.Zero)
{
s += m;
}
}
return s == UInt128.Zero;
}
return s == UInt128.Zero;
}
}
@@ -30,33 +30,42 @@ public class MersenneNumberOrderCpuTester(GpuKernelType kernelType)
}
if (shouldCheck)
{
if (_kernelType == GpuKernelType.Pow2Mod)
{
if (exponent.PowModWithCycle(q, qCycle) == 1UL)
{
Volatile.Write(ref isPrime, false);
break;
}
}
else
{
UInt128 phi = q - 1UL;
if (phi <= ulong.MaxValue)
{
ulong phi64 = (ulong)phi;
if (phi64.PowModWithCycle(q, qCycle) == 1UL)
{
UInt128 halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - 1UL;
if (halfPow.BinaryGcd(q) == 1UL)
{
ulong divMul = (ulong)((((UInt128)1 << 64) - 1UL) / exponent) + 1UL;
ulong div = phi64.FastDiv64(exponent, divMul);
UInt128 divPow = div.PowModWithCycle(q, qCycle) - 1UL;
if (divPow.BinaryGcd(q) == 1UL)
{
Volatile.Write(ref isPrime, false);
break;
}
if (_kernelType == GpuKernelType.Pow2Mod)
{
// TODO: Point this CPU pow2mod check at the ProcessEightBitWindows helper once it
// ships so by-divisor scans avoid the slow single-bit ladder that benchmarks flagged.
if (exponent.PowModWithCycle(q, qCycle) == 1UL)
{
Volatile.Write(ref isPrime, false);
break;
}
}
else
{
UInt128 phi = q - 1UL;
if (phi <= ulong.MaxValue)
{
ulong phi64 = (ulong)phi;
// TODO: Switch these phi-based powmods to the shared windowed helper so CPU
// fallback paths keep pace with the optimized GPU kernels on large divisors.
if (phi64.PowModWithCycle(q, qCycle) == 1UL)
{
// TODO: Reuse the windowed pow2 helper for halfPow as soon as it is
// available instead of recalculating via square-and-multiply.
UInt128 halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - 1UL;
if (halfPow.BinaryGcd(q) == 1UL)
{
ulong divMul = (ulong)((((UInt128)1 << 64) - 1UL) / exponent) + 1UL;
ulong div = phi64.FastDiv64(exponent, divMul);
// TODO: Replace this divisor powmod with the ProcessEightBitWindows
// implementation so order scans stop paying the slower bit-serial
// loop measured in the MulMod benchmarks.
UInt128 divPow = div.PowModWithCycle(q, qCycle) - 1UL;
if (divPow.BinaryGcd(q) == 1UL)
{
Volatile.Write(ref isPrime, false);
break;
}
}
}
}
@@ -36,6 +36,8 @@ internal struct CycleRemainderStepper
ulong remainder = prime;
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.
remainder %= cycleLength;
}
_previousPrime = prime;
@@ -63,6 +65,8 @@ internal struct CycleRemainderStepper
if (ulong.MaxValue - _currentRemainder < delta)
{
UInt128 extended = (UInt128)_currentRemainder + delta;
// TODO: Replace this `%` with the UInt128-aware cycle reducer so large deltas use the cached subtraction
// ladder instead of falling back to the slower modulo implementation.
_currentRemainder = (ulong)(extended % cycleLength);
return _currentRemainder;
}
@@ -74,6 +78,8 @@ internal struct CycleRemainderStepper
_currentRemainder -= cycleLength;
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.
_currentRemainder %= cycleLength;
}
}
@@ -44,6 +44,9 @@ internal struct ExponentRemainderStepper
if (!_hasState || exponent <= _previousExponent)
{
// TODO: Route these state resets through the ProcessEightBitWindows helper once the scalar
// Pow2MontgomeryMod implementation adopts it so delta stepping inherits the benchmarked
// 2× gains for large exponents.
_currentMontgomery = exponent.Pow2MontgomeryModMontgomery(_divisor);
_previousExponent = exponent;
_hasState = true;
@@ -51,6 +54,8 @@ internal struct ExponentRemainderStepper
}
ulong delta = exponent - _previousExponent;
// 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);
_currentMontgomery = _currentMontgomery.MontgomeryMultiply(multiplier, _modulus, _nPrime);
_previousExponent = exponent;
@@ -67,6 +72,8 @@ internal struct ExponentRemainderStepper
if (!_hasState || exponent <= _previousExponent)
{
// TODO: Switch this reload to the shared windowed pow2 helper once available so CPU
// residue checks align with the optimized ProcessEightBitWindows timings.
_currentMontgomery = exponent.Pow2MontgomeryModMontgomery(_divisor);
_previousExponent = exponent;
_hasState = true;
@@ -74,6 +81,8 @@ internal struct ExponentRemainderStepper
}
ulong delta = exponent - _previousExponent;
// 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);
_currentMontgomery = _currentMontgomery.MontgomeryMultiply(multiplier, _modulus, _nPrime);
_previousExponent = exponent;
@@ -107,6 +116,8 @@ internal struct ExponentRemainderStepper
if (!_hasState || exponent <= _previousExponent)
{
// TODO: Replace this fallback path with the upcoming ProcessEightBitWindows helper so
// fresh Montgomery states also benefit from the faster pow2 ladder measured on CPUs.
_currentMontgomery = exponent.Pow2MontgomeryModMontgomery(_divisor);
_previousExponent = exponent;
_hasState = true;
+15 -1
View File
@@ -199,6 +199,8 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
ArrayView1D<ulong, Stride1D.Dense> smallCycles)
{
ulong idx = (ulong)index.X;
// TODO: Replace these `%` computations with the precomputed Mod3/Mod5 tables so GPU kernels reuse cached residues
// instead of performing modulo operations that the benchmarks showed slower on wide sweeps.
ulong idxMod3 = idx % 3UL;
ulong idxMod5 = idx % 5UL;
// residue automaton
@@ -216,7 +218,9 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
}
else
{
ulong step3 = ((exponent % 3UL) << 1) % 3UL;
// TODO: Swap these `%` filters to the shared Mod helpers so the residue automaton matches the benchmarked
// bitmask-based implementation for GPU workloads.
ulong step3 = ((exponent % 3UL) << 1) % 3UL;
ulong r3 = q0m3 + (step3 * idxMod3); r3 -= (r3 >= 3UL) ? 3UL : 0UL;
if (r3 == 0UL)
{
@@ -264,6 +268,8 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
}
ulong phi64 = phi.Low;
// TODO: Replace these Pow2Mod calls with the ProcessEightBitWindows helper when the shared windowed
// scalar implementation lands; benchmarks showed the windowed kernel trimming per-divisor runtime by ~2.4×.
if (GpuUInt128.Pow2Mod(phi64, q) != GpuUInt128.One)
{
orders[index] = 0UL;
@@ -299,6 +305,8 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
}
ulong phi64 = phi.Low;
// TODO: Once the ProcessEightBitWindows helper is available, switch this order kernel to that faster
// Pow2Mod variant so cycle checks inherit the same gains observed in GpuPow2ModBenchmarks.
GpuUInt128 pow = GpuUInt128.Pow2Mod(phi64, q);
if (pow != GpuUInt128.One)
{
@@ -409,6 +417,8 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
return;
}
}
// TODO: Swap Pow2Minus1Mod for the eight-bit window helper once the scalar version switches;
// benchmarks show the windowed variant cuts large-divisor scans from ~51 µs to ~21 µs.
if (GpuUInt128.Pow2Minus1Mod(exponent, q) != GpuUInt128.Zero)
{
orders[index] = 0UL;
@@ -563,6 +573,8 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
}
ulong phi64 = phi.Low;
// TODO: Upgrade this pow2mod order kernel to the ProcessEightBitWindows helper once available so GPU residue
// scans avoid the single-bit ladder that benchmarks found to be 2.3× slower on large exponents.
if (GpuUInt128.Pow2Mod(phi64, q) != GpuUInt128.One)
{
return;
@@ -639,6 +651,8 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
return;
}
}
// TODO: Replace this Pow2Mod check with the ProcessEightBitWindows helper once Pow2Minus1Mod adopts it;
// residue order scans will then benefit from the same 2× speedup the benchmarked windowed kernel delivered.
if (GpuUInt128.Pow2Mod(exponent, q) != GpuUInt128.One)
{
return;
+20
View File
@@ -256,6 +256,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
return Zero;
}
// TODO: Replace this single-bit ladder with the ProcessEightBitWindows strategy measured
// in GpuPow2ModBenchmarks once the GPU kernels can share the windowed helper. The windowed
// version held 21.5 µs for 33-bit moduli versus 51.0 µs here when scanning large divisors.
GpuUInt128 result = Zero;
ulong i = 0UL;
while (i < exponent)
@@ -287,6 +290,8 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
ulong e0 = 0UL, e1 = 0UL, e2 = 0UL, e3 = 0UL, e4 = 0UL, e5 = 0UL, e6 = 0UL, e7 = 0UL;
bool a0 = false, a1 = false, a2 = false, a3 = false, a4 = false, a5 = false, a6 = false, a7 = false;
// TODO: Mirror the eight-bit window batching once the scalar helper above switches; the
// current bit-serial loop lags by ~3× on 8k–33-bit moduli compared to the windowed path.
int len = exponents.Length;
if (len > 0) { e0 = exponents[0]; a0 = true; }
if (len > 1) { e1 = exponents[1]; a1 = true; }
@@ -372,6 +377,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddMod(GpuUInt128 value, ulong modulus)
{
// TODO: Pre-reduce the operands via the Montgomery ladder used in MulMod64Benchmarks so the GPU
// compatible shim stops paying for `%` on every call; the InlineUInt128 helper ran 6–82× faster on
// large 64-bit workloads while preserving compatibility with the CPU scanner.
ulong a = Low % modulus;
ulong b = value.Low % modulus;
ulong sum = a + b;
@@ -387,6 +395,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddMod(ulong value, ulong modulus)
{
// TODO: Fold these operands with the ImmediateModulo helper once the GPU shim exposes it, avoiding
// repeated `%` reductions that the benchmarks showed are far slower than the Montgomery-based path
// for dense operands.
ulong a = Low % modulus;
ulong b = value % modulus;
ulong sum = a + b;
@@ -718,6 +729,8 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
/// </summary>
internal void MulModByLimb(GpuUInt128 other, GpuUInt128 modulus)
{
// TODO: Relocate this limb-based reducer to the benchmark project once the production
// pipeline switches to the faster allocating legacy path demonstrated in the benchmarks.
MultiplyFull(this, other, out var p3, out var p2, out var p1, out var p0);
High = p3;
@@ -808,6 +821,8 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ulong MulModWithNativeModulo(ulong value, ulong modulus)
{
// TODO: Drop this native-modulo path from production after migrating callers to MulMod,
// which benchmarked 4-8× faster on dense operands and still wins on mixed workloads.
ulong multiplicand = Low % modulus;
var remainder = value % modulus;
@@ -891,6 +906,8 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong ShiftLeftByNativeChunk(ulong value, ulong modulus)
{
// TODO: Collapse this eight-step shift ladder into the ProcessEightBitWindows helper once it lands so
// we reuse the precomputed window residues instead of emitting `% modulus` after every shift.
value = (value << 1) % modulus;
value = (value << 1) % modulus;
value = (value << 1) % modulus;
@@ -929,6 +946,8 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ulong MulModMontgomery64(GpuUInt128 other, ulong modulus, ulong nPrime, ulong r2)
{
// TODO: Retire this struct-based Montgomery path from production after adopting the extension
// helper, which benchmarks 6-7× faster across dense and near-modulus operands.
ulong modulusLocal = modulus;
ulong a = Low % modulusLocal;
ulong b = other.Low % modulusLocal;
@@ -947,6 +966,7 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ulong MulModMontgomery64(ulong other, ulong modulus, ulong nPrime, ulong r2)
{
// TODO: Same as above—migrate callers to the scalar extension to avoid this 6-7× slowdown.
ulong modulusLocal = modulus;
ulong a = Low % modulusLocal;
ulong b = other % modulusLocal;
@@ -46,7 +46,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
public bool UseDivisorCycles
{
get => _useDivisorCycles;
set => _useDivisorCycles = value;
set => _useDivisorCycles = value; // TODO: Remove this flag once GPU runs unconditionally fetch cycle lengths so every divisor check benefits from the measured speedups.
}
public void ConfigureFromMaxPrime(ulong maxPrime)
@@ -295,6 +295,9 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
for (int i = 0; i < batchSize; i++)
{
// TODO: Stage MontgomeryDivisorData in a GPU-ready cache keyed by divisor so we can copy
// precomputed ProcessEightBitWindows metadata directly instead of rehydrating the slower
// Montgomery structs for each batch item.
divisorDataSpan[i] = MontgomeryDivisorDataCache.Get(divisorSpan[i]);
}
@@ -397,6 +400,8 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ulong remainder = prime;
if (remainder >= cycleLength)
{
// TODO: Replace this `%` with the cycle-cache helper that subtracts precomputed
// remainders (or uses a lookup table) so GPU batching avoids modulo in this inner loop.
remainder %= cycleLength;
}
@@ -456,6 +461,8 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
for (int i = 0; i < batchSize; i++)
{
// TODO: Preload ProcessEightBitWindows-friendly divisor metadata for GPU batches so this
// hot loop stops rebuilding MontgomeryDivisorData structures for every scan chunk.
divisorDataSpan[i] = MontgomeryDivisorDataCache.Get(divisorSpan[i]);
}
@@ -541,7 +548,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
}
ulong exponent = exponents[index];
hits[index] = exponent.Pow2MontgomeryMod(divisor) == 1UL ? (byte)1 : (byte)0;
hits[index] = exponent.Pow2MontgomeryMod(divisor) == 1UL ? (byte)1 : (byte)0; // TODO: Replace Pow2MontgomeryMod with the ProcessEightBitWindows helper once available so GPU by-divisor scans reuse the benchmarked fast pow2 ladder.
}
public sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
@@ -762,6 +769,8 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
if (workValue >= divisorCycle)
{
// TODO: Replace this `%` with the divisor-cycle remainder helper so we reuse cached
// modulo results instead of recomputing them in the GPU staging loop.
workValue %= divisorCycle;
}
@@ -831,7 +840,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ulong exponent = exponents[index];
results[index] = exponent.Pow2MontgomeryModMontgomery(divisor);
results[index] = exponent.Pow2MontgomeryModMontgomery(divisor); // TODO: Swap this Montgomery-domain pow2 with the windowed helper slated to replace the slower ladder in Pow2Minus1Mod benchmarks.
}
public ulong DivisorLimit
@@ -75,6 +75,8 @@ public sealed class MersenneNumberDivisorGpuTester
for (int k = 0; k < len; k++)
{
(ulong dSmall, cycle) = candidates[k];
// TODO: Replace this `%` check with the shared divisor-cycle divisibility helper so GPU scans
// reuse the cached cycle remainders instead of recomputing modulo for every candidate.
if (p % cycle != 0UL)
{
continue;
@@ -109,10 +111,12 @@ public sealed class MersenneNumberDivisorGpuTester
}
UInt128 cycle128 = MersenneDivisorCycles.GetCycle(d);
if ((UInt128)p % cycle128 != UInt128.Zero)
{
continue;
}
// 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)
{
continue;
}
if (IsDivisible(p, d))
{
@@ -138,9 +142,11 @@ public sealed class MersenneNumberDivisorGpuTester
? 63 - XMath.LeadingZeroCount(divisor.Low)
: 127 - XMath.LeadingZeroCount(divisor.High);
mod = divisor;
if (x == 0)
{
baseVal = GpuUInt128.Pow2Mod(exponent, mod);
if (x == 0)
{
// TODO: Swap Pow2Mod for the ProcessEightBitWindows helper once Pow2Minus1Mod adopts it;
// GpuPow2ModBenchmarks showed the windowed kernel cutting large divisors from ~51 µs to ~21 µs.
baseVal = GpuUInt128.Pow2Mod(exponent, mod);
result[0] = baseVal.High == 0UL && baseVal.Low == 1UL ? (byte)1 : (byte)0;
return;
}
@@ -155,9 +161,11 @@ public sealed class MersenneNumberDivisorGpuTester
baseVal = new GpuUInt128(0UL, 1UL << x);
}
GpuUInt128 pow = baseVal;
pow.ModPow(exponent / ux, mod);
var part2 = GpuUInt128.Pow2Mod(exponent % ux, mod);
GpuUInt128 pow = baseVal;
pow.ModPow(exponent / ux, mod);
// TODO: Replace this trailing Pow2Mod call with the ProcessEightBitWindows variant once the shared
// windowed helper lands so mixed-radix decompositions benefit from the same GPU speedup.
var part2 = GpuUInt128.Pow2Mod(exponent % ux, mod);
GpuUInt128 product = pow;
product.MulMod(part2, mod);
result[0] = product.High == 0UL && product.Low == 1UL ? (byte)1 : (byte)0;
@@ -20,7 +20,7 @@ public class MersenneNumberIncrementalGpuTester(GpuKernelType kernelType, bool u
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
byte last = lastIsSeven ? (byte)1 : (byte)0; // ILGPU kernels do not support bool parameters
var pow2Kernel = gpuLease.Pow2ModKernel;
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;
@@ -51,6 +51,7 @@ public class MersenneNumberLucasLehmerGpuTester
// Early rejections aligned with incremental/order sieves, but safe for small p:
// - If 3 | p and p != 3, then 7 | M_p -> composite.
// - If p ≡ 1 (mod 4) and p shares a factor with (p-1), reject fast.
// TODO: Replace this `% 3` guard with ULongExtensions.Mod3 once GPU LL filtering reuses the benchmarked bitmask helper.
if ((exponent % 3UL) == 0UL && exponent != 3UL)
{
return false;
@@ -78,7 +79,7 @@ public class MersenneNumberLucasLehmerGpuTester
var gpu = GpuContextPool.RentPreferred(preferCpu: !runOnGpu);
var accelerator = gpu.Accelerator;
var kernel = GetKernel(accelerator);
var modulus = new GpuUInt128(((UInt128)1 << (int)exponent) - 1UL);
var modulus = new GpuUInt128(((UInt128)1 << (int)exponent) - 1UL); // TODO: Cache these Mersenne moduli per exponent so LL GPU runs skip rebuilding them every launch.
var buffer = accelerator.Allocate1D<GpuUInt128>(1);
kernel(1, exponent, modulus, buffer.View);
accelerator.Synchronize();
@@ -109,7 +110,7 @@ public class MersenneNumberLucasLehmerGpuTester
var gpu = GpuContextPool.RentPreferred(preferCpu: false);
var accelerator = gpu.Accelerator;
var kernel = GetBatchKernel(accelerator);
var kernel = GetBatchKernel(accelerator); // TODO: Switch this batch path to the ProcessEightBitWindows residue kernel once Lucas–Lehmer integrates the benchmarked windowed pow2 helper for small exponents.
var expBuffer = accelerator.Allocate1D<ulong>(count);
ulong[] expArray = ArrayPool<ulong>.Shared.Rent(count);
@@ -403,6 +404,8 @@ public class MersenneNumberLucasLehmerGpuTester
var factors = new List<ulong>();
for (ulong p = 2UL; p * p <= n; p += p == 2UL ? 1UL : 2UL)
{
// TODO: Replace these `%` factor checks with the shared Mod helpers (Mod3/Mod5/etc.) once the GPU
// pre-filter adopts the benchmarked bitmask operations to avoid slow modulo instructions.
if (n % p == 0UL)
{
factors.Add(p);
@@ -435,6 +438,8 @@ public class MersenneNumberLucasLehmerGpuTester
for (ulong divisor = 3UL; divisor * divisor <= n; divisor += 2UL)
{
// TODO: Swap this `%` for the divisor-cycle aware Mod helper once the residue pre-checks expose it
// so primality filtering avoids slow modulo instructions in tight loops.
if (n % divisor == 0UL)
{
return false;
@@ -451,6 +456,8 @@ public class MersenneNumberLucasLehmerGpuTester
{
if ((exponent & 1UL) != 0UL)
{
// TODO: Route these powmods through the ProcessEightBitWindows helper once it lands for GPU
// host-side fallbacks so the Lucas–Lehmer setup reuses the benchmarked windowed ladder.
result = MulMod(result, value, modulus);
}
@@ -463,6 +470,8 @@ public class MersenneNumberLucasLehmerGpuTester
private static ulong MulMod(ulong a, ulong b, ulong modulus)
{
// TODO: Swap this UInt128 `%` reduction for the GPU-compatible MulMod helper once it adopts the faster
// inline UInt128 path benchmarked in MulMod64Benchmarks so host/GPU parity avoids BigInteger-style fallbacks.
return (ulong)(((UInt128)a * b) % modulus);
}
@@ -18,11 +18,11 @@ public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuO
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
byte last = lastIsSeven ? (byte)1 : (byte)0; // ILGPU kernels do not support bool parameters
var kernel = _kernelType switch
{
GpuKernelType.Pow2Mod => gpuLease.Pow2ModOrderKernel,
_ => gpuLease.IncrementalOrderKernel,
};
var kernel = _kernelType switch
{
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.
var foundBuffer = accelerator.Allocate1D<int>(1);
exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
@@ -24,7 +24,7 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder)
UInt128 kStart = 1UL;
UInt128 limit = maxK + UInt128.One;
byte last = lastIsSeven ? (byte)1 : (byte)0;
var kernel = gpuLease.Pow2ModKernel;
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();
+5
View File
@@ -414,6 +414,8 @@ public static class NttGpuMath
// load a block of size `len` into shared memory, do butterflies, write back.
// Requires explicit grouped kernels and chosen group size.
int t = index.X;
// TODO: Replace this `%` with a bitmask when `half` is a power of two so the stage index math matches the faster
// residue strategy from the GPU residue benchmarks.
int j = t % half;
int block = t / half;
int k = block * len;
@@ -488,6 +490,8 @@ public static class NttGpuMath
private static void StageBarrett128Kernel(Index1D index, ArrayView<GpuUInt128> data, int len, int half, int stageOffset, ArrayView<GpuUInt128> twiddles, ulong modHigh, ulong modLow, ulong muHigh, ulong muLow)
{
int t = index.X;
// TODO: Use the bitmask-based remainder helper here as well to remove `%` from the butterfly stage and align with the
// optimized kernels highlighted in the GPU pow2mod benchmarks.
int j = t % half;
int block = t / half;
int k = block * len;
@@ -537,6 +541,7 @@ public static class NttGpuMath
private static void StageMontKernel(Index1D index, ArrayView<GpuUInt128> data, int len, int half, int stageOffset, ArrayView<GpuUInt128> twiddlesMont, ulong modulus, ulong nPrime)
{
int t = index.X;
// TODO: Apply the bitmask remainder helper to this stage too so every butterfly path drops the slower `%` operation.
int j = t % half;
int block = t / half;
int k = block * len;
@@ -4,7 +4,7 @@ namespace PerfectNumbers.Core;
public interface IMersenneNumberDivisorByDivisorTester
{
bool UseDivisorCycles { get; set; }
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; }
+4
View File
@@ -34,6 +34,8 @@ public static class KRangeFinder
public static bool TryFindForPrime(ERational alphaM, EInteger p, out int? min, out int? max, int kStart, int kEnd)
{
// TODO: Collapse this wrapper once the callers can consume the tuple directly; the extra call adds
// measurable overhead when we sweep millions of Euler primes while scanning the large-division range.
(min, max) = FindForPrime(alphaM, p, kStart, kEnd);
return min.HasValue && max.HasValue && min.Value <= max.Value;
}
@@ -72,6 +74,8 @@ public static class KRangeFinder
public static bool TryFind(ERational alphaM, EInteger pMin, EInteger pMax, out int? min, out int? max, int kStart, int kEnd, PrimeCache primes)
{
// TODO: Remove this pass-through once the scanning CLI switches to tuple-returning APIs; flattening
// the call chain saves the extra struct copy flagged in the profiler during the large divisor search.
(min, max) = Find(alphaM, pMin, pMax, kStart, kEnd, primes);
return min.HasValue && max.HasValue && min.Value <= max.Value;
}
+29 -9
View File
@@ -112,6 +112,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.
return CalculateCycleLength(divisor);
}
@@ -124,6 +127,9 @@ public class MersenneDivisorCycles
}
// Otherwise, find order of 2 mod divisor
// TODO: Port this UInt128 path to the unrolled-hex cycle calculator so wide
// divisors stop relying on the slow shift-and-subtract loop measured in the
// GPU benchmarks.
UInt128 order = UInt128.One, pow = UInt128Numbers.Two;
while (pow != UInt128.One)
{
@@ -193,11 +199,17 @@ public class MersenneDivisorCycles
continue;
}
if ((divisor % 3UL) == 0UL || (divisor % 5UL) == 0UL || (divisor % 7UL) == 0UL || (divisor % 11UL) == 0UL)
// TODO: Replace this modulo-based filter with the cached Mod3/Mod5/Mod7/Mod11
// helpers so the CPU generator stops paying for `%` in the small-prime sieve
// and lines up with the divisor-cycle pipeline.
if ((divisor % 3UL) == 0UL || (divisor % 5UL) == 0UL || (divisor % 7UL) == 0UL || (divisor % 11UL) == 0UL)
{
continue;
}
// TODO: Migrate this generation path to the shared
// unrolled-hex calculator so the CPU generator matches
// the GPU benchmark leader for large divisors.
ulong cycle = CalculateCycleLength(divisor);
localCycles[localCycleIndex++] = (divisor, cycle);
}
@@ -267,13 +279,16 @@ public class MersenneDivisorCycles
using var writer = new BinaryWriter(outputStream, Encoding.UTF8, leaveOpen: false);
try
{
while (skipCount > 0L && start <= maxDivisor)
{
if ((start & 1UL) == 0UL || (start % 3UL) == 0UL || (start % 5UL) == 0UL || (start % 7UL) == 0UL || (start % 11UL) == 0UL)
{
start++;
continue;
}
while (skipCount > 0L && start <= maxDivisor)
{
// TODO: Swap these modulo checks for the cached Mod3/Mod5/Mod7/Mod11 helpers so
// range initialization avoids repeated `%` calls before the unrolled-hex pipeline
// takes over.
if ((start & 1UL) == 0UL || (start % 3UL) == 0UL || (start % 5UL) == 0UL || (start % 7UL) == 0UL || (start % 11UL) == 0UL)
{
start++;
continue;
}
start++;
skipCount--;
@@ -316,7 +331,9 @@ public class MersenneDivisorCycles
{
d = divisors[i];
if ((d % 3UL) == 0UL || (d % 5UL) == 0UL || (d % 7UL) == 0UL || (d % 11UL) == 0UL)
// TODO: Replace this modulo sieve with the cached Mod helpers once the divisor-cycle cache
// is mandatory so cycle enumeration can drop the slower `%` operations in this hot loop.
if ((d % 3UL) == 0UL || (d % 5UL) == 0UL || (d % 7UL) == 0UL || (d % 11UL) == 0UL)
continue;
writer.Write(d);
@@ -436,6 +453,9 @@ public class MersenneDivisorCycles
}
// 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;
while (pow != 1)
{
@@ -232,7 +232,7 @@ public static class MersenneNumberDivisorByDivisorTester
Span<byte> hitsSpan = default;
int hitIndex = 0;
int index;
bool useDivisorCycles = tester.UseDivisorCycles;
bool useDivisorCycles = tester.UseDivisorCycles; // TODO: Remove the conditional path once all testers always enable divisor cycles to keep every divisor scan on the faster cached-length track.
ulong divisorCycle = 0UL;
try
+17 -9
View File
@@ -91,9 +91,11 @@ public sealed class MersenneNumberTester(
continue;
}
ulong ord = q.CalculateOrder();
OrderCache[q] = ord;
}
// TODO: Swap this fallback to the shared windowed order helper once CalculateOrder migrates
// to the ProcessEightBitWindows pipeline so warm-ups stop invoking the slower legacy powmod.
ulong ord = q.CalculateOrder();
OrderCache[q] = ord;
}
return;
}
@@ -177,6 +179,8 @@ public sealed class MersenneNumberTester(
ulong order = orders[i];
if (order == 0UL)
{
// TODO: Replace this CalculateOrder call with the upcoming windowed helper so
// GPU warm-ups reuse the faster pow2 ladder measured in the CPU order benchmarks.
order = qs[offset + i].CalculateOrder();
}
@@ -205,6 +209,8 @@ public sealed class MersenneNumberTester(
// 7 | M_p iff 3 | p. Avoid rejecting p == 3 where M_3 == 7 is prime.
if ((exponent % 3UL) == 0UL && exponent != 3UL)
{
// TODO: Replace this `% 3` check with ULongExtensions.Mod3 to align the early rejection with
// the benchmarked bitmask helper instead of generic modulo for CPU workloads.
return false;
}
@@ -309,12 +315,14 @@ public sealed class MersenneNumberTester(
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void LegacyOrderKernel(Index1D index, ulong exponent, ArrayView<UInt128> qs, ArrayView<ulong> orders)
{
UInt128 q = qs[index];
UInt128 pow = exponent.PowMod(q);
orders[index] = pow == 1UL ? exponent : 0UL;
}
private static void LegacyOrderKernel(Index1D index, ulong exponent, ArrayView<UInt128> qs, ArrayView<ulong> orders)
{
UInt128 q = qs[index];
// TODO: Swap this legacy kernel over to the ProcessEightBitWindows helper so GPU order scans share the
// eight-bit window pow2 implementation that beat the classic PowMod path in the GpuPow2Mod benchmarks.
UInt128 pow = exponent.PowMod(q);
orders[index] = pow == 1UL ? exponent : 0UL;
}
}
@@ -49,10 +49,14 @@ public static class MersennePrimeFactorTester
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static ulong GetOrderOf2ModPrime(ulong q, CancellationToken ct) =>
// TODO: Collapse this wrapper once all callers promote operands to UInt128 so we
// can jump straight to OrderOf2ModPrime without bouncing through multiple overloads.
(ulong)GetOrderOf2ModPrime((UInt128)q, ct);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static UInt128 GetOrderOf2ModPrime(UInt128 q, CancellationToken ct) =>
// TODO: Inline this shim once callers can invoke OrderOf2ModPrime directly; the extra
// hop shows up on hot perf traces while we chase large-order factors.
OrderOf2ModPrime(q, ct);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -113,6 +117,7 @@ public static class MersennePrimeFactorTester
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// TODO: Replace this duplicate powmod with ULongExtensions.ModPow64 so we inherit the optimized MulMod64 path that led the MulMod64Benchmarks on large operands.
private static ulong ModPow64(ulong value, ulong exponent, ulong modulus, CancellationToken ct)
{
ulong result = 1UL;
@@ -144,9 +149,10 @@ public static class MersennePrimeFactorTester
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulMod64(ulong a, ulong b, ulong mod) => (ulong)((UInt128)a * b % mod);
private static ulong MulMod64(ulong a, ulong b, ulong mod) => (ulong)((UInt128)a * b % mod); // TODO: Route this through ULongExtensions.MulMod64 so the powmod above adopts the inline UInt128 implementation that dominated the MulMod64Benchmarks.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// TODO: Switch to UInt128Extensions.ModPow once its MulMod backend adopts the faster UInt128BuiltIn path highlighted in MulHighBenchmarks to avoid the BigInteger fallback.
private static UInt128 ModPow128(UInt128 value, UInt128 exponent, UInt128 modulus, CancellationToken ct)
{
UInt128 result = 1;
@@ -180,7 +186,7 @@ public static class MersennePrimeFactorTester
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static UInt128 MulMod128(UInt128 a, UInt128 b, UInt128 mod)
{
BigInteger product = (BigInteger)a * b;
BigInteger product = (BigInteger)a * b; // TODO: Replace this BigInteger reduction with the forthcoming UInt128 intrinsic helper measured faster in Mul64Benchmarks and MulHighBenchmarks for huge operands.
product %= (BigInteger)mod;
return (UInt128)product;
}
@@ -413,6 +419,7 @@ public static class MersennePrimeFactorTester
return a;
}
// TODO: Port this Miller–Rabin routine to UInt128 intrinsics so we avoid the BigInteger.ModPow calls that lag by orders of magnitude on the large inputs highlighted in the Pow2MontgomeryMod benchmarks.
private static bool IsPrime128(UInt128 n, CancellationToken ct)
{
if (n <= 3)
@@ -13,6 +13,8 @@ public class MersenneResidueStepper
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;
}
@@ -27,18 +29,27 @@ public class MersenneResidueStepper
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.
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
// TODO: Switch to a cached divisor-cycle remainder instead of computing a fresh modular inverse for every
// 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();
}
+61 -45
View File
@@ -197,14 +197,17 @@ public sealed class ModResidueTracker
}
else
{
for (; i < divisors.Count; i++)
{
d = divisors[i];
value = PowMod128(UInt128Numbers.Two, delta, d);
// r' = pow2Delta*r + (pow2Delta - 1) mod d
value = MulMod128(value, residues[i], d) + value - UInt128.One;
if (value >= d)
{
for (; i < divisors.Count; i++)
{
d = divisors[i];
// TODO: Replace this PowMod128 call with the upcoming eight-bit window helper once the
// ProcessEightBitWindows scalar implementation lands so residue updates benefit from the
// ~2× win recorded in GpuPow2ModBenchmarks for large moduli.
value = PowMod128(UInt128Numbers.Two, delta, d);
// r' = pow2Delta*r + (pow2Delta - 1) mod d
value = MulMod128(value, residues[i], d) + value - UInt128.One;
if (value >= d)
{
value -= d;
}
@@ -238,23 +241,30 @@ public sealed class ModResidueTracker
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private UInt128 ComputeInitialResidue(UInt128 number, UInt128 divisor)
{
if (_model == ResidueModel.Identity)
{
// Fast path for 64-bit inputs
if (number <= ulong.MaxValue && divisor <= ulong.MaxValue)
{
return (ulong)number % (ulong)divisor;
}
if (_model == ResidueModel.Identity)
{
// Fast path for 64-bit inputs
if (number <= ulong.MaxValue && divisor <= ulong.MaxValue)
{
// TODO: Swap this modulo fallback for the shared divisor-cycle remainder helper so 64-bit
// residues reuse cached cycles instead of recomputing `%` on every lookup.
return (ulong)number % (ulong)divisor;
}
return number % divisor;
}
// TODO: Use the Montgomery folding helper measured faster in the Pow2Montgomery benchmarks so this
// wide residue path avoids general `%` on UInt128 inputs.
return number % divisor;
}
// residue of Mersenne number M_number = 2^number - 1 modulo divisor
UInt128 pow = PowMod128(UInt128Numbers.Two, number, divisor);
// 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.
UInt128 pow = PowMod128(UInt128Numbers.Two, number, divisor);
// q divides M_p ⇔ 2^p ≡ 1 (mod q) AND ord_q(2) | p
if (pow != UInt128.One)
return (pow - UInt128.One) % divisor;
// q divides M_p ⇔ 2^p ≡ 1 (mod q) AND ord_q(2) | p
if (pow != UInt128.One)
return (pow - UInt128.One) % divisor;
ulong ord = divisor.CalculateOrder();
if (ord == 0UL || divisor % ord != UInt128.Zero)
@@ -264,25 +274,28 @@ public sealed class ModResidueTracker
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static UInt128 PowMod128(UInt128 baseValue, UInt128 exponent, UInt128 modulus)
{
if (modulus == UInt128.One)
{
return UInt128.Zero;;
}
private static UInt128 PowMod128(UInt128 baseValue, UInt128 exponent, UInt128 modulus)
{
if (modulus == UInt128.One)
{
return UInt128.Zero;
}
UInt128 result = UInt128.One;
UInt128 value = baseValue % modulus;
UInt128 result = UInt128.One;
UInt128 value = baseValue % modulus;
while (exponent != UInt128.Zero)
{
if ((exponent & UInt128.One) != UInt128.Zero)
{
result = MulMod128(result, value, modulus);
}
while (exponent != UInt128.Zero)
{
if ((exponent & UInt128.One) != UInt128.Zero)
{
// TODO: Route this through the planned UInt128 windowed powmod so the inner MulMod adopts the
// UInt128BuiltIn path that dominated the MulHighBenchmarks on wide operands instead of the
// current square-and-multiply loop.
result = MulMod128(result, value, modulus);
}
exponent >>= 1;
if (exponent != UInt128.Zero)
exponent >>= 1;
if (exponent != UInt128.Zero)
{
value = MulMod128(value, value, modulus);
}
@@ -291,14 +304,17 @@ public sealed class ModResidueTracker
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static UInt128 MulMod128(UInt128 a, UInt128 b, UInt128 modulus)
{
// Same approach as MulMod in MersenneNumberTester: double-and-add to avoid overflow.
UInt128 result = UInt128.Zero;
a %= modulus;
private static UInt128 MulMod128(UInt128 a, UInt128 b, UInt128 modulus)
{
// Same approach as MulMod in MersenneNumberTester: double-and-add to avoid overflow.
// TODO: Replace this double-and-add fallback with the UInt128 intrinsic-backed multiplier once the
// MulHighBenchmarks-guided implementation lands; the intrinsic path measured dozens of times faster for
// dense 128-bit workloads.
UInt128 result = UInt128.Zero;
a %= modulus;
while (b != UInt128.Zero)
{
while (b != UInt128.Zero)
{
if ((b & UInt128.One) != UInt128.Zero)
{
result += result < modulus ? a : a - modulus;
+4
View File
@@ -131,6 +131,8 @@ public sealed class Ending7Automaton
}
}
// TODO: Pre-register every modulus routed through the automaton so the fallback `%` path disappears;
// the benchmarked residue trackers beat this division-heavy branch when scanning millions of values.
return (_n % modulus) == 0UL;
}
@@ -168,6 +170,8 @@ public sealed class Ending7Automaton
{
while (b != 0UL)
{
// TODO: Switch this to the binary GCD helper used in GpuUInt128BinaryGcdBenchmarks so we avoid `%`
// inside the residue automaton setup.
ulong t = a % b;
a = b;
b = t;
@@ -119,6 +119,8 @@ internal static class MontgomeryDivisorDataCache
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// TODO: Replace this generic `%` reduction with the UInt128 Montgomery folding helper measured faster in
// MontgomeryMultiplyBenchmarks so CPU paths avoid the slow BigInteger-style remainder in hot divisor cache lookups.
private static ulong ComputeMontgomeryResidue(ulong value, ulong modulus) => (ulong)((UInt128)value * (UInt128.One << 64) % modulus);
private static ulong ComputeMontgomeryNPrime(ulong modulus)
+15
View File
@@ -31,8 +31,16 @@ public static class PowerCache
if (powers[exponent].IsZero)
{
for (int i = 1; i <= exponent; i++)
{
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
// arbitrary-precision multiply is orders of magnitude slower for the exponents we
// scan when p >= 138M.
powers[i] = powers[i - 1] * baseVal;
}
}
}
return powers[exponent];
@@ -57,8 +65,15 @@ public static class PowerCache
if (powers[exponent] == null)
{
for (int i = 1; i <= exponent; i++)
{
if (powers[i] == null)
{
// TODO: Move the high-precision branch to the benchmark project once production
// switches to the divisor-cycle aware cache; maintaining this BigInteger multiply
// path in the hot pipeline keeps the slower code on the CPU scan.
powers[i] = powers[i - 1]! * baseVal;
}
}
}
return powers[exponent]!;
}
+5
View File
@@ -23,6 +23,8 @@ public sealed class PrimeCache
{
ulong current = _enumerator.Current;
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.
_primes.Add(last);
_primeMod4.Add((byte)(current & 3));
}
@@ -86,6 +88,9 @@ public sealed class PrimeCache
EInteger val = EInteger.FromUInt64(current);
_primes.Add(val);
_primeMod4.Add((byte)(current & 3));
// 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.
if (current >= s)
{
yield return val;
+26 -14
View File
@@ -10,7 +10,10 @@ namespace PerfectNumbers.Core;
public sealed class PrimeTester(bool useInternal = false)
{
public bool IsPrime(ulong n, CancellationToken ct) => IsPrimeInternal(n, ct);
public bool IsPrime(ulong n, CancellationToken ct) =>
// TODO: Eliminate this wrapper once callers can reach IsPrimeInternal directly; the extra
// indirection shows up when we sieve millions of candidates per second.
IsPrimeInternal(n, ct);
// Optional GPU-assisted primality: batched small-prime sieve on device.
public bool IsPrimeGpu(ulong n, CancellationToken ct)
@@ -52,6 +55,8 @@ public sealed class PrimeTester(bool useInternal = false)
}
else if (n > 5UL && (n % 5UL) == 0UL)
{
// TODO: Replace this modulo check with ULongExtensions.Mod5 so the CPU hot path
// reuses the benchmarked helper instead of `%` when sieving large prime ranges.
result = false;
}
else
@@ -63,8 +68,8 @@ public sealed class PrimeTester(bool useInternal = false)
else
{
var smallPrimeDivisorsLength = PrimesGenerator.SmallPrimes.Length;
uint[] smallPrimeDivisors = PrimesGenerator.SmallPrimes;
ulong[] smallPrimeDivisorsMul = PrimesGenerator.SmallPrimesPow2;
uint[] smallPrimeDivisors = PrimesGenerator.SmallPrimes;
ulong[] smallPrimeDivisorsMul = PrimesGenerator.SmallPrimesPow2;
for (int i = 0; i < smallPrimeDivisorsLength; i++)
{
if (smallPrimeDivisorsMul[i] > n)
@@ -72,7 +77,10 @@ public sealed class PrimeTester(bool useInternal = false)
break;
}
if (n % smallPrimeDivisors[i] == 0)
// TODO: Route this small-prime filtering through the shared divisor-cycle cache once
// PrimeTester can consult it directly; the cached cycles avoid the repeated `%` work
// that slows these hot loops when sieving hundreds of millions of candidates.
if (n % smallPrimeDivisors[i] == 0)
{
result = false;
break;
@@ -259,6 +267,8 @@ public sealed class PrimeTester(bool useInternal = false)
if ((n & 1UL) == 0UL || (n > 5UL && (n % 5UL) == 0UL))
{
// TODO: Replace the `% 5` branch with the GPU Mod5 helper once the sieve kernel can
// reuse the benchmarked bitmask reduction instead of modulo on every candidate.
results[index] = 0;
return;
}
@@ -278,16 +288,18 @@ public sealed class PrimeTester(bool useInternal = false)
for (int i = 0; i < len; i++)
{
ulong p = smallPrimes[i];
if (p * p > n)
{
break;
}
if ((n % p) == 0UL)
{
results[index] = 0;
return;
}
if (p * p > n)
{
break;
}
if ((n % p) == 0UL)
{
// TODO: Route this modulo through the shared divisor-cycle cache once exposed to GPU kernels
// so batched sieves avoid per-prime `%` operations that profiling showed expensive.
results[index] = 0;
return;
}
}
results[index] = 1;
+24 -17
View File
@@ -45,24 +45,27 @@ public static class PrimesGenerator
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;
}
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;
}
if (candidate % p == 0U)
{
isPrime = false;
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)
{
@@ -99,6 +102,8 @@ public static class PrimesGenerator
[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,
@@ -109,6 +114,8 @@ public static class PrimesGenerator
[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,
+6
View File
@@ -139,6 +139,9 @@ public static class RleBlacklist
return false;
}
// TODO: Cache BuildRleKey results per divisor cycle bucket so residue scans reuse the
// normalized strings; recomputing the key for every candidate shows up heavily in the RLE
// blacklist profile once we cross the 138M threshold.
string key = BuildRleKey(p);
return _patterns!.Contains(key);
}
@@ -146,6 +149,9 @@ public static class RleBlacklist
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string BuildRleKey(ulong value)
{
// TODO: Replace this per-bit loop with the lookup-table driven builder validated in the
// ModResidueTracker benchmarks; it amortizes the run detection across 8-bit blocks and cuts
// the blacklist check latency roughly in half on large batches.
// Build RLE from MSB to LSB runs.
int bitLen = 64 - int.CreateChecked(ulong.LeadingZeroCount(value));
if (bitLen <= 0)
+3
View File
@@ -50,6 +50,9 @@ public sealed class TextFileWriter : IDisposable
{
lock (_lock)
{
// TODO: Buffer these writes through an ArrayPool-backed accumulator so we only flush when the
// batch is full; flushing per line shows up as a serialization bottleneck once the scanner emits
// millions of candidate summaries.
TextWriter.WriteLine(line);
TextWriter.Flush();
}
+24 -19
View File
@@ -66,13 +66,15 @@ public static class UInt128Extensions
}
prime = smallPrimes[i];
while (order % prime == 0UL)
{
temp = order / prime;
if (temp.PowModWithCycle(q, cycle) == one)
{
order = temp;
}
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;
@@ -103,19 +105,22 @@ public static class UInt128Extensions
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;
}
for (i = 0UL; i < smallPrimesCount; i++)
{
if (smallPrimesPow2[i] > n)
{
break;
}
p = smallPrimes[i];
if (n % p == zero)
{
return n == p;
}
}
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;
}
+94 -33
View File
@@ -29,6 +29,9 @@ public static class ULongExtensions
}
prime = smallPrimes[i];
// TODO: Replace this `%` driven factor peeling with the divisor-cycle aware
// factoring helper so large orders reuse the cached remainders highlighted in
// the latest divisor-cycle benchmarks instead of recomputing slow modulo checks.
while (order % prime == 0UL)
{
temp = order / prime;
@@ -77,6 +80,9 @@ public static class ULongExtensions
}
p = smallPrimes[i];
// TODO: Swap this modulo check for the shared small-prime cycle filter once the
// divisor-cycle cache is mandatory, matching the PrimeTester improvements noted in
// the CPU sieve benchmarks.
if ((n % p) == 0UL)
{
return n == p;
@@ -86,6 +92,8 @@ public static class ULongExtensions
return true;
}
// TODO: Swap the `% 5` branches with the Mod5 lookup helper once it is wired into
// production, matching the Mod8/Mod10 benchmark optimizations for hot CLI loops.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Mod10(this ulong value) => (value & 1UL) == 0UL
? (value % 5UL) switch
@@ -108,51 +116,83 @@ public static class ULongExtensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Mod128(this ulong value) => value & 127UL;
// TODO: Promote the cached Mod5/Mod3 helpers measured fastest in the CLI benchmarks to
// remove the raw modulo operations in this combined remainder computation.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Mod10_8_5_3(this ulong value, out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3)
{
mod8 = value & 7UL;
// TODO: Swap these modulo operations for the dedicated Mod5/Mod3 helpers once the
// benchmarked lookup-based reducers land so this hot helper avoids `%` entirely.
mod5 = value % 5UL;
mod3 = value % 3UL;
byte current;
uint byteSum = 0U;
do
{
current = (byte)value;
byteSum += current;
value >>= 8;
}
while (value != 0UL);
mod3 = byteSum % 3U;
mod5 = byteSum % 5U;
mod10 = (mod8 & 1UL) == 0UL
? mod5 switch
{
0UL => 0UL,
1UL => 6UL,
2UL => 2UL,
3UL => 8UL,
_ => 4UL,
}
: mod5 switch
{
0UL => 5UL,
1UL => 1UL,
2UL => 7UL,
3UL => 3UL,
_ => 9UL,
};
? mod5 switch
{
0UL => 0UL,
1UL => 6UL,
2UL => 2UL,
3UL => 8UL,
_ => 4UL,
}
: mod5 switch
{
0UL => 5UL,
1UL => 1UL,
2UL => 7UL,
3UL => 3UL,
_ => 9UL,
};
}
// TODO: Inline the Mod5/Mod3 lookup tables here once they are shared so the stepping helper
// mirrors the benchmarked no-modulo variant planned for the CLI sieve.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Mod10_8_5_3Steps(this ulong value, out ulong step10, out ulong step8, out ulong step5, out ulong step3)
{
value.Mod10_8_5_3(out step10, out step8, out step5, out step3);
ulong mod8 = value & 7UL;
// TODO: Route these modulo computations through the cached Mod5/Mod3 tables when the
// lookup implementation from the CLI benchmarks is promoted to production.
ulong mod5 = value % 5UL;
ulong mod3 = value % 3UL;
ulong mod10 = (mod8 & 1UL) == 0UL
? mod5 switch
{
0UL => 0UL,
1UL => 6UL,
2UL => 2UL,
3UL => 8UL,
_ => 4UL,
}
: mod5 switch
{
0UL => 5UL,
1UL => 1UL,
2UL => 7UL,
3UL => 3UL,
_ => 9UL,
};
step10 = (step10 << 1).Mod10();
step8 = (step8 << 1) & 7UL;
step5 = (step5 << 1) % 5UL;
step3 = (step3 << 1) % 3UL;
step10 = mod10 + mod10;
if (step10 >= 10UL)
{
step10 -= 10UL;
}
step8 = (mod8 + mod8) & 7UL;
step5 = mod5 + mod5;
if (step5 >= 5UL)
{
step5 -= 5UL;
}
step3 = mod3 + mod3;
if (step3 >= 3UL)
{
step3 -= 3UL;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -161,6 +201,9 @@ public static class ULongExtensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong MulHigh(this ulong x, ulong y)
{
// TODO: Investigate replacing this manual decomposition with the UInt128-based implementation
// for CPU callers; the latest benchmarks show the intrinsic path is an order of magnitude
// faster, while GPU code can keep using GpuUInt128.MulHigh.
ulong xLow = (uint)x;
ulong xHigh = x >> 32;
ulong yLow = (uint)y;
@@ -209,11 +252,15 @@ public static class ULongExtensions
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
// TODO: Replace this fallback with the UInt128 Montgomery helper measured fastest in
// MulMod64Benchmarks so CPU callers stop paying for triple modulo operations.
public static ulong MulMod64(this ulong a, ulong b, ulong modulus) => (ulong)(UInt128)(((a % modulus) * (b % modulus)) % modulus);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong MulMod64GpuCompatible(this ulong a, ulong b, ulong modulus)
{
// TODO: Remove this GPU-compatible shim from production once callers migrate to MulMod64,
// which the benchmarks show is roughly 6-7× faster on dense 64-bit inputs.
GpuUInt128 state = new(a % modulus);
return state.MulMod(b, modulus);
}
@@ -221,6 +268,8 @@ public static class ULongExtensions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong MulMod64GpuCompatibleDeferred(this ulong a, ulong b, ulong modulus)
{
// TODO: Move this deferred helper to the benchmark suite; the baseline MulMod64 avoids the
// 5-40× slowdown seen across real-world operand distributions.
GpuUInt128 state = new(a);
return state.MulModWithNativeModulo(b, modulus);
}
@@ -259,6 +308,8 @@ public static class ULongExtensions
{
if ((remainingExponent & 1UL) != 0UL)
{
// TODO: Switch to the ProcessEightBitWindows helper once the scalar implementation ships so CPU callers
// inherit the same 2× speedup that the benchmarked windowed ladder delivered for large exponents.
result = result.MontgomeryMultiply(baseVal, modulus, nPrime);
}
@@ -291,6 +342,8 @@ public static class ULongExtensions
{
if ((remainingExponent & 1UL) != 0UL)
{
// TODO: Route this Montgomery-domain variant through the same windowed pow2 helper once available so delta
// stepping matches the ProcessEightBitWindows gains highlighted by GpuPow2ModBenchmarks.
result = result.MontgomeryMultiply(baseVal, modulus, divisor.NPrime);
}
@@ -335,6 +388,8 @@ public static class ULongExtensions
UInt128 result = UInt128.One;
ulong exponentLoopIndex = 0UL;
// TODO: Port this scalar PowMod fallback to the ProcessEightBitWindows helper so CPU callers get the
// eight-bit window wins measured against the classic square-and-subtract implementation.
// Return 1 because 2^0 = 1
if (exponent == 0UL)
return result;
@@ -385,6 +440,8 @@ public static class ULongExtensions
UInt128 result = UInt128.One;
ulong exponentLoopIndex = 0UL;
// TODO: Wire this cycle-aware overload into the ProcessEightBitWindows helper so the reduced exponent path
// inherits the faster windowed pow2 routine highlighted in the Pow2Montgomery benchmarks.
// Return 1 because 2^0 = 1
if (exponent == 0UL)
return result;
@@ -436,6 +493,8 @@ public static class ULongExtensions
UInt128 result = UInt128.One;
ulong exponentLoopIndex = 0UL;
// TODO: Replace this UInt128-cycle overload with the ProcessEightBitWindows helper so large-exponent CPU scans
// reuse the faster windowed pow2 ladder instead of the manual rotation loop measured to lag behind in benchmarks.
// Return 1 because 2^0 = 1
if (exponent == UInt128.Zero)
return result;
@@ -490,6 +549,8 @@ public static class ULongExtensions
zero = UInt128.Zero;
ulong exponentLoopIndex = 0UL;
// TODO: Migrate this UInt128 exponent overload to ProcessEightBitWindows so the large-cycle reductions drop the
// slow manual loop that underperforms the windowed pow2 helper in the Pow2 benchmark suite.
// Return 1 because 2^0 = 1
if (exponent == zero)
return result;