diff --git a/AGENTS.md b/AGENTS.md index a32973e..f7ba750 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ To build `EvenPerfectScanner` run: dotnet build EvenPerfectScanner.sln ``` -Run only the tests related to the solution you modify. Always run tests without --no-build parameter to avoid run failures due to missing assembly files. Tests should be always written using XUnit with FluentAssertions. Before executing any tests, make sure their running time is reasonable; skip test suites that are expected to take excessively long. +Run only the tests related to the solution you modify. Always run tests without --no-build parameter to avoid run failures due to missing assembly files. Tests should be always written using XUnit with FluentAssertions. Before executing any tests, make sure their running time is reasonable; skip test suites that are expected to take excessively long. When running tests, always provide both an explicit timeout guard and the Category=Fast filter so that only the fast suite executes. ### Test execution time policy @@ -35,7 +35,8 @@ Run only the tests related to the solution you modify. Always run tests without - Linux/macOS (bash): - Use GNU timeout (install coreutils if needed on macOS): - - `timeout 120s dotnet test .csproj -c Debug --filter "FullyQualifiedName~SomeTests"` + - `timeout 120s dotnet test .csproj -c Debug --filter "Category=Fast&FullyQualifiedName~SomeTests"` + - Always specify `timeout 120s` (or a shorter limit when appropriate) and include `Category=Fast` in every filter. - Note: exit code 124 indicates timeout; handle accordingly in scripts. - Codex/agent runs: when invoked as separate steps where inline time control isn’t available, wrap calls through the shell using the guards above rather than calling `dotnet test` directly from the tool. @@ -52,11 +53,11 @@ The only .Net SDK available in Codex environment is .Net 8.0. It'll be periodica Always enter blank line after closing "}" parenthesis, unless there is a consecutive closing "}" bracket. IF's, loops, in practice all code blocks should be put in their "{ (...) }" blocks. Even if there is only a single line in such a code block. Always use the latest officially released language features. All .csproj files should define nullable = enabled and we should always use "?" and/or "!" where appropriate. -Never ever run tests marked with trait Category = Slow. Only run tests with category "Fast", when you want to validate if you haven't broken anything. If there are no categories assigned to the tests, when you attempt running them always watch out the 2 mins execution time, to prevent hanging up on test execution. In such case, marked all timeout out tests with category Slow and tests which executed within time limit, even if they failed, mark with category Fast, so that we can recognize them in the future. +Never ever run tests marked with trait Category = Slow. Always invoke `dotnet test` (or equivalent wrappers) with both an explicit timeout and the `Category=Fast` filter; never run tests without specifying these arguments. If there are no categories assigned yet, categorize them before running or skip the execution. There is only one agents file - this one. Don't search for another one. -Always use indentation with 4 spaces in source code files. Do not use 8-space indentation or tabs. +Always use indentation with four space characters in source code files, even if a file currently uses tabs. Increase indentation level by exactly one indentation unit (four spaces) per nested block. When parameters or long expressions wrap, indent the continuation with two indentation units for clarity. Keep the divisor cycle cache limited to three blocks in memory (the base snapshot plus at most two additional blocks) and always start background computation of the next block immediately after loading the first block from disk and whenever work begins on the most recently generated block. diff --git a/PerfectNumbers.Core.Tests/AlphaCacheTests.cs b/PerfectNumbers.Core.Tests/AlphaCacheTests.cs index 7cd8edf..35c29f4 100644 --- a/PerfectNumbers.Core.Tests/AlphaCacheTests.cs +++ b/PerfectNumbers.Core.Tests/AlphaCacheTests.cs @@ -5,7 +5,6 @@ namespace PerfectNumbers.Core.Tests; public class AlphaCacheTests { [Fact(Skip = "TODO: implement test")] - [Trait("Category", "Fast")] public void Placeholder() { } diff --git a/PerfectNumbers.Core.Tests/AlphaCalculationsTests.cs b/PerfectNumbers.Core.Tests/AlphaCalculationsTests.cs index 575f834..3c7e971 100644 --- a/PerfectNumbers.Core.Tests/AlphaCalculationsTests.cs +++ b/PerfectNumbers.Core.Tests/AlphaCalculationsTests.cs @@ -5,7 +5,6 @@ namespace PerfectNumbers.Core.Tests; public class AlphaCalculationsTests { [Fact(Skip = "TODO: implement test")] - [Trait("Category", "Fast")] public void Placeholder() { } diff --git a/PerfectNumbers.Core.Tests/InputParserTests.cs b/PerfectNumbers.Core.Tests/InputParserTests.cs index 978d747..d85aa15 100644 --- a/PerfectNumbers.Core.Tests/InputParserTests.cs +++ b/PerfectNumbers.Core.Tests/InputParserTests.cs @@ -5,7 +5,6 @@ namespace PerfectNumbers.Core.Tests; public class InputParserTests { [Fact(Skip = "TODO: implement test")] - [Trait("Category", "Fast")] public void Placeholder() { } diff --git a/PerfectNumbers.Core.Tests/KRangeFinderTests.cs b/PerfectNumbers.Core.Tests/KRangeFinderTests.cs index 057ecb0..d689e14 100644 --- a/PerfectNumbers.Core.Tests/KRangeFinderTests.cs +++ b/PerfectNumbers.Core.Tests/KRangeFinderTests.cs @@ -5,7 +5,6 @@ namespace PerfectNumbers.Core.Tests; public class KRangeFinderTests { [Fact(Skip = "TODO: implement test")] - [Trait("Category", "Fast")] public void Placeholder() { } diff --git a/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs index 9317c02..8dc9343 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs @@ -1,8 +1,9 @@ +using System; +using System.Reflection; using FluentAssertions; using PerfectNumbers.Core; -using PerfectNumbers.Core.Gpu; using PerfectNumbers.Core.Cpu; -using System.Reflection; +using PerfectNumbers.Core.Gpu; using Xunit; namespace PerfectNumbers.Core.Tests; diff --git a/PerfectNumbers.Core.Tests/RleBlacklistTests.cs b/PerfectNumbers.Core.Tests/RleBlacklistTests.cs index 0d9fd2c..aef27f2 100644 --- a/PerfectNumbers.Core.Tests/RleBlacklistTests.cs +++ b/PerfectNumbers.Core.Tests/RleBlacklistTests.cs @@ -5,7 +5,6 @@ namespace PerfectNumbers.Core.Tests; public class RleBlacklistTests { [Fact(Skip = "TODO: implement test")] - [Trait("Category", "Fast")] public void Placeholder() { } diff --git a/PerfectNumbers.Core.Tests/TextFileWriterTests.cs b/PerfectNumbers.Core.Tests/TextFileWriterTests.cs index e584efe..b536864 100644 --- a/PerfectNumbers.Core.Tests/TextFileWriterTests.cs +++ b/PerfectNumbers.Core.Tests/TextFileWriterTests.cs @@ -5,7 +5,6 @@ namespace PerfectNumbers.Core.Tests; public class TextFileWriterTests { [Fact(Skip = "TODO: implement test")] - [Trait("Category", "Fast")] public void Placeholder() { } diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs index 3aaefef..30d24f7 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs @@ -14,652 +14,913 @@ namespace PerfectNumbers.Core.Gpu; public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDivisorByDivisorTester { - private int _gpuBatchSize = GpuConstants.ScanBatchSize; - private readonly object _sync = new(); - private ulong _divisorLimit; - private bool _isConfigured; - private bool _useDivisorCycles; + private int _gpuBatchSize = GpuConstants.ScanBatchSize; + private readonly object _sync = new(); + private ulong _divisorLimit; + private bool _isConfigured; + private bool _useDivisorCycles; - private readonly ConcurrentDictionary, ArrayView, ArrayView>> _kernelCache = new(); - private readonly ConcurrentDictionary, ArrayView>> _kernelExponentCache = new(); - private readonly ConcurrentDictionary> _resourcePools = new(AcceleratorReferenceComparer.Instance); - private readonly ConcurrentBag _sessionPool = new(); + private readonly ConcurrentDictionary, ArrayView, ArrayView>> _kernelCache = new(); + private readonly ConcurrentDictionary, ArrayView>> _kernelExponentCache = new(); + private readonly ConcurrentDictionary> _resourcePools = new(AcceleratorReferenceComparer.Instance); + private readonly ConcurrentBag _sessionPool = new(); - private Action, ArrayView, ArrayView> GetKernel(Accelerator accelerator) => - _kernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, ArrayView, ArrayView>(CheckKernel)); + private Action, ArrayView, ArrayView> GetKernel(Accelerator accelerator) => + _kernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, ArrayView, ArrayView>(CheckKernel)); - private Action, ArrayView> GetKernelByPrimeExponent(Accelerator accelerator) => - _kernelExponentCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, ArrayView>(ComputeMontgomeryExponentKernel)); + private Action, ArrayView> GetKernelByPrimeExponent(Accelerator accelerator) => + _kernelExponentCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel, ArrayView>(ComputeMontgomeryExponentKernel)); - public int GpuBatchSize - { - get => _gpuBatchSize; - set => _gpuBatchSize = Math.Max(1, value); - } + public int GpuBatchSize + { + get => _gpuBatchSize; + set => _gpuBatchSize = Math.Max(1, value); + } - int IMersenneNumberDivisorByDivisorTester.BatchSize - { - get => GpuBatchSize; - set => GpuBatchSize = value; - } + int IMersenneNumberDivisorByDivisorTester.BatchSize + { + get => GpuBatchSize; + set => GpuBatchSize = value; + } - public bool UseDivisorCycles - { - get => _useDivisorCycles; - set => _useDivisorCycles = value; - } + public bool UseDivisorCycles + { + get => _useDivisorCycles; + set => _useDivisorCycles = value; + } - public void ConfigureFromMaxPrime(ulong maxPrime) - { - lock (_sync) - { - _divisorLimit = ComputeDivisorLimitFromMaxPrime(maxPrime); - _isConfigured = true; - } - } - - public bool IsPrime(ulong prime, out bool divisorsExhausted) - { - ulong allowedMax; - bool useCycles; - int batchCapacity; - - lock (_sync) - { - if (!_isConfigured) - { - throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester."); - } - - allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit); - useCycles = _useDivisorCycles; - batchCapacity = _gpuBatchSize; - } - - if (allowedMax < 3UL) - { - divisorsExhausted = true; - return true; - } - - bool composite; - bool coveredRange; - ulong processedCount; - ulong lastProcessed; - - var gpuLease = GpuContextPool.RentPreferred(preferCpu: false); - var accelerator = gpuLease.Accelerator; - var kernel = GetKernel(accelerator); - BatchResources resources = RentBatchResources(accelerator, batchCapacity); - - try - { - composite = CheckDivisors( - prime, - allowedMax, - useCycles, - accelerator, - kernel, - resources.DivisorsBuffer, - resources.ExponentBuffer, - resources.HitsBuffer, - resources.Divisors, - resources.Exponents, - resources.Hits, - resources.DivisorData, - out lastProcessed, - out coveredRange, - out processedCount - ); - } - finally - { - ReturnBatchResources(accelerator, resources); - gpuLease.Dispose(); - } - - if (composite) - { - divisorsExhausted = true; - return false; - } - - divisorsExhausted = coveredRange; - return true; - } - - private static bool CheckDivisors( - ulong prime, - ulong allowedMax, - bool useCycles, - Accelerator accelerator, - Action, ArrayView, ArrayView> kernel, - MemoryBuffer1D divisorsBuffer, - MemoryBuffer1D exponentBuffer, - MemoryBuffer1D hitsBuffer, - in ulong[] divisors, - ulong[] exponents, - byte[] hits, - MontgomeryDivisorData[] divisorData, - out ulong lastProcessed, - out bool coveredRange, - out ulong processedCount) - { - lastProcessed = 0UL; - processedCount = 0UL; - - if (allowedMax < 3UL) - { - coveredRange = true; - return false; - } - - int batchCapacity = (int)divisorsBuffer.Length; - - bool composite = false; - bool processedAll = false; - ulong currentDivisor = 3UL, next, nextDivisor; - int batchSize, i; - bool reachedEndInBatch; - - Span divisorDataSpan; - Span divisorSpan; - Span exponentSpan; - Span hitsSpan; - ArrayView1D divisorView; - ArrayView1D exponentView; - ArrayView1D hitsView; - DivisorCycleCache divisorCyclesCache = DivisorCycleCache.Shared; - DivisorCycleCache.CycleBlock? cycleLease = useCycles ? divisorCyclesCache.Acquire(currentDivisor) : null; - ulong cycleLeaseEnd = cycleLease?.End ?? 0UL; - - while (currentDivisor <= allowedMax) - { - batchSize = 0; - reachedEndInBatch = false; - - while (batchSize < batchCapacity && currentDivisor <= allowedMax) - { - nextDivisor = currentDivisor + 2UL; - processedCount++; - - bool includeDivisor = true; - ulong exponent = prime; - - if (useCycles) - { - if (currentDivisor > cycleLeaseEnd) - { - cycleLease?.Dispose(); - cycleLease = divisorCyclesCache.Acquire(currentDivisor); - cycleLeaseEnd = cycleLease.End; - } - - ulong cycleLength = cycleLease!.GetCycle(currentDivisor); - if (cycleLength > 0UL) - { - ulong remainder = prime % cycleLength; - if (remainder != 0UL) - { - includeDivisor = false; - } - else - { - exponent = remainder; - } - } - } - - if (includeDivisor) - { - divisors[batchSize] = currentDivisor; - exponents[batchSize] = exponent; - batchSize++; - } - - lastProcessed = currentDivisor; - next = nextDivisor; - - if (next <= currentDivisor) - { - reachedEndInBatch = true; - break; - } - - if (next > allowedMax) - { - currentDivisor = next; - reachedEndInBatch = true; - break; - } - - currentDivisor = next; - } - - if (batchSize == 0) - { - if (reachedEndInBatch) - { - processedAll = true; - break; - } - - continue; - } - - divisorDataSpan = divisorData.AsSpan(0, batchSize); - divisorView = divisorsBuffer.View.SubView(0, batchSize); - exponentView = exponentBuffer.View.SubView(0, batchSize); - hitsView = hitsBuffer.View.SubView(0, batchSize); - - divisorSpan = divisors.AsSpan(0, batchSize); - exponentSpan = exponents.AsSpan(0, batchSize); - hitsSpan = hits.AsSpan(0, batchSize); - - for (i = 0; i < batchSize; i++) - { - divisorDataSpan[i] = MontgomeryDivisorDataCache.Get(divisorSpan[i]); - } - - divisorView.CopyFromCPU(ref MemoryMarshal.GetReference(divisorDataSpan), batchSize); - exponentView.CopyFromCPU(ref MemoryMarshal.GetReference(exponentSpan), batchSize); - kernel(batchSize, divisorView, exponentView, hitsView); - accelerator.Synchronize(); - hitsView.CopyToCPU(ref MemoryMarshal.GetReference(hitsSpan), batchSize); - - for (i = 0; i < batchSize; i++) - { - if (hitsSpan[i] != 0) - { - composite = true; - lastProcessed = divisorSpan[i]; - break; - } - } - - if (!composite) - { - lastProcessed = divisorSpan[batchSize - 1]; - } - if (composite) - { - break; - } - - if (reachedEndInBatch) - { - processedAll = true; - break; - } - } - - cycleLease?.Dispose(); - coveredRange = composite || processedAll || currentDivisor > allowedMax; - - return composite; - } - - private static ulong ComputeDivisorLimitFromMaxPrime(ulong maxPrime) - { - if (maxPrime <= 1UL) - { - return 0UL; - } - - if (maxPrime - 1UL >= 64UL) - { - return ulong.MaxValue; - } - - return (1UL << (int)(maxPrime - 1UL)) - 1UL; - } - - private static ulong ComputeAllowedMaxDivisor(ulong prime, ulong divisorLimit) - { - if (prime <= 1UL) - { - return 0UL; - } - - if (prime - 1UL >= 64UL) - { - return divisorLimit; - } - - return Math.Min((1UL << (int)(prime - 1UL)) - 1UL, divisorLimit); - } - - private static void CheckKernel(Index1D index, ArrayView divisors, ArrayView exponents, ArrayView hits) + public void ConfigureFromMaxPrime(ulong maxPrime) + { + lock (_sync) { - MontgomeryDivisorData divisor = divisors[index]; - ulong modulus = divisor.Modulus; - if (modulus <= 1UL || (modulus & 1UL) == 0UL) - { - hits[index] = 0; - return; - } - - ulong exponent = exponents[index]; - hits[index] = exponent.Pow2MontgomeryMod(divisor) == 1UL ? (byte)1 : (byte)0; + _divisorLimit = ComputeDivisorLimitFromMaxPrime(maxPrime); + _isConfigured = true; } + } - public sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession - { - private readonly MersenneNumberDivisorByDivisorGpuTester _owner; - private readonly GpuContextPool.GpuContextLease _lease; - private readonly Accelerator _accelerator; - private readonly Action, ArrayView> _kernel; - private MemoryBuffer1D _exponentsBuffer; - private MemoryBuffer1D _resultsBuffer; - private ulong[] _hostBuffer; - private int[] _positionBuffer; - private byte[] _modeBuffer; - private int _capacity; - private bool _disposed; + public bool IsPrime(ulong prime, out bool divisorsExhausted) + { + ulong allowedMax; + bool useCycles; + int batchCapacity; - internal DivisorScanSession(MersenneNumberDivisorByDivisorGpuTester owner) - { - _owner = owner; - _lease = GpuContextPool.RentPreferred(preferCpu: false); - _accelerator = _lease.Accelerator; - _kernel = owner.GetKernelByPrimeExponent(_accelerator); - _capacity = Math.Max(1, owner._gpuBatchSize); - _exponentsBuffer = _accelerator.Allocate1D(_capacity); - _resultsBuffer = _accelerator.Allocate1D(_capacity); - _hostBuffer = ArrayPool.Shared.Rent(_capacity); - _positionBuffer = ArrayPool.Shared.Rent(_capacity); - _modeBuffer = ArrayPool.Shared.Rent(_capacity); - } - - internal void Reset() - { - _disposed = false; - } - - public void CheckDivisor(ulong divisor, ulong divisorCycle, ReadOnlySpan primes, Span hits) - { - if (_disposed) - { - throw new ObjectDisposedException(nameof(DivisorScanSession)); - } - - MersenneNumberDivisorByDivisorGpuTester owner = _owner; - int gpuBatchSize = owner._gpuBatchSize; - int primesLength = primes.Length; - if (primesLength == 0) - { - return; - } - - MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(divisor); - var exponentStepper = new ExponentRemainderStepper(divisorData); - if (!exponentStepper.IsValidModulus) - { - hits.Clear(); - return; - } - - bool useDivisorCycles = owner._useDivisorCycles; - if (useDivisorCycles && divisorCycle == 0UL) - { - throw new InvalidOperationException($"Missing divisor cycle for divisor {divisor}."); - } - - CycleRemainderStepper cycleStepper = default; - ulong initialCycleRemainder = 0UL; - bool cycleRemainderPending = false; - if (useDivisorCycles) - { - cycleStepper = new CycleRemainderStepper(divisorCycle); - initialCycleRemainder = cycleStepper.Initialize(primes[0]); - cycleRemainderPending = true; - } - - Accelerator accelerator = _accelerator; - Action, ArrayView> kernel = _kernel; - ArrayView1D exponentsView = _exponentsBuffer.View; - ArrayView1D resultsView = _resultsBuffer.View; - - Span hostSpan = _hostBuffer.AsSpan(0, _capacity); - Span positionSpan = _positionBuffer.AsSpan(0, _capacity); - Span modeSpan = _modeBuffer.AsSpan(0, _capacity); - - int offset = 0; - - while (offset < primesLength) - { - int batchSize = Math.Min(gpuBatchSize, primesLength - offset); - ReadOnlySpan primesSlice = primes.Slice(offset, batchSize); - Span hitsSlice = hits.Slice(offset, batchSize); - hitsSlice.Clear(); - - int computeCount = 0; - bool localHasState = exponentStepper.HasState; - ulong localPreviousExponent = exponentStepper.PreviousExponent; - - for (int i = 0; i < batchSize; i++) - { - ulong primeValue = primesSlice[i]; - - if (useDivisorCycles) - { - ulong remainder; - if (cycleRemainderPending) - { - remainder = initialCycleRemainder; - cycleRemainderPending = false; - } - else - { - remainder = cycleStepper.ComputeNext(primeValue); - } - if (remainder != 0UL) - { - continue; - } - } - - ulong workValue; - byte mode; - - if (!localHasState || primeValue <= localPreviousExponent) - { - mode = 0; - workValue = primeValue; - localHasState = true; - } - else - { - mode = 1; - workValue = primeValue - localPreviousExponent; - } - - localPreviousExponent = primeValue; - - if (useDivisorCycles) - { - workValue %= divisorCycle; - } - - positionSpan[computeCount] = i; - hostSpan[computeCount] = workValue; - modeSpan[computeCount] = mode; - computeCount++; - } - - if (computeCount > 0) - { - Span exponentSlice = hostSpan[..computeCount]; - ArrayView1D exponentView = exponentsView.SubView(0, computeCount); - exponentView.CopyFromCPU(ref MemoryMarshal.GetReference(exponentSlice), computeCount); - - ArrayView1D resultView = resultsView.SubView(0, computeCount); - kernel(computeCount, divisorData, exponentView, resultView); - accelerator.Synchronize(); - resultView.CopyToCPU(ref MemoryMarshal.GetReference(exponentSlice), computeCount); - - for (int i = 0; i < computeCount; i++) - { - int position = positionSpan[i]; - ulong primeValue = primesSlice[position]; - ulong montgomeryResult = exponentSlice[i]; - bool isUnity; - - if (modeSpan[i] == 0) - { - exponentStepper.TryInitializeFromMontgomeryResult(primeValue, montgomeryResult, out isUnity); - } - else - { - exponentStepper.TryAdvanceWithMontgomeryDelta(primeValue, montgomeryResult, out isUnity); - } - - hitsSlice[position] = isUnity ? (byte)1 : (byte)0; - } - } - - offset += batchSize; - } - } - - public void Dispose() - { - if (_disposed) - { - return; - } - - _disposed = true; - _owner._sessionPool.Add(this); - } - } - - private static void ComputeMontgomeryExponentKernel(Index1D index, MontgomeryDivisorData divisor, ArrayView exponents, ArrayView results) + lock (_sync) { - ulong modulus = divisor.Modulus; - if (modulus <= 1UL || (modulus & 1UL) == 0UL) - { - results[index] = 0UL; - return; - } + if (!_isConfigured) + { + throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester."); + } - ulong exponent = exponents[index]; - - results[index] = exponent.Pow2MontgomeryModMontgomery(divisor); + allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit); + useCycles = _useDivisorCycles; + batchCapacity = _gpuBatchSize; } - public ulong DivisorLimit - { - get - { - lock (_sync) - { - if (!_isConfigured) - { - throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester."); - } - - return _divisorLimit; - } - } - } - - private BatchResources RentBatchResources(Accelerator accelerator, int capacity) - { - var bag = _resourcePools.GetOrAdd(accelerator, static _ => []); - if (bag.TryTake(out BatchResources? resources)) - { - return resources; - } - - return new BatchResources(accelerator, capacity); - } - - private void ReturnBatchResources(Accelerator accelerator, BatchResources resources) => _resourcePools.GetOrAdd(accelerator, static _ => []).Add(resources); - - private sealed class BatchResources : IDisposable - { - internal BatchResources(Accelerator accelerator, int capacity) - { - DivisorsBuffer = accelerator.Allocate1D(capacity); - ExponentBuffer = accelerator.Allocate1D(capacity); - HitsBuffer = accelerator.Allocate1D(capacity); - Divisors = ArrayPool.Shared.Rent(capacity); - Exponents = ArrayPool.Shared.Rent(capacity); - Hits = ArrayPool.Shared.Rent(capacity); - DivisorData = ArrayPool.Shared.Rent(capacity); - Capacity = capacity; - } - - internal readonly MemoryBuffer1D DivisorsBuffer; - - internal readonly MemoryBuffer1D ExponentBuffer; - - internal readonly MemoryBuffer1D HitsBuffer; - - internal readonly ulong[] Divisors; - - internal readonly ulong[] Exponents; - - internal readonly byte[] Hits; - - internal readonly MontgomeryDivisorData[] DivisorData; - - internal readonly int Capacity; - - public void Dispose() - { - DivisorsBuffer.Dispose(); - ExponentBuffer.Dispose(); - HitsBuffer.Dispose(); - ArrayPool.Shared.Return(Divisors, clearArray: false); - ArrayPool.Shared.Return(Exponents, clearArray: false); - ArrayPool.Shared.Return(Hits, clearArray: false); - ArrayPool.Shared.Return(DivisorData, clearArray: false); - } + if (allowedMax < 3UL) + { + divisorsExhausted = true; + return true; } - private sealed class AcceleratorReferenceComparer : IEqualityComparer - { - public static AcceleratorReferenceComparer Instance { get; } = new(); + bool composite; + bool coveredRange; + ulong processedCount; + ulong lastProcessed; - public bool Equals(Accelerator? x, Accelerator? y) => ReferenceEquals(x, y); + var gpuLease = GpuContextPool.RentPreferred(preferCpu: false); + var accelerator = gpuLease.Accelerator; + var kernel = GetKernel(accelerator); + BatchResources resources = RentBatchResources(accelerator, batchCapacity); - public int GetHashCode(Accelerator obj) => RuntimeHelpers.GetHashCode(obj); - } + try + { + composite = CheckDivisors( + prime, + allowedMax, + useCycles, + accelerator, + kernel, + resources.DivisorsBuffer, + resources.ExponentBuffer, + resources.HitsBuffer, + resources.Divisors, + resources.Exponents, + resources.Hits, + resources.DivisorData, + out lastProcessed, + out coveredRange, + out processedCount + ); + } + finally + { + ReturnBatchResources(accelerator, resources); + gpuLease.Dispose(); + } - public ulong GetAllowedMaxDivisor(ulong prime) - { - lock (_sync) - { - if (!_isConfigured) - { - throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester."); - } + if (composite) + { + divisorsExhausted = true; + return false; + } - return ComputeAllowedMaxDivisor(prime, _divisorLimit); - } - } + divisorsExhausted = coveredRange; + return true; + } - public DivisorScanSession CreateDivisorSession() - { - lock (_sync) - { - if (!_isConfigured) - { - throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester."); - } + private static bool CheckDivisors( + ulong prime, + ulong allowedMax, + bool useCycles, + Accelerator accelerator, + Action, ArrayView, ArrayView> kernel, + MemoryBuffer1D divisorsBuffer, + MemoryBuffer1D exponentBuffer, + MemoryBuffer1D hitsBuffer, + in ulong[] divisors, + ulong[] exponents, + byte[] hits, + MontgomeryDivisorData[] divisorData, + out ulong lastProcessed, + out bool coveredRange, + out ulong processedCount) + { + lastProcessed = 0UL; + processedCount = 0UL; - if (_sessionPool.TryTake(out DivisorScanSession? session)) - { - session.Reset(); - return session; - } + if (allowedMax < 3UL) + { + coveredRange = true; + return false; + } - return new DivisorScanSession(this); - } - } + return useCycles + ? CheckDivisorsWithCycles( + prime, + allowedMax, + accelerator, + kernel, + divisorsBuffer, + exponentBuffer, + hitsBuffer, + divisors, + exponents, + hits, + divisorData, + ref lastProcessed, + ref processedCount, + out coveredRange) + : CheckDivisorsWithoutCycles( + prime, + allowedMax, + accelerator, + kernel, + divisorsBuffer, + exponentBuffer, + hitsBuffer, + divisors, + exponents, + hits, + divisorData, + ref lastProcessed, + ref processedCount, + out coveredRange); + } - IMersenneNumberDivisorByDivisorTester.IDivisorScanSession IMersenneNumberDivisorByDivisorTester.CreateDivisorSession() => CreateDivisorSession(); + private static bool CheckDivisorsWithoutCycles( + ulong prime, + ulong allowedMax, + Accelerator accelerator, + Action, ArrayView, ArrayView> kernel, + MemoryBuffer1D divisorsBuffer, + MemoryBuffer1D exponentBuffer, + MemoryBuffer1D hitsBuffer, + in ulong[] divisors, + ulong[] exponents, + byte[] hits, + MontgomeryDivisorData[] divisorData, + ref ulong lastProcessed, + ref ulong processedCount, + out bool coveredRange) + { + int batchCapacity = (int)divisorsBuffer.Length; + bool composite = false; + bool processedAll = false; + ulong currentDivisor = 3UL; + + Span divisorDataSpan; + Span divisorSpan; + Span exponentSpan; + Span hitsSpan; + ArrayView1D divisorView; + ArrayView1D exponentView; + ArrayView1D hitsView; + + while (currentDivisor <= allowedMax) + { + int batchSize = 0; + bool reachedEndInBatch = false; + + // The GPU buffers are sized exactly to the configured batch capacity, so we + // must stop once the current batch is full to avoid writing past the spans. + while (batchSize < batchCapacity && currentDivisor <= allowedMax) + { + ulong nextDivisor = currentDivisor + 2UL; + processedCount++; + + divisors[batchSize] = currentDivisor; + exponents[batchSize] = prime; + batchSize++; + lastProcessed = currentDivisor; + + if (nextDivisor <= currentDivisor) + { + reachedEndInBatch = true; + break; + } + + if (nextDivisor > allowedMax) + { + currentDivisor = nextDivisor; + reachedEndInBatch = true; + break; + } + + currentDivisor = nextDivisor; + } + + if (batchSize == 0) + { + if (reachedEndInBatch) + { + processedAll = true; + break; + } + + continue; + } + + divisorDataSpan = divisorData.AsSpan(0, batchSize); + divisorSpan = divisors.AsSpan(0, batchSize); + exponentSpan = exponents.AsSpan(0, batchSize); + hitsSpan = hits.AsSpan(0, batchSize); + divisorView = divisorsBuffer.View.SubView(0, batchSize); + exponentView = exponentBuffer.View.SubView(0, batchSize); + hitsView = hitsBuffer.View.SubView(0, batchSize); + + for (int i = 0; i < batchSize; i++) + { + divisorDataSpan[i] = MontgomeryDivisorDataCache.Get(divisorSpan[i]); + } + + divisorView.CopyFromCPU(ref MemoryMarshal.GetReference(divisorDataSpan), batchSize); + exponentView.CopyFromCPU(ref MemoryMarshal.GetReference(exponentSpan), batchSize); + kernel(batchSize, divisorView, exponentView, hitsView); + hitsView.CopyToCPU(ref MemoryMarshal.GetReference(hitsSpan), batchSize); + + for (int i = 0; i < batchSize; i++) + { + if (hitsSpan[i] != 0) + { + composite = true; + lastProcessed = divisorSpan[i]; + break; + } + } + + if (!composite) + { + lastProcessed = divisorSpan[batchSize - 1]; + } + + if (composite) + { + break; + } + + if (reachedEndInBatch) + { + processedAll = true; + break; + } + } + + coveredRange = composite || processedAll || currentDivisor > allowedMax; + return composite; + } + + private static bool CheckDivisorsWithCycles( + ulong prime, + ulong allowedMax, + Accelerator accelerator, + Action, ArrayView, ArrayView> kernel, + MemoryBuffer1D divisorsBuffer, + MemoryBuffer1D exponentBuffer, + MemoryBuffer1D hitsBuffer, + in ulong[] divisors, + ulong[] exponents, + byte[] hits, + MontgomeryDivisorData[] divisorData, + ref ulong lastProcessed, + ref ulong processedCount, + out bool coveredRange) + { + int batchCapacity = (int)divisorsBuffer.Length; + bool composite = false; + bool processedAll = false; + ulong currentDivisor = 3UL; + ulong cycleLeaseEnd; + + Span divisorDataSpan; + Span divisorSpan; + Span exponentSpan; + Span hitsSpan; + ArrayView1D divisorView; + ArrayView1D exponentView; + ArrayView1D hitsView; + + DivisorCycleCache divisorCyclesCache = DivisorCycleCache.Shared; + DivisorCycleCache.CycleBlock cycleLease = divisorCyclesCache.Acquire(currentDivisor); + cycleLeaseEnd = cycleLease.End; + + try + { + while (currentDivisor <= allowedMax) + { + int batchSize = 0; + bool reachedEndInBatch = false; + + // Batch writes share the same buffers as the kernel launch, so we guard + // against exceeding the rented capacity on each iteration. + while (batchSize < batchCapacity && currentDivisor <= allowedMax) + { + ulong nextDivisor = currentDivisor + 2UL; + processedCount++; + + if (currentDivisor > cycleLeaseEnd) + { + cycleLease.Dispose(); + cycleLease = divisorCyclesCache.Acquire(currentDivisor); + cycleLeaseEnd = cycleLease.End; + } + + bool includeDivisor = true; + ulong exponent = prime; + ulong cycleLength = cycleLease.GetCycle(currentDivisor); + if (cycleLength > 0UL) + { + ulong remainder = prime; + if (remainder >= cycleLength) + { + remainder %= cycleLength; + } + + if (remainder != 0UL) + { + includeDivisor = false; + } + else + { + exponent = 0UL; + } + } + + if (includeDivisor) + { + divisors[batchSize] = currentDivisor; + exponents[batchSize] = exponent; + batchSize++; + } + + lastProcessed = currentDivisor; + + if (nextDivisor <= currentDivisor) + { + reachedEndInBatch = true; + break; + } + + if (nextDivisor > allowedMax) + { + currentDivisor = nextDivisor; + reachedEndInBatch = true; + break; + } + + currentDivisor = nextDivisor; + } + + if (batchSize == 0) + { + if (reachedEndInBatch) + { + processedAll = true; + break; + } + + continue; + } + + divisorDataSpan = divisorData.AsSpan(0, batchSize); + divisorSpan = divisors.AsSpan(0, batchSize); + exponentSpan = exponents.AsSpan(0, batchSize); + hitsSpan = hits.AsSpan(0, batchSize); + divisorView = divisorsBuffer.View.SubView(0, batchSize); + exponentView = exponentBuffer.View.SubView(0, batchSize); + hitsView = hitsBuffer.View.SubView(0, batchSize); + + for (int i = 0; i < batchSize; i++) + { + divisorDataSpan[i] = MontgomeryDivisorDataCache.Get(divisorSpan[i]); + } + + divisorView.CopyFromCPU(ref MemoryMarshal.GetReference(divisorDataSpan), batchSize); + exponentView.CopyFromCPU(ref MemoryMarshal.GetReference(exponentSpan), batchSize); + kernel(batchSize, divisorView, exponentView, hitsView); + hitsView.CopyToCPU(ref MemoryMarshal.GetReference(hitsSpan), batchSize); + + for (int i = 0; i < batchSize; i++) + { + if (hitsSpan[i] != 0) + { + composite = true; + lastProcessed = divisorSpan[i]; + break; + } + } + + if (!composite) + { + lastProcessed = divisorSpan[batchSize - 1]; + } + + if (composite) + { + break; + } + + if (reachedEndInBatch) + { + processedAll = true; + break; + } + } + } + finally + { + cycleLease.Dispose(); + } + + coveredRange = composite || processedAll || currentDivisor > allowedMax; + return composite; + } + + private static ulong ComputeDivisorLimitFromMaxPrime(ulong maxPrime) + { + if (maxPrime <= 1UL) + { + return 0UL; + } + + if (maxPrime - 1UL >= 64UL) + { + return ulong.MaxValue; + } + + return (1UL << (int)(maxPrime - 1UL)) - 1UL; + } + + private static ulong ComputeAllowedMaxDivisor(ulong prime, ulong divisorLimit) + { + if (prime <= 1UL) + { + return 0UL; + } + + if (prime - 1UL >= 64UL) + { + return divisorLimit; + } + + return Math.Min((1UL << (int)(prime - 1UL)) - 1UL, divisorLimit); + } + + private static void CheckKernel(Index1D index, ArrayView divisors, ArrayView exponents, ArrayView hits) + { + MontgomeryDivisorData divisor = divisors[index]; + ulong modulus = divisor.Modulus; + if (modulus <= 1UL || (modulus & 1UL) == 0UL) + { + hits[index] = 0; + return; + } + + ulong exponent = exponents[index]; + hits[index] = exponent.Pow2MontgomeryMod(divisor) == 1UL ? (byte)1 : (byte)0; + } + + public sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession + { + private readonly MersenneNumberDivisorByDivisorGpuTester _owner; + private readonly GpuContextPool.GpuContextLease _lease; + private readonly Accelerator _accelerator; + private readonly Action, ArrayView> _kernel; + private MemoryBuffer1D _exponentsBuffer; + private MemoryBuffer1D _resultsBuffer; + private ulong[] _hostBuffer; + private int[]? _positionBuffer; + private int _capacity; + private bool _disposed; + + internal DivisorScanSession(MersenneNumberDivisorByDivisorGpuTester owner) + { + _owner = owner; + _lease = GpuContextPool.RentPreferred(preferCpu: false); + _accelerator = _lease.Accelerator; + _kernel = owner.GetKernelByPrimeExponent(_accelerator); + _capacity = Math.Max(1, owner._gpuBatchSize); + _exponentsBuffer = _accelerator.Allocate1D(_capacity); + _resultsBuffer = _accelerator.Allocate1D(_capacity); + _hostBuffer = ArrayPool.Shared.Rent(_capacity); + _positionBuffer = null; + } + + internal void Reset() + { + _disposed = false; + } + + public void CheckDivisor(ulong divisor, ulong divisorCycle, ReadOnlySpan primes, Span hits) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(DivisorScanSession)); + } + + MersenneNumberDivisorByDivisorGpuTester owner = _owner; + int primesLength = primes.Length; + if (primesLength == 0) + { + return; + } + + // The production pipeline never adjusts the GPU batch size after a session + // is initialized, so the buffers allocated in the constructor remain + // large enough for every launch. + + MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(divisor); + var exponentStepper = new ExponentRemainderStepper(divisorData); + if (!exponentStepper.IsValidModulus) + { + hits.Clear(); + return; + } + + bool useDivisorCycles = owner._useDivisorCycles; + if (!useDivisorCycles) + { + ProcessWithoutCycle(ref exponentStepper, divisorData, primes, hits); + return; + } + + if (divisorCycle == 0UL) + { + throw new InvalidOperationException($"Missing divisor cycle for divisor {divisor}."); + } + + var cycleStepper = new CycleRemainderStepper(divisorCycle); + ulong initialCycleRemainder = cycleStepper.Initialize(primes[0]); + ProcessWithCycle(ref exponentStepper, ref cycleStepper, initialCycleRemainder, divisorCycle, divisorData, primes, hits); + } + + private void ProcessWithoutCycle( + ref ExponentRemainderStepper exponentStepper, + in MontgomeryDivisorData divisorData, + ReadOnlySpan primes, + Span hits) + { + Action, ArrayView> kernel = _kernel; + ArrayView1D exponentsView = _exponentsBuffer.View; + ArrayView1D resultsView = _resultsBuffer.View; + Span hostSpan = _hostBuffer.AsSpan(0, _capacity); + + int primesLength = primes.Length; + int gpuBatchSize = _owner._gpuBatchSize; + int offset = 0; + + while (offset < primesLength) + { + int batchSize = Math.Min(gpuBatchSize, primesLength - offset); + ReadOnlySpan primesSlice = primes.Slice(offset, batchSize); + Span hitsSlice = hits.Slice(offset, batchSize); + Span exponentSlice = hostSpan[..batchSize]; + + bool hasState = exponentStepper.HasState; + ulong previousExponent = exponentStepper.PreviousExponent; + + int startIndex = 0; + if (!hasState) + { + ulong primeValue = primesSlice[0]; + exponentSlice[0] = primeValue; + previousExponent = primeValue; + hasState = true; + startIndex = 1; + } + + for (int i = startIndex; i < batchSize; i++) + { + ulong primeValue = primesSlice[i]; + exponentSlice[i] = primeValue - previousExponent; + previousExponent = primeValue; + } + + ArrayView1D exponentView = exponentsView.SubView(0, batchSize); + exponentView.CopyFromCPU(ref MemoryMarshal.GetReference(exponentSlice), batchSize); + + ArrayView1D resultView = resultsView.SubView(0, batchSize); + kernel(batchSize, divisorData, exponentView, resultView); + resultView.CopyToCPU(ref MemoryMarshal.GetReference(exponentSlice), batchSize); + + bool requiresInitialization = !exponentStepper.HasState; + for (int i = 0; i < batchSize; i++) + { + ulong primeValue = primesSlice[i]; + ulong montgomeryResult = exponentSlice[i]; + bool isUnity; + + if (requiresInitialization) + { + exponentStepper.TryInitializeFromMontgomeryResult(primeValue, montgomeryResult, out isUnity); + requiresInitialization = false; + } + else + { + exponentStepper.TryAdvanceWithMontgomeryDelta(primeValue, montgomeryResult, out isUnity); + } + + hitsSlice[i] = isUnity ? (byte)1 : (byte)0; + } + + offset += batchSize; + } + } + + private void ProcessWithCycle( + ref ExponentRemainderStepper exponentStepper, + ref CycleRemainderStepper cycleStepper, + ulong initialCycleRemainder, + ulong divisorCycle, + in MontgomeryDivisorData divisorData, + ReadOnlySpan primes, + Span hits) + { + Action, ArrayView> kernel = _kernel; + ArrayView1D exponentsView = _exponentsBuffer.View; + ArrayView1D resultsView = _resultsBuffer.View; + int capacity = _capacity; + Span hostSpan = _hostBuffer.AsSpan(0, capacity); + int[]? positionBuffer = _positionBuffer; + if (positionBuffer is null) + { + // Session construction already enforces the batch capacity, so we only + // need to rent the position buffer on the first cycle-enabled invocation. + positionBuffer = ArrayPool.Shared.Rent(capacity); + _positionBuffer = positionBuffer; + } + + Span positionSpan = positionBuffer.AsSpan(0, capacity); + + int gpuBatchSize = _owner._gpuBatchSize; + int primesLength = primes.Length; + int offset = 0; + bool cycleRemainderPending = true; + + while (offset < primesLength) + { + int batchSize = Math.Min(gpuBatchSize, primesLength - offset); + ReadOnlySpan primesSlice = primes.Slice(offset, batchSize); + Span hitsSlice = hits.Slice(offset, batchSize); + + int computeCount = 0; + bool hasState = exponentStepper.HasState; + ulong previousExponent = exponentStepper.PreviousExponent; + bool useInitialRemainder = cycleRemainderPending; + + for (int i = 0; i < batchSize; i++) + { + hitsSlice[i] = 0; + ulong primeValue = primesSlice[i]; + + ulong remainder = useInitialRemainder + ? initialCycleRemainder + : cycleStepper.ComputeNext(primeValue); + useInitialRemainder = false; + + if (remainder != 0UL) + { + continue; + } + + ulong workValue; + if (!hasState) + { + workValue = primeValue; + hasState = true; + } + else + { + workValue = primeValue - previousExponent; + } + + previousExponent = primeValue; + + if (workValue >= divisorCycle) + { + workValue %= divisorCycle; + } + + hostSpan[computeCount] = workValue; + positionSpan[computeCount] = i; + computeCount++; + } + + cycleRemainderPending = useInitialRemainder; + + if (computeCount > 0) + { + Span exponentSlice = hostSpan[..computeCount]; + ArrayView1D exponentView = exponentsView.SubView(0, computeCount); + exponentView.CopyFromCPU(ref MemoryMarshal.GetReference(exponentSlice), computeCount); + + ArrayView1D resultView = resultsView.SubView(0, computeCount); + kernel(computeCount, divisorData, exponentView, resultView); + resultView.CopyToCPU(ref MemoryMarshal.GetReference(exponentSlice), computeCount); + + bool requiresInitialization = !exponentStepper.HasState; + for (int i = 0; i < computeCount; i++) + { + int position = positionSpan[i]; + ulong primeValue = primesSlice[position]; + ulong montgomeryResult = exponentSlice[i]; + bool isUnity; + + if (requiresInitialization) + { + exponentStepper.TryInitializeFromMontgomeryResult(primeValue, montgomeryResult, out isUnity); + requiresInitialization = false; + } + else + { + exponentStepper.TryAdvanceWithMontgomeryDelta(primeValue, montgomeryResult, out isUnity); + } + + hitsSlice[position] = isUnity ? (byte)1 : (byte)0; + } + } + + offset += batchSize; + } + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _owner._sessionPool.Add(this); + } + } + + private static void ComputeMontgomeryExponentKernel(Index1D index, MontgomeryDivisorData divisor, ArrayView exponents, ArrayView results) + { + ulong modulus = divisor.Modulus; + if (modulus <= 1UL || (modulus & 1UL) == 0UL) + { + results[index] = 0UL; + return; + } + + ulong exponent = exponents[index]; + + results[index] = exponent.Pow2MontgomeryModMontgomery(divisor); + } + + public ulong DivisorLimit + { + get + { + lock (_sync) + { + if (!_isConfigured) + { + throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester."); + } + + return _divisorLimit; + } + } + } + + private BatchResources RentBatchResources(Accelerator accelerator, int capacity) + { + var bag = _resourcePools.GetOrAdd(accelerator, static _ => []); + if (bag.TryTake(out BatchResources? resources)) + { + return resources; + } + + return new BatchResources(accelerator, capacity); + } + + private void ReturnBatchResources(Accelerator accelerator, BatchResources resources) => _resourcePools.GetOrAdd(accelerator, static _ => []).Add(resources); + + private sealed class BatchResources : IDisposable + { + internal BatchResources(Accelerator accelerator, int capacity) + { + DivisorsBuffer = accelerator.Allocate1D(capacity); + ExponentBuffer = accelerator.Allocate1D(capacity); + HitsBuffer = accelerator.Allocate1D(capacity); + Divisors = ArrayPool.Shared.Rent(capacity); + Exponents = ArrayPool.Shared.Rent(capacity); + Hits = ArrayPool.Shared.Rent(capacity); + DivisorData = ArrayPool.Shared.Rent(capacity); + Capacity = capacity; + } + + internal readonly MemoryBuffer1D DivisorsBuffer; + + internal readonly MemoryBuffer1D ExponentBuffer; + + internal readonly MemoryBuffer1D HitsBuffer; + + internal readonly ulong[] Divisors; + + internal readonly ulong[] Exponents; + + internal readonly byte[] Hits; + + internal readonly MontgomeryDivisorData[] DivisorData; + + internal readonly int Capacity; + + public void Dispose() + { + DivisorsBuffer.Dispose(); + ExponentBuffer.Dispose(); + HitsBuffer.Dispose(); + ArrayPool.Shared.Return(Divisors, clearArray: false); + ArrayPool.Shared.Return(Exponents, clearArray: false); + ArrayPool.Shared.Return(Hits, clearArray: false); + ArrayPool.Shared.Return(DivisorData, clearArray: false); + } + } + + private sealed class AcceleratorReferenceComparer : IEqualityComparer + { + public static AcceleratorReferenceComparer Instance { get; } = new(); + + public bool Equals(Accelerator? x, Accelerator? y) => ReferenceEquals(x, y); + + public int GetHashCode(Accelerator obj) => RuntimeHelpers.GetHashCode(obj); + } + + public ulong GetAllowedMaxDivisor(ulong prime) + { + lock (_sync) + { + if (!_isConfigured) + { + throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester."); + } + + return ComputeAllowedMaxDivisor(prime, _divisorLimit); + } + } + + public DivisorScanSession CreateDivisorSession() + { + lock (_sync) + { + if (!_isConfigured) + { + throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester."); + } + + if (_sessionPool.TryTake(out DivisorScanSession? session)) + { + session.Reset(); + return session; + } + + return new DivisorScanSession(this); + } + } + + IMersenneNumberDivisorByDivisorTester.IDivisorScanSession IMersenneNumberDivisorByDivisorTester.CreateDivisorSession() => CreateDivisorSession(); }