Divisor-Cycle Cache Rework

This commit is contained in:
Marek Lemańczyk committed 2025-10-02 19:53:09 +02:00
1 parent f9e548279c
commit ebe48d5f2f
11 files changed
+239 -1063

No files matched your search

@@ -1,6 +1,7 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks; namespace EvenPerfectBitScanner.Benchmarks;
+5 -35
View File
@@ -33,7 +33,6 @@ internal static class Program
private static bool _useDivisor; private static bool _useDivisor;
private static bool _useResidueMode; private static bool _useResidueMode;
private static bool _useByDivisorMode; private static bool _useByDivisorMode;
private static UInt128 _divisor;
private static MersenneNumberDivisorGpuTester? _divisorTester; private static MersenneNumberDivisorGpuTester? _divisorTester;
private static IMersenneNumberDivisorByDivisorTester? _byDivisorTester; private static IMersenneNumberDivisorByDivisorTester? _byDivisorTester;
private static Dictionary<ulong, (bool DetailedCheck, bool PassedAllTests)>? _byDivisorPreviousResults; private static Dictionary<ulong, (bool DetailedCheck, bool PassedAllTests)>? _byDivisorPreviousResults;
@@ -74,9 +73,7 @@ internal static class Program
bool useLucas = false; bool useLucas = false;
bool useResidue = false; // M_p test via residue divisors (replaces LL/incremental) bool useResidue = false; // M_p test via residue divisors (replaces LL/incremental)
bool useDivisor = false; // M_p divisibility by specific divisor bool useDivisor = false; // M_p divisibility by specific divisor
bool useByDivisor = false; // Iterative divisor scan across primes bool useByDivisor = false; // Iterative divisor scan across primes UInt128 divisor = UInt128.Zero;
bool useDivisorCycles = false; // TODO: Default this flag to true and remove the opt-out once divisor cycle lookups are mandatory per the optimized by-divisor benchmarks.
UInt128 divisor = UInt128.Zero;
// Device routing // Device routing
bool useGpuCycles = true; bool useGpuCycles = true;
bool mersenneOnGpu = true; // controls Lucas/incremental/pow2mod device bool mersenneOnGpu = true; // controls Lucas/incremental/pow2mod device
@@ -93,6 +90,7 @@ internal static class Program
ReadOnlySpan<char> mersenneOption = default; ReadOnlySpan<char> mersenneOption = default;
ulong parsedLimit = 0UL; ulong parsedLimit = 0UL;
ulong parsedResidueMax = 0UL; ulong parsedResidueMax = 0UL;
ulong remainder = 0UL;
bool startPrimeProvided = false; bool startPrimeProvided = false;
// NTT backend selection (GPU): reference vs staged // NTT backend selection (GPU): reference vs staged
@@ -158,19 +156,6 @@ internal static class Program
{ {
kernelType = GpuKernelType.Incremental; kernelType = GpuKernelType.Incremental;
} }
}
else if (arg.StartsWith("--divisor=", StringComparison.OrdinalIgnoreCase))
{
// TODO: Replace UInt128.Parse with the benchmarked fast-path parser once we expose the
// TryParse-style span helper for UInt128 command-line arguments.
divisor = UInt128.Parse(arg.AsSpan(arg.IndexOf('=') + 1));
}
else if (arg.StartsWith("--use-divisor-cycles=", StringComparison.OrdinalIgnoreCase))
{
if (bool.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out bool parsedBool))
{
useDivisorCycles = parsedBool;
}
} }
else if (arg.StartsWith("--divisor-cycles-limit=", StringComparison.OrdinalIgnoreCase)) else if (arg.StartsWith("--divisor-cycles-limit=", StringComparison.OrdinalIgnoreCase))
{ {
@@ -460,11 +445,11 @@ internal static class Program
} }
DivisorCycleCache.SetDivisorCyclesBatchSize(cyclesBatchSize); DivisorCycleCache.SetDivisorCyclesBatchSize(cyclesBatchSize);
DivisorCycleCache.Shared.ConfigureGeneratorDevice(orderOnGpu); DivisorCycleCache.Shared.ConfigureGeneratorDevice(useGpuCycles);
// TODO: Keep a single cached block loaded from disk and honor the configured device when // TODO: Keep a single cached block loaded from disk and honor the configured device when
// computing ad-hoc cycles for divisors that fall outside that snapshot instead of queuing // computing ad-hoc cycles for divisors that fall outside that snapshot instead of queuing
// generation of additional blocks. // generation of additional blocks.
DivisorCycleCache.Shared.ReloadFromCurrentSnapshot(); DivisorCycleCache.Shared.RefreshSnapshot();
// TODO: Stop reloading the full snapshot once the ad-hoc path streams results straight from // TODO: Stop reloading the full snapshot once the ad-hoc path streams results straight from
// the configured device without persisting them, so memory stays bounded while preserving // the configured device without persisting them, so memory stays bounded while preserving
// the single cached block strategy. // the single cached block strategy.
@@ -480,7 +465,6 @@ internal static class Program
_useDivisor = useDivisor; _useDivisor = useDivisor;
_useResidueMode = useResidue; _useResidueMode = useResidue;
_useByDivisorMode = useByDivisor; _useByDivisorMode = useByDivisor;
_divisor = divisor;
if (useBitTransform) if (useBitTransform)
{ {
_transformP = &TransformPBit; _transformP = &TransformPBit;
@@ -522,11 +506,8 @@ internal static class Program
}, trackAllValues: true); }, trackAllValues: true);
} }
else if (useDivisor) else if (useDivisor)
{
if (divisor == UInt128.Zero)
{ {
MersenneNumberDivisorGpuTester.BuildDivisorCandidates(); MersenneNumberDivisorGpuTester.BuildDivisorCandidates();
}
_divisorTester = new MersenneNumberDivisorGpuTester(); _divisorTester = new MersenneNumberDivisorGpuTester();
} }
@@ -536,7 +517,6 @@ internal static class Program
? new MersenneNumberDivisorByDivisorGpuTester() ? new MersenneNumberDivisorByDivisorGpuTester()
: new MersenneNumberDivisorByDivisorCpuTester(); : new MersenneNumberDivisorByDivisorCpuTester();
_byDivisorTester.BatchSize = scanBatchSize; _byDivisorTester.BatchSize = scanBatchSize;
_byDivisorTester.UseDivisorCycles = useDivisorCycles; // TODO: Collapse this assignment once the runtime enforces divisor-cycle acceleration for every by-divisor scan path.
} }
// Load RLE blacklist (optional) // Load RLE blacklist (optional)
@@ -921,7 +901,6 @@ internal static class Program
Console.WriteLine(" --threads=<value> number of worker threads"); Console.WriteLine(" --threads=<value> number of worker threads");
Console.WriteLine(" --block-size=<value> values processed per thread batch"); Console.WriteLine(" --block-size=<value> values processed per thread batch");
Console.WriteLine(" --mersenne=pow2mod|incremental|lucas|residue|divisor|bydivisor Mersenne test method"); Console.WriteLine(" --mersenne=pow2mod|incremental|lucas|residue|divisor|bydivisor Mersenne test method");
Console.WriteLine(" --divisor=<value> optional divisor for --mersenne=divisor mode");
Console.WriteLine(" --residue-max-k=<value> max k for residue Mersenne test (q = 2*p*k + 1)"); Console.WriteLine(" --residue-max-k=<value> max k for residue Mersenne test (q = 2*p*k + 1)");
Console.WriteLine(" --mersenne-device=cpu|gpu Device for Mersenne method (default gpu)"); Console.WriteLine(" --mersenne-device=cpu|gpu Device for Mersenne method (default gpu)");
Console.WriteLine(" --primes-device=cpu|gpu Device for prime-scan kernels (default gpu)"); Console.WriteLine(" --primes-device=cpu|gpu Device for prime-scan kernels (default gpu)");
@@ -945,8 +924,6 @@ internal static class Program
Console.WriteLine(" --divisor-cycles-batch=<value> batch size for cycles generation (default 512)"); Console.WriteLine(" --divisor-cycles-batch=<value> batch size for cycles generation (default 512)");
Console.WriteLine(" --divisor-cycles-continue continue divisor cycles generation"); Console.WriteLine(" --divisor-cycles-continue continue divisor cycles generation");
Console.WriteLine(" --divisor-cycles-limit=<value> cycle search iterations when --mersenne=divisor"); Console.WriteLine(" --divisor-cycles-limit=<value> cycle search iterations when --mersenne=divisor");
Console.WriteLine(" --use-divisor-cycles=true|false enable divisor cycle filtering in --mersenne=bydivisor (default false)");
Console.WriteLine(" --use-order test primality via q order"); Console.WriteLine(" --use-order test primality via q order");
Console.WriteLine(" --workaround-mod avoid '%' operator on the GPU"); Console.WriteLine(" --workaround-mod avoid '%' operator on the GPU");
// mod-automaton removed // mod-automaton removed
@@ -1389,12 +1366,6 @@ internal static class Program
return _byDivisorTester!.IsPrime(p, out detailedCheck); return _byDivisorTester!.IsPrime(p, out detailedCheck);
} }
if (_useDivisor)
{
// TODO: Adopt the windowed Pow2Minus1Mod helper here so divisor checks benefit from the ProcessEightBitWindows speedups.
return _divisorTester!.IsPrime(p, _divisor, divisorCyclesSearchLimit, out detailedCheck);
}
detailedCheck = MersenneTesters.Value!.IsMersennePrime(p); detailedCheck = MersenneTesters.Value!.IsMersennePrime(p);
return detailedCheck; return detailedCheck;
} }
@@ -1632,6 +1603,5 @@ internal static class Program
return a << shift; return a << shift;
} }
} }
@@ -1,22 +0,0 @@
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
[Trait("Category", "Fast")]
public class EulerPrimeTesterTests
{
private readonly EulerPrimeTester _tester = new(new PrimeTester());
[Theory]
[InlineData(13UL, true)]
[InlineData(29UL, true)]
[InlineData(3UL, false)]
[InlineData(21UL, false)]
[InlineData(1UL, false)]
public void IsEulerPrime_returns_expected(ulong n, bool expected)
{
_tester.IsEulerPrime(n).Should().Be(expected);
}
}
@@ -110,7 +110,6 @@ public class MersenneNumberDivisorGpuTesterTests
{ {
var tester = new MersenneNumberDivisorByDivisorGpuTester var tester = new MersenneNumberDivisorByDivisorGpuTester
{ {
UseDivisorCycles = true,
GpuBatchSize = 8, GpuBatchSize = 8,
}; };
@@ -147,7 +146,6 @@ public class MersenneNumberDivisorGpuTesterTests
{ {
var tester = new MersenneNumberDivisorByDivisorGpuTester var tester = new MersenneNumberDivisorByDivisorGpuTester
{ {
UseDivisorCycles = true,
GpuBatchSize = 2, GpuBatchSize = 2,
}; };
@@ -169,7 +167,6 @@ public class MersenneNumberDivisorGpuTesterTests
{ {
var tester = new MersenneNumberDivisorByDivisorCpuTester var tester = new MersenneNumberDivisorByDivisorCpuTester
{ {
UseDivisorCycles = true,
BatchSize = 2, BatchSize = 2,
}; };
@@ -12,14 +12,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
private ulong _divisorLimit; private ulong _divisorLimit;
private ulong _lastStatusDivisor; private ulong _lastStatusDivisor;
private bool _isConfigured; private bool _isConfigured;
private bool _useDivisorCycles;
private int _batchSize = 1_024; private int _batchSize = 1_024;
public bool UseDivisorCycles
{
get => _useDivisorCycles;
set => _useDivisorCycles = value; // TODO: Delete the toggle once divisor cycle data is always consulted so CPU scans never fall back to the slower pure-Montgomery path.
}
public int BatchSize public int BatchSize
{ {
@@ -69,8 +63,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
public bool IsPrime(ulong prime, out bool divisorsExhausted) public bool IsPrime(ulong prime, out bool divisorsExhausted)
{ {
ulong allowedMax; ulong allowedMax;
bool useCycles;
lock (_sync) lock (_sync)
{ {
if (!_isConfigured) if (!_isConfigured)
@@ -79,7 +71,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
} }
allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit); allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit);
useCycles = _useDivisorCycles;
} }
if (allowedMax < 3UL) if (allowedMax < 3UL)
@@ -95,7 +86,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
bool composite = CheckDivisors( bool composite = CheckDivisors(
prime, prime,
allowedMax, allowedMax,
useCycles,
out lastProcessed, out lastProcessed,
out processedAll, out processedAll,
out processedCount); out processedCount);
@@ -170,7 +160,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
private bool CheckDivisors( private bool CheckDivisors(
ulong prime, ulong prime,
ulong allowedMax, ulong allowedMax,
bool useCycles,
out ulong lastProcessed, out ulong lastProcessed,
out bool processedAll, out bool processedAll,
out ulong processedCount) out ulong processedCount)
@@ -194,36 +183,16 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
// TODO: Hoist MontgomeryDivisorData acquisition into the divisor-cycle cache so we reuse the // TODO: Hoist MontgomeryDivisorData acquisition into the divisor-cycle cache so we reuse the
// staged ProcessEightBitWindows-ready metadata instead of reloading the slower Montgomery // staged ProcessEightBitWindows-ready metadata instead of reloading the slower Montgomery
// structs for every divisor. // structs for every divisor.
byte hit; ulong divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
ulong divisorCycle = 0UL; if (divisorCycle != 0UL)
DivisorCycleCache.CycleBlock? cycleBlock = null;
if (useCycles)
{ {
cycleBlock = DivisorCycleCache.Shared.Acquire(divisor); byte hit = CheckDivisor(prime, divisorCycle, divisorData);
// TODO: Swap this block lease for the direct single-cycle lookup once the cache exposes it
// so the CPU scanner mirrors the benchmarked single-block policy without retaining mutable
// blocks beyond the startup snapshot.
divisorCycle = cycleBlock.GetCycle(divisor);
// TODO: When divisorCycle is zero (cache miss), compute only that single cycle on the device
// selected by the current settings, skip inserting the result into the shared cache, and keep
// operating with the single on-disk block without requesting additional blocks.
hit = CheckDivisor(prime, divisorCycle != 0UL, divisorCycle, divisorData);
}
else
{
// TODO: Remove this no-cycle branch and require on-demand cycle computation (without
// mutating the cache) so CPU scans never fall back to the slower Montgomery-only path
// identified in the benchmarks.
hit = CheckDivisor(prime, false, 0UL, divisorData);
}
cycleBlock?.Dispose();
if (hit != 0) if (hit != 0)
{ {
processedAll = true; processedAll = true;
return true; return true;
} }
}
if (divisor >= allowedMax - 1UL) if (divisor >= allowedMax - 1UL)
{ {
@@ -249,19 +218,15 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return false; return false;
} }
private static byte CheckDivisor(ulong prime, bool useCycles, ulong divisorCycle, in MontgomeryDivisorData divisorData) private static byte CheckDivisor(ulong prime, ulong divisorCycle, in MontgomeryDivisorData divisorData)
{ {
ulong modulus = divisorData.Modulus; ulong modulus = divisorData.Modulus;
if (modulus <= 1UL || (modulus & 1UL) == 0UL) if (modulus <= 1UL || (modulus & 1UL) == 0UL || divisorCycle == 0UL)
{ {
return 0; return 0;
} }
ulong residue = useCycles && divisorCycle != 0UL ulong residue = prime.Pow2MontgomeryModWithCycle(divisorCycle, divisorData);
? prime.Pow2MontgomeryModWithCycle(divisorCycle, divisorData)
: prime.Pow2MontgomeryMod(divisorData); // TODO: Switch to the upcoming ProcessEightBitWindows helper once the
// scalar Pow2Minus1Mod adopts it so CPU scans match the GPU speedups.
return residue == 1UL ? (byte)1 : (byte)0; return residue == 1UL ? (byte)1 : (byte)0;
} }
@@ -343,23 +308,22 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return; return;
} }
ulong modulus = divisor; if (divisor <= 1UL || (divisor & 1UL) == 0UL)
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{ {
hits.Clear(); hits.Clear();
return; return;
} }
bool useDivisorCycles = _owner._useDivisorCycles; if (divisorCycle == 0UL)
if (useDivisorCycles && divisorCycle == 0UL)
{ {
// TODO: Replace this guard with an on-demand CPU/GPU cycle computation path that skips divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
// cache insertion and continues operating with the single cached block instead of if (divisorCycle == 0UL)
// requesting additional cycle batches when a lookup misses. {
throw new InvalidOperationException($"Missing divisor cycle for divisor {divisor}."); hits.Clear();
return;
}
} }
bool cycleEnabled = useDivisorCycles;
MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(divisor); MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(divisor);
var exponentStepper = new ExponentRemainderStepper(divisorData); var exponentStepper = new ExponentRemainderStepper(divisorData);
@@ -369,8 +333,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return; return;
} }
if (cycleEnabled)
{
var stepper = new CycleRemainderStepper(divisorCycle); var stepper = new CycleRemainderStepper(divisorCycle);
ulong remainder = stepper.Initialize(primes[0]); ulong remainder = stepper.Initialize(primes[0]);
@@ -389,19 +351,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
hits[i] = exponentStepper.ComputeNextIsUnity(primes[i]) ? (byte)1 : (byte)0; hits[i] = exponentStepper.ComputeNextIsUnity(primes[i]) ? (byte)1 : (byte)0;
} }
return;
} }
// TODO: Remove this no-cycle fallback once divisor cycles are mandatory; the per-prime pow2 ladder here
// keeps the slower Montgomery stepping alive even though the benchmarks showed the cached cycle path is far
// faster for large divisor sets.
for (int i = 0; i < length; i++)
{
hits[i] = exponentStepper.ComputeNextIsUnity(primes[i]) ? (byte)1 : (byte)0;
}
}
public void Dispose() public void Dispose()
{ {
if (_disposed) if (_disposed)
@@ -415,3 +365,4 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
} }
} }
+154 -454
View File
@@ -1,10 +1,10 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using ILGPU; using ILGPU;
using ILGPU.Runtime; using ILGPU.Runtime;
using PerfectNumbers.Core.Gpu; using PerfectNumbers.Core.Gpu;
@@ -13,509 +13,211 @@ namespace PerfectNumbers.Core;
public sealed class DivisorCycleCache public sealed class DivisorCycleCache
{ {
public sealed class CycleBlock : IDisposable
{
private readonly DivisorCycleCache _owner;
private int _referenceCount;
internal CycleBlock(DivisorCycleCache owner, int index, ulong start, in ulong[] cycles)
{
_owner = owner;
Index = index;
Start = start;
End = start + (ulong)cycles.Length - 1UL;
Cycles = cycles;
}
internal int Index { get; }
internal ulong Start { get; }
internal ulong End { get; }
internal ulong[] Cycles { get; }
internal int ReferenceCount => Volatile.Read(ref _referenceCount);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong GetCycle(ulong divisor) => Cycles[(int)(divisor - Start)];
internal void Retain()
{
Interlocked.Increment(ref _referenceCount);
}
public void Dispose()
{
int remaining = Interlocked.Decrement(ref _referenceCount);
if (remaining < 0)
{
throw new InvalidOperationException("Cycle block released more times than it was acquired.");
}
if (remaining == 0)
{
_owner.OnCycleBlockReleased(this);
}
}
}
private const int CycleGenerationBatchSize = 262_144; // TODO: Remove the block-sized batch once the single-cycle generator
// replaces block hydration so we stop materializing large buffers
// just to satisfy one missing divisor lookup.
private const byte ByteZero = 0; private const byte ByteZero = 0;
private const byte ByteOne = 1; private const byte ByteOne = 1;
private const int StackBufferThreshold = 128;
private readonly object _sync = new(); // TODO: Delete the synchronization object once the cache exposes only snapshot reads and single-cycle computation so callers no longer contend on locks. private readonly ConcurrentDictionary<Accelerator, Action<Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>>> _gpuKernelCache = new(AcceleratorReferenceComparer.Instance);
private readonly ConcurrentDictionary<int, Task<CycleBlock>> _pending = new(); // TODO: Drop the concurrent dictionary entirely when the block pipeline is replaced with single-cycle generation; the new plan never queues background work. private volatile ulong[] _snapshot;
private readonly ConcurrentDictionary<Accelerator, Action<Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>>> _gpuKernelCache = new(AcceleratorReferenceComparer.Instance); // TODO: Swap this ConcurrentDictionary for a simple accelerator-indexed array once the single-cycle helper precompiles kernels during startup, eliminating the need for thread-safe lookups. private volatile bool _useGpuGeneration = true;
private CycleBlock _baseBlock = null!;
private CycleBlock? _activeBlock; // TODO: Remove dynamic blocks altogether once lookups route to the snapshot or compute an individual cycle without mutating shared state.
private CycleBlock? _prefetchedBlock; // TODO: Delete prefetch support so the cache never materializes extra blocks and callers always compute the missing cycle inline.
private bool _useGpuGeneration = true;
private readonly int _divisorCyclesBatchSize; private readonly int _divisorCyclesBatchSize;
private static int _sharedDivisorCyclesBatchSize = GpuConstants.GpuCycleStepsPerInvocation; private static int _sharedDivisorCyclesBatchSize = GpuConstants.GpuCycleStepsPerInvocation;
public static void SetDivisorCyclesBatchSize(int divisorCyclesBatchSize) => _sharedDivisorCyclesBatchSize = divisorCyclesBatchSize; public static void SetDivisorCyclesBatchSize(int divisorCyclesBatchSize)
{
_sharedDivisorCyclesBatchSize = Math.Max(1, divisorCyclesBatchSize);
}
public static DivisorCycleCache Shared { get; } = new DivisorCycleCache(_sharedDivisorCyclesBatchSize); public static DivisorCycleCache Shared { get; } = new DivisorCycleCache(_sharedDivisorCyclesBatchSize);
public int PreferredBatchSize => _divisorCyclesBatchSize;
private DivisorCycleCache(int divisorCyclesBatchSize) private DivisorCycleCache(int divisorCyclesBatchSize)
{ {
_divisorCyclesBatchSize = divisorCyclesBatchSize; _divisorCyclesBatchSize = Math.Max(1, divisorCyclesBatchSize);
ulong[] snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot(); _snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
Initialize(snapshot);
}
// TODO: Remove the ability to reload at runtime once the cache becomes a read-only snapshot plus
// transient single-cycle generator; future callers should load the snapshot during startup only.
public void ReloadFromCurrentSnapshot()
{
ulong[] snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
Initialize(snapshot);
} }
public void ConfigureGeneratorDevice(bool useGpu) public void ConfigureGeneratorDevice(bool useGpu)
{
lock (_sync)
{ {
_useGpuGeneration = useGpu; _useGpuGeneration = useGpu;
} }
// TODO: Remove the lock once single-cycle generation eliminates shared mutable state; a simple
// volatile write will be enough when the cache becomes read-only. public void RefreshSnapshot()
{
_snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
} }
private void Initialize(ulong[] snapshot) public ulong GetCycleLength(ulong divisor)
{ {
CycleBlock? activeToDispose = null; Span<ulong> result = stackalloc ulong[1];
CycleBlock? prefetchedToDispose = null; ReadOnlySpan<ulong> singleDivisor = stackalloc ulong[1] { divisor };
GetCycleLengths(singleDivisor, result);
lock (_sync) return result[0];
{
if (_activeBlock is not null && _activeBlock.Index != 0)
{
activeToDispose = _activeBlock;
} }
if (_prefetchedBlock is not null && _prefetchedBlock.Index != 0) public void GetCycleLengths(ReadOnlySpan<ulong> divisors, Span<ulong> cycles)
{ {
prefetchedToDispose = _prefetchedBlock; ulong[] snapshot = _snapshot;
int length = divisors.Length;
int[]? rentedMissing = null;
Span<int> missingBuffer = length <= StackBufferThreshold
? stackalloc int[length]
: new Span<int>(rentedMissing = ArrayPool<int>.Shared.Rent(length), 0, length);
int missingCount = 0;
for (int i = 0; i < length; i++)
{
ulong divisor = divisors[i];
if (divisor <= 1UL)
{
throw new InvalidDataException("Divisor must be > 1");
} }
_pending.Clear(); if (divisor < (ulong)snapshot.Length)
// TODO: Remove the dictionary once prefetching and background block tracking disappear; with single-cycle generation we will keep only the snapshot and skip additional bookkeeping entirely. {
_baseBlock = new CycleBlock(this, index: 0, start: 0UL, snapshot); ulong cached = snapshot[divisor];
_activeBlock = null; if (cached == 0UL)
_prefetchedBlock = null; {
StartPrefetchLocked(1); throw new InvalidDataException($"Divisor cycle is missing for {divisor}");
} }
activeToDispose?.Dispose(); cycles[i] = cached;
prefetchedToDispose?.Dispose();
}
public CycleBlock Acquire(ulong divisor)
{
CycleBlock block = divisor <= _baseBlock.End ? _baseBlock : AcquireDynamicBlock(divisor);
// TODO: Replace this block-based acquisition with a direct GetCycle API that computes the missing cycle synchronously on the configured device so the cache never hands out mutable blocks or requires disposal.
block.Retain();
return block;
}
private CycleBlock AcquireDynamicBlock(ulong divisor)
{
int blockIndex = GetBlockIndex(divisor);
while (true)
{
Task<CycleBlock>? pendingTask = null;
lock (_sync)
{
if (TryGetCachedBlockLocked(blockIndex, out CycleBlock? cached))
{
return cached!;
}
if (!_pending.TryGetValue(blockIndex, out pendingTask))
{
// TODO: Replace Task.Run with an inline single-cycle computation that runs on the configured
// device so we materialize exactly one missing cycle and stop scheduling thread-pool work for
// entire blocks.
pendingTask = Task.Run(() => GenerateBlock(blockIndex));
_pending[blockIndex] = pendingTask;
}
}
CycleBlock block = pendingTask.GetAwaiter().GetResult();
lock (_sync)
{
_pending.TryRemove(blockIndex, out _);
if (TryGetCachedBlockLocked(blockIndex, out CycleBlock? cachedAfterAwait))
{
return cachedAfterAwait!;
}
SetActiveBlockLocked(block);
// TODO: Stop promoting freshly generated blocks into the shared cache and, instead of
// returning a block, surface the computed cycle directly to the caller so no extra cache
// state survives beyond the single snapshot block.
return block;
}
}
}
private bool TryGetCachedBlockLocked(int blockIndex, out CycleBlock? block)
{
CycleBlock? active = _activeBlock;
if (active is not null && active.Index == blockIndex)
{
block = active;
return true;
}
CycleBlock? prefetched = _prefetchedBlock;
if (prefetched is not null && prefetched.Index == blockIndex)
{
PromotePrefetchedLocked();
block = _activeBlock!;
return true;
}
block = null;
return false;
}
private void PromotePrefetchedLocked()
{
CycleBlock? prefetched = _prefetchedBlock;
if (prefetched is null)
{
return;
}
CycleBlock? previousActive = _activeBlock;
_activeBlock = prefetched;
_prefetchedBlock = null;
prefetched.Retain();
prefetched.Dispose();
if (previousActive is not null && previousActive.Index != 0 && previousActive.Index != prefetched.Index)
{
previousActive.Dispose();
}
StartPrefetchLocked(prefetched.Index + 1); // TODO: Remove this call once prefetching is dropped
// so we strictly keep a single active block in memory.
}
private void SetActiveBlockLocked(CycleBlock block)
{
CycleBlock? previousActive = _activeBlock;
_activeBlock = block;
block.Retain();
CycleBlock? prefetched = _prefetchedBlock;
if (prefetched is not null && prefetched.Index != block.Index + 1)
{
if (prefetched.Index != 0)
{
prefetched.Dispose();
}
_prefetchedBlock = null;
}
if (previousActive is not null && previousActive.Index != 0 && previousActive.Index != block.Index)
{
previousActive.Dispose();
}
StartPrefetchLocked(block.Index + 1); // TODO: Remove once prefetching is removed and the cache
// no longer speculatively requests the next block after a miss.
}
private void StartPrefetchLocked(int blockIndex)
{
// TODO: Remove prefetching entirely so the cache maintains a single snapshot block and computes
// individual cycles on demand instead of triggering background generation for future indices.
if (blockIndex <= 0)
{
return;
}
CycleBlock? prefetched = _prefetchedBlock;
if (prefetched is not null && prefetched.Index == blockIndex)
{
return;
}
if (_pending.TryGetValue(blockIndex, out Task<CycleBlock>? existing))
{
if (existing.IsCompletedSuccessfully)
{
_pending.TryRemove(blockIndex, out _);
AssignPrefetchedBlockLocked(blockIndex, existing.Result);
}
return;
}
// TODO: Delete this once prefetching is removed; single-cycle generation will happen inline and
// no asynchronous work should be scheduled.
Task<CycleBlock> task = Task.Run(() => GenerateBlock(blockIndex));
_pending[blockIndex] = task;
task.ContinueWith(t => OnPrefetchCompleted(blockIndex, t), TaskScheduler.Default);
}
private void OnPrefetchCompleted(int blockIndex, Task<CycleBlock> task)
{
// TODO: Delete this callback when we remove prefetching. Single-cycle generation will run inline,
// eliminating the need for asynchronous continuation handlers and concurrent dictionaries.
if (!task.IsCompletedSuccessfully)
{
_pending.TryRemove(blockIndex, out _);
return;
}
lock (_sync)
{
_pending.TryRemove(blockIndex, out _);
AssignPrefetchedBlockLocked(blockIndex, task.Result);
}
}
private void AssignPrefetchedBlockLocked(int blockIndex, CycleBlock block)
{
// TODO: Remove this entire method when prefetching goes away. The single-cycle flow should never
// allocate or retain extra blocks beyond the startup snapshot, so prefetched block bookkeeping
// becomes unnecessary.
CycleBlock? active = _activeBlock;
if (active is not null && blockIndex != active.Index + 1)
{
MontgomeryDivisorDataCache.ReleaseBlock(block);
return;
}
CycleBlock? previousPrefetched = _prefetchedBlock;
if (ReferenceEquals(previousPrefetched, block))
{
return;
}
if (_prefetchedBlock is null || _prefetchedBlock.Index <= blockIndex)
{
if (previousPrefetched is not null && previousPrefetched.Index != 0)
{
previousPrefetched.Dispose();
}
_prefetchedBlock = block;
block.Retain();
} }
else else
{ {
MontgomeryDivisorDataCache.ReleaseBlock(block); missingBuffer[missingCount++] = i;
} }
} }
private void OnCycleBlockReleased(CycleBlock block) if (missingCount != 0)
{ {
if (block.Index == 0) ReadOnlySpan<int> missingIndices = missingBuffer[..missingCount];
{
return;
}
for (int attempt = 0; attempt < 8; attempt++)
{
if (block.ReferenceCount != 0)
{
return;
}
Thread.SpinWait(1 << attempt);
}
bool stillTracked;
lock (_sync)
{
stillTracked = ReferenceEquals(_activeBlock, block) || ReferenceEquals(_prefetchedBlock, block);
}
if (!stillTracked && block.ReferenceCount == 0)
{
MontgomeryDivisorDataCache.ReleaseBlock(block);
}
}
private CycleBlock GenerateBlock(int blockIndex)
{
if (blockIndex == 0)
{
return _baseBlock;
}
ulong start = GetBlockStart(blockIndex);
int length = _baseBlock.Cycles.Length;
ulong[] cycles = new ulong[length];
if (_useGpuGeneration) if (_useGpuGeneration)
{ {
ComputeCyclesGpu(start, cycles, _divisorCyclesBatchSize); ComputeCyclesGpu(divisors, cycles, missingIndices);
} }
else else
{ {
ComputeCyclesCpu(start, cycles); ComputeCyclesCpu(divisors, cycles, missingIndices);
}
// TODO: Replace this block generator with a single-cycle helper that reuses shared buffers,
// returns just the requested divisor's cycle, and avoids mutating cache state beyond the
// preloaded snapshot.
return new CycleBlock(this, blockIndex, start, cycles);
}
private int GetBlockIndex(ulong divisor)
{
if (divisor <= _baseBlock.End)
{
return 0;
}
ulong offset = divisor - (_baseBlock.End + 1UL);
return (int)(offset / (ulong)PerfectNumberConstants.MaxQForDivisorCycles) + 1;
}
private ulong GetBlockStart(int blockIndex)
{
if (blockIndex == 0)
{
return 0UL;
}
ulong baseEnd = _baseBlock.End;
return baseEnd + 1UL + (ulong)(blockIndex - 1) * (ulong)PerfectNumberConstants.MaxQForDivisorCycles;
}
private void ComputeCyclesCpu(ulong start, ulong[] destination)
{
ulong divisor = start;
for (int i = 0; i < destination.Length; i++)
{
// TODO: Port this CPU fallback to the unrolled-hex cycle generator once it is shared so misses outside the snapshot
// stop looping through CalculateCycleLength, which benchmarks showed is far slower than the GPU-assisted pipeline.
destination[i] = MersenneDivisorCycles.CalculateCycleLength(divisor);
divisor++;
} }
} }
private void ComputeCyclesGpu(ulong start, ulong[] destination, int divisorCyclesBatchSize) if (rentedMissing is not null)
{ {
var gpuLease = GpuKernelPool.GetKernel(_useGpuGeneration); ArrayPool<int>.Shared.Return(rentedMissing, clearArray: false);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ComputeCyclesCpu(ReadOnlySpan<ulong> divisors, Span<ulong> cycles, ReadOnlySpan<int> indices)
{
for (int i = 0; i < indices.Length; i++)
{
int targetIndex = indices[i];
cycles[targetIndex] = MersenneDivisorCycles.CalculateCycleLength(divisors[targetIndex]);
}
}
private void ComputeCyclesGpu(ReadOnlySpan<ulong> divisors, Span<ulong> cycles, ReadOnlySpan<int> indices)
{
int count = indices.Length;
ulong[]? rentedDivisors = null;
Span<ulong> workDivisors = count <= StackBufferThreshold
? stackalloc ulong[count]
: new Span<ulong>(rentedDivisors = ArrayPool<ulong>.Shared.Rent(count), 0, count);
ulong[]? rentedResults = null;
Span<ulong> workResults = count <= StackBufferThreshold
? stackalloc ulong[count]
: new Span<ulong>(rentedResults = ArrayPool<ulong>.Shared.Rent(count), 0, count);
for (int i = 0; i < count; i++)
{
workDivisors[i] = divisors[indices[i]];
}
ComputeCyclesGpuCore(workDivisors, workResults);
for (int i = 0; i < count; i++)
{
cycles[indices[i]] = workResults[i];
}
if (rentedDivisors is not null)
{
ArrayPool<ulong>.Shared.Return(rentedDivisors, clearArray: false);
ArrayPool<ulong>.Shared.Return(rentedResults!, clearArray: false);
}
}
private void ComputeCyclesGpuCore(ReadOnlySpan<ulong> divisors, Span<ulong> destination)
{
int length = divisors.Length;
var gpuLease = GpuKernelPool.GetKernel(useGpuOrder: true);
var execution = gpuLease.EnterExecutionScope(); var execution = gpuLease.EnterExecutionScope();
try
{
Accelerator accelerator = gpuLease.Accelerator; Accelerator accelerator = gpuLease.Accelerator;
Action<Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>> kernel = _gpuKernelCache.GetOrAdd(accelerator, LoadKernel); var kernel = _gpuKernelCache.GetOrAdd(accelerator, LoadKernel);
// TODO: Once single-cycle generation is in place, bypass this block-sized kernel entirely and
// call the ProcessEightBitWindows-based GPU helper that materializes only the requested
// divisor, reusing a pinned upload buffer instead of allocating per block.
int length = destination.Length; using MemoryBuffer1D<ulong, Stride1D.Dense> divisorBuffer = accelerator.Allocate1D<ulong>(length);
int chunkCapacity = Math.Min(length, CycleGenerationBatchSize); using MemoryBuffer1D<ulong, Stride1D.Dense> powBuffer = accelerator.Allocate1D<ulong>(length);
ulong[] divisors = ArrayPool<ulong>.Shared.Rent(chunkCapacity); using MemoryBuffer1D<ulong, Stride1D.Dense> orderBuffer = accelerator.Allocate1D<ulong>(length);
ulong[] powScratch = ArrayPool<ulong>.Shared.Rent(chunkCapacity); using MemoryBuffer1D<ulong, Stride1D.Dense> resultBuffer = accelerator.Allocate1D<ulong>(length);
ulong[] orderScratch = ArrayPool<ulong>.Shared.Rent(chunkCapacity); using MemoryBuffer1D<byte, Stride1D.Dense> statusBuffer = accelerator.Allocate1D<byte>(length);
byte[] statusScratch = ArrayPool<byte>.Shared.Rent(chunkCapacity);
try ulong[]? rentedPow = null;
{ Span<ulong> powSpan = length <= StackBufferThreshold
using MemoryBuffer1D<ulong, Stride1D.Dense> divisorBuffer = accelerator.Allocate1D<ulong>(chunkCapacity); ? stackalloc ulong[length]
using MemoryBuffer1D<ulong, Stride1D.Dense> powBuffer = accelerator.Allocate1D<ulong>(chunkCapacity); : new Span<ulong>(rentedPow = ArrayPool<ulong>.Shared.Rent(length), 0, length);
using MemoryBuffer1D<ulong, Stride1D.Dense> orderBuffer = accelerator.Allocate1D<ulong>(chunkCapacity);
using MemoryBuffer1D<ulong, Stride1D.Dense> resultBuffer = accelerator.Allocate1D<ulong>(chunkCapacity);
using MemoryBuffer1D<byte, Stride1D.Dense> statusBuffer = accelerator.Allocate1D<byte>(chunkCapacity);
int offset = 0; ulong[]? rentedOrder = null;
while (offset < length) Span<ulong> orderSpan = length <= StackBufferThreshold
{ ? stackalloc ulong[length]
int batchSize = Math.Min(chunkCapacity, length - offset); : new Span<ulong>(rentedOrder = ArrayPool<ulong>.Shared.Rent(length), 0, length);
Span<ulong> divisorsSpan = divisors.AsSpan(0, batchSize);
Span<ulong> destinationSpan = destination.AsSpan(offset, batchSize);
Span<ulong> powSpan = powScratch.AsSpan(0, batchSize);
Span<ulong> orderSpan = orderScratch.AsSpan(0, batchSize);
Span<byte> statusSpan = statusScratch.AsSpan(0, batchSize);
int pending = 0; ulong[]? rentedResult = null;
for (int i = 0; i < batchSize; i++) Span<ulong> resultSpan = length <= StackBufferThreshold
{ ? stackalloc ulong[length]
ulong divisorValue = start + (ulong)(offset + i); : new Span<ulong>(rentedResult = ArrayPool<ulong>.Shared.Rent(length), 0, length);
divisorsSpan[i] = divisorValue;
if ((divisorValue & (divisorValue - 1UL)) == 0UL) byte[]? rentedStatus = null;
{ Span<byte> statusSpan = length <= StackBufferThreshold
destinationSpan[i] = 1UL; ? stackalloc byte[length]
powSpan[i] = 0UL; : new Span<byte>(rentedStatus = ArrayPool<byte>.Shared.Rent(length), 0, length);
orderSpan[i] = 1UL;
statusSpan[i] = ByteOne; ref ulong divisorRef = ref MemoryMarshal.GetReference(divisors);
} divisorBuffer.View.CopyFromCPU(ref divisorRef, length);
else
for (int i = 0; i < length; i++)
{ {
ulong divisor = divisors[i];
ulong initialPow = 2UL; ulong initialPow = 2UL;
if (initialPow >= divisorValue) if (initialPow >= divisor)
{ {
initialPow -= divisorValue; initialPow -= divisor;
} }
destinationSpan[i] = 0UL;
powSpan[i] = initialPow; powSpan[i] = initialPow;
orderSpan[i] = 1UL; orderSpan[i] = 1UL;
resultSpan[i] = 0UL;
statusSpan[i] = ByteZero; statusSpan[i] = ByteZero;
pending++;
}
} }
ArrayView1D<ulong, Stride1D.Dense> divisorView = divisorBuffer.View.SubView(0, batchSize); powBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(powSpan), length);
ArrayView1D<ulong, Stride1D.Dense> powView = powBuffer.View.SubView(0, batchSize); orderBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(orderSpan), length);
ArrayView1D<ulong, Stride1D.Dense> orderView = orderBuffer.View.SubView(0, batchSize); resultBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(resultSpan), length);
ArrayView1D<ulong, Stride1D.Dense> resultView = resultBuffer.View.SubView(0, batchSize); statusBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(statusSpan), length);
ArrayView1D<byte, Stride1D.Dense> statusView = statusBuffer.View.SubView(0, batchSize);
divisorView.CopyFromCPU(ref MemoryMarshal.GetReference(divisorsSpan), batchSize); int pending;
powView.CopyFromCPU(ref MemoryMarshal.GetReference(powSpan), batchSize); do
orderView.CopyFromCPU(ref MemoryMarshal.GetReference(orderSpan), batchSize);
resultView.CopyFromCPU(ref MemoryMarshal.GetReference(destinationSpan), batchSize);
statusView.CopyFromCPU(ref MemoryMarshal.GetReference(statusSpan), batchSize);
while (pending > 0)
{ {
kernel(batchSize, divisorCyclesBatchSize, divisorView, powView, orderView, resultView, statusView); kernel(length, _divisorCyclesBatchSize, divisorBuffer.View, powBuffer.View, orderBuffer.View, resultBuffer.View, statusBuffer.View);
accelerator.Synchronize(); accelerator.Synchronize();
statusView.CopyToCPU(ref MemoryMarshal.GetReference(statusSpan), batchSize); statusBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(statusSpan), length);
pending = 0; pending = 0;
for (int i = 0; i < batchSize; i++) for (int i = 0; i < length; i++)
{ {
if (statusSpan[i] == ByteZero) if (statusSpan[i] == ByteZero)
{ {
@@ -523,26 +225,21 @@ public sealed class DivisorCycleCache
} }
} }
} }
while (pending > 0);
resultView.CopyToCPU(ref MemoryMarshal.GetReference(destinationSpan), batchSize); resultBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(resultSpan), length);
resultSpan.CopyTo(destination);
if (rentedPow is not null)
{
ArrayPool<ulong>.Shared.Return(rentedPow, clearArray: false);
ArrayPool<ulong>.Shared.Return(rentedOrder!, clearArray: false);
ArrayPool<ulong>.Shared.Return(rentedResult!, clearArray: false);
ArrayPool<byte>.Shared.Return(rentedStatus!, clearArray: false);
}
offset += batchSize;
}
}
finally
{
ArrayPool<ulong>.Shared.Return(divisors, clearArray: false);
ArrayPool<ulong>.Shared.Return(powScratch, clearArray: false);
ArrayPool<ulong>.Shared.Return(orderScratch, clearArray: false);
ArrayPool<byte>.Shared.Return(statusScratch, clearArray: false);
}
}
finally
{
execution.Dispose(); execution.Dispose();
gpuLease.Dispose(); gpuLease.Dispose();
} }
}
private static Action<Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>> LoadKernel(Accelerator accelerator) private static Action<Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>> LoadKernel(Accelerator accelerator)
{ {
@@ -602,3 +299,6 @@ public sealed class DivisorCycleCache
} }
} }
-22
View File
@@ -1,22 +0,0 @@
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core;
public sealed class EulerPrimeTester
{
private readonly PrimeTester _primeTester;
public EulerPrimeTester(PrimeTester primeTester)
{
_primeTester = primeTester;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsEulerPrime(ulong n)
{
// TODO: Inline this helper into callers so we skip the wrapper and pass the divisibility check directly to the optimized
// prime tester path highlighted in the CPU benchmarks.
return (n & 3UL) == 1UL && _primeTester.IsPrime(n, CancellationToken.None);
}
}
@@ -18,7 +18,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
private readonly object _sync = new(); private readonly object _sync = new();
private ulong _divisorLimit; private ulong _divisorLimit;
private bool _isConfigured; private bool _isConfigured;
private bool _useDivisorCycles;
private readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>>> _kernelCache = new(); private readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>>> _kernelCache = new();
private readonly ConcurrentDictionary<Accelerator, Action<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>>> _kernelExponentCache = new(); private readonly ConcurrentDictionary<Accelerator, Action<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>>> _kernelExponentCache = new();
@@ -43,12 +42,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
set => GpuBatchSize = value; set => GpuBatchSize = value;
} }
public bool UseDivisorCycles
{
get => _useDivisorCycles;
set => _useDivisorCycles = value; // TODO: Remove this flag once GPU runs unconditionally fetch cycle lengths so every divisor check benefits from the measured speedups.
}
public void ConfigureFromMaxPrime(ulong maxPrime) public void ConfigureFromMaxPrime(ulong maxPrime)
{ {
lock (_sync) lock (_sync)
@@ -61,7 +54,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
public bool IsPrime(ulong prime, out bool divisorsExhausted) public bool IsPrime(ulong prime, out bool divisorsExhausted)
{ {
ulong allowedMax; ulong allowedMax;
bool useCycles;
int batchCapacity; int batchCapacity;
lock (_sync) lock (_sync)
@@ -72,7 +64,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
} }
allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit); allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit);
useCycles = _useDivisorCycles;
batchCapacity = _gpuBatchSize; batchCapacity = _gpuBatchSize;
} }
@@ -97,7 +88,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
composite = CheckDivisors( composite = CheckDivisors(
prime, prime,
allowedMax, allowedMax,
useCycles,
accelerator, accelerator,
kernel, kernel,
resources.DivisorsBuffer, resources.DivisorsBuffer,
@@ -156,7 +146,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
private static bool CheckDivisors( private static bool CheckDivisors(
ulong prime, ulong prime,
ulong allowedMax, ulong allowedMax,
bool useCycles,
Accelerator accelerator, Accelerator accelerator,
Action<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>> kernel, Action<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>> kernel,
MemoryBuffer1D<MontgomeryDivisorData, Stride1D.Dense> divisorsBuffer, MemoryBuffer1D<MontgomeryDivisorData, Stride1D.Dense> divisorsBuffer,
@@ -170,68 +159,12 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
out bool coveredRange, out bool coveredRange,
out ulong processedCount) out ulong processedCount)
{ {
lastProcessed = 0UL; int batchCapacity = (int)divisorsBuffer.Length;
bool composite = false;
bool processedAll = false;
ulong currentDivisor = 3UL;
processedCount = 0UL; processedCount = 0UL;
lastProcessed = 0UL;
if (allowedMax < 3UL)
{
coveredRange = true;
return false;
}
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);
}
private static bool CheckDivisorsWithoutCycles(
ulong prime,
ulong allowedMax,
Accelerator accelerator,
Action<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>> kernel,
MemoryBuffer1D<MontgomeryDivisorData, Stride1D.Dense> divisorsBuffer,
MemoryBuffer1D<ulong, Stride1D.Dense> exponentBuffer,
MemoryBuffer1D<byte, Stride1D.Dense> 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<MontgomeryDivisorData> divisorDataSpan; Span<MontgomeryDivisorData> divisorDataSpan;
Span<ulong> divisorSpan; Span<ulong> divisorSpan;
@@ -241,138 +174,15 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ArrayView1D<ulong, Stride1D.Dense> exponentView; ArrayView1D<ulong, Stride1D.Dense> exponentView;
ArrayView1D<byte, Stride1D.Dense> hitsView; ArrayView1D<byte, Stride1D.Dense> hitsView;
while (currentDivisor <= allowedMax) DivisorCycleCache cycleCache = DivisorCycleCache.Shared;
int cycleBatchCapacity = Math.Min(batchCapacity, cycleCache.PreferredBatchSize);
if (cycleBatchCapacity <= 0)
{ {
int batchSize = 0; cycleBatchCapacity = batchCapacity;
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) ulong[] cycleDivisors = ArrayPool<ulong>.Shared.Rent(cycleBatchCapacity);
{ ulong[] cycleLengths = ArrayPool<ulong>.Shared.Rent(cycleBatchCapacity);
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++)
{
// TODO: Stage MontgomeryDivisorData in a GPU-ready cache keyed by divisor so we can copy
// precomputed ProcessEightBitWindows metadata directly instead of rehydrating the slower
// Montgomery structs for each batch item.
divisorDataSpan[i] = MontgomeryDivisorDataCache.Get(divisorSpan[i]);
}
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<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>> kernel,
MemoryBuffer1D<MontgomeryDivisorData, Stride1D.Dense> divisorsBuffer,
MemoryBuffer1D<ulong, Stride1D.Dense> exponentBuffer,
MemoryBuffer1D<byte, Stride1D.Dense> 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<MontgomeryDivisorData> divisorDataSpan;
Span<ulong> divisorSpan;
Span<ulong> exponentSpan;
Span<byte> hitsSpan;
ArrayView1D<MontgomeryDivisorData, Stride1D.Dense> divisorView;
ArrayView1D<ulong, Stride1D.Dense> exponentView;
ArrayView1D<byte, Stride1D.Dense> hitsView;
DivisorCycleCache divisorCyclesCache = DivisorCycleCache.Shared;
DivisorCycleCache.CycleBlock cycleLease = divisorCyclesCache.Acquire(currentDivisor);
// TODO: Replace the block lease with a direct single-cycle lookup so the GPU path
// follows the benchmarked single-block policy and computes the missing cycle inline on the
// configured device without mutating or swapping cache blocks after startup.
cycleLeaseEnd = cycleLease.End;
try try
{ {
@@ -381,33 +191,48 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
int batchSize = 0; int batchSize = 0;
bool reachedEndInBatch = false; 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) while (batchSize < batchCapacity && currentDivisor <= allowedMax)
{ {
ulong nextDivisor = currentDivisor + 2UL; int chunkCount = Math.Min(cycleBatchCapacity, batchCapacity - batchSize);
processedCount++; ulong remainingWidth = allowedMax - currentDivisor;
ulong maxOddCount = (remainingWidth >> 1) + 1UL;
if (currentDivisor > cycleLeaseEnd) if (maxOddCount < (ulong)chunkCount)
{ {
cycleLease.Dispose(); chunkCount = (int)maxOddCount;
cycleLease = divisorCyclesCache.Acquire(currentDivisor);
// TODO: Drop this block hand-off once the cache exposes a direct
// GetCycle helper that materializes only the requested divisor length
// without touching additional blocks or scheduling background work.
cycleLeaseEnd = cycleLease.End;
} }
if (chunkCount <= 0)
{
reachedEndInBatch = true;
break;
}
Span<ulong> chunkDivisors = cycleDivisors.AsSpan(0, chunkCount);
Span<ulong> chunkCycles = cycleLengths.AsSpan(0, chunkCount);
ulong fillDivisor = currentDivisor;
for (int i = 0; i < chunkCount; i++)
{
chunkDivisors[i] = fillDivisor;
fillDivisor += 2UL;
}
cycleCache.GetCycleLengths(chunkDivisors, chunkCycles);
for (int i = 0; i < chunkCount && batchSize < batchCapacity; i++)
{
ulong divisorValue = chunkDivisors[i];
ulong cycleLength = chunkCycles[i];
processedCount++;
bool includeDivisor = true; bool includeDivisor = true;
ulong exponent = prime; ulong exponent = prime;
ulong cycleLength = cycleLease.GetCycle(currentDivisor);
if (cycleLength > 0UL) if (cycleLength > 0UL)
{ {
ulong remainder = prime; ulong remainder = prime;
if (remainder >= cycleLength) if (remainder >= cycleLength)
{ {
// TODO: Replace this `%` with the cycle-cache helper that subtracts precomputed
// remainders (or uses a lookup table) so GPU batching avoids modulo in this inner loop.
remainder %= cycleLength; remainder %= cycleLength;
} }
@@ -420,36 +245,23 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
exponent = 0UL; exponent = 0UL;
} }
} }
else
{
// TODO: Trigger an immediate GPU computation for the single missing cycle without storing
// the result back or requesting extra cache blocks so very large divisors stay within the
// single-block constraint while still reusing the snapshot for other divisors.
}
if (includeDivisor) if (includeDivisor)
{ {
divisors[batchSize] = currentDivisor; divisors[batchSize] = divisorValue;
exponents[batchSize] = exponent; exponents[batchSize] = exponent;
batchSize++; batchSize++;
} }
lastProcessed = currentDivisor; lastProcessed = divisorValue;
currentDivisor = divisorValue + 2UL;
if (nextDivisor <= currentDivisor) if (currentDivisor > allowedMax)
{ {
reachedEndInBatch = true; reachedEndInBatch = true;
break; break;
} }
if (nextDivisor > allowedMax)
{
currentDivisor = nextDivisor;
reachedEndInBatch = true;
break;
} }
currentDivisor = nextDivisor;
} }
if (batchSize == 0) if (batchSize == 0)
@@ -473,8 +285,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
for (int i = 0; i < batchSize; i++) for (int i = 0; i < batchSize; i++)
{ {
// TODO: Preload ProcessEightBitWindows-friendly divisor metadata for GPU batches so this
// hot loop stops rebuilding MontgomeryDivisorData structures for every scan chunk.
divisorDataSpan[i] = MontgomeryDivisorDataCache.Get(divisorSpan[i]); divisorDataSpan[i] = MontgomeryDivisorDataCache.Get(divisorSpan[i]);
} }
@@ -512,7 +322,8 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
} }
finally finally
{ {
cycleLease.Dispose(); ArrayPool<ulong>.Shared.Return(cycleDivisors, clearArray: false);
ArrayPool<ulong>.Shared.Return(cycleLengths, clearArray: false);
} }
coveredRange = composite || processedAll || currentDivisor > allowedMax; coveredRange = composite || processedAll || currentDivisor > allowedMax;
@@ -601,17 +412,12 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
throw new ObjectDisposedException(nameof(DivisorScanSession)); throw new ObjectDisposedException(nameof(DivisorScanSession));
} }
MersenneNumberDivisorByDivisorGpuTester owner = _owner;
int primesLength = primes.Length; int primesLength = primes.Length;
if (primesLength == 0) if (primesLength == 0)
{ {
return; 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); MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(divisor);
var exponentStepper = new ExponentRemainderStepper(divisorData); var exponentStepper = new ExponentRemainderStepper(divisorData);
if (!exponentStepper.IsValidModulus) if (!exponentStepper.IsValidModulus)
@@ -620,19 +426,14 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
return; return;
} }
bool useDivisorCycles = owner._useDivisorCycles;
if (!useDivisorCycles)
{
ProcessWithoutCycle(ref exponentStepper, divisorData, primes, hits);
return;
}
if (divisorCycle == 0UL) if (divisorCycle == 0UL)
{ {
// TODO: Replace this guard with an on-demand cycle computation that runs on the device selected divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
// for the session, keeps the result transient instead of caching it, and continues operating with the if (divisorCycle == 0UL)
// single shared block loaded from disk. {
throw new InvalidOperationException($"Missing divisor cycle for divisor {divisor}."); hits.Clear();
return;
}
} }
var cycleStepper = new CycleRemainderStepper(divisorCycle); var cycleStepper = new CycleRemainderStepper(divisorCycle);
@@ -640,78 +441,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ProcessWithCycle(ref exponentStepper, ref cycleStepper, initialCycleRemainder, divisorCycle, divisorData, primes, hits); ProcessWithCycle(ref exponentStepper, ref cycleStepper, initialCycleRemainder, divisorCycle, divisorData, primes, hits);
} }
private void ProcessWithoutCycle(
ref ExponentRemainderStepper exponentStepper,
in MontgomeryDivisorData divisorData,
ReadOnlySpan<ulong> primes,
Span<byte> hits)
{
Action<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>> kernel = _kernel;
ArrayView1D<ulong, Stride1D.Dense> exponentsView = _exponentsBuffer.View;
ArrayView1D<ulong, Stride1D.Dense> resultsView = _resultsBuffer.View;
Span<ulong> 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<ulong> primesSlice = primes.Slice(offset, batchSize);
Span<byte> hitsSlice = hits.Slice(offset, batchSize);
Span<ulong> 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<ulong, Stride1D.Dense> exponentView = exponentsView.SubView(0, batchSize);
exponentView.CopyFromCPU(ref MemoryMarshal.GetReference(exponentSlice), batchSize);
ArrayView1D<ulong, Stride1D.Dense> 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( private void ProcessWithCycle(
ref ExponentRemainderStepper exponentStepper, ref ExponentRemainderStepper exponentStepper,
@@ -973,3 +702,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
IMersenneNumberDivisorByDivisorTester.IDivisorScanSession IMersenneNumberDivisorByDivisorTester.CreateDivisorSession() => CreateDivisorSession(); IMersenneNumberDivisorByDivisorTester.IDivisorScanSession IMersenneNumberDivisorByDivisorTester.CreateDivisorSession() => CreateDivisorSession();
} }
@@ -4,8 +4,6 @@ namespace PerfectNumbers.Core;
public interface IMersenneNumberDivisorByDivisorTester public interface IMersenneNumberDivisorByDivisorTester
{ {
bool UseDivisorCycles { get; set; } // TODO: Remove the setter once divisor cycle usage becomes mandatory so all implementations always leverage the cached cycles.
int BatchSize { get; set; } int BatchSize { get; set; }
void ConfigureFromMaxPrime(ulong maxPrime); void ConfigureFromMaxPrime(ulong maxPrime);
@@ -25,4 +23,3 @@ public interface IMersenneNumberDivisorByDivisorTester
void CheckDivisor(ulong divisor, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits); void CheckDivisor(ulong divisor, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits);
} }
} }
@@ -237,7 +237,6 @@ public static class MersenneNumberDivisorByDivisorTester
Span<byte> hitsSpan = default; Span<byte> hitsSpan = default;
int hitIndex = 0; int hitIndex = 0;
int index; int index;
bool useDivisorCycles = tester.UseDivisorCycles; // TODO: Remove the conditional path once all testers always enable divisor cycles to keep every divisor scan on the faster cached-length track.
ulong divisorCycle = 0UL; ulong divisorCycle = 0UL;
try try
@@ -255,23 +254,7 @@ public static class MersenneNumberDivisorByDivisorTester
divisor = AcquireNextDivisor(ref nextDivisor, divisorLimit, ref divisorsExhaustedFlag, ref finalDivisorBits, out exhausted, ref localDivisorCursor, ref localDivisorsRemaining); divisor = AcquireNextDivisor(ref nextDivisor, divisorLimit, ref divisorsExhaustedFlag, ref finalDivisorBits, out exhausted, ref localDivisorCursor, ref localDivisorsRemaining);
if (divisor != 0UL) if (divisor != 0UL)
{ {
DivisorCycleCache.CycleBlock? cycleBlock = null; divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
if (useDivisorCycles)
{
cycleBlock = DivisorCycleCache.Shared.Acquire(divisor);
// TODO: Remove this block leasing once the cache exposes a direct
// single-cycle retrieval API so coordinator threads never mutate the
// cache state beyond the snapshot loaded at startup.
divisorCycle = cycleBlock.GetCycle(divisor);
// TODO: When divisorCycle returns zero, compute only that single cycle on the
// device selected by the current configuration, keep the result transient, and
// continue operating with the single cached block instead of scheduling additional
// batches.
}
else
{
divisorCycle = 0UL;
}
activeCount = BuildPrimeBuffer( activeCount = BuildPrimeBuffer(
divisor, divisor,
@@ -296,7 +279,6 @@ public static class MersenneNumberDivisorByDivisorTester
if (activeCount == 0) if (activeCount == 0)
{ {
cycleBlock?.Dispose();
continue; continue;
} }
@@ -330,11 +312,8 @@ public static class MersenneNumberDivisorByDivisorTester
FlushPendingResults(compositesBuffer, ref compositesCount, markComposite, clearComposite, printResult); FlushPendingResults(compositesBuffer, ref compositesCount, markComposite, clearComposite, printResult);
} }
cycleBlock?.Dispose();
continue; continue;
} }
if (!exhausted) if (!exhausted)
{ {
if (Volatile.Read(ref remainingStates) == 0) if (Volatile.Read(ref remainingStates) == 0)
@@ -738,3 +717,6 @@ public static class MersenneNumberDivisorByDivisorTester
internal bool PassedAllTests { get; } internal bool PassedAllTests { get; }
} }
} }
+3 -113
View File
@@ -17,13 +17,7 @@ public readonly struct MontgomeryDivisorData(ulong modulus, ulong nPrime, ulong
internal static class MontgomeryDivisorDataCache internal static class MontgomeryDivisorDataCache
{ {
private const ulong BaseBlockEnd = PerfectNumberConstants.MaxQForDivisorCycles; private static readonly ConcurrentDictionary<ulong, MontgomeryDivisorData> Cache = new();
private static readonly ConcurrentDictionary<ulong, CacheEntry> Cache = new(); // TODO: Replace this with a plain Dictionary that never mutates after startup once the divisor-cycle snapshot becomes read-only so Montgomery lookups avoid any concurrent collections.
// TODO: Delete the block-membership tracking entirely when the divisor-cycle cache stops producing
// dynamic blocks. The single-cycle plan keeps the snapshot immutable, so we should not retain block
// indices or concurrent maps at all.
private static readonly ConcurrentDictionary<int, ConcurrentDictionary<ulong, byte>> BlockMembership = new();
public static MontgomeryDivisorData Get(ulong modulus) public static MontgomeryDivisorData Get(ulong modulus)
{ {
@@ -32,97 +26,7 @@ internal static class MontgomeryDivisorDataCache
return new MontgomeryDivisorData(modulus, 0UL, 0UL, 0UL); return new MontgomeryDivisorData(modulus, 0UL, 0UL, 0UL);
} }
int blockIndex = ComputeBlockIndex(modulus); return Cache.GetOrAdd(modulus, static m => Create(m));
// TODO: Short-circuit block indices greater than zero once the cache stops prefetching future
// blocks; when only the startup snapshot remains we should bypass block arithmetic entirely and
// compute single cycles inline on cache misses without updating shared state.
return GetManaged(modulus, blockIndex);
}
internal static void ReleaseBlock(DivisorCycleCache.CycleBlock block)
{
// TODO: Remove this entire release hook once the cycle cache no longer hands out dynamic blocks;
// the single-cycle pipeline will never transfer ownership back to Montgomery metadata.
if (block.Index <= 0)
{
return;
}
if (BlockMembership.TryRemove(block.Index, out ConcurrentDictionary<ulong, byte>? members))
{
foreach (ulong modulus in members.Keys)
{
Cache.TryRemove(modulus, out _);
}
}
}
private static MontgomeryDivisorData GetManaged(ulong modulus, int blockIndex)
{
while (true)
{
if (Cache.TryGetValue(modulus, out CacheEntry entry))
{
if (entry.BlockIndex != blockIndex)
{
if (blockIndex > 0 && entry.BlockIndex <= 0)
{
CacheEntry updated = new(entry.Data, blockIndex);
if (Cache.TryUpdate(modulus, updated, entry))
{
RegisterBlockMembership(modulus, blockIndex);
return entry.Data;
}
continue;
}
if (blockIndex > 0)
{
RegisterBlockMembership(modulus, blockIndex);
}
return entry.Data;
}
return entry.Data;
}
MontgomeryDivisorData created = Create(modulus);
if (Cache.TryAdd(modulus, new CacheEntry(created, blockIndex)))
{
RegisterBlockMembership(modulus, blockIndex);
return created;
}
}
// TODO: Collapse this retry loop once the cache becomes a simple snapshot dictionary. With the
// single-cycle policy we can compute the requested cycle, populate the dictionary exactly once,
// and return without spinning through concurrent retries.
}
private static void RegisterBlockMembership(ulong modulus, int blockIndex)
{
// TODO: Delete this method once block membership goes away; the single snapshot model must avoid
// tracking additional indices or allocating concurrent dictionaries for Montgomery lookups.
if (blockIndex <= 0)
{
return;
}
ConcurrentDictionary<ulong, byte> set = BlockMembership.GetOrAdd(blockIndex, static _ => new ConcurrentDictionary<ulong, byte>());
set.TryAdd(modulus, 0);
}
private static int ComputeBlockIndex(ulong modulus)
{
if (modulus <= BaseBlockEnd)
{
return 0;
}
// TODO: Return 0 here once the divisor-cycle cache no longer tracks additional blocks so
// MontgomeryDivisorData lookups never attempt to follow prefetched ranges or trigger new cycle
// computations for entire blocks.
return (int)((modulus - (BaseBlockEnd + 1UL)) / (ulong)PerfectNumberConstants.MaxQForDivisorCycles) + 1;
} }
private static MontgomeryDivisorData Create(ulong modulus) private static MontgomeryDivisorData Create(ulong modulus)
@@ -135,8 +39,6 @@ internal static class MontgomeryDivisorDataCache
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
// TODO: Replace this generic `%` reduction with the UInt128 Montgomery folding helper measured faster in
// MontgomeryMultiplyBenchmarks so CPU paths avoid the slow BigInteger-style remainder in hot divisor cache lookups.
private static ulong ComputeMontgomeryResidue(ulong value, ulong modulus) => (ulong)((UInt128)value * (UInt128.One << 64) % modulus); private static ulong ComputeMontgomeryResidue(ulong value, ulong modulus) => (ulong)((UInt128)value * (UInt128.One << 64) % modulus);
private static ulong ComputeMontgomeryNPrime(ulong modulus) private static ulong ComputeMontgomeryNPrime(ulong modulus)
@@ -148,19 +50,7 @@ internal static class MontgomeryDivisorDataCache
inv *= unchecked(2UL - modulus * inv); inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv); inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv); inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
return unchecked(0UL - inv); return unchecked(0UL - inv);
} }
private readonly struct CacheEntry
{
internal CacheEntry(MontgomeryDivisorData data, int blockIndex)
{
Data = data;
BlockIndex = blockIndex;
}
internal MontgomeryDivisorData Data { get; }
internal int BlockIndex { get; }
}
} }