From c53966e481a8fbe6927fc0977e78691133963b1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Tue, 28 Oct 2025 18:39:18 +0100 Subject: [PATCH] Enable residue-aware GPU sieve in PrimeTester --- .../Gpu/GpuKernelCompilationTests.cs | 4 +- .../PrimeTester/PrimeTesterIsPrimeTests.cs | 18 ++ .../PrimesGeneratorTests.cs | 29 ++- PerfectNumbers.Core/DivisorGenerator.cs | 214 +++++++++++++++++- .../Gpu/Kernels/PrimeTesterKernels.cs | 38 +++- PerfectNumbers.Core/PrimeTester.cs | 72 +++++- PerfectNumbers.Core/PrimesGenerator.cs | 152 +------------ 7 files changed, 359 insertions(+), 168 deletions(-) diff --git a/PerfectNumbers.Core.Tests/Gpu/GpuKernelCompilationTests.cs b/PerfectNumbers.Core.Tests/Gpu/GpuKernelCompilationTests.cs index 1a8b1ae..6ace3ca 100644 --- a/PerfectNumbers.Core.Tests/Gpu/GpuKernelCompilationTests.cs +++ b/PerfectNumbers.Core.Tests/Gpu/GpuKernelCompilationTests.cs @@ -121,7 +121,7 @@ public class GpuKernelCompilationTests private static readonly Action, ArrayView1D> MersenneDivisorCyclesKernel = DivisorCycleKernels.GpuDivisorCycleKernel; - private static readonly Action, ArrayView, ArrayView> PrimeTesterSmallPrimeKernel = + private static readonly Action, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView> PrimeTesterSmallPrimeKernel = PrimeTesterKernels.SmallPrimeSieveKernel; private static readonly Action, ArrayView> PrimeTesterSharesFactorKernel = @@ -336,7 +336,7 @@ public class GpuKernelCompilationTests private static void CompilePrimeTesterSmallPrimeKernel(Accelerator accelerator) { - var kernel = accelerator.LoadAutoGroupedStreamKernel, ArrayView, ArrayView>(PrimeTesterSmallPrimeKernel); + var kernel = accelerator.LoadAutoGroupedStreamKernel, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView>(PrimeTesterSmallPrimeKernel); _ = kernel; } diff --git a/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs b/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs index d33dd32..c99d670 100644 --- a/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs +++ b/PerfectNumbers.Core.Tests/PrimeTester/PrimeTesterIsPrimeTests.cs @@ -17,6 +17,10 @@ public class PrimeTesterIsPrimeTests [InlineData(97UL, true)] [InlineData(133UL, false)] [InlineData(137UL, true)] + [InlineData(143UL, false)] + [InlineData(199UL, true)] + [InlineData(209UL, false)] + [InlineData(403UL, false)] [InlineData(341UL, false)] public void IsPrime_returns_expected_results(ulong n, bool expected) { @@ -25,6 +29,20 @@ public class PrimeTesterIsPrimeTests tester.IsPrime(n, CancellationToken.None).Should().Be(expected); } + [Theory] + [Trait("Category", "Fast")] + [InlineData(139UL, true)] + [InlineData(189UL, false)] + [InlineData(193UL, true)] + [InlineData(279UL, false)] + public void IsPrimeGpu_uses_residue_specific_tables(ulong n, bool expected) + { + GpuContextPool.ForceCpu = false; + var tester = new PrimeTester(); + + tester.IsPrimeGpu(n, CancellationToken.None).Should().Be(expected); + } + [Fact] [Trait("Category", "Fast")] public void IsPrimeGpu_falls_back_to_cpu_when_forced() diff --git a/PerfectNumbers.Core.Tests/PrimesGeneratorTests.cs b/PerfectNumbers.Core.Tests/PrimesGeneratorTests.cs index cb42e32..95df8a2 100644 --- a/PerfectNumbers.Core.Tests/PrimesGeneratorTests.cs +++ b/PerfectNumbers.Core.Tests/PrimesGeneratorTests.cs @@ -32,14 +32,24 @@ public class PrimesGeneratorTests var lastOnePow2 = PrimesGenerator.SmallPrimesPow2LastOne; var lastSeven = PrimesGenerator.SmallPrimesLastSeven; var lastSevenPow2 = PrimesGenerator.SmallPrimesPow2LastSeven; + var lastThree = DivisorGenerator.SmallPrimesLastThree; + var lastThreePow2 = DivisorGenerator.SmallPrimesPow2LastThree; + var lastNine = DivisorGenerator.SmallPrimesLastNine; + var lastNinePow2 = DivisorGenerator.SmallPrimesPow2LastNine; lastOne.Length.Should().Be((int)PerfectNumberConstants.PrimesLimit); lastSeven.Length.Should().Be((int)PerfectNumberConstants.PrimesLimit); + lastThree.Length.Should().Be((int)PerfectNumberConstants.PrimesLimit); + lastNine.Length.Should().Be((int)PerfectNumberConstants.PrimesLimit); lastOnePow2.Length.Should().Be(lastOne.Length); lastSevenPow2.Length.Should().Be(lastSeven.Length); + lastThreePow2.Length.Should().Be(lastThree.Length); + lastNinePow2.Length.Should().Be(lastNine.Length); lastOne[..5].Should().Equal([3U, 7U, 11U, 13U, 19U]); lastSeven[..5].Should().Equal([3U, 7U, 11U, 13U, 17U]); + lastThree[..6].Should().Equal([3U, 7U, 11U, 13U, 17U, 19U]); + lastNine[..6].Should().Equal([3U, 7U, 11U, 13U, 17U, 19U]); for (int i = 0; i < lastOne.Length; i++) { @@ -58,6 +68,23 @@ public class PrimesGeneratorTests .Should().BeTrue(); lastSevenPow2[i].Should().Be(prime * (ulong)prime); } + + for (int i = 0; i < lastThree.Length; i++) + { + uint prime = lastThree[i]; + uint mod10 = prime % 10U; + (mod10 == 3U || mod10 == 7U || prime == 11U || prime == 19U) + .Should().BeTrue(); + lastThreePow2[i].Should().Be(prime * (ulong)prime); + } + + for (int i = 0; i < lastNine.Length; i++) + { + uint prime = lastNine[i]; + uint mod10 = prime % 10U; + (mod10 == 3U || mod10 == 7U || mod10 == 9U || prime == 11U) + .Should().BeTrue(); + lastNinePow2[i].Should().Be(prime * (ulong)prime); + } } } - diff --git a/PerfectNumbers.Core/DivisorGenerator.cs b/PerfectNumbers.Core/DivisorGenerator.cs index fdb648c..20b8c81 100644 --- a/PerfectNumbers.Core/DivisorGenerator.cs +++ b/PerfectNumbers.Core/DivisorGenerator.cs @@ -1,16 +1,228 @@ +using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; namespace PerfectNumbers.Core; internal static class DivisorGenerator { + public static readonly uint[] SmallPrimes; + public static readonly ulong[] SmallPrimesPow2; + public static readonly uint[] SmallPrimesLastOne; + public static readonly ulong[] SmallPrimesPow2LastOne; + public static readonly uint[] SmallPrimesLastSeven; + public static readonly ulong[] SmallPrimesPow2LastSeven; + public static readonly uint[] SmallPrimesLastThree; + public static readonly ulong[] SmallPrimesPow2LastThree; + public static readonly uint[] SmallPrimesLastNine; + public static readonly ulong[] SmallPrimesPow2LastNine; + private const ushort DecimalMaskWhenLastIsSeven = (1 << 3) | (1 << 7) | (1 << 9); + private const ushort DecimalMaskWhenLastIsThree = (1 << 1) | (1 << 3) | (1 << 7) | (1 << 9); + private const ushort DecimalMaskWhenLastIsNine = (1 << 1) | (1 << 3) | (1 << 7) | (1 << 9); private const ushort DecimalMaskOtherwise = (1 << 1) | (1 << 3) | (1 << 9); + static DivisorGenerator() + { + BuildSmallPrimes( + out SmallPrimes, + out SmallPrimesPow2, + out SmallPrimesLastOne, + out SmallPrimesPow2LastOne, + out SmallPrimesLastSeven, + out SmallPrimesPow2LastSeven, + out SmallPrimesLastThree, + out SmallPrimesPow2LastThree, + out SmallPrimesLastNine, + out SmallPrimesPow2LastNine); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ushort GetDecimalMask(LastDigit lastDigit) { - return lastDigit == LastDigit.Seven ? DecimalMaskWhenLastIsSeven : DecimalMaskOtherwise; + return lastDigit switch + { + LastDigit.Seven => DecimalMaskWhenLastIsSeven, + LastDigit.Three => DecimalMaskWhenLastIsThree, + LastDigit.Nine => DecimalMaskWhenLastIsNine, + _ => DecimalMaskOtherwise, + }; } + private static void BuildSmallPrimes( + out uint[] all, + out ulong[] allPow2, + out uint[] lastOne, + out ulong[] lastOnePow2, + out uint[] lastSeven, + out ulong[] lastSevenPow2, + out uint[] lastThree, + out ulong[] lastThreePow2, + out uint[] lastNine, + out ulong[] lastNinePow2) + { + uint target = PerfectNumberConstants.PrimesLimit; + int targetInt = checked((int)target); + all = new uint[targetInt]; + allPow2 = new ulong[targetInt]; + lastOne = new uint[targetInt]; + lastOnePow2 = new ulong[targetInt]; + lastSeven = new uint[targetInt]; + lastSevenPow2 = new ulong[targetInt]; + lastThree = new uint[targetInt]; + lastThreePow2 = new ulong[targetInt]; + lastNine = new uint[targetInt]; + lastNinePow2 = new ulong[targetInt]; + + List primes = new(targetInt * 4 / 3 + 16); + uint candidate = 2U; + int allCount = 0; + int lastOneCount = 0; + int lastSevenCount = 0; + int lastThreeCount = 0; + int lastNineCount = 0; + + while (allCount < targetInt || lastOneCount < targetInt || lastSevenCount < targetInt || lastThreeCount < targetInt || lastNineCount < targetInt) + { + bool isPrime = true; + int primesCount = primes.Count; + for (int i = 0; i < primesCount; i++) + { + uint p = primes[i]; + // TODO: Reuse a single squared local for `p` within this loop so we avoid recalculating + // `p * p` repeatedly without storing every squared prime globally (the cache must remain + // limited to the small-divisor tables to honor the memory constraints). + if (p * p > candidate) + { + break; + } + + // TODO: Replace this trial-division `%` with the sieve-based generator that avoids + // per-candidate modulo work so building the small-prime tables stops dominating + // startup time for large scans. + if (candidate % p == 0U) + { + isPrime = false; + break; + } + } + + if (isPrime) + { + primes.Add(candidate); + if (allCount < targetInt) + { + // TODO: Reuse a single cached `candidateSquared` per iteration so we avoid issuing + // three separate 64-bit multiplications when populating the pow2 arrays here and + // in the last-one/last-seven branches below. + all[allCount] = candidate; + allPow2[allCount] = checked((ulong)candidate * candidate); + allCount++; + } + + if (candidate != 2U) + { + 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, lastDigit)) + { + lastSeven[lastSevenCount] = candidate; + lastSevenPow2[lastSevenCount] = checked((ulong)candidate * candidate); + lastSevenCount++; + } + + if (lastThreeCount < targetInt && IsAllowedForLastThree(candidate, lastDigit)) + { + lastThree[lastThreeCount] = candidate; + lastThreePow2[lastThreeCount] = checked((ulong)candidate * candidate); + lastThreeCount++; + } + + if (lastNineCount < targetInt && IsAllowedForLastNine(candidate, lastDigit)) + { + lastNine[lastNineCount] = candidate; + lastNinePow2[lastNineCount] = checked((ulong)candidate * candidate); + lastNineCount++; + } + } + } + + // TODO: Switch this increment to the Mod6 stepping table identified in the Mod6ComparisonBenchmarks + // so we skip the composite residues without relying on per-iteration `%` checks. + candidate = candidate == 2U ? 3U : candidate + 2U; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static LastDigit GetLastDigit(uint prime) + { + return (prime % 10U) switch + { + 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 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 lastDigit switch + { + LastDigit.Three or LastDigit.Seven or LastDigit.Nine => true, + _ => prime == 11U, + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsAllowedForLastThree(uint prime, LastDigit lastDigit) + { + // TODO: Replace the `% 10` residue classification with ULongExtensions.Mod10 once the helper + // migrates to UInt32 so we can reuse the optimized decimal residue path everywhere. + return lastDigit switch + { + LastDigit.Three or LastDigit.Seven => true, + LastDigit.One => prime == 11U, + LastDigit.Nine => prime == 19U, + _ => false, + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsAllowedForLastNine(uint prime, LastDigit lastDigit) + { + // TODO: Replace the `% 10` residue classification with ULongExtensions.Mod10 when the helper + // supports UInt32 inputs so the residue tables share the optimized decimal path. + return lastDigit switch + { + LastDigit.Three or LastDigit.Seven or LastDigit.Nine => true, + LastDigit.One => prime == 11U, + _ => false, + }; + } } diff --git a/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs b/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs index bf1e357..c3f4c5c 100644 --- a/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs +++ b/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs @@ -7,22 +7,42 @@ namespace PerfectNumbers.Core.Gpu; internal static class PrimeTesterKernels { // GPU kernel: small-prime sieve only. Returns 1 if passes sieve (probable prime), 0 otherwise. - public static void SmallPrimeSieveKernel(Index1D index, ArrayView numbers, ArrayView smallPrimes, ArrayView results) + public static void SmallPrimeSieveKernel( + Index1D index, + ArrayView numbers, + ArrayView smallPrimes, + ArrayView smallPrimesLastOne, + ArrayView smallPrimesLastSeven, + ArrayView smallPrimesLastThree, + ArrayView smallPrimesLastNine, + ArrayView results) { ulong n = numbers[index]; byte result = 1; - // EvenPerfectBitScanner filters candidates so production GPU launches only see: - // - n >= 31 (the scanner starts at 136,279,841 and test mode seeds 31) - // - n odd (candidate generators and ModResidueTracker eliminate even exponents) - // - n not divisible by 5 (residue prefiltering and AddPrimes transform cover these composites) - // Tests should honor the same constraints so the host wrapper can remain branchless. + ArrayView primes = smallPrimes; + ulong lastDigit = n % 10UL; + switch (lastDigit) + { + case 1UL: + primes = smallPrimesLastOne; + break; + case 3UL: + primes = smallPrimesLastThree; + break; + case 7UL: + primes = smallPrimesLastSeven; + break; + case 9UL: + primes = smallPrimesLastNine; + break; + } - long length = smallPrimes.Length; + int length = (int)primes.Length; for (int i = 0; i < length; i++) { - ulong prime = smallPrimes[i]; - ulong primeSquare = (ulong)prime * prime; + ulong prime = primes[i]; + ulong primeSquare = prime * prime; if (primeSquare > n) { break; diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index d64cc30..975c7e9 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -48,9 +48,31 @@ public sealed class PrimeTester if (result) { - var smallPrimeDivisorsLength = PrimesGenerator.SmallPrimes.Length; uint[] smallPrimeDivisors = PrimesGenerator.SmallPrimes; ulong[] smallPrimeDivisorsMul = PrimesGenerator.SmallPrimesPow2; + + ulong nMod10 = n.Mod10(); + switch (nMod10) + { + case 1UL: + smallPrimeDivisors = PrimesGenerator.SmallPrimesLastOne; + smallPrimeDivisorsMul = PrimesGenerator.SmallPrimesPow2LastOne; + break; + case 3UL: + smallPrimeDivisors = DivisorGenerator.SmallPrimesLastThree; + smallPrimeDivisorsMul = DivisorGenerator.SmallPrimesPow2LastThree; + break; + case 7UL: + smallPrimeDivisors = PrimesGenerator.SmallPrimesLastSeven; + smallPrimeDivisorsMul = PrimesGenerator.SmallPrimesPow2LastSeven; + break; + case 9UL: + smallPrimeDivisors = DivisorGenerator.SmallPrimesLastNine; + smallPrimeDivisorsMul = DivisorGenerator.SmallPrimesPow2LastNine; + break; + } + + int smallPrimeDivisorsLength = smallPrimeDivisors.Length; for (int i = 0; i < smallPrimeDivisorsLength; i++) { if (smallPrimeDivisorsMul[i] > n) @@ -132,7 +154,15 @@ public sealed class PrimeTester values.Slice(pos, count).CopyTo(temp); input.View.CopyFromCPU(ref temp[0], count); - state.Kernel(count, input.View, state.DevicePrimes.View, output.View); + state.Kernel( + count, + input.View, + state.DevicePrimesDefault.View, + state.DevicePrimesLastOne.View, + state.DevicePrimesLastSeven.View, + state.DevicePrimesLastThree.View, + state.DevicePrimesLastNine.View, + output.View); accelerator.Synchronize(); output.View.CopyToCPU(ref results[pos], count); @@ -225,9 +255,13 @@ public sealed class PrimeTester // Per-accelerator GPU state for prime sieve (kernel + uploaded primes). internal sealed class KernelState { - public Action, ArrayView, ArrayView> Kernel { get; } + public Action, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView> Kernel { get; } public Action, ulong, ArrayView> HeuristicTrialDivisionKernel { get; } - public MemoryBuffer1D DevicePrimes { get; } + public MemoryBuffer1D DevicePrimesDefault { get; } + public MemoryBuffer1D DevicePrimesLastOne { get; } + public MemoryBuffer1D DevicePrimesLastSeven { get; } + public MemoryBuffer1D DevicePrimesLastThree { get; } + public MemoryBuffer1D DevicePrimesLastNine { get; } private readonly Accelerator _accel; private readonly System.Collections.Concurrent.ConcurrentBag _scratchPool = []; // TODO: Replace this ConcurrentBag with the lock-free ring buffer variant validated in @@ -238,12 +272,28 @@ public sealed class PrimeTester { _accel = accelerator; // Compile once per accelerator and upload primes once. - Kernel = accelerator.LoadAutoGroupedStreamKernel, ArrayView, ArrayView>(PrimeTesterKernels.SmallPrimeSieveKernel); + Kernel = accelerator.LoadAutoGroupedStreamKernel, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView, ArrayView>(PrimeTesterKernels.SmallPrimeSieveKernel); HeuristicTrialDivisionKernel = accelerator.LoadAutoGroupedStreamKernel, ulong, ArrayView>(PrimeTesterKernels.HeuristicTrialDivisionKernel); - var primes = PrimesGenerator.SmallPrimes; - DevicePrimes = accelerator.Allocate1D(primes.Length); - DevicePrimes.View.CopyFromCPU(primes); + var primesDefault = DivisorGenerator.SmallPrimes; + DevicePrimesDefault = accelerator.Allocate1D(primesDefault.Length); + DevicePrimesDefault.View.CopyFromCPU(primesDefault); + + var primesLastOne = DivisorGenerator.SmallPrimesLastOne; + DevicePrimesLastOne = accelerator.Allocate1D(primesLastOne.Length); + DevicePrimesLastOne.View.CopyFromCPU(primesLastOne); + + var primesLastSeven = DivisorGenerator.SmallPrimesLastSeven; + DevicePrimesLastSeven = accelerator.Allocate1D(primesLastSeven.Length); + DevicePrimesLastSeven.View.CopyFromCPU(primesLastSeven); + + var primesLastThree = DivisorGenerator.SmallPrimesLastThree; + DevicePrimesLastThree = accelerator.Allocate1D(primesLastThree.Length); + DevicePrimesLastThree.View.CopyFromCPU(primesLastThree); + + var primesLastNine = DivisorGenerator.SmallPrimesLastNine; + DevicePrimesLastNine = accelerator.Allocate1D(primesLastNine.Length); + DevicePrimesLastNine.View.CopyFromCPU(primesLastNine); } internal sealed class ScratchBuffers @@ -296,7 +346,11 @@ public sealed class PrimeTester sb.Dispose(); } - DevicePrimes.Dispose(); + DevicePrimesDefault.Dispose(); + DevicePrimesLastOne.Dispose(); + DevicePrimesLastSeven.Dispose(); + DevicePrimesLastThree.Dispose(); + DevicePrimesLastNine.Dispose(); } } diff --git a/PerfectNumbers.Core/PrimesGenerator.cs b/PerfectNumbers.Core/PrimesGenerator.cs index da1f050..9856333 100644 --- a/PerfectNumbers.Core/PrimesGenerator.cs +++ b/PerfectNumbers.Core/PrimesGenerator.cs @@ -1,151 +1,11 @@ -using System; -using System.Runtime.CompilerServices; - namespace PerfectNumbers.Core; public static class PrimesGenerator { - public static readonly uint[] SmallPrimes; - public static readonly ulong[] SmallPrimesPow2; - public static readonly uint[] SmallPrimesLastOne; - public static readonly ulong[] SmallPrimesPow2LastOne; - public static readonly uint[] SmallPrimesLastSeven; - public static readonly ulong[] SmallPrimesPow2LastSeven; - - static PrimesGenerator() - { - BuildSmallPrimes( - out SmallPrimes, - out SmallPrimesPow2, - out SmallPrimesLastOne, - out SmallPrimesPow2LastOne, - out SmallPrimesLastSeven, - out SmallPrimesPow2LastSeven); - } - - private static void BuildSmallPrimes( - out uint[] all, - out ulong[] allPow2, - out uint[] lastOne, - out ulong[] lastOnePow2, - out uint[] lastSeven, - out ulong[] lastSevenPow2) - { - uint target = PerfectNumberConstants.PrimesLimit; - int targetInt = checked((int)target); - all = new uint[targetInt]; - allPow2 = new ulong[targetInt]; - lastOne = new uint[targetInt]; - lastOnePow2 = new ulong[targetInt]; - lastSeven = new uint[targetInt]; - lastSevenPow2 = new ulong[targetInt]; - - List primes = new(targetInt * 4 / 3 + 16); - uint candidate = 2U; - int allCount = 0; - int lastOneCount = 0; - int lastSevenCount = 0; - - while (allCount < targetInt || lastOneCount < targetInt || lastSevenCount < targetInt) - { - bool isPrime = true; - int primesCount = primes.Count; - for (int i = 0; i < primesCount; i++) - { - uint p = primes[i]; - // TODO: Reuse a single squared local for `p` within this loop so we avoid recalculating - // `p * p` repeatedly without storing every squared prime globally (the cache must remain - // limited to the small-divisor tables to honor the memory constraints). - if (p * p > candidate) - { - break; - } - - // TODO: Replace this trial-division `%` with the sieve-based generator that avoids - // per-candidate modulo work so building the small-prime tables stops dominating - // startup time for large scans. - if (candidate % p == 0U) - { - isPrime = false; - break; - } - } - - if (isPrime) - { - primes.Add(candidate); - if (allCount < targetInt) - { - // TODO: Reuse a single cached `candidateSquared` per iteration so we avoid issuing - // three separate 64-bit multiplications when populating the pow2 arrays here and - // in the last-one/last-seven branches below. - all[allCount] = candidate; - allPow2[allCount] = checked((ulong)candidate * candidate); - allCount++; - } - - if (candidate != 2U) - { - 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, lastDigit)) - { - lastSeven[lastSevenCount] = candidate; - lastSevenPow2[lastSevenCount] = checked((ulong)candidate * candidate); - lastSevenCount++; - } - } - } - - // TODO: Switch this increment to the Mod6 stepping table identified in the Mod6ComparisonBenchmarks - // so we skip the composite residues without relying on per-iteration `%` checks. - candidate = candidate == 2U ? 3U : candidate + 2U; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static LastDigit GetLastDigit(uint prime) - { - return (prime % 10U) switch - { - 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 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 lastDigit switch - { - LastDigit.Three or LastDigit.Seven or LastDigit.Nine => true, - _ => prime == 11U, - }; - } + public static readonly uint[] SmallPrimes = DivisorGenerator.SmallPrimes; + public static readonly ulong[] SmallPrimesPow2 = DivisorGenerator.SmallPrimesPow2; + public static readonly uint[] SmallPrimesLastOne = DivisorGenerator.SmallPrimesLastOne; + public static readonly ulong[] SmallPrimesPow2LastOne = DivisorGenerator.SmallPrimesPow2LastOne; + public static readonly uint[] SmallPrimesLastSeven = DivisorGenerator.SmallPrimesLastSeven; + public static readonly ulong[] SmallPrimesPow2LastSeven = DivisorGenerator.SmallPrimesPow2LastSeven; }