Add TODOs for divisor metadata caching
This commit is contained in:
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]!;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user