Merge pull request #103 from mlemanczyk/codex/review-and-add-todos-for-optimization
Document cycle stepper wrapper removal TODOs
This commit is contained in:
52 files changed
+689
-84
No files matched your search
@@ -57,9 +57,10 @@ Never ever run tests marked with trait Category = Slow. Always invoke `dotnet te
|
|||||||
|
|
||||||
There is only one agents file - this one. Don't search for another one.
|
There is only one agents file - this one. Don't search for another one.
|
||||||
|
|
||||||
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.
|
Always use indentation with four space characters in source code files, even if a file currently uses tabs or appears to rely on eight-space indentation. 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.
|
||||||
|
Explicitly prefer reusing existing variables once their previous values are no longer required instead of declaring new locals of the same type. Call out the variable reuse in comments when its meaning changes so the intent stays clear and register pressure remains minimal.
|
||||||
|
|
||||||
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.
|
Keep the divisor cycle cache limited to a single block in memory (only the on-disk snapshot). When a lookup misses, compute the required cycle on the configured device and discard the transient result instead of scheduling background generation or caching additional blocks.
|
||||||
|
|
||||||
All code, comments, commit messages, branch names and PR descriptions must be written in American English.
|
All code, comments, commit messages, branch names and PR descriptions must be written in American English.
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ internal static class Program
|
|||||||
|
|
||||||
private static ulong ParseRangeValue(in string option, in string value)
|
private static ulong ParseRangeValue(in string option, in string value)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace TryParse with the Utf8Parser-based span helper once the results parser accepts ReadOnlySpan<char>
|
||||||
|
// inputs so numeric option parsing matches the fastest CLI path identified in benchmarks.
|
||||||
if (!ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong parsed))
|
if (!ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong parsed))
|
||||||
{
|
{
|
||||||
throw new FormatException($"Value '{value}' for option '{option}' is not a valid non-negative integer.");
|
throw new FormatException($"Value '{value}' for option '{option}' is not a valid non-negative integer.");
|
||||||
@@ -161,6 +163,7 @@ internal static class Program
|
|||||||
|
|
||||||
private static int ParsePositiveInt(in string option, in string value)
|
private static int ParsePositiveInt(in string option, in string value)
|
||||||
{
|
{
|
||||||
|
// TODO: Switch to the Utf8Parser-based fast path so integer arguments avoid transient strings while parsing.
|
||||||
if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed) || parsed < 1)
|
if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed) || parsed < 1)
|
||||||
{
|
{
|
||||||
throw new FormatException($"Value '{value}' for option '{option}' is not a valid positive integer.");
|
throw new FormatException($"Value '{value}' for option '{option}' is not a valid positive integer.");
|
||||||
@@ -186,6 +189,8 @@ internal static class Program
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Swap this string slicing for ReadOnlySpan<char>-based parsing once the CLI helpers expose
|
||||||
|
// Utf8Parser-compatible overloads so option handling avoids allocating substrings for every argument.
|
||||||
option = current[..equalsIndex];
|
option = current[..equalsIndex];
|
||||||
value = current[(equalsIndex + 1)..];
|
value = current[(equalsIndex + 1)..];
|
||||||
index++;
|
index++;
|
||||||
@@ -226,6 +231,9 @@ internal static class Program
|
|||||||
private static void ProcessFile(in string inputPath, ulong pMin, ulong pMax, int threadCount, int bufferSize)
|
private static void ProcessFile(in string inputPath, ulong pMin, ulong pMax, int threadCount, int bufferSize)
|
||||||
{
|
{
|
||||||
using StreamReader reader = new(inputPath);
|
using StreamReader reader = new(inputPath);
|
||||||
|
// TODO: Replace this vanilla StreamReader with a pooled FileStreamOptions + ArrayPool-backed reader so large
|
||||||
|
// reloads reuse the benchmarked zero-allocation buffered pipeline instead of allocating new decoder buffers per
|
||||||
|
// run.
|
||||||
string? headerLine = reader.ReadLine();
|
string? headerLine = reader.ReadLine();
|
||||||
if (headerLine is null)
|
if (headerLine is null)
|
||||||
{
|
{
|
||||||
@@ -239,6 +247,9 @@ internal static class Program
|
|||||||
|
|
||||||
string header = string.IsNullOrWhiteSpace(headerLine) ? DefaultHeader : headerLine.Trim();
|
string header = string.IsNullOrWhiteSpace(headerLine) ? DefaultHeader : headerLine.Trim();
|
||||||
|
|
||||||
|
// TODO: Stream candidates through a pooled buffer pipeline so we can filter and dispatch without allocating
|
||||||
|
// the entire list upfront; the benchmarks showed that retaining every row simultaneously increases GC pressure
|
||||||
|
// during large result replays.
|
||||||
List<CandidateResult> candidates = LoadCandidates(reader, pMin, pMax, threadCount, bufferSize);
|
List<CandidateResult> candidates = LoadCandidates(reader, pMin, pMax, threadCount, bufferSize);
|
||||||
Console.WriteLine("Building sorted prime results...");
|
Console.WriteLine("Building sorted prime results...");
|
||||||
List<CandidateResult> sortedPrimeResults = ExtractSortedPrimeCandidates(candidates);
|
List<CandidateResult> sortedPrimeResults = ExtractSortedPrimeCandidates(candidates);
|
||||||
@@ -303,18 +314,24 @@ internal static class Program
|
|||||||
throw new FormatException("Input line does not contain expected comma separators.");
|
throw new FormatException("Input line does not contain expected comma separators.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Replace ulong.Parse with the Utf8Parser-based span helper so CSV reload stays on the zero-allocation
|
||||||
|
// path identified as fastest in the shared CLI benchmarks.
|
||||||
ulong p = ulong.Parse(span[..firstCommaIndex], NumberStyles.Integer, CultureInfo.InvariantCulture);
|
ulong p = ulong.Parse(span[..firstCommaIndex], NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||||
if (p < pMin || p > pMax)
|
if (p < pMin || p > pMax)
|
||||||
{
|
{
|
||||||
return CandidateResult.Empty;
|
return CandidateResult.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Swap bool.Parse for the Utf8Parser-powered boolean reader once exposed so reload avoids allocating
|
||||||
|
// temporary strings per record.
|
||||||
bool passedAllTests = bool.Parse(span[(lastCommaIndex + 1)..]);
|
bool passedAllTests = bool.Parse(span[(lastCommaIndex + 1)..]);
|
||||||
return new CandidateResult(p, passedAllTests, line);
|
return new CandidateResult(p, passedAllTests, line);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildOutputPath(in string inputPath, in string prefix)
|
private static string BuildOutputPath(in string inputPath, in string prefix)
|
||||||
{
|
{
|
||||||
|
// TODO: Switch this path composition to Span<char>-based stack buffers once we expose the pooled
|
||||||
|
// formatter helpers so output names avoid intermediate strings for directory/file splits.
|
||||||
string fileName = Path.GetFileName(inputPath);
|
string fileName = Path.GetFileName(inputPath);
|
||||||
string prefixedName = prefix + fileName;
|
string prefixedName = prefix + fileName;
|
||||||
string? directory = Path.GetDirectoryName(inputPath);
|
string? directory = Path.GetDirectoryName(inputPath);
|
||||||
@@ -329,6 +346,8 @@ internal static class Program
|
|||||||
private static void WriteCsv(in string path, in string header, bool append, IEnumerable<CandidateResult> entries)
|
private static void WriteCsv(in string path, in string header, bool append, IEnumerable<CandidateResult> entries)
|
||||||
{
|
{
|
||||||
using StreamWriter writer = new(path, append, Encoding.UTF8);
|
using StreamWriter writer = new(path, append, Encoding.UTF8);
|
||||||
|
// TODO: Switch WriteCsv to a span-based Utf8Formatter pipeline backed by ArrayPool-rented buffers so
|
||||||
|
// appends reuse the zero-allocation formatting path highlighted in the results writer benchmarks.
|
||||||
if (!append)
|
if (!append)
|
||||||
{
|
{
|
||||||
writer.WriteLine(header);
|
writer.WriteLine(header);
|
||||||
@@ -336,8 +355,12 @@ internal static class Program
|
|||||||
|
|
||||||
foreach (CandidateResult entry in entries)
|
foreach (CandidateResult entry in entries)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace this per-entry WriteLine with a batched chunk writer that reuses pooled
|
||||||
|
// StringBuilder instances so large files avoid the per-line flush overhead observed in the
|
||||||
|
// chunked writer benchmarks.
|
||||||
writer.WriteLine(entry.Csv);
|
writer.WriteLine(entry.Csv);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<CandidateResult> LoadCandidates(
|
private static List<CandidateResult> LoadCandidates(
|
||||||
@@ -354,12 +377,16 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
|
|
||||||
int workerLimit = Math.Max(1, threadCount);
|
int workerLimit = Math.Max(1, threadCount);
|
||||||
|
// TODO: Replace the Task-based scheduling with the pooled work queue used in the scanner so candidate
|
||||||
|
// parsing leverages the same low-overhead thread handoff profiled as fastest in the CLI benchmarks.
|
||||||
List<Task<ChunkResult>> activeChunks = new(workerLimit);
|
List<Task<ChunkResult>> activeChunks = new(workerLimit);
|
||||||
Dictionary<int, ChunkResult> completedChunks = new();
|
Dictionary<int, ChunkResult> completedChunks = new();
|
||||||
int nextChunkToFlush = 0;
|
int nextChunkToFlush = 0;
|
||||||
int consoleProgress = 0;
|
int consoleProgress = 0;
|
||||||
ulong lastReportedCandidate = 0UL;
|
ulong lastReportedCandidate = 0UL;
|
||||||
|
|
||||||
|
// TODO: Rent this line buffer from ArrayPool<string> to eliminate repeated allocations when scanning
|
||||||
|
// large result files.
|
||||||
string[] buffer = new string[bufferSize];
|
string[] buffer = new string[bufferSize];
|
||||||
int buffered = 0;
|
int buffered = 0;
|
||||||
int chunkIndex = 0;
|
int chunkIndex = 0;
|
||||||
@@ -422,10 +449,14 @@ internal static class Program
|
|||||||
if (length == buffer.Length)
|
if (length == buffer.Length)
|
||||||
{
|
{
|
||||||
chunkLines = buffer;
|
chunkLines = buffer;
|
||||||
|
// TODO: Rent this full-capacity buffer from ArrayPool<string> and reuse it between reads instead
|
||||||
|
// of allocating a fresh array each time steady-state batches are dispatched.
|
||||||
buffer = new string[bufferSize];
|
buffer = new string[bufferSize];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// TODO: Rent the partial chunk buffer from ArrayPool<string> instead of allocating a fresh
|
||||||
|
// array for every remainder dispatch.
|
||||||
chunkLines = new string[length];
|
chunkLines = new string[length];
|
||||||
Array.Copy(buffer, chunkLines, length);
|
Array.Copy(buffer, chunkLines, length);
|
||||||
}
|
}
|
||||||
@@ -465,6 +496,8 @@ internal static class Program
|
|||||||
|
|
||||||
private static ChunkResult ProcessChunk(string[] lines, int index, ulong pMin, ulong pMax)
|
private static ChunkResult ProcessChunk(string[] lines, int index, ulong pMin, ulong pMax)
|
||||||
{
|
{
|
||||||
|
// TODO: Rent the candidate array from ArrayPool<CandidateResult> so chunk processing remains allocation-free
|
||||||
|
// for hot reload paths.
|
||||||
CandidateResult[] items = new CandidateResult[lines.Length];
|
CandidateResult[] items = new CandidateResult[lines.Length];
|
||||||
int count = 0;
|
int count = 0;
|
||||||
ulong pEmpty = CandidateResult.Empty.P;
|
ulong pEmpty = CandidateResult.Empty.P;
|
||||||
@@ -495,10 +528,14 @@ internal static class Program
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Replace this copy with an in-place filtering pipeline that reuses pooled buffers; duplicating the list
|
||||||
|
// doubled working-set usage in the reload benchmarks when processing multi-million-entry snapshots.
|
||||||
List<CandidateResult> sortedCandidates = [.. candidates];
|
List<CandidateResult> sortedCandidates = [.. candidates];
|
||||||
sortedCandidates.Sort(CandidateResultComparer.Instance);
|
sortedCandidates.Sort(CandidateResultComparer.Instance);
|
||||||
|
|
||||||
List<CandidateResult> primeCandidates = [];
|
List<CandidateResult> primeCandidates = [];
|
||||||
|
// TODO: Keep using the Open.Numeric prime enumerator for the merge walk but hoist a shared instance so reloads
|
||||||
|
// reuse it instead of allocating a brand new enumerator for every pass.
|
||||||
using IEnumerator<ulong> primeEnumerator = Prime.Numbers.GetEnumerator();
|
using IEnumerator<ulong> primeEnumerator = Prime.Numbers.GetEnumerator();
|
||||||
if (!primeEnumerator.MoveNext())
|
if (!primeEnumerator.MoveNext())
|
||||||
{
|
{
|
||||||
@@ -538,6 +575,8 @@ internal static class Program
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Replace this Dictionary allocation with the pooled span-based frequency map once the ValueListBuilder
|
||||||
|
// helper lands so raw prime reconstruction stops allocating temporary hash tables for every reload.
|
||||||
Dictionary<ulong, int> remainingCounts = new(primeCount);
|
Dictionary<ulong, int> remainingCounts = new(primeCount);
|
||||||
for (int i = 0; i < primeCount; i++)
|
for (int i = 0; i < primeCount; i++)
|
||||||
{
|
{
|
||||||
@@ -551,6 +590,8 @@ internal static class Program
|
|||||||
remainingCounts.Add(candidate.P, 1);
|
remainingCounts.Add(candidate.P, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Rent the raw prime accumulator from ArrayPool<List<CandidateResult>> or migrate to a pooled struct
|
||||||
|
// builder so large reloads avoid repeated List allocations along this path.
|
||||||
List<CandidateResult> rawPrimes = new(primeCount);
|
List<CandidateResult> rawPrimes = new(primeCount);
|
||||||
for (int i = 0; i < candidates.Count; i++)
|
for (int i = 0; i < candidates.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -617,11 +658,18 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
|
|
||||||
int workerCount = Math.Min(threadCount, count);
|
int workerCount = Math.Min(threadCount, count);
|
||||||
|
// TODO: Rent these segment arrays from ArrayPool<List<CandidateResult>> so parallel splitting avoids allocating
|
||||||
|
// new jagged arrays on every reload; the pooled work queue already used by the scanner benchmarks faster.
|
||||||
List<CandidateResult>[] passedSegments = new List<CandidateResult>[workerCount];
|
List<CandidateResult>[] passedSegments = new List<CandidateResult>[workerCount];
|
||||||
List<CandidateResult>[] rejectedSegments = new List<CandidateResult>[workerCount];
|
List<CandidateResult>[] rejectedSegments = new List<CandidateResult>[workerCount];
|
||||||
Task[] tasks = new Task[workerCount];
|
Task[] tasks = new Task[workerCount];
|
||||||
|
// TODO: Rent the tasks array from ArrayPool<Task> so repeated reloads avoid allocating new
|
||||||
|
// scheduling buffers; the pooled queue used by the scanner benchmarks noticeably faster.
|
||||||
|
|
||||||
int start = 0;
|
int start = 0;
|
||||||
|
// TODO: Replace the separate division/modulo here with Math.DivRem (or the branchless chunk
|
||||||
|
// planner from the parallel partition benchmarks) so we avoid paying for two divisions when
|
||||||
|
// distributing work across threads.
|
||||||
int baseChunk = count / workerCount;
|
int baseChunk = count / workerCount;
|
||||||
int remainder = count % workerCount;
|
int remainder = count % workerCount;
|
||||||
for (int worker = 0; worker < workerCount; worker++)
|
for (int worker = 0; worker < workerCount; worker++)
|
||||||
@@ -639,10 +687,15 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
|
|
||||||
int workerIndex = worker;
|
int workerIndex = worker;
|
||||||
|
// TODO: Replace Task.Run with the pooled work queue from the scanner so scheduling
|
||||||
|
// overhead matches the benchmarked fast path instead of spawning transient threads.
|
||||||
tasks[workerIndex] = Task.Run(() =>
|
tasks[workerIndex] = Task.Run(() =>
|
||||||
{
|
{
|
||||||
List<CandidateResult> localPassed = new(length);
|
List<CandidateResult> localPassed = new(length);
|
||||||
List<CandidateResult> localRejected = new(length);
|
List<CandidateResult> localRejected = new(length);
|
||||||
|
// TODO: Rent these per-worker lists from a dedicated pool so parallel splits reuse
|
||||||
|
// pre-sized buffers; allocating new List instances showed up heavily in the multi-
|
||||||
|
// threaded reload benchmarks once files crossed tens of millions of entries.
|
||||||
int end = chunkStart + length;
|
int end = chunkStart + length;
|
||||||
for (int i = chunkStart; i < end; i++)
|
for (int i = chunkStart; i < end; i++)
|
||||||
{
|
{
|
||||||
@@ -665,6 +718,9 @@ internal static class Program
|
|||||||
|
|
||||||
passedResults = new List<CandidateResult>(count);
|
passedResults = new List<CandidateResult>(count);
|
||||||
rejectedResults = new List<CandidateResult>(count);
|
rejectedResults = new List<CandidateResult>(count);
|
||||||
|
// TODO: Promote these final result lists to pooled builders so we can reuse the buffers
|
||||||
|
// across reloads; the pooled aggregators from the scanner benchmarks avoid repeated large
|
||||||
|
// allocations during long parsing sessions.
|
||||||
for (int worker = 0; worker < workerCount; worker++)
|
for (int worker = 0; worker < workerCount; worker++)
|
||||||
{
|
{
|
||||||
if (passedSegments[worker].Count > 0)
|
if (passedSegments[worker].Count > 0)
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
else if (arg.StartsWith("--prime=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--prime=", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
// TODO: Replace this ulong.Parse with the Utf8Parser-based span helper once CLI option parsing
|
||||||
|
// adopts the zero-allocation fast path proven fastest in the parser benchmarks.
|
||||||
currentP = ulong.Parse(arg.AsSpan(arg.IndexOf('=') + 1));
|
currentP = ulong.Parse(arg.AsSpan(arg.IndexOf('=') + 1));
|
||||||
startPrimeProvided = true;
|
startPrimeProvided = true;
|
||||||
}
|
}
|
||||||
@@ -119,6 +121,8 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
else if (arg.StartsWith("--threads=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--threads=", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
// TODO: Swap int.Parse for the span-based Utf8Parser fast path to avoid transient strings when
|
||||||
|
// processing CLI numeric options.
|
||||||
threadCount = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
threadCount = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||||
}
|
}
|
||||||
else if (arg.StartsWith("--mersenne=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--mersenne=", StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -157,6 +161,8 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
else if (arg.StartsWith("--divisor=", StringComparison.OrdinalIgnoreCase))
|
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));
|
divisor = UInt128.Parse(arg.AsSpan(arg.IndexOf('=') + 1));
|
||||||
}
|
}
|
||||||
else if (arg.StartsWith("--use-divisor-cycles=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--use-divisor-cycles=", StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -168,6 +174,8 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
else if (arg.StartsWith("--divisor-cycles-limit=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--divisor-cycles-limit=", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
// TODO: Replace ulong.TryParse with the Utf8Parser fast-path once CLI parsing utilities expose
|
||||||
|
// span-based helpers for optional arguments.
|
||||||
if (ulong.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out parsedLimit))
|
if (ulong.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out parsedLimit))
|
||||||
{
|
{
|
||||||
divisorCyclesSearchLimit = parsedLimit;
|
divisorCyclesSearchLimit = parsedLimit;
|
||||||
@@ -175,6 +183,8 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
else if (arg.StartsWith("--residue-max-k=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--residue-max-k=", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
// TODO: Swap this TryParse for the zero-allocation Utf8Parser helper to keep residue CLI option
|
||||||
|
// parsing in the fast path.
|
||||||
if (ulong.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out parsedResidueMax))
|
if (ulong.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out parsedResidueMax))
|
||||||
{
|
{
|
||||||
residueKMax = parsedResidueMax;
|
residueKMax = parsedResidueMax;
|
||||||
@@ -198,6 +208,8 @@ internal static class Program
|
|||||||
{
|
{
|
||||||
// stored and used below when initializing testers
|
// stored and used below when initializing testers
|
||||||
// Reusing parsedLimit to capture the --order-warmup-limit argument.
|
// Reusing parsedLimit to capture the --order-warmup-limit argument.
|
||||||
|
// TODO: Use the Utf8Parser-based reader here to eliminate the temporary substring allocation
|
||||||
|
// when parsing warm-up limits.
|
||||||
if (ulong.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out parsedLimit))
|
if (ulong.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out parsedLimit))
|
||||||
{
|
{
|
||||||
_orderWarmupLimitOverride = parsedLimit;
|
_orderWarmupLimitOverride = parsedLimit;
|
||||||
@@ -258,6 +270,8 @@ internal static class Program
|
|||||||
else if (arg.StartsWith("--rle-hard-max=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--rle-hard-max=", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
ulong rleMaxP;
|
ulong rleMaxP;
|
||||||
|
// TODO: Replace this TryParse with the Utf8Parser span helper once shared CLI parsing utilities
|
||||||
|
// land, mirroring the other numeric option optimizations.
|
||||||
if (ulong.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out rleMaxP))
|
if (ulong.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out rleMaxP))
|
||||||
{
|
{
|
||||||
_rleHardMaxP = rleMaxP;
|
_rleHardMaxP = rleMaxP;
|
||||||
@@ -271,6 +285,8 @@ internal static class Program
|
|||||||
else if (arg.StartsWith("--zero-hard=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--zero-hard=", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
double zeroFraction;
|
double zeroFraction;
|
||||||
|
// TODO: Swap to the Utf8Parser double fast-path so zero-hard parsing avoids culture-dependent
|
||||||
|
// conversions in the hot CLI path.
|
||||||
if (double.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out zeroFraction))
|
if (double.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out zeroFraction))
|
||||||
{
|
{
|
||||||
_zeroFracHard = zeroFraction;
|
_zeroFracHard = zeroFraction;
|
||||||
@@ -285,6 +301,8 @@ internal static class Program
|
|||||||
{
|
{
|
||||||
double zeroFrac;
|
double zeroFrac;
|
||||||
int maxZero;
|
int maxZero;
|
||||||
|
// TODO: Replace these double/int TryParse calls with span-based Utf8Parser helpers once
|
||||||
|
// available so parsing zero-conj stays allocation-free.
|
||||||
if (double.TryParse(value[..colon], out zeroFrac) && int.TryParse(value[(colon + 1)..], out maxZero))
|
if (double.TryParse(value[..colon], out zeroFrac) && int.TryParse(value[(colon + 1)..], out maxZero))
|
||||||
{
|
{
|
||||||
_zeroFracConj = zeroFrac;
|
_zeroFracConj = zeroFrac;
|
||||||
@@ -317,22 +335,31 @@ internal static class Program
|
|||||||
|
|
||||||
else if (arg.StartsWith("--gpu-prime-threads=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--gpu-prime-threads=", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
// TODO: Convert this int.Parse to the Utf8Parser-based helper to keep CLI option parsing
|
||||||
|
// allocation-free in the hot startup path.
|
||||||
gpuPrimeThreads = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
gpuPrimeThreads = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||||
}
|
}
|
||||||
else if (arg.StartsWith("--gpu-prime-batch=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--gpu-prime-batch=", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
// TODO: Swap int.Parse for Utf8Parser to align with the faster CLI numeric parsing path.
|
||||||
gpuPrimeBatch = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
gpuPrimeBatch = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||||
}
|
}
|
||||||
else if (arg.StartsWith("--ll-slice=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--ll-slice=", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
// TODO: Replace int.Parse with Utf8Parser-based parsing to eliminate temporary strings when
|
||||||
|
// reading slice configuration.
|
||||||
sliceSize = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
sliceSize = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||||
}
|
}
|
||||||
else if (arg.StartsWith("--gpu-scan-batch=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--gpu-scan-batch=", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
// TODO: Use the Utf8Parser-based fast path here to match the other CLI numeric parsing
|
||||||
|
// optimizations we plan to adopt.
|
||||||
scanBatchSize = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
scanBatchSize = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||||
}
|
}
|
||||||
else if (arg.StartsWith("--block-size=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--block-size=", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
// TODO: Inline the Utf8Parser-based int reader here once shared CLI parsing utilities land so
|
||||||
|
// we avoid repeated string allocations.
|
||||||
blockSize = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
blockSize = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||||
}
|
}
|
||||||
else if (arg.StartsWith("--filter-p=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--filter-p=", StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -341,6 +368,8 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
else if (arg.StartsWith("--write-batch-size=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--write-batch-size=", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
// TODO: Replace int.Parse with the shared Utf8Parser fast-path to keep hot CLI option parsing
|
||||||
|
// allocation-free.
|
||||||
_writeBatchSize = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
_writeBatchSize = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||||
}
|
}
|
||||||
if (arg.StartsWith("--divisor-cycles="))
|
if (arg.StartsWith("--divisor-cycles="))
|
||||||
@@ -353,6 +382,8 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
else if (arg.StartsWith("--divisor-cycles-batch=", StringComparison.OrdinalIgnoreCase))
|
else if (arg.StartsWith("--divisor-cycles-batch=", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
// TODO: Convert this int.Parse to Utf8Parser once the optimized CLI parsing helper is shared
|
||||||
|
// across tools.
|
||||||
cyclesBatchSize = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
cyclesBatchSize = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||||
}
|
}
|
||||||
else if (arg.Equals("--divisor-cycles-continue", StringComparison.OrdinalIgnoreCase))
|
else if (arg.Equals("--divisor-cycles-continue", StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -430,7 +461,13 @@ internal static class Program
|
|||||||
|
|
||||||
DivisorCycleCache.SetDivisorCyclesBatchSize(cyclesBatchSize);
|
DivisorCycleCache.SetDivisorCyclesBatchSize(cyclesBatchSize);
|
||||||
DivisorCycleCache.Shared.ConfigureGeneratorDevice(orderOnGpu);
|
DivisorCycleCache.Shared.ConfigureGeneratorDevice(orderOnGpu);
|
||||||
|
// 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
|
||||||
|
// generation of additional blocks.
|
||||||
DivisorCycleCache.Shared.ReloadFromCurrentSnapshot();
|
DivisorCycleCache.Shared.ReloadFromCurrentSnapshot();
|
||||||
|
// 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 single cached block strategy.
|
||||||
|
|
||||||
Console.WriteLine("Divisor cycles are ready");
|
Console.WriteLine("Divisor cycles are ready");
|
||||||
|
|
||||||
@@ -581,6 +618,9 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// TODO: Replace this File.WriteAllText call with the pooled TextFileWriter pipeline once the
|
||||||
|
// scanner's output flush adopts the benchmarked buffered writer so we avoid allocating the
|
||||||
|
// entire header string and opening the file twice per run.
|
||||||
File.WriteAllText(ResultsFileName, $"p,searchedMersenne,detailedCheck,passedAllTests{Environment.NewLine}");
|
File.WriteAllText(ResultsFileName, $"p,searchedMersenne,detailedCheck,passedAllTests{Environment.NewLine}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,6 +639,8 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// TODO: Rent this filter buffer from ArrayPool<ulong> and reuse it across reload batches so
|
||||||
|
// we do not allocate fresh arrays while replaying large result filters.
|
||||||
localFilter = new ulong[1024];
|
localFilter = new ulong[1024];
|
||||||
filterCount = 0;
|
filterCount = 0;
|
||||||
LoadResultsFile(filterFile, (p, detailedCheck, passedAllTests) =>
|
LoadResultsFile(filterFile, (p, detailedCheck, passedAllTests) =>
|
||||||
@@ -672,6 +714,8 @@ internal static class Program
|
|||||||
int count, j;
|
int count, j;
|
||||||
ulong p;
|
ulong p;
|
||||||
bool passedAllTests, searchedMersenne, detailedCheck;
|
bool passedAllTests, searchedMersenne, detailedCheck;
|
||||||
|
// TODO: Rent this buffer from ArrayPool once the pooled prime-block allocator lands so worker
|
||||||
|
// threads stop allocating fresh arrays for every reservation.
|
||||||
ulong[] buffer = new ulong[blockSize];
|
ulong[] buffer = new ulong[blockSize];
|
||||||
bool reachedMax = false;
|
bool reachedMax = false;
|
||||||
while (!Volatile.Read(ref _limitReached))
|
while (!Volatile.Read(ref _limitReached))
|
||||||
@@ -753,6 +797,8 @@ internal static class Program
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Swap this char-by-char parser for the benchmarked Utf8Parser-based fast-path once
|
||||||
|
// the CLI plumbing accepts spans to avoid redundant range checks while collecting primes.
|
||||||
int index = 0;
|
int index = 0;
|
||||||
while (index < span.Length)
|
while (index < span.Length)
|
||||||
{
|
{
|
||||||
@@ -769,6 +815,8 @@ internal static class Program
|
|||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Swap ulong.TryParse for the Utf8Parser-based fast path once the shared span helpers
|
||||||
|
// arrive so by-divisor candidate loading avoids per-value string allocations.
|
||||||
if (ulong.TryParse(span[start..index], NumberStyles.None, CultureInfo.InvariantCulture, out ulong parsed))
|
if (ulong.TryParse(span[start..index], NumberStyles.None, CultureInfo.InvariantCulture, out ulong parsed))
|
||||||
{
|
{
|
||||||
candidates.Add(parsed);
|
candidates.Add(parsed);
|
||||||
@@ -786,6 +834,8 @@ internal static class Program
|
|||||||
throw new InvalidOperationException("By-divisor tester has not been initialized.");
|
throw new InvalidOperationException("By-divisor tester has not been initialized.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Inline this helper by invoking MersenneNumberDivisorByDivisorTester.Run directly at call sites so
|
||||||
|
// the CLI avoids this extra frame during the hot composite-reporting path.
|
||||||
MersenneNumberDivisorByDivisorTester.Run(
|
MersenneNumberDivisorByDivisorTester.Run(
|
||||||
candidates,
|
candidates,
|
||||||
_byDivisorTester,
|
_byDivisorTester,
|
||||||
@@ -832,6 +882,8 @@ internal static class Program
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Replace this ulong.Parse call with the Utf8Parser-based span fast-path we benchmarked so results
|
||||||
|
// reload skips the slower string allocation and culture-aware parsing.
|
||||||
parsedP = ulong.Parse(span[..first]);
|
parsedP = ulong.Parse(span[..first]);
|
||||||
span = span[(first + 1)..];
|
span = span[(first + 1)..];
|
||||||
second = span.IndexOf(',');
|
second = span.IndexOf(',');
|
||||||
@@ -850,6 +902,8 @@ internal static class Program
|
|||||||
detailedSpan = span[..third];
|
detailedSpan = span[..third];
|
||||||
passedAllTestsSpan = span[(third + 1)..];
|
passedAllTestsSpan = span[(third + 1)..];
|
||||||
|
|
||||||
|
// TODO: Replace these bool.TryParse calls with the span-based fast path once we expose the
|
||||||
|
// Utf8Parser-powered helpers so results reload avoids per-line string allocations for booleans.
|
||||||
if (bool.TryParse(detailedSpan, out detailed) && bool.TryParse(passedAllTestsSpan, out passedAllTests))
|
if (bool.TryParse(detailedSpan, out detailed) && bool.TryParse(passedAllTestsSpan, out passedAllTests))
|
||||||
{
|
{
|
||||||
lineProcessorAction(parsedP, detailed, passedAllTests);
|
lineProcessorAction(parsedP, detailed, passedAllTests);
|
||||||
@@ -917,6 +971,9 @@ internal static class Program
|
|||||||
string order = useOrder ? "order-on" : "order-off";
|
string order = useOrder ? "order-on" : "order-off";
|
||||||
string gcd = useGcd ? "gcd-on" : "gcd-off";
|
string gcd = useGcd ? "gcd-on" : "gcd-off";
|
||||||
string work = useModWorkaround ? "modfix-on" : "modfix-off";
|
string work = useModWorkaround ? "modfix-on" : "modfix-off";
|
||||||
|
// TODO: Replace this string interpolation with the pooled ValueStringBuilder pipeline from the
|
||||||
|
// results-writer benchmarks so filename generation reuses the zero-allocation formatter once
|
||||||
|
// we consolidate output paths.
|
||||||
return $"even_perfect_bit_scan_inc-{inc}_thr-{threads}_blk-{block}_mers-{mers}_mersdev-{mersenneDevice}_ntt-{ntt}_red-{red}_primesdev-{primesDevice}_{order}_orderdev-{orderDevice}_{gcd}_{work}_gputh-{gpuPrimeThreads}_llslice-{llSlice}_scanb-{gpuScanBatch}_warm-{warmupLimit}.csv";
|
return $"even_perfect_bit_scan_inc-{inc}_thr-{threads}_blk-{block}_mers-{mers}_mersdev-{mersenneDevice}_ntt-{ntt}_red-{red}_primesdev-{primesDevice}_{order}_orderdev-{orderDevice}_{gcd}_{work}_gputh-{gpuPrimeThreads}_llslice-{llSlice}_scanb-{gpuScanBatch}_warm-{warmupLimit}.csv";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -937,6 +994,8 @@ internal static class Program
|
|||||||
while (count < blockSize && !Volatile.Read(ref _limitReached))
|
while (count < blockSize && !Volatile.Read(ref _limitReached))
|
||||||
{
|
{
|
||||||
bufferSpan[count++] = p;
|
bufferSpan[count++] = p;
|
||||||
|
// TODO: Inline the fast-path transform here once we collapse the delegate* indirection so
|
||||||
|
// block reservations stop paying the extra jump highlighted in the prime-stepping benchmarks.
|
||||||
p = _transformP(p, ref remainder);
|
p = _transformP(p, ref remainder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1014,6 +1073,8 @@ internal static class Program
|
|||||||
bool lastWasComposite = _lastCompositeP;
|
bool lastWasComposite = _lastCompositeP;
|
||||||
bool primeCandidate = passedAllTests && !lastWasComposite && Volatile.Read(ref _primeFoundAfterInit);
|
bool primeCandidate = passedAllTests && !lastWasComposite && Volatile.Read(ref _primeFoundAfterInit);
|
||||||
|
|
||||||
|
// TODO: Switch this StringBuilder-based formatter to a span-friendly Utf8Formatter pipeline so
|
||||||
|
// console/file output avoids intermediate builders in the hot logging loop.
|
||||||
StringBuilder localBuilder = StringBuilderPool.Rent();
|
StringBuilder localBuilder = StringBuilderPool.Rent();
|
||||||
_ = localBuilder
|
_ = localBuilder
|
||||||
.Append(currentP).Append(',')
|
.Append(currentP).Append(',')
|
||||||
@@ -1073,6 +1134,8 @@ internal static class Program
|
|||||||
{
|
{
|
||||||
lock (FileWriteSync)
|
lock (FileWriteSync)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace these per-flush FileStream/StreamWriter allocations with a pooled TextFileWriter-style
|
||||||
|
// helper so batched result writes keep the hot path on the benchmarked persistent-handle pipeline.
|
||||||
using FileStream stream = new(ResultsFileName, FileMode.Append, FileAccess.Write, FileShare.Read);
|
using FileStream stream = new(ResultsFileName, FileMode.Append, FileAccess.Write, FileShare.Read);
|
||||||
using StreamWriter writer = new(stream) { AutoFlush = false };
|
using StreamWriter writer = new(stream) { AutoFlush = false };
|
||||||
writer.Write(builderToFlush);
|
writer.Write(builderToFlush);
|
||||||
@@ -1089,6 +1152,8 @@ internal static class Program
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
internal static ulong CountOnes(ulong value)
|
internal static ulong CountOnes(ulong value)
|
||||||
{
|
{
|
||||||
|
// TODO: Remove this wrapper once callers can invoke BitOperations.PopCount directly so the hot bit-stat path
|
||||||
|
// avoids the extra call layer.
|
||||||
return (ulong)BitOperations.PopCount(value);
|
return (ulong)BitOperations.PopCount(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1118,6 +1183,8 @@ internal static class Program
|
|||||||
|
|
||||||
ulong next = (value << 1) | 1UL;
|
ulong next = (value << 1) | 1UL;
|
||||||
remainder = (remainder << 1) + 1UL;
|
remainder = (remainder << 1) + 1UL;
|
||||||
|
// TODO: Replace this manual subtraction loop with the lookup-driven Mod6 helper highlighted
|
||||||
|
// in the Mod6 benchmarks so the prime-bit stepping mirrors the fastest remainder update path.
|
||||||
while (remainder >= 6UL)
|
while (remainder >= 6UL)
|
||||||
{
|
{
|
||||||
remainder -= 6UL;
|
remainder -= 6UL;
|
||||||
@@ -1140,6 +1207,8 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
|
|
||||||
remainder += value;
|
remainder += value;
|
||||||
|
// TODO: Route this remainder fold through the Mod6 helper validated in the Mod6 benchmarks to
|
||||||
|
// remove the repeated subtraction and keep the add-by-bit path on the optimal cycle.
|
||||||
while (remainder >= 6UL)
|
while (remainder >= 6UL)
|
||||||
{
|
{
|
||||||
remainder -= 6UL;
|
remainder -= 6UL;
|
||||||
@@ -1161,6 +1230,8 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
|
|
||||||
remainder += diff;
|
remainder += diff;
|
||||||
|
// TODO: Switch this reduction to the Mod6 helper from the Mod6 benchmarks so add stepping shares
|
||||||
|
// the same optimized remainder pipeline as the bit-transform path.
|
||||||
while (remainder >= 6UL)
|
while (remainder >= 6UL)
|
||||||
{
|
{
|
||||||
remainder -= 6UL;
|
remainder -= 6UL;
|
||||||
@@ -1195,6 +1266,8 @@ internal static class Program
|
|||||||
|
|
||||||
candidate += diff;
|
candidate += diff;
|
||||||
addRemainder += diff;
|
addRemainder += diff;
|
||||||
|
// TODO: Apply the Mod6 lookup helper here as well so prime-plus stepping reuses the
|
||||||
|
// benchmarked fastest remainder updates instead of looping subtraction.
|
||||||
while (addRemainder >= 6UL)
|
while (addRemainder >= 6UL)
|
||||||
{
|
{
|
||||||
addRemainder -= 6UL;
|
addRemainder -= 6UL;
|
||||||
@@ -1333,6 +1406,8 @@ internal static class Program
|
|||||||
private static bool IsCompositeByResidues(ulong p)
|
private static bool IsCompositeByResidues(ulong p)
|
||||||
{
|
{
|
||||||
var tracker = PResidue.Value!;
|
var tracker = PResidue.Value!;
|
||||||
|
// TODO: Integrate the divisor-cycle cache here so the small-prime sweep reuses precomputed remainders instead of
|
||||||
|
// running MergeOrAppend for every candidate and missing the cycle-accelerated early exits.
|
||||||
tracker.BeginMerge(p);
|
tracker.BeginMerge(p);
|
||||||
// Use the small prime list from PerfectNumbers.Core to the extent of sqrt(p)
|
// Use the small prime list from PerfectNumbers.Core to the extent of sqrt(p)
|
||||||
var primes = PrimesGenerator.SmallPrimes;
|
var primes = PrimesGenerator.SmallPrimes;
|
||||||
@@ -1387,6 +1462,9 @@ internal static class Program
|
|||||||
inspectedByte |= (byte)(0xFF << bitsInTopByte);
|
inspectedByte |= (byte)(0xFF << bitsInTopByte);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Replace this byte-by-byte scan with the lookup-table based statistics collector validated in the
|
||||||
|
// BitStats benchmarks so zero runs leverage cached results instead of recomputing per bit.
|
||||||
|
|
||||||
zeroCountInByte = ByteZeroCount[inspectedByte];
|
zeroCountInByte = ByteZeroCount[inspectedByte];
|
||||||
zeroCount += zeroCountInByte;
|
zeroCount += zeroCountInByte;
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ public sealed class AlphaCache
|
|||||||
|
|
||||||
public AlphaValues Get(EInteger p, int k)
|
public AlphaValues Get(EInteger p, int k)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace this dictionary lookup with a lock-free cache that reuses pooled AlphaValues so alpha requests avoid
|
||||||
|
// redoing the expensive PowerCache.Get computations across threads.
|
||||||
if (!_cache.TryGetValue((p, k), out var values))
|
if (!_cache.TryGetValue((p, k), out var values))
|
||||||
{
|
{
|
||||||
ERational alphaPValue = AlphaCalculations.ComputeAlphaP(p, k);
|
ERational alphaPValue = AlphaCalculations.ComputeAlphaP(p, k);
|
||||||
@@ -21,6 +23,8 @@ public sealed class AlphaCache
|
|||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
|
// TODO: Return pooled AlphaValues instances to a shared cache before clearing so repeated warm-ups do not thrash the
|
||||||
|
// allocator when alpha tables refresh during large divisor scans.
|
||||||
_cache.Clear();
|
_cache.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ public static class AlphaCalculations
|
|||||||
{
|
{
|
||||||
int exponent = checked(4 * k + 1);
|
int exponent = checked(4 * k + 1);
|
||||||
EInteger y = PowerCache.Get(p, exponent);
|
EInteger y = PowerCache.Get(p, exponent);
|
||||||
|
// TODO: Reuse pooled numerator/denominator buffers here so the alphaP computation stops allocating
|
||||||
|
// intermediate EInteger instances every invocation.
|
||||||
return ERational.Create(y * p - 1, y * (p - 1));
|
return ERational.Create(y * p - 1, y * (p - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,6 +17,9 @@ public static class AlphaCalculations
|
|||||||
{
|
{
|
||||||
EInteger num = PowerCache.Get(q, a2 + 1) - EInteger.One;
|
EInteger num = PowerCache.Get(q, a2 + 1) - EInteger.One;
|
||||||
EInteger den = PowerCache.Get(q, a2) * (q - EInteger.One);
|
EInteger den = PowerCache.Get(q, a2) * (q - EInteger.One);
|
||||||
|
// TODO: Reuse pooled ERational builders here so repeated factor computations pull precomputed
|
||||||
|
// numerator/denominator tuples instead of re-executing PowerCache chains each time, matching the
|
||||||
|
// fastest alpha benchmarks.
|
||||||
return ERational.Create(num, den);
|
return ERational.Create(num, den);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +27,11 @@ public static class AlphaCalculations
|
|||||||
{
|
{
|
||||||
ERational result = ERational.One;
|
ERational result = ERational.One;
|
||||||
for (int i = 0; i < maxR; i++)
|
for (int i = 0; i < maxR; i++)
|
||||||
|
{
|
||||||
|
// TODO: Replace the repeated Multiply calls with a span-based aggregator that batches factors,
|
||||||
|
// avoiding transient ERational instances when maxR grows large.
|
||||||
result = result.Multiply(ComputeAlphaFactor(primes[i], a2));
|
result = result.Multiply(ComputeAlphaFactor(primes[i], a2));
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ namespace PerfectNumbers.Core;
|
|||||||
|
|
||||||
public enum AlphaMScannerCandidateStatus
|
public enum AlphaMScannerCandidateStatus
|
||||||
{
|
{
|
||||||
|
// TODO: Store these statuses as byte-coded constants so hot-path evaluations can stay branchless when scanning candidates.
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
PRODUCT_ABOVE_TARGET,
|
PRODUCT_ABOVE_TARGET,
|
||||||
PRODUCT_BELOW_TARGET,
|
PRODUCT_BELOW_TARGET,
|
||||||
|
|||||||
@@ -6,4 +6,8 @@ public static class CpuConstants
|
|||||||
public const int LastSevenMask10 = (1 << 7) | (1 << 9);
|
public const int LastSevenMask10 = (1 << 7) | (1 << 9);
|
||||||
// For M_p ending with 1, restrict to {1,3,9}; 7 considered only for q == 7 (not possible for large p)
|
// For M_p ending with 1, restrict to {1,3,9}; 7 considered only for q == 7 (not possible for large p)
|
||||||
public const int LastOneMask10 = (1 << 1) | (1 << 3) | (1 << 9);
|
public const int LastOneMask10 = (1 << 1) | (1 << 3) | (1 << 9);
|
||||||
|
// TODO: Generate these masks at startup from the benchmarked Mod10 automaton tables so CPU filtering stays aligned with
|
||||||
|
// the optimized divisor-cycle residue steps.
|
||||||
|
// TODO: Fold in the Mod6ComparisonBenchmarks stride table here so callers can merge the mod 6 skips with these masks and
|
||||||
|
// avoid redundant residue work in the CPU hot loop.
|
||||||
}
|
}
|
||||||
@@ -200,14 +200,20 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
|||||||
if (useCycles)
|
if (useCycles)
|
||||||
{
|
{
|
||||||
cycleBlock = DivisorCycleCache.Shared.Acquire(divisor);
|
cycleBlock = DivisorCycleCache.Shared.Acquire(divisor);
|
||||||
|
// 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);
|
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);
|
hit = CheckDivisor(prime, divisorCycle != 0UL, divisorCycle, divisorData);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO: Force this path to hydrate divisor cycles from disk once the cache guarantees
|
// TODO: Remove this no-cycle branch and require on-demand cycle computation (without
|
||||||
// coverage so we can retire the slower Montgomery-only fallback highlighted by the CPU
|
// mutating the cache) so CPU scans never fall back to the slower Montgomery-only path
|
||||||
// divisor benchmarks.
|
// identified in the benchmarks.
|
||||||
hit = CheckDivisor(prime, false, 0UL, divisorData);
|
hit = CheckDivisor(prime, false, 0UL, divisorData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,6 +353,9 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
|||||||
bool useDivisorCycles = _owner._useDivisorCycles;
|
bool useDivisorCycles = _owner._useDivisorCycles;
|
||||||
if (useDivisorCycles && divisorCycle == 0UL)
|
if (useDivisorCycles && divisorCycle == 0UL)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace this guard with an on-demand CPU/GPU cycle computation path that skips
|
||||||
|
// cache insertion and continues operating with the single cached block instead of
|
||||||
|
// requesting additional cycle batches when a lookup misses.
|
||||||
throw new InvalidOperationException($"Missing divisor cycle for divisor {divisor}.");
|
throw new InvalidOperationException($"Missing divisor cycle for divisor {divisor}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,23 +7,30 @@ public class MersenneNumberIncrementalCpuTester(GpuKernelType kernelType)
|
|||||||
|
|
||||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||||
{
|
{
|
||||||
UInt128 k, one = k = UInt128.One;
|
UInt128 k = UInt128.One;
|
||||||
|
UInt128 one = k;
|
||||||
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
|
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
|
||||||
// Use residue automaton always on CPU
|
// Use residue automaton always on CPU
|
||||||
var auto = new MersenneResidueAutomaton(exponent);
|
var auto = new MersenneResidueAutomaton(exponent);
|
||||||
|
|
||||||
bool reject, shouldCheck;
|
bool reject;
|
||||||
UInt128 q = auto.CurrentQ(),
|
bool shouldCheck;
|
||||||
divPow, halfPow, phi, qCycle;
|
UInt128 q = auto.CurrentQ();
|
||||||
|
UInt128 divPow;
|
||||||
|
UInt128 halfPow;
|
||||||
|
UInt128 phi;
|
||||||
|
UInt128 qCycle;
|
||||||
|
|
||||||
ulong div, mod8, phi64;
|
ulong div;
|
||||||
|
ulong mod8;
|
||||||
|
ulong phi64;
|
||||||
|
|
||||||
while (k <= maxK && Volatile.Read(ref isPrime))
|
while (k <= maxK && Volatile.Read(ref isPrime))
|
||||||
{
|
{
|
||||||
|
// TODO: Replace this direct GetCycle call with DivisorCycleCache.Lookup so we reuse the single snapshot block
|
||||||
|
// and compute missing cycles on the configured device without queuing additional block generation.
|
||||||
qCycle = MersenneDivisorCycles.GetCycle(q);
|
qCycle = MersenneDivisorCycles.GetCycle(q);
|
||||||
shouldCheck = lastIsSeven
|
shouldCheck = auto.Mod10R != 5UL;
|
||||||
? (auto.Mod10R != 5UL)
|
|
||||||
: (auto.Mod10R != 5UL);
|
|
||||||
if (shouldCheck)
|
if (shouldCheck)
|
||||||
{
|
{
|
||||||
mod8 = auto.Mod8R;
|
mod8 = auto.Mod8R;
|
||||||
@@ -33,6 +40,8 @@ public class MersenneNumberIncrementalCpuTester(GpuKernelType kernelType)
|
|||||||
}
|
}
|
||||||
else if (auto.Mod3R == 0UL || auto.Mod5R == 0UL)
|
else if (auto.Mod3R == 0UL || auto.Mod5R == 0UL)
|
||||||
{
|
{
|
||||||
|
// TODO: Swap these Mod3/Mod5 zero checks to the cached residue tables once the automaton exposes
|
||||||
|
// the benchmarked lookup-based helpers so CPU scans avoid runtime modulo instructions.
|
||||||
shouldCheck = false;
|
shouldCheck = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +98,5 @@ public class MersenneNumberIncrementalCpuTester(GpuKernelType kernelType)
|
|||||||
auto.Next();
|
auto.Next();
|
||||||
q = auto.CurrentQ();
|
q = auto.CurrentQ();
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,9 @@ public class MersenneNumberOrderCpuTester(GpuKernelType kernelType)
|
|||||||
while (k <= maxK && Volatile.Read(ref isPrime))
|
while (k <= maxK && Volatile.Read(ref isPrime))
|
||||||
{
|
{
|
||||||
qCycle = MersenneDivisorCycles.GetCycle(q);
|
qCycle = MersenneDivisorCycles.GetCycle(q);
|
||||||
|
// TODO: When this lookup misses the snapshot, invoke the configured device to compute the
|
||||||
|
// single required cycle on demand without persisting it so the CPU order path retains the
|
||||||
|
// cycle-stepping speedups while honoring the no-extra-cache constraint for large divisors.
|
||||||
int allowMask2 = lastIsSeven ? CpuConstants.LastSevenMask10 : CpuConstants.LastOneMask10;
|
int allowMask2 = lastIsSeven ? CpuConstants.LastSevenMask10 : CpuConstants.LastOneMask10;
|
||||||
bool shouldCheck = ((allowMask2 >> (int)auto.Mod10R) & 1) != 0;
|
bool shouldCheck = ((allowMask2 >> (int)auto.Mod10R) & 1) != 0;
|
||||||
if (shouldCheck)
|
if (shouldCheck)
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ public class MersenneNumberResidueCpuTester
|
|||||||
_mersenneResidueTracker ??= new(ResidueModel.Mersenne);
|
_mersenneResidueTracker ??= new(ResidueModel.Mersenne);
|
||||||
// For small divisors q <= 4,000,000 use cached cycle length to fast-path checks
|
// For small divisors q <= 4,000,000 use cached cycle length to fast-path checks
|
||||||
// and avoid per-q powmods when possible when scanning lanes.
|
// and avoid per-q powmods when possible when scanning lanes.
|
||||||
|
// TODO: Wire this residue scan into DivisorCycleCache so the mandatory cycle acceleration from the
|
||||||
|
// by-divisor benchmarks applies here instead of relying solely on ModResidueTracker powmods.
|
||||||
|
// TODO: When a required cycle is missing from the snapshot, compute only that single cycle on the device
|
||||||
|
// selected by the current settings, skip persisting the result, and avoid scheduling additional cache blocks so
|
||||||
|
// we keep operating with the single shared block.
|
||||||
_mersenneResidueTracker.BeginMerge(exponent);
|
_mersenneResidueTracker.BeginMerge(exponent);
|
||||||
|
|
||||||
// Start at k = 1: q0 = 2*p*1 + 1
|
// Start at k = 1: q0 = 2*p*1 + 1
|
||||||
@@ -83,6 +88,9 @@ public class MersenneNumberResidueCpuTester
|
|||||||
if (lanes >= 1 &&
|
if (lanes >= 1 &&
|
||||||
(((allowMask >> (int)r10_0) & 1) != 0 && (r8_0 == 1UL || r8_0 == 7UL) && r3_0 != 0UL && r5_0 != 0UL))
|
(((allowMask >> (int)r10_0) & 1) != 0 && (r8_0 == 1UL || r8_0 == 7UL) && r3_0 != 0UL && r5_0 != 0UL))
|
||||||
{
|
{
|
||||||
|
// TODO: Once cycle data is exposed for residue scans, consult the cached divisor cycle here
|
||||||
|
// (and below) so every qualifying q reuses precomputed lengths instead of recomputing via
|
||||||
|
// tracker.MergeOrAppend.
|
||||||
// cycle-based quick check for small q
|
// cycle-based quick check for small q
|
||||||
if (localIsPrime && tracker.MergeOrAppend(exponent, q, out isDivider) && isDivider &&
|
if (localIsPrime && tracker.MergeOrAppend(exponent, q, out isDivider) && isDivider &&
|
||||||
(q <= ulong.MaxValue ? ((ulong)q).IsPrimeCandidate() : q.IsPrimeCandidate()))
|
(q <= ulong.MaxValue ? ((ulong)q).IsPrimeCandidate() : q.IsPrimeCandidate()))
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ internal struct CycleRemainderStepper
|
|||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
|
// TODO: Inline this reset at the call sites so the hot loops reuse struct reinitialization
|
||||||
|
// measured fastest in MersenneDivisorCycleLengthGpuBenchmarks, avoiding the extra
|
||||||
|
// method call when scanners need to restart stepping.
|
||||||
_previousPrime = 0UL;
|
_previousPrime = 0UL;
|
||||||
_currentRemainder = 0UL;
|
_currentRemainder = 0UL;
|
||||||
_hasState = false;
|
_hasState = false;
|
||||||
@@ -37,7 +40,8 @@ internal struct CycleRemainderStepper
|
|||||||
if (remainder >= cycleLength)
|
if (remainder >= cycleLength)
|
||||||
{
|
{
|
||||||
// TODO: Swap this `%` for the shared divisor-cycle remainder helper so initialization reuses the cached
|
// TODO: Swap this `%` for the shared divisor-cycle remainder helper so initialization reuses the cached
|
||||||
// cycle deltas benchmarked faster than on-the-fly modulo work.
|
// cycle deltas benchmarked faster than on-the-fly modulo work, matching the
|
||||||
|
// MersenneDivisorCycleLengthGpuBenchmarks winner for both CPU and GPU call sites.
|
||||||
remainder %= cycleLength;
|
remainder %= cycleLength;
|
||||||
}
|
}
|
||||||
_previousPrime = prime;
|
_previousPrime = prime;
|
||||||
@@ -79,7 +83,8 @@ internal struct CycleRemainderStepper
|
|||||||
if (_currentRemainder >= cycleLength)
|
if (_currentRemainder >= cycleLength)
|
||||||
{
|
{
|
||||||
// TODO: Route this `%` through the shared divisor-cycle helper so repeated wrap-arounds avoid
|
// TODO: Route this `%` through the shared divisor-cycle helper so repeated wrap-arounds avoid
|
||||||
// modulo operations and match the benchmarked fast path.
|
// modulo operations and match the benchmarked fast path highlighted in
|
||||||
|
// MersenneDivisorCycleLengthGpuBenchmarks.
|
||||||
_currentRemainder %= cycleLength;
|
_currentRemainder %= cycleLength;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,6 +93,8 @@ internal struct CycleRemainderStepper
|
|||||||
|
|
||||||
public void CaptureState(out ulong previousPrime, out ulong previousRemainder, out bool hasState)
|
public void CaptureState(out ulong previousPrime, out ulong previousRemainder, out bool hasState)
|
||||||
{
|
{
|
||||||
|
// TODO: Expose these fields directly once the residue scanners adopt the single-cycle helper
|
||||||
|
// so the hot path can read them without paying for an additional wrapper call per iteration.
|
||||||
previousPrime = _previousPrime;
|
previousPrime = _previousPrime;
|
||||||
previousRemainder = _currentRemainder;
|
previousRemainder = _currentRemainder;
|
||||||
hasState = _hasState;
|
hasState = _hasState;
|
||||||
|
|||||||
@@ -60,16 +60,18 @@ public sealed class DivisorCycleCache
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const int CycleGenerationBatchSize = 262_144;
|
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 readonly object _sync = new();
|
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<int, Task<CycleBlock>> _pending = new();
|
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 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<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 CycleBlock _baseBlock = null!;
|
private CycleBlock _baseBlock = null!;
|
||||||
private CycleBlock? _activeBlock;
|
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;
|
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 bool _useGpuGeneration = true;
|
||||||
private readonly int _divisorCyclesBatchSize;
|
private readonly int _divisorCyclesBatchSize;
|
||||||
private static int _sharedDivisorCyclesBatchSize = GpuConstants.GpuCycleStepsPerInvocation;
|
private static int _sharedDivisorCyclesBatchSize = GpuConstants.GpuCycleStepsPerInvocation;
|
||||||
@@ -84,6 +86,8 @@ public sealed class DivisorCycleCache
|
|||||||
Initialize(snapshot);
|
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()
|
public void ReloadFromCurrentSnapshot()
|
||||||
{
|
{
|
||||||
ulong[] snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
|
ulong[] snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
|
||||||
@@ -96,6 +100,8 @@ public sealed class DivisorCycleCache
|
|||||||
{
|
{
|
||||||
_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.
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Initialize(ulong[] snapshot)
|
private void Initialize(ulong[] snapshot)
|
||||||
@@ -116,6 +122,7 @@ public sealed class DivisorCycleCache
|
|||||||
}
|
}
|
||||||
|
|
||||||
_pending.Clear();
|
_pending.Clear();
|
||||||
|
// 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);
|
_baseBlock = new CycleBlock(this, index: 0, start: 0UL, snapshot);
|
||||||
_activeBlock = null;
|
_activeBlock = null;
|
||||||
_prefetchedBlock = null;
|
_prefetchedBlock = null;
|
||||||
@@ -129,6 +136,7 @@ public sealed class DivisorCycleCache
|
|||||||
public CycleBlock Acquire(ulong divisor)
|
public CycleBlock Acquire(ulong divisor)
|
||||||
{
|
{
|
||||||
CycleBlock block = divisor <= _baseBlock.End ? _baseBlock : AcquireDynamicBlock(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();
|
block.Retain();
|
||||||
return block;
|
return block;
|
||||||
}
|
}
|
||||||
@@ -149,6 +157,9 @@ public sealed class DivisorCycleCache
|
|||||||
|
|
||||||
if (!_pending.TryGetValue(blockIndex, out pendingTask))
|
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));
|
pendingTask = Task.Run(() => GenerateBlock(blockIndex));
|
||||||
_pending[blockIndex] = pendingTask;
|
_pending[blockIndex] = pendingTask;
|
||||||
}
|
}
|
||||||
@@ -165,6 +176,9 @@ public sealed class DivisorCycleCache
|
|||||||
}
|
}
|
||||||
|
|
||||||
SetActiveBlockLocked(block);
|
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;
|
return block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,7 +224,8 @@ public sealed class DivisorCycleCache
|
|||||||
previousActive.Dispose();
|
previousActive.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
StartPrefetchLocked(prefetched.Index + 1);
|
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)
|
private void SetActiveBlockLocked(CycleBlock block)
|
||||||
@@ -235,11 +250,14 @@ public sealed class DivisorCycleCache
|
|||||||
previousActive.Dispose();
|
previousActive.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
StartPrefetchLocked(block.Index + 1);
|
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)
|
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)
|
if (blockIndex <= 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -262,6 +280,8 @@ public sealed class DivisorCycleCache
|
|||||||
return;
|
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));
|
Task<CycleBlock> task = Task.Run(() => GenerateBlock(blockIndex));
|
||||||
_pending[blockIndex] = task;
|
_pending[blockIndex] = task;
|
||||||
task.ContinueWith(t => OnPrefetchCompleted(blockIndex, t), TaskScheduler.Default);
|
task.ContinueWith(t => OnPrefetchCompleted(blockIndex, t), TaskScheduler.Default);
|
||||||
@@ -269,6 +289,8 @@ public sealed class DivisorCycleCache
|
|||||||
|
|
||||||
private void OnPrefetchCompleted(int blockIndex, Task<CycleBlock> task)
|
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)
|
if (!task.IsCompletedSuccessfully)
|
||||||
{
|
{
|
||||||
_pending.TryRemove(blockIndex, out _);
|
_pending.TryRemove(blockIndex, out _);
|
||||||
@@ -284,6 +306,9 @@ public sealed class DivisorCycleCache
|
|||||||
|
|
||||||
private void AssignPrefetchedBlockLocked(int blockIndex, CycleBlock block)
|
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;
|
CycleBlock? active = _activeBlock;
|
||||||
if (active is not null && blockIndex != active.Index + 1)
|
if (active is not null && blockIndex != active.Index + 1)
|
||||||
{
|
{
|
||||||
@@ -363,6 +388,9 @@ public sealed class DivisorCycleCache
|
|||||||
ComputeCyclesCpu(start, cycles);
|
ComputeCyclesCpu(start, cycles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
return new CycleBlock(this, blockIndex, start, cycles);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,6 +421,8 @@ public sealed class DivisorCycleCache
|
|||||||
ulong divisor = start;
|
ulong divisor = start;
|
||||||
for (int i = 0; i < destination.Length; i++)
|
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);
|
destination[i] = MersenneDivisorCycles.CalculateCycleLength(divisor);
|
||||||
divisor++;
|
divisor++;
|
||||||
}
|
}
|
||||||
@@ -407,6 +437,9 @@ public sealed class DivisorCycleCache
|
|||||||
{
|
{
|
||||||
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);
|
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);
|
||||||
|
// 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;
|
int length = destination.Length;
|
||||||
int chunkCapacity = Math.Min(length, CycleGenerationBatchSize);
|
int chunkCapacity = Math.Min(length, CycleGenerationBatchSize);
|
||||||
@@ -568,3 +601,4 @@ public sealed class DivisorCycleCache
|
|||||||
public int GetHashCode(Accelerator obj) => RuntimeHelpers.GetHashCode(obj);
|
public int GetHashCode(Accelerator obj) => RuntimeHelpers.GetHashCode(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,6 +14,9 @@ public sealed class EulerPrimeTester
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool IsEulerPrime(ulong n)
|
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);
|
return (n & 3UL) == 1UL && _primeTester.IsPrime(n, CancellationToken.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +54,9 @@ internal struct ExponentRemainderStepper
|
|||||||
}
|
}
|
||||||
|
|
||||||
ulong delta = exponent - _previousExponent;
|
ulong delta = exponent - _previousExponent;
|
||||||
|
// TODO: Once divisor cycle lengths are mandatory, pull the delta multiplier from the
|
||||||
|
// single-block divisor-cycle snapshot so we can skip the powmod entirely and reuse the
|
||||||
|
// cached Montgomery residue ladder highlighted in MersenneDivisorCycleLengthGpuBenchmarks.
|
||||||
// TODO: Replace this per-delta powmod with the upcoming windowed ladder so incremental
|
// TODO: Replace this per-delta powmod with the upcoming windowed ladder so incremental
|
||||||
// updates stop paying the single-bit cost highlighted in GpuPow2ModBenchmarks.
|
// updates stop paying the single-bit cost highlighted in GpuPow2ModBenchmarks.
|
||||||
ulong multiplier = delta.Pow2MontgomeryModMontgomery(_divisor);
|
ulong multiplier = delta.Pow2MontgomeryModMontgomery(_divisor);
|
||||||
@@ -81,6 +84,8 @@ internal struct ExponentRemainderStepper
|
|||||||
}
|
}
|
||||||
|
|
||||||
ulong delta = exponent - _previousExponent;
|
ulong delta = exponent - _previousExponent;
|
||||||
|
// TODO: Reuse the divisor-cycle derived Montgomery delta once the cache exposes single-cycle
|
||||||
|
// lookups so this branch also avoids recomputing powmods when the snapshot lacks the divisor.
|
||||||
// TODO: Use the windowed delta pow2 helper here as well to avoid the single-bit ladder that
|
// TODO: Use the windowed delta pow2 helper here as well to avoid the single-bit ladder that
|
||||||
// currently lags behind the benchmarked implementation for huge divisor cycles.
|
// currently lags behind the benchmarked implementation for huge divisor cycles.
|
||||||
ulong multiplier = delta.Pow2MontgomeryModMontgomery(_divisor);
|
ulong multiplier = delta.Pow2MontgomeryModMontgomery(_divisor);
|
||||||
@@ -125,6 +130,9 @@ internal struct ExponentRemainderStepper
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Once the divisor-cycle cache exposes a direct Montgomery delta, multiply it here instead
|
||||||
|
// of relying on the caller-provided delta so incremental scans remain in sync with the snapshot
|
||||||
|
// without computing additional cycles or mutating cache state.
|
||||||
_currentMontgomery = _currentMontgomery.MontgomeryMultiply(montgomeryDelta, _modulus, _nPrime);
|
_currentMontgomery = _currentMontgomery.MontgomeryMultiply(montgomeryDelta, _modulus, _nPrime);
|
||||||
_previousExponent = exponent;
|
_previousExponent = exponent;
|
||||||
isUnity = _currentMontgomery == _montgomeryOne;
|
isUnity = _currentMontgomery == _montgomeryOne;
|
||||||
|
|||||||
@@ -4,5 +4,8 @@ namespace PerfectNumbers.Core.Gpu
|
|||||||
{
|
{
|
||||||
public const int ScanBatchSize = 2_097_152;
|
public const int ScanBatchSize = 2_097_152;
|
||||||
public const int GpuCycleStepsPerInvocation = 256;
|
public const int GpuCycleStepsPerInvocation = 256;
|
||||||
|
// TODO: Auto-tune these GPU constants per accelerator so batch sizing aligns with the fastest kernel
|
||||||
|
// configuration reported by the Pow2Mod and divisor-cycle benchmarks.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,8 @@ namespace PerfectNumbers.Core.Gpu;
|
|||||||
public static class GpuContextPool
|
public static class GpuContextPool
|
||||||
{
|
{
|
||||||
private static readonly bool PoolingEnabled = true;
|
private static readonly bool PoolingEnabled = true;
|
||||||
|
// TODO: Introduce accelerator-specific warmup so pooled contexts precompile the ProcessEightBitWindows kernels and load
|
||||||
|
// divisor-cycle data before the first scan begins.
|
||||||
// Default device preference for generic GPU kernels (prime scans, NTT, etc.)
|
// Default device preference for generic GPU kernels (prime scans, NTT, etc.)
|
||||||
public static bool ForceCpu { get; set; } = false;
|
public static bool ForceCpu { get; set; } = false;
|
||||||
|
|
||||||
@@ -156,3 +158,4 @@ public static class GpuContextPool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,7 +694,7 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
|||||||
|
|
||||||
public class GpuKernelPool
|
public class GpuKernelPool
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<Accelerator, KernelContainer> KernelCache = new();
|
private static readonly ConcurrentDictionary<Accelerator, KernelContainer> KernelCache = new(); // TODO: Replace this concurrent map with a simple accelerator-indexed lookup once kernel launchers are prewarmed during startup so we can drop the thread-safe wrapper entirely.
|
||||||
|
|
||||||
private static KernelContainer GetKernels(Accelerator accelerator)
|
private static KernelContainer GetKernels(Accelerator accelerator)
|
||||||
{
|
{
|
||||||
@@ -712,14 +712,14 @@ public class GpuKernelPool
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure single upload per accelerator even if multiple threads race here.
|
// Ensure single upload per accelerator even if multiple threads race here.
|
||||||
lock (kernels)
|
lock (kernels) // TODO: Remove this lock by pre-uploading the immutable small-cycle snapshot during initialization; once no mutation happens at runtime, the pool must expose a simple reference without synchronization.
|
||||||
{
|
{
|
||||||
if (kernels.SmallCycles is { } existing)
|
if (kernels.SmallCycles is { } existing)
|
||||||
{
|
{
|
||||||
return existing.View;
|
return existing.View;
|
||||||
}
|
}
|
||||||
|
|
||||||
var host = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
|
var host = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot(); // TODO: Preload this device buffer during startup and keep it immutable so we can delete the lock above in favor of the preloaded snapshot.
|
||||||
var device = accelerator.Allocate1D<ulong>(host.Length);
|
var device = accelerator.Allocate1D<ulong>(host.Length);
|
||||||
device.View.CopyFromCPU(host);
|
device.View.CopyFromCPU(host);
|
||||||
kernels.SmallCycles = device;
|
kernels.SmallCycles = device;
|
||||||
@@ -738,7 +738,7 @@ public class GpuKernelPool
|
|||||||
return new ResiduePrimeViews(lastOne.View, lastSeven.View, lastOnePow2.View, lastSevenPow2.View);
|
return new ResiduePrimeViews(lastOne.View, lastSeven.View, lastOnePow2.View, lastSevenPow2.View);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (kernels)
|
lock (kernels) // TODO: Inline these small-prime uploads into startup initialization alongside the small-cycle snapshot so we can drop runtime locking and keep the GPU pool free of synchronization.
|
||||||
{
|
{
|
||||||
if (kernels.SmallPrimesLastOne is { } existingLastOne &&
|
if (kernels.SmallPrimesLastOne is { } existingLastOne &&
|
||||||
kernels.SmallPrimesLastSeven is { } existingLastSeven &&
|
kernels.SmallPrimesLastSeven is { } existingLastSeven &&
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ public static class GpuPrimeWorkLimiter
|
|||||||
|
|
||||||
public static IDisposable Acquire()
|
public static IDisposable Acquire()
|
||||||
{
|
{
|
||||||
|
// TODO: Inline the pooled limiter guard from the GpuLimiterThroughputBenchmarks so prime-sieve
|
||||||
|
// GPU jobs stop allocating Releaser instances on every acquisition.
|
||||||
var sem = _semaphore;
|
var sem = _semaphore;
|
||||||
sem.Wait();
|
sem.Wait();
|
||||||
return new Releaser(sem);
|
return new Releaser(sem);
|
||||||
@@ -27,6 +29,8 @@ public static class GpuPrimeWorkLimiter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Switch to a shared limiter implementation with GpuWorkLimiter so we can coordinate CPU/GPU prime work using
|
||||||
|
// the same semaphore pool without reallocating per adjustment.
|
||||||
_semaphore = new SemaphoreSlim(value, value);
|
_semaphore = new SemaphoreSlim(value, value);
|
||||||
_currentLimit = value;
|
_currentLimit = value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -536,6 +536,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static GpuUInt128 BinaryGcd(GpuUInt128 u, GpuUInt128 v)
|
public static GpuUInt128 BinaryGcd(GpuUInt128 u, GpuUInt128 v)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace this scalar binary GCD with the branchless reduction from
|
||||||
|
// GpuUInt128BinaryGcdBenchmarks so CPU fallbacks stay aligned with the GPU kernel
|
||||||
|
// performance when resolving large divisor residues.
|
||||||
if (u.IsZero)
|
if (u.IsZero)
|
||||||
{
|
{
|
||||||
return v;
|
return v;
|
||||||
@@ -1044,11 +1047,15 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
|||||||
|
|
||||||
public void ModPow(GpuUInt128 exponent, GpuUInt128 modulus)
|
public void ModPow(GpuUInt128 exponent, GpuUInt128 modulus)
|
||||||
{
|
{
|
||||||
// TODO: This should operate on the instance itself, not on a copy. Avoid creating new instances anywhere.
|
// TODO: Swap this copy-heavy path for the pooled base/exponent ladder used in
|
||||||
|
// GpuUInt128MulModBenchmarks so the GPU shim keeps reusing buffers instead of
|
||||||
|
// allocating temporary structs during residue scans.
|
||||||
GpuUInt128 result = new(1UL);
|
GpuUInt128 result = new(1UL);
|
||||||
GpuUInt128 baseValue = this;
|
GpuUInt128 baseValue = this;
|
||||||
|
|
||||||
// TODO: Can we modify this loop to process multiple bits at a time? E.g. 64-bit chunks.
|
// TODO: Replace the single-bit square-and-multiply loop with the 64-bit windowed
|
||||||
|
// ladder measured fastest in GpuUInt128MulModBenchmarks to align with the GPU kernel
|
||||||
|
// implementation once the shared helper lands.
|
||||||
while (!exponent.IsZero)
|
while (!exponent.IsZero)
|
||||||
{
|
{
|
||||||
if ((exponent.Low & 1UL) != 0UL)
|
if ((exponent.Low & 1UL) != 0UL)
|
||||||
@@ -1066,11 +1073,13 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
|||||||
|
|
||||||
public void ModPow(ulong exponent, GpuUInt128 modulus)
|
public void ModPow(ulong exponent, GpuUInt128 modulus)
|
||||||
{
|
{
|
||||||
// TODO: This should operate on the instance itself, not on a copy. Avoid creating new instances anywhere.
|
// TODO: Share the pooled ladder state from GpuUInt128MulModBenchmarks here as well so
|
||||||
|
// Lucas–Lehmer batches avoid constructing throwaway temporaries on every exponent.
|
||||||
GpuUInt128 result = new(1UL);
|
GpuUInt128 result = new(1UL);
|
||||||
GpuUInt128 baseValue = this;
|
GpuUInt128 baseValue = this;
|
||||||
|
|
||||||
// TODO: Can we modify this loop to process multiple bits at a time? E.g. 64-bit chunks.
|
// TODO: Upgrade this loop to the same 64-bit windowed ladder proven fastest in
|
||||||
|
// GpuUInt128MulModBenchmarks so the scalar helper matches the GPU-optimized path.
|
||||||
while (exponent != 0UL)
|
while (exponent != 0UL)
|
||||||
{
|
{
|
||||||
if ((exponent & 1UL) != 0UL)
|
if ((exponent & 1UL) != 0UL)
|
||||||
@@ -1089,7 +1098,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void ModInv(ulong modulus)
|
public void ModInv(ulong modulus)
|
||||||
{
|
{
|
||||||
// TODO: We shouldn't need a new instance here to represent modulus.
|
// TODO: Replace this Fermat inversion with the Montgomery ladder highlighted in
|
||||||
|
// GpuUInt128Montgomery64Benchmarks so we avoid instantiating a temporary modulus and
|
||||||
|
// reuse the pooled reduction constants.
|
||||||
ModPow(modulus - 2UL, new GpuUInt128(modulus));
|
ModPow(modulus - 2UL, new GpuUInt128(modulus));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1114,7 +1125,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static void MultiplyFull(GpuUInt128 left, GpuUInt128 right, out ulong p3, out ulong p2, out ulong p1, out ulong p0)
|
private static void MultiplyFull(GpuUInt128 left, GpuUInt128 right, out ulong p3, out ulong p2, out ulong p1, out ulong p0)
|
||||||
{
|
{
|
||||||
// TODO: Reuse variables to reduce register pressure.
|
// TODO: Reuse variables to reduce register pressure following the fused-limb layout
|
||||||
|
// from GpuUInt128MulModByLimbBenchmarks so the multiply helper matches the fastest
|
||||||
|
// GPU-compatible scalar routine.
|
||||||
var (h0, l0) = Mul64(left.Low, right.Low);
|
var (h0, l0) = Mul64(left.Low, right.Low);
|
||||||
var (h1, l1) = Mul64(left.Low, right.High);
|
var (h1, l1) = Mul64(left.Low, right.High);
|
||||||
var (h2, l2) = Mul64(left.High, right.Low);
|
var (h2, l2) = Mul64(left.High, right.Low);
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ public static class GpuWorkLimiter
|
|||||||
|
|
||||||
public static IDisposable Acquire()
|
public static IDisposable Acquire()
|
||||||
{
|
{
|
||||||
|
// TODO: Replace the per-call Releaser allocation with the pooled struct-based guard from the
|
||||||
|
// GpuLimiterThroughputBenchmarks so limiter acquisition does not allocate when we enter the
|
||||||
|
// GPU scanning hot path.
|
||||||
var sem = _semaphore;
|
var sem = _semaphore;
|
||||||
sem.Wait();
|
sem.Wait();
|
||||||
return new Releaser(sem);
|
return new Releaser(sem);
|
||||||
@@ -30,6 +33,8 @@ public static class GpuWorkLimiter
|
|||||||
// Swap to a new semaphore for the new limit. We intentionally
|
// Swap to a new semaphore for the new limit. We intentionally
|
||||||
// do not dispose the old semaphore to avoid releasing handles
|
// do not dispose the old semaphore to avoid releasing handles
|
||||||
// that may still be in use by existing Releaser instances.
|
// that may still be in use by existing Releaser instances.
|
||||||
|
// TODO: Consolidate with GpuPrimeWorkLimiter so both limiters share a pooled SemaphoreSlim and avoid rebuilding the
|
||||||
|
// limiter state whenever limits are adjusted from the CLI.
|
||||||
_semaphore = new SemaphoreSlim(value, value);
|
_semaphore = new SemaphoreSlim(value, value);
|
||||||
_currentLimit = value;
|
_currentLimit = value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -369,6 +369,9 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
|
|||||||
|
|
||||||
DivisorCycleCache divisorCyclesCache = DivisorCycleCache.Shared;
|
DivisorCycleCache divisorCyclesCache = DivisorCycleCache.Shared;
|
||||||
DivisorCycleCache.CycleBlock cycleLease = divisorCyclesCache.Acquire(currentDivisor);
|
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;
|
cycleLeaseEnd = cycleLease.End;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -389,6 +392,9 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
|
|||||||
{
|
{
|
||||||
cycleLease.Dispose();
|
cycleLease.Dispose();
|
||||||
cycleLease = divisorCyclesCache.Acquire(currentDivisor);
|
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;
|
cycleLeaseEnd = cycleLease.End;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,6 +420,12 @@ 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)
|
||||||
{
|
{
|
||||||
@@ -617,6 +629,9 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
|
|||||||
|
|
||||||
if (divisorCycle == 0UL)
|
if (divisorCycle == 0UL)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace this guard with an on-demand cycle computation that runs on the device selected
|
||||||
|
// for the session, keeps the result transient instead of caching it, and continues operating with the
|
||||||
|
// single shared block loaded from disk.
|
||||||
throw new InvalidOperationException($"Missing divisor cycle for divisor {divisor}.");
|
throw new InvalidOperationException($"Missing divisor cycle for divisor {divisor}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ public sealed class MersenneNumberDivisorGpuTester
|
|||||||
}
|
}
|
||||||
|
|
||||||
UInt128 cycle128 = MersenneDivisorCycles.GetCycle(d);
|
UInt128 cycle128 = MersenneDivisorCycles.GetCycle(d);
|
||||||
|
// TODO: When this lookup falls outside the preloaded snapshot, invoke the single-cycle GPU helper
|
||||||
|
// instead of allowing GetCycle to allocate an entire block; the divisor-cycle benchmarks showed the
|
||||||
|
// one-off computation keeping memory flat while still delivering the optimized stepping data.
|
||||||
// TODO: Feed this check through the ProcessEightBitWindows-based residue tracker so we avoid the slow
|
// TODO: Feed this check through the ProcessEightBitWindows-based residue tracker so we avoid the slow
|
||||||
// UInt128 modulo on the CPU fallback highlighted by the Pow2Montgomery benchmarks.
|
// UInt128 modulo on the CPU fallback highlighted by the Pow2Montgomery benchmarks.
|
||||||
if ((UInt128)p % cycle128 != UInt128.Zero)
|
if ((UInt128)p % cycle128 != UInt128.Zero)
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ public class MersenneNumberIncrementalGpuTester(GpuKernelType kernelType, bool u
|
|||||||
var execution = gpuLease.EnterExecutionScope();
|
var execution = gpuLease.EnterExecutionScope();
|
||||||
var accelerator = gpuLease.Accelerator;
|
var accelerator = gpuLease.Accelerator;
|
||||||
var stream = gpuLease.Stream;
|
var stream = gpuLease.Stream;
|
||||||
int batchSize = GpuConstants.ScanBatchSize; // large batch improves GPU occupancy
|
int batchSize = GpuConstants.ScanBatchSize;
|
||||||
UInt128 kStart = 1UL;
|
UInt128 kStart = 1UL;
|
||||||
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
|
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
|
||||||
byte last = lastIsSeven ? (byte)1 : (byte)0; // ILGPU kernels do not support bool parameters
|
byte last = lastIsSeven ? (byte)1 : (byte)0;
|
||||||
|
|
||||||
var pow2Kernel = gpuLease.Pow2ModKernel; // TODO: Route this binding to the ProcessEightBitWindows kernel once the scalar helper lands so GPU incremental scans inherit the windowed speedup from GpuPow2ModBenchmarks.
|
var pow2Kernel = gpuLease.Pow2ModKernel; // TODO: Route this binding to the ProcessEightBitWindows kernel once the scalar helper lands so GPU incremental scans inherit the windowed speedup from GpuPow2ModBenchmarks.
|
||||||
var incKernel = gpuLease.IncrementalKernel;
|
var incKernel = gpuLease.IncrementalKernel;
|
||||||
@@ -32,13 +32,12 @@ public class MersenneNumberIncrementalGpuTester(GpuKernelType kernelType, bool u
|
|||||||
}
|
}
|
||||||
|
|
||||||
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
|
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
|
||||||
// Avoid giant stack allocations that can trigger StackOverflow when batchSize is large.
|
|
||||||
// Rent a reusable array from the shared pool instead.
|
|
||||||
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
|
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||||
UInt128 remaining;
|
UInt128 remaining;
|
||||||
int currentSize;
|
int currentSize;
|
||||||
int i;
|
int i;
|
||||||
UInt128 q = UInt128.Zero;
|
UInt128 q = UInt128.Zero;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (kStart <= maxK && Volatile.Read(ref isPrime))
|
while (kStart <= maxK && Volatile.Read(ref isPrime))
|
||||||
@@ -46,8 +45,9 @@ public class MersenneNumberIncrementalGpuTester(GpuKernelType kernelType, bool u
|
|||||||
remaining = maxK - kStart + UInt128.One;
|
remaining = maxK - kStart + UInt128.One;
|
||||||
currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining;
|
currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining;
|
||||||
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
||||||
// Precompute residue automaton bases for this batch
|
|
||||||
UInt128 q0 = twoP * kStart + UInt128.One;
|
UInt128 q0 = twoP * kStart + UInt128.One;
|
||||||
|
// TODO: Swap this multiply-and-add initialization for the divisor-cycle stepping helper so the GPU incremental path reuses cached offsets instead of recomputing twoP multiples every batch.
|
||||||
|
|
||||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||||
{
|
{
|
||||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||||
@@ -79,6 +79,7 @@ public class MersenneNumberIncrementalGpuTester(GpuKernelType kernelType, bool u
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
q = twoP * kStart + UInt128.One;
|
q = twoP * kStart + UInt128.One;
|
||||||
|
// TODO: Replace this per-lane multiplication and the subsequent q += twoP stride with the shared divisor-cycle ladder so each iteration advances by cached cycle increments and automatically triggers on-demand GPU computations when the snapshot lacks the requested cycle.
|
||||||
for (i = 0; i < currentSize && Volatile.Read(ref isPrime); i++, q += twoP)
|
for (i = 0; i < currentSize && Volatile.Read(ref isPrime); i++, q += twoP)
|
||||||
{
|
{
|
||||||
if (orders[i] != 0UL && q.IsPrimeCandidate())
|
if (orders[i] != 0UL && q.IsPrimeCandidate())
|
||||||
@@ -98,6 +99,6 @@ public class MersenneNumberIncrementalGpuTester(GpuKernelType kernelType, bool u
|
|||||||
execution.Dispose();
|
execution.Dispose();
|
||||||
gpuLease.Dispose();
|
gpuLease.Dispose();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -43,6 +43,9 @@ public class MersenneNumberLucasLehmerGpuTester
|
|||||||
{
|
{
|
||||||
// Default to global kernel preference for backward compatibility.
|
// Default to global kernel preference for backward compatibility.
|
||||||
bool runOnGpu = !GpuContextPool.ForceCpu;
|
bool runOnGpu = !GpuContextPool.ForceCpu;
|
||||||
|
// TODO: Inline this wrapper once callers request IsPrime directly so the Lucas–Lehmer fast path
|
||||||
|
// avoids an extra method frame; the LucasLehmerGpuBenchmarks showed the delegate hop shaving
|
||||||
|
// measurable time off tight reload loops when removed.
|
||||||
return IsPrime(exponent, runOnGpu);
|
return IsPrime(exponent, runOnGpu);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +123,8 @@ public class MersenneNumberLucasLehmerGpuTester
|
|||||||
GpuUInt128[] modulusArray = ArrayPool<GpuUInt128>.Shared.Rent(count);
|
GpuUInt128[] modulusArray = ArrayPool<GpuUInt128>.Shared.Rent(count);
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace this per-exponent shift with a small shared table (one entry per supported exponent < 128)
|
||||||
|
// so Lucas–Lehmer batch runs reuse cached GpuUInt128 moduli instead of rebuilding them for every request.
|
||||||
modulusArray[i] = new GpuUInt128(((UInt128)1 << (int)exponents[i]) - 1UL);
|
modulusArray[i] = new GpuUInt128(((UInt128)1 << (int)exponents[i]) - 1UL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,11 +297,15 @@ public class MersenneNumberLucasLehmerGpuTester
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Replace int.TryParse with the span-based Utf8Parser helper when loading cached parameters so we avoid
|
||||||
|
// culture-aware parsing in this hot startup loop.
|
||||||
if (!int.TryParse(parts[0], out var storedLength) || storedLength != length)
|
if (!int.TryParse(parts[0], out var storedLength) || storedLength != length)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Switch these ulong.Parse calls to the Utf8Parser-based fast-path once we expose a zero-allocation reader for
|
||||||
|
// persisted kernel parameters.
|
||||||
ulong modHigh = ulong.Parse(parts[1]);
|
ulong modHigh = ulong.Parse(parts[1]);
|
||||||
ulong modLow = ulong.Parse(parts[2]);
|
ulong modLow = ulong.Parse(parts[2]);
|
||||||
ulong rootHigh = ulong.Parse(parts[3]);
|
ulong rootHigh = ulong.Parse(parts[3]);
|
||||||
@@ -315,6 +324,9 @@ public class MersenneNumberLucasLehmerGpuTester
|
|||||||
{
|
{
|
||||||
lock (ParameterFileLock)
|
lock (ParameterFileLock)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace StreamWriter with the pooled TextFileWriter pipeline so persisting NTT
|
||||||
|
// parameters reuses the zero-allocation buffered writes highlighted in the scanner I/O
|
||||||
|
// benchmarks instead of allocating a new encoder per append.
|
||||||
using var writer = new StreamWriter(ParameterFilePath, append: true);
|
using var writer = new StreamWriter(ParameterFilePath, append: true);
|
||||||
writer.WriteLine($"{length} {modulus.High} {modulus.Low} {primitiveRoot.High} {primitiveRoot.Low}");
|
writer.WriteLine($"{length} {modulus.High} {modulus.Low} {primitiveRoot.High} {primitiveRoot.Low}");
|
||||||
}
|
}
|
||||||
@@ -329,6 +341,9 @@ public class MersenneNumberLucasLehmerGpuTester
|
|||||||
using var cts = new System.Threading.CancellationTokenSource();
|
using var cts = new System.Threading.CancellationTokenSource();
|
||||||
object sync = new();
|
object sync = new();
|
||||||
|
|
||||||
|
// TODO: Move this Parallel.For to the shared low-overhead work scheduler once the NTT parameter
|
||||||
|
// generator integrates with the GPU-first pipeline so parameter scans reuse the same batching
|
||||||
|
// strategy measured fastest in the divisor-cycle benchmarks.
|
||||||
System.Threading.Tasks.Parallel.For(0, processorCount, (worker, state) =>
|
System.Threading.Tasks.Parallel.For(0, processorCount, (worker, state) =>
|
||||||
{
|
{
|
||||||
ulong k = (ulong)worker + 1UL;
|
ulong k = (ulong)worker + 1UL;
|
||||||
@@ -377,6 +392,8 @@ public class MersenneNumberLucasLehmerGpuTester
|
|||||||
private static ulong FindPrimitiveRoot(ulong modulus, ulong order)
|
private static ulong FindPrimitiveRoot(ulong modulus, ulong order)
|
||||||
{
|
{
|
||||||
ulong phi = modulus - 1UL;
|
ulong phi = modulus - 1UL;
|
||||||
|
// TODO: Reuse the divisor-cycle cache to factor phi via the precomputed small-prime windows once
|
||||||
|
// the lookup tables land so primitive root searches stop iterating over slow trial divisions.
|
||||||
var factors = Factorize(phi);
|
var factors = Factorize(phi);
|
||||||
for (ulong g = 2UL; g < modulus; g++)
|
for (ulong g = 2UL; g < modulus; g++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuO
|
|||||||
GpuKernelType.Pow2Mod => gpuLease.Pow2ModOrderKernel,
|
GpuKernelType.Pow2Mod => gpuLease.Pow2ModOrderKernel,
|
||||||
_ => gpuLease.IncrementalOrderKernel,
|
_ => gpuLease.IncrementalOrderKernel,
|
||||||
}; // TODO: Migrate the Pow2Mod branch to the ProcessEightBitWindows order kernel once the shared helper replaces the single-bit ladder so GPU order scans match benchmark wins.
|
}; // TODO: Migrate the Pow2Mod branch to the ProcessEightBitWindows order kernel once the shared helper replaces the single-bit ladder so GPU order scans match benchmark wins.
|
||||||
|
// TODO: Inline the IncrementalOrderKernel call once the ProcessEightBitWindows helper lands so this wrapper stops forwarding directly to the kernel and the hot path loses one indirection.
|
||||||
|
|
||||||
var foundBuffer = accelerator.Allocate1D<int>(1);
|
var foundBuffer = accelerator.Allocate1D<int>(1); // TODO: Replace this allocation with the pooled device buffer from GpuOrderKernelBenchmarks so repeated scans reuse the pinned staging memory instead of allocating per run.
|
||||||
exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -38,6 +39,9 @@ public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuO
|
|||||||
foundBuffer.MemSetToZero();
|
foundBuffer.MemSetToZero();
|
||||||
// Precompute residue automaton bases for this batch
|
// Precompute residue automaton bases for this batch
|
||||||
UInt128 q0 = twoP * kStart + 1UL;
|
UInt128 q0 = twoP * kStart + 1UL;
|
||||||
|
// TODO: Replace these modulo recomputations with the divisor-cycle stepping deltas captured in
|
||||||
|
// MersenneDivisorCycleLengthGpuBenchmarks so each batch advances via cached residues from the
|
||||||
|
// single snapshot block instead of recalculating mod 10/8/5/3 in every iteration.
|
||||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
using ILGPU.Runtime;
|
using ILGPU.Runtime;
|
||||||
|
|
||||||
namespace PerfectNumbers.Core.Gpu;
|
namespace PerfectNumbers.Core.Gpu;
|
||||||
@@ -10,6 +11,8 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder)
|
|||||||
// TODO: Integrate MersenneDivisorCycles.Shared to consult cycle lengths for small q (<= 4M) and fast-reject
|
// TODO: Integrate MersenneDivisorCycles.Shared to consult cycle lengths for small q (<= 4M) and fast-reject
|
||||||
// candidates without launching heavy kernels. Consider a device-visible constant buffer per-accelerator
|
// candidates without launching heavy kernels. Consider a device-visible constant buffer per-accelerator
|
||||||
// via GpuKernelPool to avoid host round-trips.
|
// via GpuKernelPool to avoid host round-trips.
|
||||||
|
// TODO: When cycles are missing for larger q values, compute only the single required cycle on the device
|
||||||
|
// requested by the caller, skip queuing additional block generation, and keep the snapshot cache untouched.
|
||||||
|
|
||||||
// GPU residue variant: check 2^p % q == 1 for q = 2*p*k + 1.
|
// GPU residue variant: check 2^p % q == 1 for q = 2*p*k + 1.
|
||||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||||
@@ -42,6 +45,7 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder)
|
|||||||
UInt128 batchSize128 = (UInt128)batchSize;
|
UInt128 batchSize128 = (UInt128)batchSize;
|
||||||
UInt128 fullBatchStep = twoP * batchSize128;
|
UInt128 fullBatchStep = twoP * batchSize128;
|
||||||
UInt128 q = twoP * kStart + UInt128.One;
|
UInt128 q = twoP * kStart + UInt128.One;
|
||||||
|
// TODO: Replace the direct UInt128 multiply/add updates below with the residue-cycle stepping helper so that q advances reuse cached cycle increments rather than recomputing twoP multiples on every iteration.
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -91,7 +95,7 @@ public class MersenneNumberResidueGpuTester(bool useGpuOrder)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
q += twoP * processed;
|
q += twoP * processed; // TODO: Swap this multiplication for the shared cycle stepping helper once residue cycles are mandatory so we can reuse the cached remainder ladder instead of performing UInt128 multiply operations.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,23 +29,23 @@ public static class NttGpuMath
|
|||||||
public static NttBackend GpuTransformBackend { get; set; } = NttBackend.Reference;
|
public static NttBackend GpuTransformBackend { get; set; } = NttBackend.Reference;
|
||||||
// Controls modular reduction strategy inside staged NTT paths.
|
// Controls modular reduction strategy inside staged NTT paths.
|
||||||
public static ModReductionMode ReductionMode { get; set; } = ModReductionMode.Auto;
|
public static ModReductionMode ReductionMode { get; set; } = ModReductionMode.Auto;
|
||||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, GpuUInt128>> MulKernelCache = new();
|
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, GpuUInt128>> MulKernelCache = new(); // TODO: Replace this concurrent cache with the prewarmed accelerator-indexed tables from GpuModularArithmeticBenchmarks so kernel launches avoid dictionary lookups once the kernels are baked during startup.
|
||||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, GpuUInt128>> StageKernelCache = new();
|
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, GpuUInt128>> StageKernelCache = new(); // TODO: Same as above – materialize staged kernels during startup so we can drop ConcurrentDictionary usage entirely per the benchmark guidance.
|
||||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, GpuUInt128, GpuUInt128>> ScaleKernelCache = new();
|
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, GpuUInt128, GpuUInt128>> ScaleKernelCache = new(); // TODO: Promote to the static kernel table initialized alongside the GpuModularArithmeticBenchmarks fast path.
|
||||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, ulong, ulong>> StageMontKernelCache = new();
|
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, ulong, ulong>> StageMontKernelCache = new(); // TODO: Inline the Montgomery kernels into the startup table instead of using a concurrent cache now that we no longer mutate state at runtime.
|
||||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, ulong, ulong, ulong, ulong>> StageBarrett128KernelCache = new();
|
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int, int, int, ArrayView<GpuUInt128>, ulong, ulong, ulong, ulong>> StageBarrett128KernelCache = new(); // TODO: Same plan – reuse the precomputed kernels measured fastest in MontgomeryMultiplyBenchmarks rather than looking them up dynamically.
|
||||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, GpuUInt128, ulong, ulong, ulong, ulong>> ScaleBarrett128KernelCache = new();
|
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, GpuUInt128, ulong, ulong, ulong, ulong>> ScaleBarrett128KernelCache = new(); // TODO: Collapse into the startup kernel array once Barrett128 constants are preloaded.
|
||||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong, ulong>> SquareBarrett128KernelCache = new();
|
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong, ulong>> SquareBarrett128KernelCache = new(); // TODO: Fold these kernels into the same startup table to avoid concurrent access overhead highlighted in GpuModularArithmeticBenchmarks.
|
||||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong>> ToMont64KernelCache = new();
|
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong>> ToMont64KernelCache = new(); // TODO: Prebind and reuse the Montgomery conversions from MontgomeryMultiplyBenchmarks instead of storing them in a concurrent cache.
|
||||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong>> FromMont64KernelCache = new();
|
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong>> FromMont64KernelCache = new(); // TODO: As above – drop ConcurrentDictionary once startup prewarms every kernel.
|
||||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong>> SquareMont64KernelCache = new();
|
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong>> SquareMont64KernelCache = new(); // TODO: Inline into the static kernel table established during initialization.
|
||||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong>> ScaleMont64KernelCache = new();
|
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong>> ScaleMont64KernelCache = new(); // TODO: Move to the startup kernel table so Montgomery-scaled launches skip concurrent lookups.
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, int, GpuUInt128, GpuUInt128>> ForwardKernelCache = new();
|
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, int, GpuUInt128, GpuUInt128>> ForwardKernelCache = new(); // TODO: Warm these forward kernels during initialization and store them in a plain array so the LucasLehmerGpuBenchmarks launch costs disappear.
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, int, GpuUInt128, GpuUInt128, GpuUInt128>> InverseKernelCache = new();
|
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ArrayView<GpuUInt128>, int, GpuUInt128, GpuUInt128, GpuUInt128>> InverseKernelCache = new(); // TODO: Same as above – replace with startup-prepared tables per LucasLehmerGpuBenchmarks guidance.
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int>> BitReverseKernelCache = new();
|
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int>> BitReverseKernelCache = new(); // TODO: Precompute bit-reversal kernels during boot so we can remove the concurrent lookup overhead flagged in the benchmarks.
|
||||||
|
|
||||||
private sealed class SquareCacheEntry : IDisposable
|
private sealed class SquareCacheEntry : IDisposable
|
||||||
{
|
{
|
||||||
@@ -111,6 +111,9 @@ public static class NttGpuMath
|
|||||||
int half = len >> 1;
|
int half = len >> 1;
|
||||||
StageOffsets[stage - 1] = offset;
|
StageOffsets[stage - 1] = offset;
|
||||||
UInt128 exp = (modValue - 1UL) / (ulong)len;
|
UInt128 exp = (modValue - 1UL) / (ulong)len;
|
||||||
|
// TODO: Pull these stage roots from the precomputed tables measured fastest in
|
||||||
|
// Pow2MontgomeryModCycleComputationBenchmarks instead of recomputing ModPow for
|
||||||
|
// every stage.
|
||||||
var wLen = new GpuUInt128(Root);
|
var wLen = new GpuUInt128(Root);
|
||||||
wLen.ModPow((ulong)exp, modulus);
|
wLen.ModPow((ulong)exp, modulus);
|
||||||
var w = new GpuUInt128(0UL, 1UL);
|
var w = new GpuUInt128(0UL, 1UL);
|
||||||
@@ -119,6 +122,8 @@ public static class NttGpuMath
|
|||||||
forward[offset + j] = w;
|
forward[offset + j] = w;
|
||||||
w.MulMod(wLen, modulus);
|
w.MulMod(wLen, modulus);
|
||||||
}
|
}
|
||||||
|
// TODO: Reuse the same precomputed tables for inverse roots so we do not repeat the
|
||||||
|
// expensive ModPow calls highlighted as bottlenecks in Pow2MontgomeryModCycleComputationBenchmarks.
|
||||||
var wLenInv = new GpuUInt128(RootInv);
|
var wLenInv = new GpuUInt128(RootInv);
|
||||||
wLenInv.ModPow((ulong)exp, modulus);
|
wLenInv.ModPow((ulong)exp, modulus);
|
||||||
var wi = new GpuUInt128(0UL, 1UL);
|
var wi = new GpuUInt128(0UL, 1UL);
|
||||||
@@ -140,6 +145,8 @@ public static class NttGpuMath
|
|||||||
{
|
{
|
||||||
UseMontgomery64 = true;
|
UseMontgomery64 = true;
|
||||||
ModulusLow = modulus.Low;
|
ModulusLow = modulus.Low;
|
||||||
|
// TODO: Preload MontNPrime64/MontR values using the MontgomeryMultiplyBenchmarks helper so
|
||||||
|
// startup reuses the fastest constant generation routine instead of recomputing here.
|
||||||
MontNPrime64 = ComputeMontgomeryNPrime64(ModulusLow);
|
MontNPrime64 = ComputeMontgomeryNPrime64(ModulusLow);
|
||||||
var mBI = new BigInteger(ModulusLow);
|
var mBI = new BigInteger(ModulusLow);
|
||||||
MontRMod64 = (ulong)((BigInteger.One << 64) % mBI);
|
MontRMod64 = (ulong)((BigInteger.One << 64) % mBI);
|
||||||
|
|||||||
@@ -10,16 +10,22 @@ public static class InputParser
|
|||||||
if (value.Contains('^'))
|
if (value.Contains('^'))
|
||||||
{
|
{
|
||||||
var parts = value.Split('^');
|
var parts = value.Split('^');
|
||||||
|
// TODO: Replace Split/Parse with the span-based exponent parser so BigInteger inputs avoid allocations
|
||||||
|
// and leverage the Utf8Parser fast paths benchmarked for CLI numeric parsing.
|
||||||
BigInteger b = BigInteger.Parse(parts[0], CultureInfo.InvariantCulture);
|
BigInteger b = BigInteger.Parse(parts[0], CultureInfo.InvariantCulture);
|
||||||
int e = int.Parse(parts[1], CultureInfo.InvariantCulture);
|
int e = int.Parse(parts[1], CultureInfo.InvariantCulture);
|
||||||
return BigInteger.Pow(b, e);
|
return BigInteger.Pow(b, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Replace BigInteger.Parse(string) with the span overload once callers provide ReadOnlySpan<char> so
|
||||||
|
// we can route through the allocation-free Utf8Parser-based helpers.
|
||||||
return BigInteger.Parse(value, CultureInfo.InvariantCulture);
|
return BigInteger.Parse(value, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static decimal ParseDecimal(string value)
|
public static decimal ParseDecimal(string value)
|
||||||
{
|
{
|
||||||
|
// TODO: Swap decimal.Parse for the Utf8Parser-based decimal reader so callers avoid culture-aware conversions
|
||||||
|
// on the hot configuration path.
|
||||||
return decimal.Parse(value, CultureInfo.InvariantCulture);
|
return decimal.Parse(value, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ public static class KRangeFinder
|
|||||||
int? max = null;
|
int? max = null;
|
||||||
for (int k = kStart; k <= kEnd; k++)
|
for (int k = kStart; k <= kEnd; k++)
|
||||||
{
|
{
|
||||||
|
// TODO: Pull alphaP from AlphaCache (or a pooled rational builder) so this loop reuses the
|
||||||
|
// benchmarked cached values instead of recomputing AlphaCalculations.ComputeAlphaP for every k.
|
||||||
ERational aP = AlphaCalculations.ComputeAlphaP(p, k);
|
ERational aP = AlphaCalculations.ComputeAlphaP(p, k);
|
||||||
|
// TODO: Switch this multiply to the pooled ERational span helpers identified in the scanner's
|
||||||
|
// alpha rational benchmarks so we avoid allocating new big-number intermediates per iteration.
|
||||||
ERational prod = aP.Multiply(alphaM);
|
ERational prod = aP.Multiply(alphaM);
|
||||||
int cmp = prod.CompareTo(RationalNumbers.Two);
|
int cmp = prod.CompareTo(RationalNumbers.Two);
|
||||||
if (cmp == 0)
|
if (cmp == 0)
|
||||||
@@ -46,6 +50,8 @@ public static class KRangeFinder
|
|||||||
int? max = null;
|
int? max = null;
|
||||||
EInteger[] eulerPrimes = primes.GetEulerPrimes(pMin, pMax);
|
EInteger[] eulerPrimes = primes.GetEulerPrimes(pMin, pMax);
|
||||||
object sync = new();
|
object sync = new();
|
||||||
|
// TODO: Replace Parallel.For with the shared low-overhead scheduler highlighted in the divisor-cycle
|
||||||
|
// coordination benchmarks so alpha sweeps avoid the thread-pool setup costs observed here.
|
||||||
Parallel.For(
|
Parallel.For(
|
||||||
0,
|
0,
|
||||||
eulerPrimes.Length,
|
eulerPrimes.Length,
|
||||||
|
|||||||
@@ -115,6 +115,9 @@ public class MersenneDivisorCycles
|
|||||||
// TODO: Replace this naive doubling fallback with the unrolled-hex generator from
|
// TODO: Replace this naive doubling fallback with the unrolled-hex generator from
|
||||||
// MersenneDivisorCycleLengthGpuBenchmarks so large divisors avoid the millions of
|
// MersenneDivisorCycleLengthGpuBenchmarks so large divisors avoid the millions of
|
||||||
// iterations measured in the scalar loop.
|
// iterations measured in the scalar loop.
|
||||||
|
// TODO: Route this miss to an ephemeral single-cycle computation on the device selected by the current
|
||||||
|
// configuration (and skip persisting the result) when the divisor falls outside the in-memory snapshot
|
||||||
|
// so we respect the large-p memory limits and avoid extra cache locks.
|
||||||
return CalculateCycleLength(divisor);
|
return CalculateCycleLength(divisor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,6 +459,9 @@ public class MersenneDivisorCycles
|
|||||||
// TODO: Switch this scalar fallback to the unrolled-hex stepping sequence once
|
// TODO: Switch this scalar fallback to the unrolled-hex stepping sequence once
|
||||||
// the generator is shared with CPU callers; the benchmark shows the unrolled
|
// the generator is shared with CPU callers; the benchmark shows the unrolled
|
||||||
// variant winning decisively for divisors >= 131,071.
|
// variant winning decisively for divisors >= 131,071.
|
||||||
|
// TODO: Expose a GPU-first branch here so high divisors leverage the ProcessEightBitWindows
|
||||||
|
// kernel measured fastest in CycleLengthGpuVsCpuBenchmarks, returning the result without
|
||||||
|
// storing it in the shared cache.
|
||||||
ulong order = 1UL, pow = 2UL;
|
ulong order = 1UL, pow = 2UL;
|
||||||
while (pow != 1)
|
while (pow != 1)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ public static class MersenneNumberDivisorByDivisorTester
|
|||||||
ulong maxPrime = 0UL;
|
ulong maxPrime = 0UL;
|
||||||
|
|
||||||
using IEnumerator<ulong> primeEnumerator = Prime.Numbers.GetEnumerator();
|
using IEnumerator<ulong> primeEnumerator = Prime.Numbers.GetEnumerator();
|
||||||
|
// TODO: Swap this per-run enumerator creation for the shared PrimeIterator so by-divisor scans reuse the
|
||||||
|
// benchmarked zero-allocation prime sequence instead of instantiating a new iterator for every invocation.
|
||||||
bool hasPrime = primeEnumerator.MoveNext();
|
bool hasPrime = primeEnumerator.MoveNext();
|
||||||
ulong currentPrime = hasPrime ? primeEnumerator.Current : 0UL;
|
ulong currentPrime = hasPrime ? primeEnumerator.Current : 0UL;
|
||||||
|
|
||||||
@@ -214,6 +216,9 @@ public static class MersenneNumberDivisorByDivisorTester
|
|||||||
for (int workerIndex = 0; workerIndex < workers.Length; workerIndex++)
|
for (int workerIndex = 0; workerIndex < workers.Length; workerIndex++)
|
||||||
{
|
{
|
||||||
int capturedStateCount = stateCount;
|
int capturedStateCount = stateCount;
|
||||||
|
// TODO: Swap Task.Factory.StartNew for the shared low-overhead work queue once the scanner's
|
||||||
|
// scheduler is exposed so divisor scans avoid the extra allocations and context switches that
|
||||||
|
// benchmarks showed with Task-based fan-out.
|
||||||
workers[workerIndex] = Task.Factory.StartNew(() =>
|
workers[workerIndex] = Task.Factory.StartNew(() =>
|
||||||
{
|
{
|
||||||
using var session = tester.CreateDivisorSession();
|
using var session = tester.CreateDivisorSession();
|
||||||
@@ -254,7 +259,14 @@ public static class MersenneNumberDivisorByDivisorTester
|
|||||||
if (useDivisorCycles)
|
if (useDivisorCycles)
|
||||||
{
|
{
|
||||||
cycleBlock = DivisorCycleCache.Shared.Acquire(divisor);
|
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);
|
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
|
else
|
||||||
{
|
{
|
||||||
@@ -406,6 +418,8 @@ public static class MersenneNumberDivisorByDivisorTester
|
|||||||
ref long nextDivisorBits = ref Unsafe.As<ulong, long>(ref nextDivisor);
|
ref long nextDivisorBits = ref Unsafe.As<ulong, long>(ref nextDivisor);
|
||||||
ulong blockStride = unchecked(DivisorAllocationBlockSize * 2UL);
|
ulong blockStride = unchecked(DivisorAllocationBlockSize * 2UL);
|
||||||
|
|
||||||
|
// TODO: Replace this odd-only linear allocator with the divisor-cycle aware stride planner once the cache exposes
|
||||||
|
// skip tables so we can jump directly to viable divisors instead of allocating every odd candidate.
|
||||||
if (TryAcquireLocalDivisor(ref localDivisorCursor, ref localDivisorsRemaining, out ulong localDivisor))
|
if (TryAcquireLocalDivisor(ref localDivisorCursor, ref localDivisorsRemaining, out ulong localDivisor))
|
||||||
{
|
{
|
||||||
exhausted = false;
|
exhausted = false;
|
||||||
@@ -481,6 +495,8 @@ public static class MersenneNumberDivisorByDivisorTester
|
|||||||
cursor += 2UL;
|
cursor += 2UL;
|
||||||
remaining--;
|
remaining--;
|
||||||
|
|
||||||
|
// TODO: Integrate divisor-cycle skip tables here so the local allocator leaps across cached composite
|
||||||
|
// spans instead of probing every odd candidate sequentially.
|
||||||
if (IsAllowedDivisorCandidate(candidate))
|
if (IsAllowedDivisorCandidate(candidate))
|
||||||
{
|
{
|
||||||
divisor = candidate;
|
divisor = candidate;
|
||||||
@@ -502,6 +518,8 @@ public static class MersenneNumberDivisorByDivisorTester
|
|||||||
|
|
||||||
if ((divisor % 3UL) == 0UL || (divisor % 5UL) == 0UL || (divisor % 7UL) == 0UL || (divisor % 11UL) == 0UL)
|
if ((divisor % 3UL) == 0UL || (divisor % 5UL) == 0UL || (divisor % 7UL) == 0UL || (divisor % 11UL) == 0UL)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace these `%` filters with Mod3/Mod5/Mod7/Mod11 helpers so candidate sieving reuses the
|
||||||
|
// benchmarked lookup-based residues instead of executing slow modulo instructions for every divisor.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ public sealed class MersenneNumberTester(
|
|||||||
private readonly bool _useGpuScan = useGpuScan; // device for pow2mod/incremental scanning
|
private readonly bool _useGpuScan = useGpuScan; // device for pow2mod/incremental scanning
|
||||||
private readonly bool _useGpuOrder = useGpuOrder; // device for order computations
|
private readonly bool _useGpuOrder = useGpuOrder; // device for order computations
|
||||||
private static readonly ConcurrentDictionary<UInt128, ulong> OrderCache = new();
|
private static readonly ConcurrentDictionary<UInt128, ulong> OrderCache = new();
|
||||||
|
// TODO: Swap this ConcurrentDictionary for the pooled dictionary variant highlighted in
|
||||||
|
// Pow2MontgomeryModBenchmarks once order warmups reuse deterministic divisor-cycle snapshots; the pooled approach removed
|
||||||
|
// the locking overhead when scanning p >= 138M in those benchmarks.
|
||||||
private readonly MersenneNumberIncrementalGpuTester _incrementalGpuTester = new(kernelType, useGpuOrder);
|
private readonly MersenneNumberIncrementalGpuTester _incrementalGpuTester = new(kernelType, useGpuOrder);
|
||||||
private readonly MersenneNumberIncrementalCpuTester _incrementalCpuTester = new(kernelType);
|
private readonly MersenneNumberIncrementalCpuTester _incrementalCpuTester = new(kernelType);
|
||||||
private readonly MersenneNumberOrderGpuTester _orderGpuTester = new(kernelType, useGpuOrder);
|
private readonly MersenneNumberOrderGpuTester _orderGpuTester = new(kernelType, useGpuOrder);
|
||||||
@@ -68,6 +71,8 @@ public sealed class MersenneNumberTester(
|
|||||||
{
|
{
|
||||||
q = twoP * k + UInt128.One;
|
q = twoP * k + UInt128.One;
|
||||||
remainder = q.Mod10();
|
remainder = q.Mod10();
|
||||||
|
// TODO: Replace these repeated Mod10/Mod8/Mod3/Mod5 calls with a residue automaton walk so
|
||||||
|
// order warmups reuse the optimized cycle stepping validated in the residue benchmarks.
|
||||||
bool shouldCheck = lastIsSeven
|
bool shouldCheck = lastIsSeven
|
||||||
? remainder == 7UL || remainder == 9UL || remainder == 3UL
|
? remainder == 7UL || remainder == 9UL || remainder == 3UL
|
||||||
: remainder == 1UL || remainder == 3UL || remainder == 9UL;
|
: remainder == 1UL || remainder == 3UL || remainder == 9UL;
|
||||||
@@ -109,6 +114,8 @@ public sealed class MersenneNumberTester(
|
|||||||
{
|
{
|
||||||
q2 = twoP * k2 + 1UL;
|
q2 = twoP * k2 + 1UL;
|
||||||
remainder2 = q2.Mod10();
|
remainder2 = q2.Mod10();
|
||||||
|
// TODO: Reuse the same residue-automaton fast path here so the GPU warmup staging avoids `%` and
|
||||||
|
// branches the benchmarks showed slower than the tracked stepping helper.
|
||||||
bool shouldCheck2 = lastIsSeven
|
bool shouldCheck2 = lastIsSeven
|
||||||
? remainder2 == 7UL || remainder2 == 9UL || remainder2 == 3UL
|
? remainder2 == 7UL || remainder2 == 9UL || remainder2 == 3UL
|
||||||
: remainder2 == 1UL || remainder2 == 3UL || remainder2 == 9UL;
|
: remainder2 == 1UL || remainder2 == 3UL || remainder2 == 9UL;
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ public static class MersennePrimeFactorTester
|
|||||||
}
|
}
|
||||||
|
|
||||||
ulong value = checked(p << 1);
|
ulong value = checked(p << 1);
|
||||||
|
// TODO: Replace this `%` with the benchmarked Math.DivRem fast path so the hot filter avoids
|
||||||
|
// 128-bit division when evaluating candidates.
|
||||||
if ((q - 1UL) % value != 0UL)
|
if ((q - 1UL) % value != 0UL)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -45,6 +47,8 @@ public static class MersennePrimeFactorTester
|
|||||||
return !ct.IsCancellationRequested && order == p;
|
return !ct.IsCancellationRequested && order == p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Replace this dictionary with the divisor-cycle order cache once the benchmarks confirm the
|
||||||
|
// shared cache can stream snapshot results without extra locking.
|
||||||
private static readonly Dictionary<UInt128, UInt128> _orderCache = [];
|
private static readonly Dictionary<UInt128, UInt128> _orderCache = [];
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
@@ -64,6 +68,9 @@ public static class MersennePrimeFactorTester
|
|||||||
{
|
{
|
||||||
lock (_orderCache)
|
lock (_orderCache)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace this lock with the lock-free order cache once the divisor-cycle snapshot exposes
|
||||||
|
// deterministic ordering for single-cycle computations; we measured heavy contention in the
|
||||||
|
// factor benchmarks when many threads warm the cache concurrently.
|
||||||
if (_orderCache.TryGetValue(q, out var cached))
|
if (_orderCache.TryGetValue(q, out var cached))
|
||||||
{
|
{
|
||||||
return cached;
|
return cached;
|
||||||
@@ -74,6 +81,8 @@ public static class MersennePrimeFactorTester
|
|||||||
UInt128[] factors = phi <= ulong.MaxValue
|
UInt128[] factors = phi <= ulong.MaxValue
|
||||||
? Array.ConvertAll(Factor64((ulong)phi, ct), x => (UInt128)x)
|
? Array.ConvertAll(Factor64((ulong)phi, ct), x => (UInt128)x)
|
||||||
: Factor128(phi, ct);
|
: Factor128(phi, ct);
|
||||||
|
// TODO: Pull these factor arrays from ArrayPool once the factoring helpers adopt the pooled
|
||||||
|
// buffers measured faster in FactorizationBenchmarks.
|
||||||
UInt128 order = phi;
|
UInt128 order = phi;
|
||||||
|
|
||||||
UInt128 candidate, prime;
|
UInt128 candidate, prime;
|
||||||
@@ -86,6 +95,8 @@ public static class MersennePrimeFactorTester
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Swap this `%`/`/` loop for the Math.DivRem-based reducer highlighted in the
|
||||||
|
// order benchmarks so we avoid repeated 128-bit division while trimming the order.
|
||||||
while (order % prime == 0)
|
while (order % prime == 0)
|
||||||
{
|
{
|
||||||
if (ct.IsCancellationRequested)
|
if (ct.IsCancellationRequested)
|
||||||
@@ -105,6 +116,9 @@ public static class MersennePrimeFactorTester
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Integrate divisor-cycle data here so repeated order refinement uses cached cycle
|
||||||
|
// lengths when available and computes missing orders on the configured device without
|
||||||
|
// persisting them in the shared cache.
|
||||||
if (!ct.IsCancellationRequested)
|
if (!ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
lock (_orderCache)
|
lock (_orderCache)
|
||||||
@@ -239,7 +253,9 @@ public static class MersennePrimeFactorTester
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dict = RentDictionary();
|
var dict = RentDictionary();
|
||||||
var queue = new Queue<ulong>();
|
var queue = new Queue<ulong>(); // TODO: Replace this Queue with the pooled stack from the
|
||||||
|
// FactorizationBenchmarks fast path so factoring large
|
||||||
|
// composites avoids per-node allocations.
|
||||||
queue.Enqueue(n);
|
queue.Enqueue(n);
|
||||||
|
|
||||||
while (queue.Count != 0)
|
while (queue.Count != 0)
|
||||||
@@ -277,6 +293,8 @@ public static class MersennePrimeFactorTester
|
|||||||
}
|
}
|
||||||
|
|
||||||
ulong[] result = [.. dict.Keys];
|
ulong[] result = [.. dict.Keys];
|
||||||
|
// TODO: Rent the result array from ArrayPool once the factoring pipeline consumes spans so we
|
||||||
|
// can recycle buffers across large factorizations.
|
||||||
// lock (FactorCacheLock)
|
// lock (FactorCacheLock)
|
||||||
// {
|
// {
|
||||||
// FactorCache[n] = result;
|
// FactorCache[n] = result;
|
||||||
@@ -294,7 +312,9 @@ public static class MersennePrimeFactorTester
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dict = RentDictionary128();
|
var dict = RentDictionary128();
|
||||||
var queue = new Queue<UInt128>();
|
var queue = new Queue<UInt128>(); // TODO: Replace this with the pooled UInt128 stack once the
|
||||||
|
// wide-factorization benchmarks finalize the faster span
|
||||||
|
// based traversal.
|
||||||
queue.Enqueue(n);
|
queue.Enqueue(n);
|
||||||
|
|
||||||
while (queue.Count != 0)
|
while (queue.Count != 0)
|
||||||
@@ -331,7 +351,9 @@ public static class MersennePrimeFactorTester
|
|||||||
queue.Enqueue(m / d);
|
queue.Enqueue(m / d);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = new UInt128[dict.Count];
|
var result = new UInt128[dict.Count]; // TODO: Rent this array from ArrayPool<UInt128> so
|
||||||
|
// wide-factorization batches stop allocating fresh
|
||||||
|
// buffers per candidate.
|
||||||
int index = 0;
|
int index = 0;
|
||||||
foreach (var key in dict.Keys)
|
foreach (var key in dict.Keys)
|
||||||
{
|
{
|
||||||
@@ -349,7 +371,9 @@ public static class MersennePrimeFactorTester
|
|||||||
return 2UL;
|
return 2UL;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rng = new Random(1234567);
|
var rng = new Random(1234567); // TODO: Replace this RNG with the deterministic span-based
|
||||||
|
// sequence measured faster in PollardRhoBenchmarks so we avoid
|
||||||
|
// per-call Random allocations.
|
||||||
|
|
||||||
ulong c, d, x, y, diff;
|
ulong c, d, x, y, diff;
|
||||||
while (true)
|
while (true)
|
||||||
@@ -371,19 +395,24 @@ public static class MersennePrimeFactorTester
|
|||||||
return 0UL;
|
return 0UL;
|
||||||
}
|
}
|
||||||
|
|
||||||
x = (MulMod64(x, x, n) + c) % n;
|
x = (MulMod64(x, x, n) + c) % n; // TODO: Swap the `% n` with the Montgomery folding
|
||||||
|
// helper from the PollardRho benchmarks once the
|
||||||
|
// specialized reducer lands.
|
||||||
if (ct.IsCancellationRequested)
|
if (ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
return 0UL;
|
return 0UL;
|
||||||
}
|
}
|
||||||
|
|
||||||
y = (MulMod64(y, y, n) + c) % n;
|
y = (MulMod64(y, y, n) + c) % n; // TODO: Use the same Montgomery folding helper here
|
||||||
|
// to keep the tortoise sequence on the optimized
|
||||||
|
// path.
|
||||||
if (ct.IsCancellationRequested)
|
if (ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
return 0UL;
|
return 0UL;
|
||||||
}
|
}
|
||||||
|
|
||||||
y = (MulMod64(y, y, n) + c) % n;
|
y = (MulMod64(y, y, n) + c) % n; // TODO: Replace this modulo with the optimized helper
|
||||||
|
// so both steps share the fast reduction.
|
||||||
diff = x > y ? x - y : y - x;
|
diff = x > y ? x - y : y - x;
|
||||||
if (ct.IsCancellationRequested)
|
if (ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@@ -411,7 +440,8 @@ public static class MersennePrimeFactorTester
|
|||||||
return 1UL; // We're returning 1 to avoid division by 0 in any place
|
return 1UL; // We're returning 1 to avoid division by 0 in any place
|
||||||
}
|
}
|
||||||
|
|
||||||
t = a % b;
|
t = a % b; // TODO: Replace this with the binary GCD helper used in the gcd benchmarks so we
|
||||||
|
// avoid slow modulo operations while factoring large composites.
|
||||||
a = b;
|
a = b;
|
||||||
b = t;
|
b = t;
|
||||||
}
|
}
|
||||||
@@ -493,7 +523,8 @@ public static class MersennePrimeFactorTester
|
|||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rng = new Random(1234567);
|
var rng = new Random(1234567); // TODO: Replace with the deterministic UInt128 sequence from
|
||||||
|
// PollardRho128Benchmarks so we eliminate Random allocations.
|
||||||
|
|
||||||
UInt128 c, d, x, y, diff;
|
UInt128 c, d, x, y, diff;
|
||||||
while (true)
|
while (true)
|
||||||
@@ -515,26 +546,30 @@ public static class MersennePrimeFactorTester
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
x = (MulMod128(x, x, n) + c) % n;
|
x = (MulMod128(x, x, n) + c) % n; // TODO: Swap this modulo for the UInt128 Montgomery
|
||||||
|
// reducer once the optimized helper lands.
|
||||||
if (ct.IsCancellationRequested)
|
if (ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
y = (MulMod128(y, y, n) + c) % n;
|
y = (MulMod128(y, y, n) + c) % n; // TODO: Use the same optimized reducer for the
|
||||||
|
// tortoise step.
|
||||||
if (ct.IsCancellationRequested)
|
if (ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
y = (MulMod128(y, y, n) + c) % n;
|
y = (MulMod128(y, y, n) + c) % n; // TODO: Replace with the optimized reducer so both
|
||||||
|
// steps avoid BigInteger-based modulo.
|
||||||
diff = x > y ? x - y : y - x;
|
diff = x > y ? x - y : y - x;
|
||||||
if (ct.IsCancellationRequested)
|
if (ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
d = Gcd128(diff, n, ct);
|
d = Gcd128(diff, n, ct); // TODO: Move this to the binary GCD implementation once the
|
||||||
|
// UInt128 benchmarks validate the optimized routine.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (d != n)
|
if (d != n)
|
||||||
@@ -555,7 +590,8 @@ public static class MersennePrimeFactorTester
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
t = a % b;
|
t = a % b; // TODO: Replace with the binary GCD helper once the UInt128 benchmarks land so
|
||||||
|
// we remove the slow `%` from the wide gcd loop.
|
||||||
a = b;
|
a = b;
|
||||||
b = t;
|
b = t;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,36 +10,54 @@ public class MersenneResidueStepper
|
|||||||
public MersenneResidueStepper(UInt128 modulus, ulong p0, UInt128 M_mod_m_at_p0)
|
public MersenneResidueStepper(UInt128 modulus, ulong p0, UInt128 M_mod_m_at_p0)
|
||||||
{
|
{
|
||||||
if (modulus == 0)
|
if (modulus == 0)
|
||||||
|
{
|
||||||
throw new ArgumentException("Modulus must be positive.");
|
throw new ArgumentException("Modulus must be positive.");
|
||||||
|
}
|
||||||
|
|
||||||
m = modulus;
|
m = modulus;
|
||||||
p = p0;
|
p = p0;
|
||||||
|
|
||||||
// TODO: Replace these `%` reductions with the shared ProcessEightBitWindows helper so initialization benefits from the
|
// TODO: Replace these `%` reductions with the shared ProcessEightBitWindows helper so initialization benefits from the
|
||||||
// windowed pow2mod pipeline proven faster than the generic modulo path.
|
// windowed pow2mod pipeline proven faster than the generic modulo path.
|
||||||
M_mod = M_mod_m_at_p0 % m;
|
M_mod = M_mod_m_at_p0 % m;
|
||||||
pow2_mod = (M_mod + 1) % m;
|
pow2_mod = (M_mod + 1) % m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Inline this accessor into callers so the hot residue path reads the backing fields directly
|
||||||
|
// instead of jumping through a wrapper method every iteration.
|
||||||
public (ulong, UInt128) State() => (p, M_mod);
|
public (ulong, UInt128) State() => (p, M_mod);
|
||||||
|
|
||||||
public (ulong, UInt128) Step(long delta)
|
public (ulong, UInt128) Step(long delta)
|
||||||
{
|
{
|
||||||
if (delta == 0)
|
if (delta == 0)
|
||||||
|
{
|
||||||
return State();
|
return State();
|
||||||
|
}
|
||||||
|
|
||||||
UInt128 g = UInt128.One;
|
UInt128 g = UInt128.One;
|
||||||
ulong absDelta = (ulong)Math.Abs(delta);
|
ulong absDelta = (ulong)Math.Abs(delta);
|
||||||
|
|
||||||
for (ulong i = 0; i < absDelta; i++)
|
for (ulong i = 0; i < absDelta; i++)
|
||||||
{
|
{
|
||||||
// TODO: Replace this repeated doubling with the shared ProcessEightBitWindows helper (or cycle-aware lookup)
|
// TODO: Replace this repeated doubling with the shared ProcessEightBitWindows helper (or cycle-aware lookup)
|
||||||
// once the scalar pow2mod upgrade lands; benchmarking showed the windowed ladder slashes latency for large
|
// once the scalar pow2mod upgrade lands; benchmarking showed the windowed ladder slashes latency for large
|
||||||
// deltas compared to this linear loop.
|
// deltas compared to this linear loop.
|
||||||
|
// TODO: Once divisor cycles power the residue stepper, prefer stepping by cycle lengths instead of incrementing
|
||||||
|
// one bit at a time so large jumps stay on the mandatory cycle-accelerated path.
|
||||||
g <<= 1;
|
g <<= 1;
|
||||||
|
|
||||||
if (g >= m)
|
if (g >= m)
|
||||||
|
{
|
||||||
g -= m;
|
g -= m;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (delta > 0)
|
if (delta > 0)
|
||||||
|
{
|
||||||
// TODO: Once the scalar windowed pow2 helper lands, reroute this `%` path through it to avoid the slower
|
// TODO: Once the scalar windowed pow2 helper lands, reroute this `%` path through it to avoid the slower
|
||||||
// multiply-and-mod sequence measured in the legacy benchmarks.
|
// multiply-and-mod sequence measured in the legacy benchmarks.
|
||||||
pow2_mod = (pow2_mod * g) % m;
|
pow2_mod = (pow2_mod * g) % m;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Modular inverse for negative delta
|
// Modular inverse for negative delta
|
||||||
@@ -47,34 +65,56 @@ public class MersenneResidueStepper
|
|||||||
// backward jump; the divisor cycle benchmarks demonstrated significant savings once shared cycle data was used.
|
// backward jump; the divisor cycle benchmarks demonstrated significant savings once shared cycle data was used.
|
||||||
pow2_mod = (pow2_mod * ModInverse(g, m)) % m;
|
pow2_mod = (pow2_mod * ModInverse(g, m)) % m;
|
||||||
}
|
}
|
||||||
|
|
||||||
p = (ulong)((long)p + delta);
|
p = (ulong)((long)p + delta);
|
||||||
|
|
||||||
// TODO: Use the shared pow2mod remainder cache here so this `%` becomes a subtraction-based fold instead of the
|
// TODO: Use the shared pow2mod remainder cache here so this `%` becomes a subtraction-based fold instead of the
|
||||||
// slower modulo operator for large moduli.
|
// slower modulo operator for large moduli.
|
||||||
M_mod = (pow2_mod + m - 1) % m;
|
M_mod = (pow2_mod + m - 1) % m;
|
||||||
|
|
||||||
return State();
|
return State();
|
||||||
}
|
}
|
||||||
|
|
||||||
public (ulong, UInt128) To(ulong p_target)
|
public (ulong, UInt128) To(ulong p_target)
|
||||||
{
|
{
|
||||||
|
// TODO: Collapse this wrapper once callers can invoke Step directly; the extra indirection shows up
|
||||||
|
// in the profiler when residue adjustments happen per candidate.
|
||||||
long delta = (long)p_target - (long)p;
|
long delta = (long)p_target - (long)p;
|
||||||
|
|
||||||
return Step(delta);
|
return Step(delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static UInt128 ModInverse(UInt128 a, UInt128 m)
|
private static UInt128 ModInverse(UInt128 a, UInt128 m)
|
||||||
{
|
{
|
||||||
UInt128 m0 = m, t, q;
|
UInt128 m0 = m;
|
||||||
UInt128 x0 = 0, x1 = 1;
|
UInt128 t;
|
||||||
if (m == 1) return 0;
|
UInt128 q;
|
||||||
|
UInt128 x0 = 0;
|
||||||
|
UInt128 x1 = 1;
|
||||||
|
|
||||||
|
if (m == 1)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
while (a > 1)
|
while (a > 1)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace this division-heavy Euclidean loop with the binary modular inverse that reuses divisor
|
||||||
|
// cycles so we drop repeated `/` and `%` operations during backtracking steps.
|
||||||
q = a / m;
|
q = a / m;
|
||||||
t = m;
|
t = m;
|
||||||
m = a % m; a = t;
|
m = a % m;
|
||||||
|
a = t;
|
||||||
t = x0;
|
t = x0;
|
||||||
x0 = x1 - q * x0;
|
x0 = x1 - q * x0;
|
||||||
x1 = t;
|
x1 = t;
|
||||||
}
|
}
|
||||||
if (x1 < 0) x1 += m0;
|
|
||||||
|
if (x1 < 0)
|
||||||
|
{
|
||||||
|
x1 += m0;
|
||||||
|
}
|
||||||
|
|
||||||
return x1;
|
return x1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,7 +186,10 @@ public sealed class ModResidueTracker
|
|||||||
{
|
{
|
||||||
d = divisors[i];
|
d = divisors[i];
|
||||||
value = residues[i] + delta;
|
value = residues[i] + delta;
|
||||||
// For tiny moduli (2..6) we may need a few subtractions; keep the loop tight.
|
// TODO: Replace this subtraction loop with the divisor-cycle stepping helper so identity
|
||||||
|
// residues reuse cached cycle deltas, computing the missing cycle on the configured
|
||||||
|
// device without caching the result or scheduling extra blocks when the snapshot lacks
|
||||||
|
// the divisor.
|
||||||
while (value >= d)
|
while (value >= d)
|
||||||
{
|
{
|
||||||
value -= d;
|
value -= d;
|
||||||
@@ -259,7 +262,8 @@ public sealed class ModResidueTracker
|
|||||||
// residue of Mersenne number M_number = 2^number - 1 modulo divisor
|
// residue of Mersenne number M_number = 2^number - 1 modulo divisor
|
||||||
// TODO: Consult MersenneDivisorCycles.Shared (or similar caches) here so we reuse precomputed cycle lengths
|
// TODO: Consult MersenneDivisorCycles.Shared (or similar caches) here so we reuse precomputed cycle lengths
|
||||||
// instead of recomputing powmods for every divisor; the divisor-cycle benchmarks showed large wins once the
|
// instead of recomputing powmods for every divisor; the divisor-cycle benchmarks showed large wins once the
|
||||||
// cached orders were used across scans.
|
// cached orders were used across scans, and a miss should trigger the configured device to compute the cycle
|
||||||
|
// immediately without storing it back or requesting additional cache blocks.
|
||||||
UInt128 pow = PowMod128(UInt128Numbers.Two, number, divisor);
|
UInt128 pow = PowMod128(UInt128Numbers.Two, number, divisor);
|
||||||
|
|
||||||
// q divides M_p ⇔ 2^p ≡ 1 (mod q) AND ord_q(2) | p
|
// q divides M_p ⇔ 2^p ≡ 1 (mod q) AND ord_q(2) | p
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ public sealed class MersenneResidueAutomaton
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
// TODO: Remove this wrapper and expose the field directly once callers switch to struct-based residue
|
||||||
|
// tracking so we can trim another layer from the tight scanning loop.
|
||||||
public UInt128 CurrentQ() => _currentQ;
|
public UInt128 CurrentQ() => _currentQ;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -65,11 +67,16 @@ public sealed class MersenneResidueAutomaton
|
|||||||
Mod3R = r3;
|
Mod3R = r3;
|
||||||
|
|
||||||
ulong r5 = Mod5R + _step5;
|
ulong r5 = Mod5R + _step5;
|
||||||
|
|
||||||
if (r5 >= 5UL)
|
if (r5 >= 5UL)
|
||||||
{
|
{
|
||||||
r5 -= 5UL;
|
r5 -= 5UL;
|
||||||
}
|
}
|
||||||
|
|
||||||
Mod5R = r5;
|
Mod5R = r5;
|
||||||
|
|
||||||
|
// TODO: Once divisor cycle lookups feed residue scanning, reuse the cached cycle length here to skip directly to the
|
||||||
|
// next admissible q instead of iterating through every residue update.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +102,7 @@ public sealed class Ending7Automaton
|
|||||||
_res = Array.Empty<ulong>();
|
_res = Array.Empty<ulong>();
|
||||||
_reachable = Array.Empty<bool>();
|
_reachable = Array.Empty<bool>();
|
||||||
_n = AlignTo7(start);
|
_n = AlignTo7(start);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,20 +112,33 @@ public sealed class Ending7Automaton
|
|||||||
_steps = new ulong[len];
|
_steps = new ulong[len];
|
||||||
_res = new ulong[len];
|
_res = new ulong[len];
|
||||||
_reachable = new bool[len];
|
_reachable = new bool[len];
|
||||||
|
|
||||||
for (int i = 0; i < len; i++)
|
for (int i = 0; i < len; i++)
|
||||||
{
|
{
|
||||||
ulong m = moduli[i];
|
ulong m = moduli[i];
|
||||||
_moduli[i] = m;
|
_moduli[i] = m;
|
||||||
|
|
||||||
|
// TODO: Replace the `% 10` computation with the Mod10 helper once Ending7Automaton plugs into the
|
||||||
|
// shared residue tables so we avoid generic divisions while initializing the step cache.
|
||||||
ulong step = 10UL % m;
|
ulong step = 10UL % m;
|
||||||
_steps[i] = step;
|
_steps[i] = step;
|
||||||
|
|
||||||
|
// TODO: Precompute the initial residues via the divisor-cycle cache so this setup phase reuses the
|
||||||
|
// GPU-generated cycle data instead of issuing on-the-fly modulo operations.
|
||||||
_res[i] = _n % m; // one-time division at construction
|
_res[i] = _n % m; // one-time division at construction
|
||||||
|
|
||||||
// Zero is reachable iff gcd(step, m) | res
|
// Zero is reachable iff gcd(step, m) | res
|
||||||
ulong g = Gcd(step, m);
|
ulong g = Gcd(step, m);
|
||||||
|
|
||||||
|
// TODO: Swap this `%` check for the binary-gcd aware reducer once the automaton wiring shares the
|
||||||
|
// branchless Mod helpers benchmarked faster than repeated modulo instructions.
|
||||||
_reachable[i] = (g == 0UL) ? false : (_res[i] % g == 0UL);
|
_reachable[i] = (g == 0UL) ? false : (_res[i] % g == 0UL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
// TODO: Inline this getter into the automaton consumers so residue scans can read the backing field
|
||||||
|
// without paying the delegate call overhead highlighted in the profiler snapshots.
|
||||||
public ulong Current() => _n;
|
public ulong Current() => _n;
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
@@ -140,6 +161,7 @@ public sealed class Ending7Automaton
|
|||||||
public void Next()
|
public void Next()
|
||||||
{
|
{
|
||||||
_n += 10UL;
|
_n += 10UL;
|
||||||
|
|
||||||
for (int i = 0; i < _moduli.Length; i++)
|
for (int i = 0; i < _moduli.Length; i++)
|
||||||
{
|
{
|
||||||
if (!_reachable[i])
|
if (!_reachable[i])
|
||||||
@@ -149,12 +171,17 @@ public sealed class Ending7Automaton
|
|||||||
|
|
||||||
ulong m = _moduli[i];
|
ulong m = _moduli[i];
|
||||||
ulong r = _res[i] + _steps[i];
|
ulong r = _res[i] + _steps[i];
|
||||||
|
|
||||||
if (r >= m)
|
if (r >= m)
|
||||||
{
|
{
|
||||||
r -= m;
|
r -= m;
|
||||||
}
|
}
|
||||||
|
|
||||||
_res[i] = r;
|
_res[i] = r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Once divisor cycles are mandatory, reuse the cached cycle offset per modulus to skip directly to valid
|
||||||
|
// candidates so these per-step additions disappear from the hot path.
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
@@ -162,6 +189,7 @@ public sealed class Ending7Automaton
|
|||||||
{
|
{
|
||||||
ulong r = start.Mod10();
|
ulong r = start.Mod10();
|
||||||
ulong add = (7UL + 10UL - r).Mod10();
|
ulong add = (7UL + 10UL - r).Mod10();
|
||||||
|
|
||||||
return start + add;
|
return start + add;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +204,7 @@ public sealed class Ending7Automaton
|
|||||||
a = b;
|
a = b;
|
||||||
b = t;
|
b = t;
|
||||||
}
|
}
|
||||||
|
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,10 @@ internal static class MontgomeryDivisorDataCache
|
|||||||
{
|
{
|
||||||
private const ulong BaseBlockEnd = PerfectNumberConstants.MaxQForDivisorCycles;
|
private const ulong BaseBlockEnd = PerfectNumberConstants.MaxQForDivisorCycles;
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<ulong, CacheEntry> 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();
|
private static readonly ConcurrentDictionary<int, ConcurrentDictionary<ulong, byte>> BlockMembership = new();
|
||||||
|
|
||||||
public static MontgomeryDivisorData Get(ulong modulus)
|
public static MontgomeryDivisorData Get(ulong modulus)
|
||||||
@@ -30,11 +33,16 @@ internal static class MontgomeryDivisorDataCache
|
|||||||
}
|
}
|
||||||
|
|
||||||
int blockIndex = ComputeBlockIndex(modulus);
|
int blockIndex = ComputeBlockIndex(modulus);
|
||||||
|
// 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);
|
return GetManaged(modulus, blockIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void ReleaseBlock(DivisorCycleCache.CycleBlock block)
|
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)
|
if (block.Index <= 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -86,10 +94,15 @@ internal static class MontgomeryDivisorDataCache
|
|||||||
return created;
|
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)
|
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)
|
if (blockIndex <= 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -106,6 +119,9 @@ internal static class MontgomeryDivisorDataCache
|
|||||||
return 0;
|
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;
|
return (int)((modulus - (BaseBlockEnd + 1UL)) / (ulong)PerfectNumberConstants.MaxQForDivisorCycles) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,5 +7,8 @@ public static class PerfectNumberConstants
|
|||||||
public const ulong ExtraDivisorCycleSearchLimit = 64UL;
|
public const ulong ExtraDivisorCycleSearchLimit = 64UL;
|
||||||
public const int MaxQForDivisorCycles = 4_000_000;
|
public const int MaxQForDivisorCycles = 4_000_000;
|
||||||
public static readonly uint PrimesLimit = 1_000_000; //(ulong)Array.MaxLength;// 1_000_000;
|
public static readonly uint PrimesLimit = 1_000_000; //(ulong)Array.MaxLength;// 1_000_000;
|
||||||
|
// TODO: Load these limits from the benchmark-driven configuration so CPU and GPU scans stay aligned with the optimal
|
||||||
|
// divisor-cycle datasets we generate offline.
|
||||||
|
// TODO: Promote these magic numbers into a runtime profile derived from EvenPerfectBitScanner.Benchmarks so we can retune
|
||||||
|
// them automatically when the optimal ranges for 2kp+1 >= 138M change.
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,9 @@ public static class PowerCache
|
|||||||
{
|
{
|
||||||
if (!BigCache.TryGetValue(baseVal, out var powers))
|
if (!BigCache.TryGetValue(baseVal, out var powers))
|
||||||
{
|
{
|
||||||
|
// TODO: Rent this buffer from ArrayPool<BigInteger> (or a dedicated pooled cache) so expanding
|
||||||
|
// exponent ranges stop allocating fresh arrays as we sweep across large divisor sets in the
|
||||||
|
// Pow2MontgomeryModBenchmarks scenarios.
|
||||||
powers = new BigInteger[exponent + 1];
|
powers = new BigInteger[exponent + 1];
|
||||||
powers[0] = BigInteger.One;
|
powers[0] = BigInteger.One;
|
||||||
BigCache[baseVal] = powers;
|
BigCache[baseVal] = powers;
|
||||||
@@ -21,6 +24,8 @@ public static class PowerCache
|
|||||||
if (powers.Length <= exponent)
|
if (powers.Length <= exponent)
|
||||||
{
|
{
|
||||||
int oldLength = powers.Length;
|
int oldLength = powers.Length;
|
||||||
|
// TODO: Replace Array.Resize with a pooled copy so we reuse previously rented buffers instead
|
||||||
|
// of forcing GC work whenever the cache grows; benchmarks showed resize thrash when p >= 138M.
|
||||||
Array.Resize(ref powers, exponent + 1);
|
Array.Resize(ref powers, exponent + 1);
|
||||||
for (int i = oldLength; i <= exponent; i++)
|
for (int i = oldLength; i <= exponent; i++)
|
||||||
powers[i] = BigInteger.Zero;
|
powers[i] = BigInteger.Zero;
|
||||||
@@ -35,7 +40,7 @@ public static class PowerCache
|
|||||||
if (powers[i].IsZero)
|
if (powers[i].IsZero)
|
||||||
{
|
{
|
||||||
// TODO: Swap this BigInteger chain with the UInt128-based Montgomery ladder from
|
// TODO: Swap this BigInteger chain with the UInt128-based Montgomery ladder from
|
||||||
// Pow2MontgomeryMod once callers guarantee 64-bit inputs; the benchmarks show the
|
// Pow2MontgomeryModBenchmarks once callers guarantee 64-bit inputs; the benchmarks show the
|
||||||
// arbitrary-precision multiply is orders of magnitude slower for the exponents we
|
// arbitrary-precision multiply is orders of magnitude slower for the exponents we
|
||||||
// scan when p >= 138M.
|
// scan when p >= 138M.
|
||||||
powers[i] = powers[i - 1] * baseVal;
|
powers[i] = powers[i - 1] * baseVal;
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ public sealed class PrimeCache
|
|||||||
last = current;
|
last = current;
|
||||||
// TODO: Swap the Open.Numeric enumerator for the staged sieve batches we benchmarked; the
|
// TODO: Swap the Open.Numeric enumerator for the staged sieve batches we benchmarked; the
|
||||||
// iterator allocations here throttle the cache fill when we extend the search past 138M.
|
// iterator allocations here throttle the cache fill when we extend the search past 138M.
|
||||||
|
// TODO: Fold in the Mod6 stride planner that topped Mod6ComparisonBenchmarks so the CPU cache
|
||||||
|
// can skip even/composite candidates instead of walking each enumerator element.
|
||||||
_primes.Add(last);
|
_primes.Add(last);
|
||||||
_primeMod4.Add((byte)(current & 3));
|
_primeMod4.Add((byte)(current & 3));
|
||||||
}
|
}
|
||||||
@@ -91,6 +93,8 @@ public sealed class PrimeCache
|
|||||||
// TODO: Move this incremental append to the shared sieve batches so we amortize the
|
// TODO: Move this incremental append to the shared sieve batches so we amortize the
|
||||||
// conversions; walking the enumerator element-by-element is noticeably slower in the
|
// conversions; walking the enumerator element-by-element is noticeably slower in the
|
||||||
// updated prime cache benchmarks.
|
// updated prime cache benchmarks.
|
||||||
|
// TODO: Apply the Mod6 stride scheduler here as well so on-demand extensions mirror the
|
||||||
|
// Mod6ComparisonBenchmarks winner when pulling primes for divisor-cycle generation.
|
||||||
if (current >= s)
|
if (current >= s)
|
||||||
{
|
{
|
||||||
yield return val;
|
yield return val;
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ public sealed class PrimeTester(bool useInternal = false)
|
|||||||
|
|
||||||
Span<ulong> one = stackalloc ulong[1];
|
Span<ulong> one = stackalloc ulong[1];
|
||||||
Span<byte> outFlags = stackalloc byte[1];
|
Span<byte> outFlags = stackalloc byte[1];
|
||||||
|
// TODO: Inline the single-value GPU sieve fast path from GpuModularArithmeticBenchmarks so this wrapper
|
||||||
|
// can skip stackalloc buffers and reuse the pinned upload span the benchmark identified as fastest.
|
||||||
one[0] = n;
|
one[0] = n;
|
||||||
IsPrimeBatchGpu(one, outFlags);
|
IsPrimeBatchGpu(one, outFlags);
|
||||||
|
|
||||||
@@ -116,6 +118,9 @@ public sealed class PrimeTester(bool useInternal = false)
|
|||||||
var input = scratch.Input;
|
var input = scratch.Input;
|
||||||
var output = scratch.Output;
|
var output = scratch.Output;
|
||||||
ulong[] temp = ArrayPool<ulong>.Shared.Rent(batchSize);
|
ulong[] temp = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||||
|
// TODO: Replace this ad-hoc ArrayPool buffer with the pinned span cache from
|
||||||
|
// PrimeSieveGpuBenchmarks so batch uploads reuse preallocated GPU-friendly
|
||||||
|
// memory and avoid the extra copy before every kernel launch.
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -156,6 +161,9 @@ public sealed class PrimeTester(bool useInternal = false)
|
|||||||
public MemoryBuffer1D<uint, Stride1D.Dense> DevicePrimes { get; }
|
public MemoryBuffer1D<uint, Stride1D.Dense> DevicePrimes { get; }
|
||||||
private readonly Accelerator _accel;
|
private readonly Accelerator _accel;
|
||||||
private readonly System.Collections.Concurrent.ConcurrentBag<ScratchBuffers> _scratchPool = [];
|
private readonly System.Collections.Concurrent.ConcurrentBag<ScratchBuffers> _scratchPool = [];
|
||||||
|
// TODO: Replace this ConcurrentBag with the lock-free ring buffer variant validated in
|
||||||
|
// GpuModularArithmeticBenchmarks so renting scratch buffers stops contending on the bag's internal locks when
|
||||||
|
// thousands of GPU batches execute per second.
|
||||||
|
|
||||||
public KernelState(Accelerator accelerator)
|
public KernelState(Accelerator accelerator)
|
||||||
{
|
{
|
||||||
@@ -226,6 +234,9 @@ public sealed class PrimeTester(bool useInternal = false)
|
|||||||
{
|
{
|
||||||
// Map accelerator to cached state; use Lazy to serialize kernel creation
|
// Map accelerator to cached state; use Lazy to serialize kernel creation
|
||||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<Accelerator, Lazy<KernelState>> States = new();
|
private static readonly System.Collections.Concurrent.ConcurrentDictionary<Accelerator, Lazy<KernelState>> States = new();
|
||||||
|
// TODO: Prewarm this per-accelerator cache during startup (and reuse a simple array keyed by accelerator index)
|
||||||
|
// once the kernel pool exposes deterministic ordering; the Lazy wrappers showed measurable overhead in the
|
||||||
|
// GpuModularArithmeticBenchmarks hot path.
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static KernelState GetOrCreate(Accelerator accelerator)
|
public static KernelState GetOrCreate(Accelerator accelerator)
|
||||||
@@ -308,12 +319,18 @@ public sealed class PrimeTester(bool useInternal = false)
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
internal static bool SharesFactorWithMaxExponent(ulong n)
|
internal static bool SharesFactorWithMaxExponent(ulong n)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace this on-the-fly GCD probe with the cached factor table derived from
|
||||||
|
// ResidueComputationBenchmarks so divisor-cycle metadata can short-circuit the test
|
||||||
|
// instead of recomputing binary GCD for every candidate.
|
||||||
ulong m = (ulong)BitOperations.Log2(n);
|
ulong m = (ulong)BitOperations.Log2(n);
|
||||||
return BinaryGcd(n, m) != 1UL;
|
return BinaryGcd(n, m) != 1UL;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void SharesFactorWithMaxExponentBatch(ReadOnlySpan<ulong> values, Span<byte> results)
|
internal static void SharesFactorWithMaxExponentBatch(ReadOnlySpan<ulong> values, Span<byte> results)
|
||||||
{
|
{
|
||||||
|
// TODO: Route this batch helper through the shared GPU kernel pool from
|
||||||
|
// GpuUInt128BinaryGcdBenchmarks so we reuse cached kernels, pinned host buffers,
|
||||||
|
// and divisor-cycle staging instead of allocating new device buffers per call.
|
||||||
var gpu = GpuContextPool.Rent();
|
var gpu = GpuContextPool.Rent();
|
||||||
var accelerator = gpu.Accelerator;
|
var accelerator = gpu.Accelerator;
|
||||||
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<byte>>(SharesFactorKernel);
|
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<byte>>(SharesFactorKernel);
|
||||||
@@ -337,6 +354,9 @@ public sealed class PrimeTester(bool useInternal = false)
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static ulong BinaryGcd(ulong u, ulong v)
|
private static ulong BinaryGcd(ulong u, ulong v)
|
||||||
{
|
{
|
||||||
|
// TODO: Swap this handwritten binary GCD for the optimized helper measured in
|
||||||
|
// GpuUInt128BinaryGcdBenchmarks so CPU callers share the faster subtract-less
|
||||||
|
// ladder once the common implementation is promoted into PerfectNumbers.Core.
|
||||||
if (u == 0UL)
|
if (u == 0UL)
|
||||||
{
|
{
|
||||||
return v;
|
return v;
|
||||||
@@ -375,6 +395,9 @@ public sealed class PrimeTester(bool useInternal = false)
|
|||||||
|
|
||||||
private static ulong BinaryGcdGpu(ulong u, ulong v)
|
private static ulong BinaryGcdGpu(ulong u, ulong v)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace this inline GPU binary GCD with the kernel extracted from
|
||||||
|
// GpuUInt128BinaryGcdBenchmarks via GpuKernelPool so device callers reuse the
|
||||||
|
// fully unrolled ladder instead of this branchy fallback.
|
||||||
if (u == 0UL)
|
if (u == 0UL)
|
||||||
{
|
{
|
||||||
return v;
|
return v;
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ public static class PrimesGenerator
|
|||||||
for (int i = 0; i < primesCount; i++)
|
for (int i = 0; i < primesCount; i++)
|
||||||
{
|
{
|
||||||
uint p = primes[i];
|
uint p = primes[i];
|
||||||
|
// TODO: Reuse a single squared local for `p` within this loop so we avoid recalculating
|
||||||
|
// `p * p` repeatedly without storing every squared prime globally (the cache must remain
|
||||||
|
// limited to the small-divisor tables to honor the memory constraints).
|
||||||
if (p * p > candidate)
|
if (p * p > candidate)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@@ -72,6 +75,9 @@ public static class PrimesGenerator
|
|||||||
primes.Add(candidate);
|
primes.Add(candidate);
|
||||||
if (allCount < targetInt)
|
if (allCount < targetInt)
|
||||||
{
|
{
|
||||||
|
// TODO: Reuse a single cached `candidateSquared` per iteration so we avoid issuing
|
||||||
|
// three separate 64-bit multiplications when populating the pow2 arrays here and
|
||||||
|
// in the last-one/last-seven branches below.
|
||||||
all[allCount] = candidate;
|
all[allCount] = candidate;
|
||||||
allPow2[allCount] = checked((ulong)candidate * candidate);
|
allPow2[allCount] = checked((ulong)candidate * candidate);
|
||||||
allCount++;
|
allCount++;
|
||||||
@@ -95,6 +101,8 @@ public static class PrimesGenerator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Switch this increment to the Mod6 stepping table identified in the Mod6ComparisonBenchmarks
|
||||||
|
// so we skip the composite residues without relying on per-iteration `%` checks.
|
||||||
candidate = candidate == 2U ? 3U : candidate + 2U;
|
candidate = candidate == 2U ? 3U : candidate + 2U;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,17 +10,24 @@ public static class RationalHelper
|
|||||||
long numerator = checked((long)Math.Round(value * scale));
|
long numerator = checked((long)Math.Round(value * scale));
|
||||||
long denominator = scale;
|
long denominator = scale;
|
||||||
long gcd = (long)BigInteger.GreatestCommonDivisor(numerator, denominator);
|
long gcd = (long)BigInteger.GreatestCommonDivisor(numerator, denominator);
|
||||||
|
// TODO: Replace the BigInteger-based GCD with the UInt128-friendly binary GCD helper so this rounding path avoids
|
||||||
|
// allocating big integers for hot rational conversions.
|
||||||
return ERational.Create(EInteger.FromInt64(numerator / gcd), EInteger.FromInt64(denominator / gcd));
|
return ERational.Create(EInteger.FromInt64(numerator / gcd), EInteger.FromInt64(denominator / gcd));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static EInteger ToEInteger(BigInteger value)
|
public static EInteger ToEInteger(BigInteger value)
|
||||||
{
|
{
|
||||||
|
// TODO: Cache a reusable byte buffer here so converting to EInteger no longer allocates per call when parsing alpha
|
||||||
|
// tables.
|
||||||
return EInteger.FromBytes(value.ToByteArray(), true);
|
return EInteger.FromBytes(value.ToByteArray(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ulong FloorToUInt64(ERational value)
|
public static ulong FloorToUInt64(ERational value)
|
||||||
{
|
{
|
||||||
EInteger ei = value.ToEInteger();
|
EInteger ei = value.ToEInteger();
|
||||||
|
// TODO: Inline this floor conversion with the span-based ladder that dominated
|
||||||
|
// ResidueComputationBenchmarks so the CPU path skips the expensive ERational ->
|
||||||
|
// EInteger materialization when reducing large 2kp+1 divisors.
|
||||||
return ei.ToUInt64Checked();
|
return ei.ToUInt64Checked();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +36,9 @@ public static class RationalHelper
|
|||||||
EInteger ei = value.ToEInteger();
|
EInteger ei = value.ToEInteger();
|
||||||
if (value.CompareTo(ERational.Create(ei, EInteger.One)) > 0)
|
if (value.CompareTo(ERational.Create(ei, EInteger.One)) > 0)
|
||||||
ei += EInteger.One;
|
ei += EInteger.One;
|
||||||
|
// TODO: Replace this ceiling branch with the branchless adjustment measured fastest
|
||||||
|
// in ResidueComputationBenchmarks so large-divisor scans avoid repeated ERational
|
||||||
|
// conversions on the CPU hot path.
|
||||||
return ei.ToUInt64Checked();
|
return ei.ToUInt64Checked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,7 @@ namespace PerfectNumbers.Core;
|
|||||||
public static class RationalNumbers
|
public static class RationalNumbers
|
||||||
{
|
{
|
||||||
public static ERational Two { get; } = ERational.FromInt32(2);
|
public static ERational Two { get; } = ERational.FromInt32(2);
|
||||||
|
// TODO: Precompute additional frequently used rationals (e.g., OneHalf, ThreeHalves) so hot alpha computations stop
|
||||||
|
// constructing them via ERational.FromInt32 at runtime.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,6 +28,8 @@ public static class RleBlacklist
|
|||||||
}
|
}
|
||||||
|
|
||||||
var set = new HashSet<string>(StringComparer.Ordinal);
|
var set = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
// TODO: Replace this HashSet with the pooled ValueHashSet from the blacklist benchmarks so repeated
|
||||||
|
// reloads stop allocating new buckets when refreshing the filtered pattern list.
|
||||||
foreach (var line in File.ReadLines(path))
|
foreach (var line in File.ReadLines(path))
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(line))
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
@@ -242,6 +244,8 @@ public static class RleBlacklist
|
|||||||
|
|
||||||
if (j > i)
|
if (j > i)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace int.TryParse with the Utf8Parser-based span helper so blacklist normalization avoids
|
||||||
|
// repeated culture-aware integer parsing in hot loading paths.
|
||||||
if (int.TryParse(span.Slice(i, j - i), out int v) && v > 0)
|
if (int.TryParse(span.Slice(i, j - i), out int v) && v > 0)
|
||||||
{
|
{
|
||||||
tmp[count++] = v;
|
tmp[count++] = v;
|
||||||
@@ -323,6 +327,8 @@ public static class RleBlacklist
|
|||||||
|
|
||||||
if (j > i)
|
if (j > i)
|
||||||
{
|
{
|
||||||
|
// TODO: Swap this int.TryParse for the upcoming Utf8Parser fast path so nested patterns reuse the
|
||||||
|
// zero-allocation integer parser during blacklist ingestion.
|
||||||
if (int.TryParse(inner.Slice(i, j - i), out int v) && v > 0)
|
if (int.TryParse(inner.Slice(i, j - i), out int v) && v > 0)
|
||||||
{
|
{
|
||||||
tmp[count++] = v;
|
tmp[count++] = v;
|
||||||
|
|||||||
@@ -7,11 +7,17 @@ public static class StringBuilderPool
|
|||||||
{
|
{
|
||||||
private static readonly ConcurrentQueue<StringBuilder> _stringBuilderPool = new();
|
private static readonly ConcurrentQueue<StringBuilder> _stringBuilderPool = new();
|
||||||
|
|
||||||
public static StringBuilder Rent() => _stringBuilderPool.TryDequeue(out var sb) ? sb : new();
|
public static StringBuilder Rent()
|
||||||
|
{
|
||||||
|
// TODO: Ensure oversized builders remain eligible for reuse without trimming so call sites keep their full capacity.
|
||||||
|
return _stringBuilderPool.TryDequeue(out var sb) ? sb : new();
|
||||||
|
}
|
||||||
|
|
||||||
public static void Return(StringBuilder sb)
|
public static void Return(StringBuilder sb)
|
||||||
{
|
{
|
||||||
_ = sb.Clear();
|
_ = sb.Clear();
|
||||||
|
// TODO: Preserve the builder's capacity when returning it so the pool hands the same buffer back without shrinkage.
|
||||||
_stringBuilderPool.Enqueue(sb);
|
_stringBuilderPool.Enqueue(sb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +23,16 @@ public sealed class TextFileWriter : IDisposable
|
|||||||
|
|
||||||
public long Position
|
public long Position
|
||||||
{
|
{
|
||||||
get { lock (_lock) { return _stream.Position; } }
|
get
|
||||||
|
{
|
||||||
|
// TODO: Surface a lock-free position snapshot (for example via Interlocked.Read on a cached long)
|
||||||
|
// so the high-frequency status probes measured in the writer throughput profiling stop contending on
|
||||||
|
// the flush lock while runs follow the buffered pipeline planned for the scanner fast path.
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _stream.Position;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private StreamWriter TextWriter => _textWriter ??= new StreamWriter(_stream, leaveOpen: true);
|
private StreamWriter TextWriter => _textWriter ??= new StreamWriter(_stream, leaveOpen: true);
|
||||||
@@ -36,6 +45,9 @@ public sealed class TextFileWriter : IDisposable
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
|
// TODO: Use the pooled writer buffers planned for the TextFileWriter fast path so truncation flushes
|
||||||
|
// reuse the span caches identified alongside the Mod10_8_5_3Benchmarks instead of forcing synchronous
|
||||||
|
// SetLength/Flush work on every restart.
|
||||||
_stream.SetLength(0);
|
_stream.SetLength(0);
|
||||||
_stream.Position = 0;
|
_stream.Position = 0;
|
||||||
_textWriter?.Flush();
|
_textWriter?.Flush();
|
||||||
@@ -65,6 +77,8 @@ public sealed class TextFileWriter : IDisposable
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
|
// TODO: Buffer binary writes with the same ArrayPool-backed accumulator planned for text so GPU result
|
||||||
|
// dumps can batch disk flushes instead of flushing on every invocation.
|
||||||
action(BinaryWriter);
|
action(BinaryWriter);
|
||||||
BinaryWriter.Flush();
|
BinaryWriter.Flush();
|
||||||
}
|
}
|
||||||
@@ -72,6 +86,8 @@ public sealed class TextFileWriter : IDisposable
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
// TODO: Return the writers to a pooled wrapper once the buffered pipeline lands so disposing the
|
||||||
|
// TextFileWriter mirrors the zero-allocation strategy validated by the Mod10_8_5_3Benchmarks helpers.
|
||||||
_binaryWriter?.Dispose();
|
_binaryWriter?.Dispose();
|
||||||
_textWriter?.Dispose();
|
_textWriter?.Dispose();
|
||||||
_stream.Dispose();
|
_stream.Dispose();
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ public static class UInt128Extensions
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static UInt128 BinaryGcd(this UInt128 u, UInt128 v)
|
public static UInt128 BinaryGcd(this UInt128 u, UInt128 v)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace this hand-rolled binary GCD with the shared subtract-free ladder from
|
||||||
|
// GpuUInt128BinaryGcdBenchmarks so wide operands reuse the optimized helper instead of
|
||||||
|
// repeating the slower shift/subtract loop on both CPU and GPU paths.
|
||||||
UInt128 zero = UInt128.Zero;
|
UInt128 zero = UInt128.Zero;
|
||||||
if (u == zero)
|
if (u == zero)
|
||||||
{
|
{
|
||||||
@@ -57,6 +60,10 @@ public static class UInt128Extensions
|
|||||||
|
|
||||||
int i = 0, primesLength = smallPrimes.Length;
|
int i = 0, primesLength = smallPrimes.Length;
|
||||||
UInt128 cycle = MersenneDivisorCycles.GetCycle(q);
|
UInt128 cycle = MersenneDivisorCycles.GetCycle(q);
|
||||||
|
// TODO: If the cache lacks this cycle, immediately schedule the configured device
|
||||||
|
// (GPU by default) to compute it on the fly and skip inserting it into the cache so
|
||||||
|
// wide-order factoring can still leverage cycle stepping without breaching the
|
||||||
|
// memory cap or introducing extra synchronization.
|
||||||
ulong prime, temp;
|
ulong prime, temp;
|
||||||
for (; i < primesLength; i++)
|
for (; i < primesLength; i++)
|
||||||
{
|
{
|
||||||
@@ -128,6 +135,8 @@ public static class UInt128Extensions
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static ulong Mod3(this UInt128 value)
|
public static ulong Mod3(this UInt128 value)
|
||||||
{
|
{
|
||||||
|
// TODO: Fold these reductions into the multiply-high trick captured in Mod3BenchmarkResults so
|
||||||
|
// the UInt128 modulo helpers avoid `%` altogether and align with the faster CPU/GPU residue filters.
|
||||||
ulong remainder = ((ulong)value) % 3UL + ((ulong)(value >> 64)) % 3UL;
|
ulong remainder = ((ulong)value) % 3UL + ((ulong)(value >> 64)) % 3UL;
|
||||||
return remainder >= 3UL ? remainder - 3UL : remainder;
|
return remainder >= 3UL ? remainder - 3UL : remainder;
|
||||||
}
|
}
|
||||||
@@ -135,6 +144,8 @@ public static class UInt128Extensions
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static ulong Mod5(this UInt128 value)
|
public static ulong Mod5(this UInt128 value)
|
||||||
{
|
{
|
||||||
|
// TODO: Replace the `% 5` operations with the precomputed multiply-high constants from the Mod5
|
||||||
|
// benchmarks so the UInt128 path matches the 64-bit helpers without extra modulo instructions.
|
||||||
ulong remainder = (((ulong)value) % 5UL) + (((ulong)(value >> 64)) % 5UL);
|
ulong remainder = (((ulong)value) % 5UL) + (((ulong)(value >> 64)) % 5UL);
|
||||||
return remainder >= 5UL ? remainder - 5UL : remainder;
|
return remainder >= 5UL ? remainder - 5UL : remainder;
|
||||||
}
|
}
|
||||||
@@ -153,6 +164,8 @@ public static class UInt128Extensions
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static ulong Mod7(this UInt128 value)
|
public static ulong Mod7(this UInt128 value)
|
||||||
{
|
{
|
||||||
|
// TODO: Inline the lookup-based Mod7 reducer validated in the residue benchmarks so this helper
|
||||||
|
// stops relying on `% 7` in hot loops and mirrors the optimized CPU cycle filters.
|
||||||
ulong low = (ulong)value % 7UL;
|
ulong low = (ulong)value % 7UL;
|
||||||
ulong high = (ulong)(value >> 64) % 7UL;
|
ulong high = (ulong)(value >> 64) % 7UL;
|
||||||
ulong remainder = low + (high * 2UL);
|
ulong remainder = low + (high * 2UL);
|
||||||
@@ -175,6 +188,8 @@ public static class UInt128Extensions
|
|||||||
public static ulong Mod10(this UInt128 value128)
|
public static ulong Mod10(this UInt128 value128)
|
||||||
{
|
{
|
||||||
// Split and fold under mod 10: 2^64 ≡ 6 (mod 10)
|
// Split and fold under mod 10: 2^64 ≡ 6 (mod 10)
|
||||||
|
// TODO: Precompute the 2^64 ≡ 6 folding constants once so this path stops recomputing `% 10`
|
||||||
|
// on each half and instead uses the span-based lookup captured in Mod10BenchmarkResults.
|
||||||
ulong low = (ulong)value128;
|
ulong low = (ulong)value128;
|
||||||
ulong high = (ulong)(value128 >> 64);
|
ulong high = (ulong)(value128 >> 64);
|
||||||
return ((low % 10UL) + ((high % 10UL) * 6UL)) % 10UL;
|
return ((low % 10UL) + ((high % 10UL) * 6UL)) % 10UL;
|
||||||
@@ -196,6 +211,8 @@ public static class UInt128Extensions
|
|||||||
while (value != zero);
|
while (value != zero);
|
||||||
|
|
||||||
mod5 = byteSum.Mod5();
|
mod5 = byteSum.Mod5();
|
||||||
|
// TODO: Collapse this Mod10 switch into the shared lookup table from the CLI benchmarks so we
|
||||||
|
// avoid the nested switch expressions once the pooled residue tables become available.
|
||||||
mod10 = (mod8 & 1UL) == 0UL
|
mod10 = (mod8 & 1UL) == 0UL
|
||||||
? mod5 switch
|
? mod5 switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,5 +6,10 @@ namespace PerfectNumbers.Core
|
|||||||
public static readonly UInt128 Four = 4UL;
|
public static readonly UInt128 Four = 4UL;
|
||||||
public static readonly UInt128 SixtyFour = 64UL;
|
public static readonly UInt128 SixtyFour = 64UL;
|
||||||
public static readonly UInt128 OneHundredTwentySeven = 127UL;
|
public static readonly UInt128 OneHundredTwentySeven = 127UL;
|
||||||
|
// TODO: Populate this table from the UInt128 residue benchmarks so GPU kernels can pull precomputed constants
|
||||||
|
// without re-deriving them at runtime.
|
||||||
|
// TODO: Extend the table with cached Mod3/Mod5 folding constants so both CPU and GPU residue helpers can reuse
|
||||||
|
// the benchmarked multiply-high reductions without recomputing them per call.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6,6 +6,8 @@ public static class UIntExtensions
|
|||||||
{
|
{
|
||||||
private const ulong Mod3Mask = 0xAAAAAAABUL;
|
private const ulong Mod3Mask = 0xAAAAAAABUL;
|
||||||
public const ulong Mod5Mask = 0xCCCCCCCDUL;
|
public const ulong Mod5Mask = 0xCCCCCCCDUL;
|
||||||
|
// TODO: Extend this helper set with the Mod7/Mod11 lookup constants so CPU residue automation can drop `%` entirely in
|
||||||
|
// the small-prime sieves.
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static uint Mod3(uint value) => value - (uint)((value * Mod3Mask) >> 33) * 3U;
|
public static uint Mod3(uint value) => value - (uint)((value * Mod3Mask) >> 33) * 3U;
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ public static class ULongExtensions
|
|||||||
int i = 0, primesLength = smallPrimes.Length;
|
int i = 0, primesLength = smallPrimes.Length;
|
||||||
UInt128 q128 = q,
|
UInt128 q128 = q,
|
||||||
cycle = MersenneDivisorCycles.GetCycle(q128);
|
cycle = MersenneDivisorCycles.GetCycle(q128);
|
||||||
|
// TODO: When the shared cycle snapshot cannot serve this divisor, trigger an on-demand
|
||||||
|
// GPU computation (respecting the configured device) without promoting the result into
|
||||||
|
// the cache so the order calculator still benefits from cycle stepping while keeping the
|
||||||
|
// single-block memory plan intact.
|
||||||
|
|
||||||
for (; i < primesLength; i++)
|
for (; i < primesLength; i++)
|
||||||
{
|
{
|
||||||
@@ -235,6 +239,8 @@ public static class ULongExtensions
|
|||||||
public static ulong ModPow64(this ulong value, ulong exponent, ulong modulus)
|
public static ulong ModPow64(this ulong value, ulong exponent, ulong modulus)
|
||||||
{
|
{
|
||||||
ulong result = 1UL;
|
ulong result = 1UL;
|
||||||
|
// TODO: Replace this `%` with the Montgomery folding helper highlighted in MulMod64Benchmarks so the
|
||||||
|
// modular exponentiation avoids the slow integer division before the ladder even starts.
|
||||||
value %= modulus;
|
value %= modulus;
|
||||||
|
|
||||||
while (exponent != 0UL)
|
while (exponent != 0UL)
|
||||||
@@ -367,6 +373,8 @@ public static class ULongExtensions
|
|||||||
return exponent.Pow2MontgomeryMod(divisor);
|
return exponent.Pow2MontgomeryMod(divisor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Swap this modulo with the divisor-cycle remainder helper from Pow2MontgomeryModCycleComputationBenchmarks so we
|
||||||
|
// advance the exponent using cached cycle deltas instead of paying for a fresh `%` on every query.
|
||||||
ulong rotationCount = exponent % cycleLength;
|
ulong rotationCount = exponent % cycleLength;
|
||||||
return rotationCount.Pow2MontgomeryModFromCycleRemainder(divisor);
|
return rotationCount.Pow2MontgomeryModFromCycleRemainder(divisor);
|
||||||
}
|
}
|
||||||
@@ -474,6 +482,8 @@ public static class ULongExtensions
|
|||||||
exponentLoopIndex = 0UL;
|
exponentLoopIndex = 0UL;
|
||||||
// Reusing result after resetting it for the rotation-accumulation pass.
|
// Reusing result after resetting it for the rotation-accumulation pass.
|
||||||
result = UInt128.One;
|
result = UInt128.One;
|
||||||
|
// TODO: Replace this modulo with the cached cycle remainder produced by the divisor-cycle cache so PowModWithCycle avoids
|
||||||
|
// repeated `%` work, matching the ProcessEightBitWindows wins captured in Pow2MontgomeryModCycleComputationBenchmarks.
|
||||||
ulong rotationCount = exponent % cycleLength;
|
ulong rotationCount = exponent % cycleLength;
|
||||||
for (; exponentLoopIndex < rotationCount; exponentLoopIndex++)
|
for (; exponentLoopIndex < rotationCount; exponentLoopIndex++)
|
||||||
{
|
{
|
||||||
@@ -525,6 +535,8 @@ public static class ULongExtensions
|
|||||||
|
|
||||||
// Reusing result after resetting it for the rotation-driven accumulation phase.
|
// Reusing result after resetting it for the rotation-driven accumulation phase.
|
||||||
result = UInt128.One;
|
result = UInt128.One;
|
||||||
|
// TODO: Swap this modulo with the upcoming UInt128 cycle remainder helper so large-exponent scans reuse cached
|
||||||
|
// reductions instead of recomputing `%` for every lookup, as demonstrated in Pow2MontgomeryModCycleComputationBenchmarks.
|
||||||
UInt128 rotationCount = exponent % cycleLength;
|
UInt128 rotationCount = exponent % cycleLength;
|
||||||
UInt128 rotationIndex = UInt128.Zero;
|
UInt128 rotationIndex = UInt128.Zero;
|
||||||
while (rotationIndex < rotationCount)
|
while (rotationIndex < rotationCount)
|
||||||
@@ -581,6 +593,8 @@ public static class ULongExtensions
|
|||||||
|
|
||||||
// Reusing result after resetting it for the rotation-driven accumulation phase.
|
// Reusing result after resetting it for the rotation-driven accumulation phase.
|
||||||
result = one;
|
result = one;
|
||||||
|
// TODO: Swap this modulo with the shared UInt128 cycle remainder helper once available so CRT powmods reuse cached
|
||||||
|
// reductions in the windowed ladder, avoiding the `%` cost highlighted in Pow2MontgomeryModCycleComputationBenchmarks.
|
||||||
UInt128 rotationCount = exponent % cycleLength;
|
UInt128 rotationCount = exponent % cycleLength;
|
||||||
|
|
||||||
// We're reusing "zero" as rotation index for just a little better performance
|
// We're reusing "zero" as rotation index for just a little better performance
|
||||||
@@ -626,6 +640,8 @@ public static class ULongExtensions
|
|||||||
// Since currentModulus and m are coprime, use CRT:
|
// Since currentModulus and m are coprime, use CRT:
|
||||||
// x = result + currentModulus * t, where t ≡ (remM - result) * inv(currentModulus, m) mod m
|
// x = result + currentModulus * t, where t ≡ (remM - result) * inv(currentModulus, m) mod m
|
||||||
|
|
||||||
|
// TODO: Replace this `% modulusCandidate` with the cached residue helper derived from Mod10_8_5_3Benchmarks so CRT
|
||||||
|
// composition avoids repeated modulo divisions when combining residues for large divisor sets.
|
||||||
result += currentModulus * ((remainderForCandidate + modulusCandidate - (result % modulusCandidate)) * ModInverse(currentModulus, modulusCandidate) % modulusCandidate);
|
result += currentModulus * ((remainderForCandidate + modulusCandidate - (result % modulusCandidate)) * ModInverse(currentModulus, modulusCandidate) % modulusCandidate);
|
||||||
currentModulus *= modulusCandidate;
|
currentModulus *= modulusCandidate;
|
||||||
|
|
||||||
@@ -633,6 +649,8 @@ public static class ULongExtensions
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Swap this final `% modulus` with the pooled remainder cache so the CRT result write-back avoids one more division,
|
||||||
|
// aligning with the optimizations captured in Mod10_8_5_3Benchmarks.
|
||||||
return result % modulus;
|
return result % modulus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user