diff --git a/PerfectNumbers.Core/HeuristicPrimeSieves.cs b/PerfectNumbers.Core/HeuristicPrimeSieves.cs index 0f26646..33488fe 100644 --- a/PerfectNumbers.Core/HeuristicPrimeSieves.cs +++ b/PerfectNumbers.Core/HeuristicPrimeSieves.cs @@ -15,195 +15,111 @@ public static class HeuristicPrimeSieves private static readonly ushort[] GroupBResiduesEnding7 = [17, 37, 47, 67, 97, 107, 127, 137, 157, 167, 187, 197]; private static readonly ushort[] GroupBResiduesEnding9 = [19, 29, 59, 79, 89, 109, 139, 149, 169, 179, 199, 209]; - private static int[]? s_groupADivisors; - private static ulong[]? s_groupADivisorSquares; + private static readonly int[] GroupADivisorsStorage; + private static readonly ulong[] GroupADivisorSquaresStorage; - private static int[]? s_groupBDivisorsEnding1; - private static ulong[]? s_groupBDivisorSquaresEnding1; + private static readonly int[] GroupBDivisorsEnding1Storage; + private static readonly ulong[] GroupBDivisorSquaresEnding1Storage; - private static int[]? s_groupBDivisorsEnding3; - private static ulong[]? s_groupBDivisorSquaresEnding3; + private static readonly int[] GroupBDivisorsEnding3Storage; + private static readonly ulong[] GroupBDivisorSquaresEnding3Storage; - private static int[]? s_groupBDivisorsEnding7; - private static ulong[]? s_groupBDivisorSquaresEnding7; + private static readonly int[] GroupBDivisorsEnding7Storage; + private static readonly ulong[] GroupBDivisorSquaresEnding7Storage; - private static int[]? s_groupBDivisorsEnding9; - private static ulong[]? s_groupBDivisorSquaresEnding9; + private static readonly int[] GroupBDivisorsEnding9Storage; + private static readonly ulong[] GroupBDivisorSquaresEnding9Storage; - private static readonly object InitLock = new(); - - public static ReadOnlySpan GroupADivisors + static HeuristicPrimeSieves() { - get - { - EnsureGroupAInitialized(); - return s_groupADivisors!; - } + Console.WriteLine("[HeuristicPrimeSieves] Initializing heuristic divisor sieves..."); + + (GroupADivisorsStorage, GroupADivisorSquaresStorage) = BuildGroupADivisors(); + (GroupBDivisorsEnding1Storage, GroupBDivisorSquaresEnding1Storage) = BuildGroupBResidueSieve(GroupBResiduesEnding1); + (GroupBDivisorsEnding3Storage, GroupBDivisorSquaresEnding3Storage) = BuildGroupBResidueSieve(GroupBResiduesEnding3); + (GroupBDivisorsEnding7Storage, GroupBDivisorSquaresEnding7Storage) = BuildGroupBResidueSieve(GroupBResiduesEnding7); + (GroupBDivisorsEnding9Storage, GroupBDivisorSquaresEnding9Storage) = BuildGroupBResidueSieve(GroupBResiduesEnding9); } - public static ReadOnlySpan GroupADivisorSquares + public static void EnsureInitialized() { - get - { - EnsureGroupAInitialized(); - return s_groupADivisorSquares!; - } + // Intentionally left blank. Accessing this method forces the static constructor to run. } - public static ReadOnlySpan GroupBDivisorsEnding1 + public static ReadOnlySpan GroupADivisors => GroupADivisorsStorage; + + public static ReadOnlySpan GroupADivisorSquares => GroupADivisorSquaresStorage; + + public static ReadOnlySpan GroupBDivisorsEnding1 => GroupBDivisorsEnding1Storage; + + public static ReadOnlySpan GroupBDivisorSquaresEnding1 => GroupBDivisorSquaresEnding1Storage; + + public static ReadOnlySpan GroupBDivisorsEnding3 => GroupBDivisorsEnding3Storage; + + public static ReadOnlySpan GroupBDivisorSquaresEnding3 => GroupBDivisorSquaresEnding3Storage; + + public static ReadOnlySpan GroupBDivisorsEnding7 => GroupBDivisorsEnding7Storage; + + public static ReadOnlySpan GroupBDivisorSquaresEnding7 => GroupBDivisorSquaresEnding7Storage; + + public static ReadOnlySpan GroupBDivisorsEnding9 => GroupBDivisorsEnding9Storage; + + public static ReadOnlySpan GroupBDivisorSquaresEnding9 => GroupBDivisorSquaresEnding9Storage; + + private static (int[] divisors, ulong[] squares) BuildGroupADivisors() { - get + var divisors = new int[SieveLength]; + var squares = new ulong[SieveLength]; + + int index = 0; + for (; index < GroupAConstants.Length && index < divisors.Length; index++) { - EnsureGroupBInitialized(ref s_groupBDivisorsEnding1, ref s_groupBDivisorSquaresEnding1, GroupBResiduesEnding1); - return s_groupBDivisorsEnding1!; + int value = GroupAConstants[index]; + divisors[index] = value; + squares[index] = (ulong)value * (ulong)value; } + + int candidate = 23; + int incrementIndex = 0; + while (index < divisors.Length) + { + divisors[index] = candidate; + squares[index] = (ulong)candidate * (ulong)candidate; + + candidate += GroupAIncrementPattern[incrementIndex]; + incrementIndex ^= 1; + index++; + } + + return (divisors, squares); } - public static ReadOnlySpan GroupBDivisorSquaresEnding1 + private static (int[] divisors, ulong[] squares) BuildGroupBResidueSieve(ReadOnlySpan residues) { - get - { - EnsureGroupBInitialized(ref s_groupBDivisorsEnding1, ref s_groupBDivisorSquaresEnding1, GroupBResiduesEnding1); - return s_groupBDivisorSquaresEnding1!; - } - } + var divisors = new int[SieveLength]; + var squares = new ulong[SieveLength]; - public static ReadOnlySpan GroupBDivisorsEnding3 - { - get - { - EnsureGroupBInitialized(ref s_groupBDivisorsEnding3, ref s_groupBDivisorSquaresEnding3, GroupBResiduesEnding3); - return s_groupBDivisorsEnding3!; - } - } + int index = 0; + ulong baseValue = 0UL; - public static ReadOnlySpan GroupBDivisorSquaresEnding3 - { - get + while (index < divisors.Length) { - EnsureGroupBInitialized(ref s_groupBDivisorsEnding3, ref s_groupBDivisorSquaresEnding3, GroupBResiduesEnding3); - return s_groupBDivisorSquaresEnding3!; - } - } - - public static ReadOnlySpan GroupBDivisorsEnding7 - { - get - { - EnsureGroupBInitialized(ref s_groupBDivisorsEnding7, ref s_groupBDivisorSquaresEnding7, GroupBResiduesEnding7); - return s_groupBDivisorsEnding7!; - } - } - - public static ReadOnlySpan GroupBDivisorSquaresEnding7 - { - get - { - EnsureGroupBInitialized(ref s_groupBDivisorsEnding7, ref s_groupBDivisorSquaresEnding7, GroupBResiduesEnding7); - return s_groupBDivisorSquaresEnding7!; - } - } - - public static ReadOnlySpan GroupBDivisorsEnding9 - { - get - { - EnsureGroupBInitialized(ref s_groupBDivisorsEnding9, ref s_groupBDivisorSquaresEnding9, GroupBResiduesEnding9); - return s_groupBDivisorsEnding9!; - } - } - - public static ReadOnlySpan GroupBDivisorSquaresEnding9 - { - get - { - EnsureGroupBInitialized(ref s_groupBDivisorsEnding9, ref s_groupBDivisorSquaresEnding9, GroupBResiduesEnding9); - return s_groupBDivisorSquaresEnding9!; - } - } - - private static void EnsureGroupAInitialized() - { - if (s_groupADivisors is not null) - { - return; - } - - lock (InitLock) - { - if (s_groupADivisors is not null) + for (int i = 0; i < residues.Length && index < divisors.Length; i++) { - return; - } + ulong candidate = baseValue + residues[i]; + if (candidate <= 13UL) + { + continue; + } - var divisors = new int[SieveLength]; - var squares = new ulong[SieveLength]; - - int index = 0; - for (; index < GroupAConstants.Length && index < divisors.Length; index++) - { - int value = GroupAConstants[index]; - divisors[index] = value; - squares[index] = (ulong)value * (ulong)value; - } - - int candidate = 23; - int incrementIndex = 0; - while (index < divisors.Length) - { - divisors[index] = candidate; - squares[index] = (ulong)candidate * (ulong)candidate; - - candidate += GroupAIncrementPattern[incrementIndex]; - incrementIndex ^= 1; + divisors[index] = (int)candidate; + squares[index] = candidate * candidate; index++; } - s_groupADivisors = divisors; - s_groupADivisorSquares = squares; - } - } - - private static void EnsureGroupBInitialized(ref int[]? divisorsField, ref ulong[]? squaresField, ReadOnlySpan residues) - { - if (divisorsField is not null) - { - return; + baseValue += (ulong)Wheel210; } - lock (InitLock) - { - if (divisorsField is not null) - { - return; - } - - var divisors = new int[SieveLength]; - var squares = new ulong[SieveLength]; - - int index = 0; - ulong baseValue = 0UL; - - while (index < divisors.Length) - { - for (int i = 0; i < residues.Length && index < divisors.Length; i++) - { - ulong candidate = baseValue + residues[i]; - if (candidate <= 13UL) - { - continue; - } - - divisors[index] = (int)candidate; - squares[index] = candidate * candidate; - index++; - } - - baseValue += (ulong)Wheel210; - } - - divisorsField = divisors; - squaresField = squares; - } + return (divisors, squares); } } diff --git a/PerfectNumbers.Core/PrimeTester.cs b/PerfectNumbers.Core/PrimeTester.cs index e82d0eb..63ce16f 100644 --- a/PerfectNumbers.Core/PrimeTester.cs +++ b/PerfectNumbers.Core/PrimeTester.cs @@ -14,6 +14,11 @@ namespace PerfectNumbers.Core; public sealed class PrimeTester(bool useInternal = false) { + static PrimeTester() + { + HeuristicPrimeSieves.EnsureInitialized(); + } + // TODO: Don't use these tiny arrays. Hard-code the values / checks instead private static readonly ulong[] GroupAConstantDivisors = [3UL, 7UL, 11UL, 13UL]; private static readonly byte[] GroupAIncrementPattern = [20, 10]; @@ -197,20 +202,13 @@ public sealed class PrimeTester(bool useInternal = false) private static bool HeuristicTrialDivisionCpu(ulong n, ulong sqrtLimit, byte nMod10, bool includeGroupB) { - Span groupBBuffer = stackalloc HeuristicGroupBSequenceState[MaxGroupBSequences]; - var enumerator = new HeuristicDivisorEnumerator(sqrtLimit, nMod10, groupBBuffer); - - while (enumerator.TryGetNext(out HeuristicDivisorCandidate candidate)) + ReadOnlySpan groupADivisors = HeuristicPrimeSieves.GroupADivisors; + for (int i = 0; i < groupADivisors.Length; i++) { - if (!includeGroupB && candidate.Group == HeuristicDivisorGroup.GroupB) + ulong divisor = (ulong)groupADivisors[i]; + if (divisor > sqrtLimit) { - return EvaluateWithOpenNumericFallback(n); - } - - ulong divisor = candidate.Value; - if (divisor <= 1UL) - { - continue; + break; } if (n % divisor == 0UL) @@ -219,20 +217,71 @@ public sealed class PrimeTester(bool useInternal = false) } } - return includeGroupB ? true : EvaluateWithOpenNumericFallback(n); + if (!includeGroupB) + { + return EvaluateWithOpenNumericFallback(n); + } + + ReadOnlySpan endingOrder = GetGroupBEndingOrder(nMod10); + if (endingOrder.IsEmpty) + { + return EvaluateWithOpenNumericFallback(n); + } + + Span indices = endingOrder.Length <= 8 + ? stackalloc int[endingOrder.Length] + : new int[endingOrder.Length]; + + while (true) + { + ulong bestCandidate = ulong.MaxValue; + int bestEndingIndex = -1; + + for (int i = 0; i < endingOrder.Length; i++) + { + ReadOnlySpan divisors = GetGroupBDivisors(endingOrder[i]); + int index = indices[i]; + if ((uint)index >= (uint)divisors.Length) + { + continue; + } + + ulong candidate = (ulong)divisors[index]; + if (candidate > sqrtLimit) + { + indices[i] = divisors.Length; + continue; + } + + if (bestEndingIndex == -1 || candidate < bestCandidate) + { + bestCandidate = candidate; + bestEndingIndex = i; + } + } + + if (bestEndingIndex == -1) + { + break; + } + + if (n % bestCandidate == 0UL) + { + return false; + } + + indices[bestEndingIndex]++; + } + + return true; } private bool HeuristicTrialDivisionGpuDetectsDivisor(ulong n, ulong sqrtLimit, byte nMod10) { - Span groupBBuffer = stackalloc HeuristicGroupBSequenceState[MaxGroupBSequences]; - var enumerator = new HeuristicDivisorEnumerator(sqrtLimit, nMod10, groupBBuffer); - int batchCapacity = Math.Max(1, HeuristicGpuDivisorBatchSize); - var candidatePool = ArrayPool.Shared; var divisorPool = ThreadStaticPools.UlongPool; var hitPool = ThreadStaticPools.BytePool; - HeuristicDivisorCandidate[]? candidateArray = null; ulong[]? divisorArray = null; byte[]? hitFlags = null; @@ -243,7 +292,6 @@ public sealed class PrimeTester(bool useInternal = false) try { - candidateArray = candidatePool.Rent(batchCapacity); divisorArray = divisorPool.Rent(batchCapacity); hitFlags = hitPool.Rent(batchCapacity); @@ -259,6 +307,11 @@ public sealed class PrimeTester(bool useInternal = false) bool ProcessBatch(int length) { + if (length <= 0) + { + return false; + } + scratch.Input.View.CopyFromCPU(ref divisorArray![0], length); state.HeuristicTrialDivisionKernel(length, scratch.Input.View, n, scratch.Output.View); accelerator.Synchronize(); @@ -271,7 +324,7 @@ public sealed class PrimeTester(bool useInternal = false) continue; } - ulong divisor = candidateArray![i].Value; + ulong divisor = divisorArray![i]; if (divisor > 1UL && n % divisor == 0UL) { return true; @@ -281,36 +334,87 @@ public sealed class PrimeTester(bool useInternal = false) return false; } - while (enumerator.TryGetNext(out HeuristicDivisorCandidate candidate)) + bool QueueDivisor(ulong divisor) { - ulong divisor = candidate.Value; - if (divisor <= 1UL) - { - continue; - } - - candidateArray[count] = candidate; - divisorArray[count] = divisor; + divisorArray![count] = divisor; count++; if (count == batchCapacity) { if (ProcessBatch(count)) { - compositeDetected = true; - break; + return true; } count = 0; } + + return false; + } + + ReadOnlySpan groupADivisors = HeuristicPrimeSieves.GroupADivisors; + for (int i = 0; i < groupADivisors.Length && !compositeDetected; i++) + { + ulong divisor = (ulong)groupADivisors[i]; + if (divisor > sqrtLimit) + { + break; + } + + compositeDetected = QueueDivisor(divisor); + } + + if (!compositeDetected) + { + ReadOnlySpan endingOrder = GetGroupBEndingOrder(nMod10); + if (!endingOrder.IsEmpty) + { + Span indices = endingOrder.Length <= 8 + ? stackalloc int[endingOrder.Length] + : new int[endingOrder.Length]; + + while (!compositeDetected) + { + ulong bestCandidate = ulong.MaxValue; + int bestEndingIndex = -1; + + for (int i = 0; i < endingOrder.Length; i++) + { + ReadOnlySpan divisors = GetGroupBDivisors(endingOrder[i]); + int index = indices[i]; + if ((uint)index >= (uint)divisors.Length) + { + continue; + } + + ulong candidate = (ulong)divisors[index]; + if (candidate > sqrtLimit) + { + indices[i] = divisors.Length; + continue; + } + + if (bestEndingIndex == -1 || candidate < bestCandidate) + { + bestCandidate = candidate; + bestEndingIndex = i; + } + } + + if (bestEndingIndex == -1) + { + break; + } + + compositeDetected = QueueDivisor(bestCandidate); + indices[bestEndingIndex]++; + } + } } if (!compositeDetected && count > 0) { - if (ProcessBatch(count)) - { - compositeDetected = true; - } + compositeDetected = ProcessBatch(count); } } finally @@ -331,11 +435,6 @@ public sealed class PrimeTester(bool useInternal = false) divisorPool.Return(divisorArray); } - if (candidateArray is not null) - { - candidatePool.Return(candidateArray); - } - gpu.Dispose(); limiter.Dispose(); } @@ -380,6 +479,15 @@ public sealed class PrimeTester(bool useInternal = false) _ => ReadOnlySpan.Empty, }; + private static ReadOnlySpan GetGroupBDivisors(byte ending) => ending switch + { + 1 => HeuristicPrimeSieves.GroupBDivisorsEnding1, + 3 => HeuristicPrimeSieves.GroupBDivisorsEnding3, + 7 => HeuristicPrimeSieves.GroupBDivisorsEnding7, + 9 => HeuristicPrimeSieves.GroupBDivisorsEnding9, + _ => ReadOnlySpan.Empty, + }; + private static ReadOnlySpan GetWheel210ResiduesForEnding(byte ending) => ending switch { 1 => Wheel210ResiduesEnding1, diff --git a/docs/HeuristicIsPrimePlan.md b/docs/HeuristicIsPrimePlan.md index fd2f1ca..96da69e 100644 --- a/docs/HeuristicIsPrimePlan.md +++ b/docs/HeuristicIsPrimePlan.md @@ -116,7 +116,7 @@ The plan incorporates the updated divisor-class heuristics (Groups A/B, wheels, 1. [done] **Snapshot current implementations** – preserve `IsPrimeGpu` and `IsPrimeInternal` as references for regression comparisons and migration checkpoints (see Section 2). 2. [done] **Introduce placeholder heuristic entry points** – add `HeuristicIsPrimeCpu` and `HeuristicIsPrimeGpu` by copying the current internal and GPU methods so subsequent commits can iterate without disrupting legacy behavior. 3. [done] **Design shared heuristic scaffolding** - * [done] Introduced `HeuristicDivisorEnumerator` and stackalloc-backed Group B sequence states so CPU/GPU heuristics share the Group A/B wheel metadata and candidate ordering. + * [done] Added `HeuristicPrimeSieves` to precompute 4M-entry Group A/B divisor tables so heuristic prime checks reuse cached sequences without regenerating wheel steps at runtime; the enumerator remains available for residue-driven consumers. * [done] Surface helpers that accept the precomputed `R = ⌊√n⌋` and `nMod10` values from callers via the new overload of `HeuristicIsPrimeCpu`; PrimeTester still computes them locally until call sites forward the parameters. * [done] Simplified the heuristic execution path so it runs without cancellation tokens or callback plumbing while keeping the shared enumerator available to other components. 4. **Share residue and wheel state with by-divisor sessions** @@ -127,11 +127,11 @@ The plan incorporates the updated divisor-class heuristics (Groups A/B, wheels, * [done] Extend the heuristic scaffolding so it returns `MontgomeryDivisorData` descriptors and cycle-length hints for each candidate divisor via `HeuristicDivisorPreparation` and `PrepareHeuristicDivisor`. * Wire these outputs into the existing cycle calculators to avoid duplicate cache lookups inside the CPU/GPU divisor scans. 6. [done] **Implement `HeuristicIsPrimeCpu`** - * Baseline Group A/B enumeration now executes inside `HeuristicIsPrimeCpu` via `HeuristicDivisorEnumerator`, computing `⌊√n⌋` locally and short-circuiting on the first divisor. + * Baseline Group A/B enumeration now executes inside `HeuristicIsPrimeCpu` via the cached `HeuristicPrimeSieves` tables, computing `⌊√n⌋` locally and short-circuiting on the first divisor. * [done] The CPU heuristic now operates as a pure trial-division sweep without maintaining per-call summaries, relying on `PrepareHeuristicDivisor` only when downstream consumers request Montgomery data. * [done] Introduced the temporary `UseHeuristicGroupBTrialDivision` gate so current builds execute Group A locally and then fall back to `Open.Numeric.Primes.Prime.Numbers.IsPrime`, keeping the full Group B implementation available for reactivation. 7. [done] **Implement `HeuristicIsPrimeGpu`** - * [done] Mirror CPU structure while batching divisor checks on the accelerator via `HeuristicTrialDivisionGpu`, reusing caller-provided `sqrtLimit`/`nMod10` inputs and falling back to CPU when GPUs are disabled. + * [done] Mirror CPU structure while batching divisor checks on the accelerator via `HeuristicTrialDivisionGpu`, drawing divisors from `HeuristicPrimeSieves`, reusing caller-provided `sqrtLimit`/`nMod10` inputs, and falling back to CPU when GPUs are disabled. * [done] Added `PrimeTesterKernels.HeuristicTrialDivisionKernel` so GPU batches respect Group A/B ordering while emitting hit flags for early exits. * [done] Kept GPU divisibility in standard modular arithmetic while reserving Montgomery transforms for CPU confirmation logic. * [done] When the temporary Group B gate is disabled, delegate GPU calls to the CPU fallback so Group A coverage remains in place while the accelerator path stays available for future re-enablement. diff --git a/docs/HeuristicIsPrimeSpecification.md b/docs/HeuristicIsPrimeSpecification.md index f365ad9..30de040 100644 --- a/docs/HeuristicIsPrimeSpecification.md +++ b/docs/HeuristicIsPrimeSpecification.md @@ -71,9 +71,9 @@ The by-divisor executors (`MersenneNumberDivisorByDivisorCpuTester` and `Mersenn * **Residue filters** – The CPU scan maintains rolling residues modulo 10/8/5/3/7/11 to match the filters in `MersenneCpuDivisorScanSession.CheckDivisors`. `HeuristicIsPrime*` must either share the residue update helpers or produce identical stepping deltas so the divisor scanners can reuse the pre-filtered candidate stream without recomputing moduli. The shared `MersenneDivisorResidueStepper` now centralises those modulo calculations so both paths reuse the same state transitions, and the GPU by-divisor tester instantiates the same stepper before filtering candidates. * **Stepper integration** – CPU sessions advance residues and unity checks through `ExponentRemainderStepperCpu` together with `CycleRemainderStepper`; GPU kernels mirror this contract via `ExponentRemainderStepperGpu`. The heuristic implementation must call into these steppers (or emit the same remainder deltas) so the shared stepping state remains aligned across CPU/GPU scans and factor validation. -* **Wheel alignment** – The wheel 30/210 sequences emitted by `PrimeTester.CreateHeuristicDivisorEnumerator` now drive both the CPU and GPU by-divisor testers, keeping the divisor cache and kernel inputs in lockstep without bespoke residue loops. -* **Divisor streaming API** – `PrimeTester.CreateHeuristicDivisorEnumerator` now feeds `MersenneNumberDivisorByDivisorCpuTester.CheckDivisors` and `MersenneNumberDivisorByDivisorGpuTester.CheckDivisors`, replacing bespoke residue loops while supplying candidates directly to Montgomery preparation and GPU kernels. -* **GPU batching** – `HeuristicIsPrimeGpu` streams Group A/B candidates through the same enumerator, packs them into batches controlled by `PrimeTester.HeuristicGpuDivisorBatchSize`, and launches `PrimeTesterKernels.HeuristicTrialDivisionKernel` to test each divisor with standard `%` arithmetic. When GPUs are disabled, the method delegates to the CPU heuristic so fallbacks reuse the same candidate ordering without additional plumbing. +* **Wheel alignment** – `HeuristicPrimeSieves` precomputes the wheel 30/210 sequences once at startup so both the heuristic prime tester and the by-divisor executors consume identical Group A/B streams without regenerating residues on every call. +* **Divisor streaming API** – `PrimeTester.CreateHeuristicDivisorEnumerator` exposes the same precomputed sieves so by-divisor scans can pull candidates directly into Montgomery preparation and GPU kernels without bespoke residue loops. +* **GPU batching** – `HeuristicIsPrimeGpu` streams Group A/B candidates from `HeuristicPrimeSieves`, packs them into batches controlled by `PrimeTester.HeuristicGpuDivisorBatchSize`, and launches `PrimeTesterKernels.HeuristicTrialDivisionKernel` to test each divisor with standard `%` arithmetic. When GPUs are disabled, the method delegates to the CPU heuristic so fallbacks reuse the same candidate ordering without additional plumbing. * **Cycle preparation** – When the heuristic discovers a divisor candidate, it should populate `MontgomeryDivisorData` and expose the computed cycle length (or the need to recompute it) so that `DivisorCycleCache` and `MersenneDivisorCycles` do not duplicate work for the same `(prime, divisor)` pair during factor verification. `PrimeTester.PrepareHeuristicDivisor` together with `PrimeTester.ResolveHeuristicCycleLength` now provides this payload to both CPU and GPU by-divisor scans, and the trial-division summaries capture the data whenever a divisor is found. * **Caching contract** – The by-divisor GPU implementation pools `DivisorScanSession` instances and reuses GPU buffers. The heuristic must accept externally allocated spans/buffers for divisor candidates, cycle lengths, and hits so the existing resource pools remain valid. @@ -97,8 +97,8 @@ The following summaries cover the first 500,000 odd composite numbers after remo ### Heuristic cheat sheet -* Group A: test `3, 7, 11, 13` and every `d ≡ 3 (mod 10)` up to `⌊√n⌋` (wheel 30). -* Group B: test only the allowed endings determined by `n mod 10`, scanning in ascending order (wheel 210) and applying the per-class priority order: `n ≡ 1` → `9` then `1`; `n ≡ 3` → `9` then `7`; `n ≡ 7` → `7` then `1`; `n ≡ 9` → `9`, `7`, `1`. +* Group A: test `3, 7, 11, 13` and every `d ≡ 3 (mod 10)` up to `⌊√n⌋` from the precomputed wheel-30 sieve. +* Group B: test only the allowed endings determined by `n mod 10`, scanning in ascending order from the wheel-210 sieves and applying the per-class priority order: `n ≡ 1` → `9` then `1`; `n ≡ 3` → `9` then `7`; `n ≡ 7` → `7` then `1`; `n ≡ 9` → `9`, `7`, `1`. * Because `min(B)` is overwhelmingly smaller than `A_max(n)` for the “no Group A divisor” cohort, always start Group B from its smallest candidates instead of resuming above `A_max(n)`. ## Benchmark Reporting