Use LastDigit enum across Mersenne scanners

This commit is contained in:
Marek Lemańczyk committed 2025-10-26 16:04:05 +01:00
1 parent 43c7bafd5d
commit a83ae56caf
15 files changed
+83 -49

No files matched your search

@@ -1,4 +1,5 @@
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Cpu;
using Xunit;
using UInt128 = System.UInt128;
@@ -7,7 +8,7 @@ namespace PerfectNumbers.Core.Tests;
public class MersenneNumberIncrementalCpuTesterTests
{
private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL;
private static LastDigit GetLastDigit(ulong exponent) => (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One;
[Theory]
[InlineData(GpuKernelType.Incremental)]
@@ -30,7 +31,7 @@ public class MersenneNumberIncrementalCpuTesterTests
private static void RunCase(MersenneNumberIncrementalCpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
tester.Scan(exponent, (UInt128)exponent << 1, GetLastDigit(exponent), (UInt128)maxK, ref isPrime);
isPrime.Should().Be(expectedPrime);
}
}
@@ -1,4 +1,5 @@
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
using Xunit;
using UInt128 = System.UInt128;
@@ -7,7 +8,7 @@ namespace PerfectNumbers.Core.Tests;
public class MersenneNumberIncrementalGpuTesterTests
{
private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL;
private static LastDigit GetLastDigit(ulong exponent) => (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One;
[Theory(Skip = "Disabled until the GPU incremental cycle heuristics are ported.")]
[InlineData(GpuKernelType.Pow2Mod, false)]
@@ -32,7 +33,7 @@ public class MersenneNumberIncrementalGpuTesterTests
private static void RunCase(MersenneNumberIncrementalGpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
tester.Scan(exponent, (UInt128)exponent << 1, GetLastDigit(exponent), (UInt128)maxK, ref isPrime);
isPrime.Should().Be(expectedPrime);
}
}
@@ -1,4 +1,5 @@
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Cpu;
using Xunit;
using UInt128 = System.UInt128;
@@ -7,7 +8,7 @@ namespace PerfectNumbers.Core.Tests;
public class MersenneNumberOrderCpuTesterTests
{
private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL;
private static LastDigit GetLastDigit(ulong exponent) => (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One;
[Theory]
[InlineData(GpuKernelType.Incremental)]
@@ -30,7 +31,7 @@ public class MersenneNumberOrderCpuTesterTests
private static void RunCase(MersenneNumberOrderCpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
tester.Scan(exponent, (UInt128)exponent << 1, GetLastDigit(exponent), (UInt128)maxK, ref isPrime);
isPrime.Should().Be(expectedPrime);
}
}
@@ -1,5 +1,6 @@
using System.Reflection;
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
using Xunit;
using UInt128 = System.UInt128;
@@ -8,7 +9,7 @@ namespace PerfectNumbers.Core.Tests;
public class MersenneNumberOrderGpuTesterTests
{
private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL;
private static LastDigit GetLastDigit(ulong exponent) => (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One;
[Theory]
[InlineData(GpuKernelType.Pow2Mod, false)]
@@ -64,7 +65,7 @@ public class MersenneNumberOrderGpuTesterTests
tester.Scan(
exponent,
(UInt128)exponent << 1,
LastDigitIsSeven(exponent),
GetLastDigit(exponent),
(UInt128)10UL,
ref isPrime);
@@ -86,7 +87,7 @@ public class MersenneNumberOrderGpuTesterTests
private static void RunCase(MersenneNumberOrderGpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
tester.Scan(exponent, (UInt128)exponent << 1, GetLastDigit(exponent), (UInt128)maxK, ref isPrime);
isPrime.Should().Be(expectedPrime);
}
}
@@ -1,4 +1,5 @@
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Cpu;
using Xunit;
using UInt128 = System.UInt128;
@@ -7,7 +8,7 @@ namespace PerfectNumbers.Core.Tests;
public class MersenneNumberResidueCpuTesterTests
{
private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL;
private static LastDigit GetLastDigit(ulong exponent) => (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One;
[Fact]
[Trait("Category", "Fast")]
@@ -24,7 +25,7 @@ public class MersenneNumberResidueCpuTesterTests
private static void RunCase(MersenneNumberResidueCpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
tester.Scan(exponent, (UInt128)exponent << 1, GetLastDigit(exponent), (UInt128)maxK, ref isPrime);
isPrime.Should().Be(expectedPrime);
}
}
@@ -1,4 +1,5 @@
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
using Xunit;
using UInt128 = System.UInt128;
@@ -7,7 +8,7 @@ namespace PerfectNumbers.Core.Tests;
public class MersenneNumberResidueGpuTesterTests
{
private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL;
private static LastDigit GetLastDigit(ulong exponent) => (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One;
[Theory]
[InlineData(false)]
@@ -27,7 +28,7 @@ public class MersenneNumberResidueGpuTesterTests
private static void RunCase(MersenneNumberResidueGpuTester tester, ulong exponent, ulong maxK, bool expectedPrime)
{
bool isPrime = true;
tester.Scan(exponent, (UInt128)exponent << 1, LastDigitIsSeven(exponent), (UInt128)maxK, ref isPrime);
tester.Scan(exponent, (UInt128)exponent << 1, GetLastDigit(exponent), (UInt128)maxK, ref isPrime);
isPrime.Should().Be(expectedPrime);
}
}
@@ -5,7 +5,7 @@ public class MersenneNumberIncrementalCpuTester(GpuKernelType kernelType)
private static readonly MersenneDivisorCycles _cycles = MersenneDivisorCycles.Shared;
private readonly GpuKernelType _kernelType = kernelType;
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
public void Scan(ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime)
{
UInt128 k = UInt128.One;
UInt128 one = k;
@@ -4,7 +4,7 @@ public class MersenneNumberOrderCpuTester(GpuKernelType kernelType)
{
private readonly GpuKernelType _kernelType = kernelType;
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
public void Scan(ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime)
{
UInt128 k = 1UL;
var auto = new MersenneResidueAutomaton(exponent);
@@ -17,7 +17,7 @@ public class MersenneNumberOrderCpuTester(GpuKernelType kernelType)
// TODO: When this lookup misses the snapshot, invoke the configured device to compute the
// single required cycle on demand without persisting it so the CPU order path retains the
// cycle-stepping speedups while honoring the no-extra-cache constraint for large divisors.
int allowMask2 = lastIsSeven ? CpuConstants.LastSevenMask10 : CpuConstants.LastOneMask10;
int allowMask2 = lastDigit == LastDigit.Seven ? CpuConstants.LastSevenMask10 : CpuConstants.LastOneMask10;
bool shouldCheck = ((allowMask2 >> (int)auto.Mod10R) & 1) != 0;
if (shouldCheck)
{
@@ -5,7 +5,7 @@ public class MersenneNumberResidueCpuTester
private ModResidueTracker? _mersenneResidueTracker;
// CPU residue variant using tracker + unrolled residue updates (no method calls in the loop).
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
public void Scan(ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime)
{
// Initialize/update Mersenne residue tracker and start a merge walk for ascending q divisors
@@ -29,9 +29,9 @@ public class MersenneNumberResidueCpuTester
step10 = step10.Mod10();
// Allowed last-digit sets for q depending on last digit of M_p
// lastIsSeven == true => allow {7,9}
// lastIsSeven == false => allow {1,3,7,9}
int allowMask = lastIsSeven ? CpuConstants.LastSevenMask10 : CpuConstants.LastOneMask10;
// LastDigit.Seven => allow {7,9}
// otherwise => allow {1,3,7,9}
int allowMask = lastDigit == LastDigit.Seven ? CpuConstants.LastSevenMask10 : CpuConstants.LastOneMask10;
// Predeclare temps to avoid redeclaration overhead in the loop
UInt128 remaining;
@@ -12,7 +12,7 @@ public class MersenneNumberIncrementalGpuTester(GpuKernelType kernelType, bool u
private readonly GpuKernelType _kernelType = kernelType;
private readonly bool _useGpuOrder = useGpuOrder;
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
public void Scan(ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime)
{
throw new NotImplementedException($"GPU incremental scanning requires the device cycle heuristics implementation (kernel: {_kernelType}, GPU order: {_useGpuOrder}).");
@@ -23,7 +23,7 @@ public class MersenneNumberIncrementalGpuTester(GpuKernelType kernelType, bool u
int batchSize = GpuConstants.ScanBatchSize; // large batch improves GPU occupancy
UInt128 kStart = 1UL;
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
byte last = lastIsSeven ? (byte)1 : (byte)0; // ILGPU kernels do not support bool parameters
byte last = lastDigit == LastDigit.Seven ? (byte)1 : (byte)0; // ILGPU kernels do not support bool parameters
var pow2Kernel = gpuLease.Pow2ModKernel;
var incKernel = gpuLease.IncrementalKernel;
@@ -7,7 +7,7 @@ public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuO
private readonly GpuKernelType _kernelType = kernelType;
private readonly bool _useGpuOrder = useGpuOrder;
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
public void Scan(ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime)
{
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
var execution = gpuLease.EnterExecutionScope();
@@ -16,7 +16,7 @@ public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuO
int batchSize = GpuConstants.ScanBatchSize; // large batch improves GPU occupancy and avoids TDR
UInt128 kStart = UInt128.One;
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
byte last = lastIsSeven ? (byte)1 : (byte)0; // ILGPU kernels do not support bool parameters
byte last = lastDigit == LastDigit.Seven ? (byte)1 : (byte)0; // ILGPU kernels do not support bool parameters
var kernel = _kernelType switch
{
@@ -24,7 +24,7 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder)
// requested by the caller, skip queuing additional block generation, and keep the snapshot cache untouched.
// GPU residue variant: check 2^p % q == 1 for q = 2*p*k + 1.
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
public void Scan(ulong exponent, UInt128 twoP, LastDigit lastDigit, UInt128 maxK, ref bool isPrime)
{
var limiter = GpuPrimeWorkLimiter.Acquire();
var gpuLease = RentAccelerator();
@@ -46,7 +46,7 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder)
UInt128 kStart = 1UL;
UInt128 limit = maxK + UInt128.One;
byte last = lastIsSeven ? (byte)1 : (byte)0;
byte last = lastDigit == LastDigit.Seven ? (byte)1 : (byte)0;
twoP.Mod10_8_5_3(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
step10 = step10.Mod10();
+10
View File
@@ -0,0 +1,10 @@
namespace PerfectNumbers.Core;
public enum LastDigit : byte
{
One,
Three,
Five,
Seven,
Nine,
}
+13 -13
View File
@@ -58,7 +58,7 @@ public sealed class MersenneNumberTester(
}
UInt128 twoP = (UInt128)exponent << 1; // 2 * p
bool lastIsSeven = (exponent & 3UL) == 3UL;
LastDigit lastDigit = (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One;
// CPU path: compute orders directly and cache
if (!_useGpuOrder)
{
@@ -71,7 +71,7 @@ public sealed class MersenneNumberTester(
remainder = q.Mod10();
// TODO: Replace these repeated Mod10/Mod8/Mod3/Mod5 calls with a residue automaton walk so
// order warmups reuse the optimized cycle stepping validated in the residue benchmarks.
bool shouldCheck = lastIsSeven
bool shouldCheck = lastDigit == LastDigit.Seven
? remainder == 7UL || remainder == 9UL || remainder == 3UL
: remainder == 1UL || remainder == 3UL || remainder == 9UL;
@@ -112,7 +112,7 @@ public sealed class MersenneNumberTester(
remainder2 = q2.Mod10();
// TODO: Reuse the same residue-automaton fast path here so the GPU warmup staging avoids `%` and
// branches the benchmarks showed slower than the tracked stepping helper.
bool shouldCheck2 = lastIsSeven
bool shouldCheck2 = lastDigit == LastDigit.Seven
? remainder2 == 7UL || remainder2 == 9UL || remainder2 == 3UL
: remainder2 == 1UL || remainder2 == 3UL || remainder2 == 9UL;
@@ -230,7 +230,7 @@ public sealed class MersenneNumberTester(
UInt128 twoP = (UInt128)exponent << 1; // 2 * p
// UInt128 maxDivisor = new(ulong.MaxValue, ulong.MaxValue);
UInt128 maxK = UInt128Numbers.Two.Pow(exponent) / 2 + 1;
bool lastIsSeven = (exponent & 3UL) == 3UL;
LastDigit lastDigit = (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One;
// In residue mode the residue scan is the final primality check.
if (_useResidue)
@@ -242,11 +242,11 @@ public sealed class MersenneNumberTester(
if (_useGpuScan)
{
_residueGpuTester!.Scan(exponent, twoP, lastIsSeven, maxK, ref prePrime);
_residueGpuTester!.Scan(exponent, twoP, lastDigit, maxK, ref prePrime);
}
else
{
_residueCpuTester!.Scan(exponent, twoP, lastIsSeven, maxK, ref prePrime);
_residueCpuTester!.Scan(exponent, twoP, lastDigit, maxK, ref prePrime);
}
return prePrime;
@@ -258,14 +258,14 @@ public sealed class MersenneNumberTester(
// Scan a limited range of k to quickly reject obvious cases.
UInt128 twoPPre = (UInt128)exponent << 1;
UInt128 maxKPre = (UInt128)PreLucasPreScanK;
bool lastIsSevenPre = (exponent & 3UL) == 3UL;
LastDigit lastDigitPre = (exponent & 3UL) == 3UL ? LastDigit.Seven : LastDigit.One;
if (_useGpuScan)
{
_incrementalGpuTester.Scan(exponent, twoPPre, lastIsSevenPre, maxKPre, ref prePrime);
_incrementalGpuTester.Scan(exponent, twoPPre, lastDigitPre, maxKPre, ref prePrime);
}
else
{
_incrementalCpuTester.Scan(exponent, twoPPre, lastIsSevenPre, maxKPre, ref prePrime);
_incrementalCpuTester.Scan(exponent, twoPPre, lastDigitPre, maxKPre, ref prePrime);
}
if (!prePrime)
@@ -284,22 +284,22 @@ public sealed class MersenneNumberTester(
{
if (_useGpuOrder)
{
_orderGpuTester.Scan(exponent, twoP, lastIsSeven, maxK, ref isPrime);
_orderGpuTester.Scan(exponent, twoP, lastDigit, maxK, ref isPrime);
}
else
{
_orderCpuTester.Scan(exponent, twoP, lastIsSeven, maxK, ref isPrime);
_orderCpuTester.Scan(exponent, twoP, lastDigit, maxK, ref isPrime);
}
}
else
{
if (_useGpuScan)
{
_incrementalGpuTester.Scan(exponent, twoP, lastIsSeven, maxK, ref isPrime);
_incrementalGpuTester.Scan(exponent, twoP, lastDigit, maxK, ref isPrime);
}
else
{
_incrementalCpuTester.Scan(exponent, twoP, lastIsSeven, maxK, ref isPrime);
_incrementalCpuTester.Scan(exponent, twoP, lastDigit, maxK, ref isPrime);
}
}
+28 -10
View File
@@ -1,3 +1,4 @@
using System;
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core;
@@ -85,14 +86,16 @@ public static class PrimesGenerator
if (candidate != 2U)
{
if (lastOneCount < targetInt && IsAllowedForLastOne(candidate))
LastDigit lastDigit = GetLastDigit(candidate);
if (lastOneCount < targetInt && IsAllowedForLastOne(candidate, lastDigit))
{
lastOne[lastOneCount] = candidate;
lastOnePow2[lastOneCount] = checked((ulong)candidate * candidate);
lastOneCount++;
}
if (lastSevenCount < targetInt && IsAllowedForLastSeven(candidate))
if (lastSevenCount < targetInt && IsAllowedForLastSeven(candidate, lastDigit))
{
lastSeven[lastSevenCount] = candidate;
lastSevenPow2[lastSevenCount] = checked((ulong)candidate * candidate);
@@ -108,25 +111,40 @@ public static class PrimesGenerator
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAllowedForLastOne(uint prime)
private static LastDigit GetLastDigit(uint prime)
{
// TODO: Swap the `% 10` usage for ULongExtensions.Mod10 so the hot classification path
// reuses the benchmarked residue helper instead of repeated divisions.
return (prime % 10U) switch
{
1U or 3U or 9U => true,
_ => prime == 7U || prime == 11U,
1U => LastDigit.One,
3U => LastDigit.Three,
5U => LastDigit.Five,
7U => LastDigit.Seven,
9U => LastDigit.Nine,
_ => throw new ArgumentOutOfRangeException(nameof(prime)),
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAllowedForLastSeven(uint prime)
private static bool IsAllowedForLastOne(uint prime, LastDigit lastDigit)
{
// TODO: Swap the `% 10` usage for ULongExtensions.Mod10 so the hot classification path
// reuses the benchmarked residue helper instead of repeated divisions.
return lastDigit switch
{
LastDigit.One or LastDigit.Three or LastDigit.Nine => true,
LastDigit.Seven => prime == 7U,
_ => prime == 11U,
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAllowedForLastSeven(uint prime, LastDigit lastDigit)
{
// TODO: Route this `% 10` classification through ULongExtensions.Mod10 to match the faster
// residue helper used elsewhere in the scanner.
return (prime % 10U) switch
return lastDigit switch
{
3U or 7U or 9U => true,
LastDigit.Three or LastDigit.Seven or LastDigit.Nine => true,
_ => prime == 11U,
};
}