Merge pull request #103 from mlemanczyk/codex/review-and-add-todos-for-optimization
Document cycle stepper wrapper removal TODOs
This commit is contained in:
53 files changed
+1736
-1131
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -149,22 +149,25 @@ internal static class Program
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ulong ParseRangeValue(in string option, in string value)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
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))
|
||||
{
|
||||
throw new FormatException($"Value '{value}' for option '{option}' is not a valid non-negative integer.");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private static int ParsePositiveInt(in string option, in string value)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
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)
|
||||
{
|
||||
throw new FormatException($"Value '{value}' for option '{option}' is not a valid positive integer.");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
@@ -175,22 +178,24 @@ internal static class Program
|
||||
out string option,
|
||||
out string value)
|
||||
{
|
||||
option = string.Empty;
|
||||
value = string.Empty;
|
||||
string current = arguments[index];
|
||||
int equalsIndex = current.IndexOf('=');
|
||||
if (equalsIndex <= 0 || equalsIndex == current.Length - 1)
|
||||
{
|
||||
Console.Error.WriteLine($"Option '{current}' must use the --name=value syntax.");
|
||||
PrintHelp();
|
||||
return false;
|
||||
}
|
||||
option = string.Empty;
|
||||
value = string.Empty;
|
||||
string current = arguments[index];
|
||||
int equalsIndex = current.IndexOf('=');
|
||||
if (equalsIndex <= 0 || equalsIndex == current.Length - 1)
|
||||
{
|
||||
Console.Error.WriteLine($"Option '{current}' must use the --name=value syntax.");
|
||||
PrintHelp();
|
||||
return false;
|
||||
}
|
||||
|
||||
option = current[..equalsIndex];
|
||||
value = current[(equalsIndex + 1)..];
|
||||
index++;
|
||||
return true;
|
||||
}
|
||||
// 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];
|
||||
value = current[(equalsIndex + 1)..];
|
||||
index++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
@@ -225,7 +230,10 @@ internal static class Program
|
||||
|
||||
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();
|
||||
if (headerLine is null)
|
||||
{
|
||||
@@ -239,7 +247,10 @@ internal static class Program
|
||||
|
||||
string header = string.IsNullOrWhiteSpace(headerLine) ? DefaultHeader : headerLine.Trim();
|
||||
|
||||
List<CandidateResult> candidates = LoadCandidates(reader, pMin, pMax, threadCount, bufferSize);
|
||||
// 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);
|
||||
Console.WriteLine("Building sorted prime results...");
|
||||
List<CandidateResult> sortedPrimeResults = ExtractSortedPrimeCandidates(candidates);
|
||||
Console.WriteLine("Building RAW prime results...");
|
||||
@@ -303,23 +314,29 @@ internal static class Program
|
||||
throw new FormatException("Input line does not contain expected comma separators.");
|
||||
}
|
||||
|
||||
ulong p = ulong.Parse(span[..firstCommaIndex], NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
// 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);
|
||||
if (p < pMin || p > pMax)
|
||||
{
|
||||
return CandidateResult.Empty;
|
||||
}
|
||||
|
||||
bool passedAllTests = bool.Parse(span[(lastCommaIndex + 1)..]);
|
||||
// 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)..]);
|
||||
return new CandidateResult(p, passedAllTests, line);
|
||||
}
|
||||
|
||||
private static string BuildOutputPath(in string inputPath, in string prefix)
|
||||
{
|
||||
string fileName = Path.GetFileName(inputPath);
|
||||
string prefixedName = prefix + fileName;
|
||||
string? directory = Path.GetDirectoryName(inputPath);
|
||||
if (string.IsNullOrEmpty(directory))
|
||||
{
|
||||
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 prefixedName = prefix + fileName;
|
||||
string? directory = Path.GetDirectoryName(inputPath);
|
||||
if (string.IsNullOrEmpty(directory))
|
||||
{
|
||||
return prefixedName;
|
||||
}
|
||||
|
||||
@@ -328,39 +345,49 @@ internal static class Program
|
||||
|
||||
private static void WriteCsv(in string path, in string header, bool append, IEnumerable<CandidateResult> entries)
|
||||
{
|
||||
using StreamWriter writer = new(path, append, Encoding.UTF8);
|
||||
if (!append)
|
||||
{
|
||||
writer.WriteLine(header);
|
||||
}
|
||||
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)
|
||||
{
|
||||
writer.WriteLine(header);
|
||||
}
|
||||
|
||||
foreach (CandidateResult entry in entries)
|
||||
{
|
||||
writer.WriteLine(entry.Csv);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
private static List<CandidateResult> LoadCandidates(
|
||||
}
|
||||
|
||||
private static List<CandidateResult> LoadCandidates(
|
||||
StreamReader reader,
|
||||
ulong pMin,
|
||||
ulong pMax,
|
||||
int threadCount,
|
||||
int bufferSize)
|
||||
{
|
||||
List<CandidateResult> candidates = [];
|
||||
if (bufferSize < 1)
|
||||
{
|
||||
return candidates;
|
||||
}
|
||||
List<CandidateResult> candidates = [];
|
||||
if (bufferSize < 1)
|
||||
{
|
||||
return candidates;
|
||||
}
|
||||
|
||||
int workerLimit = Math.Max(1, threadCount);
|
||||
List<Task<ChunkResult>> activeChunks = new(workerLimit);
|
||||
// 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);
|
||||
Dictionary<int, ChunkResult> completedChunks = new();
|
||||
int nextChunkToFlush = 0;
|
||||
int consoleProgress = 0;
|
||||
ulong lastReportedCandidate = 0UL;
|
||||
|
||||
string[] buffer = new string[bufferSize];
|
||||
// TODO: Rent this line buffer from ArrayPool<string> to eliminate repeated allocations when scanning
|
||||
// large result files.
|
||||
string[] buffer = new string[bufferSize];
|
||||
int buffered = 0;
|
||||
int chunkIndex = 0;
|
||||
|
||||
@@ -419,14 +446,18 @@ internal static class Program
|
||||
void DispatchBuffer(int length)
|
||||
{
|
||||
string[] chunkLines;
|
||||
if (length == buffer.Length)
|
||||
{
|
||||
chunkLines = buffer;
|
||||
buffer = new string[bufferSize];
|
||||
}
|
||||
if (length == buffer.Length)
|
||||
{
|
||||
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];
|
||||
}
|
||||
else
|
||||
{
|
||||
chunkLines = new string[length];
|
||||
// TODO: Rent the partial chunk buffer from ArrayPool<string> instead of allocating a fresh
|
||||
// array for every remainder dispatch.
|
||||
chunkLines = new string[length];
|
||||
Array.Copy(buffer, chunkLines, length);
|
||||
}
|
||||
|
||||
@@ -465,7 +496,9 @@ internal static class Program
|
||||
|
||||
private static ChunkResult ProcessChunk(string[] lines, int index, ulong pMin, ulong pMax)
|
||||
{
|
||||
CandidateResult[] items = new CandidateResult[lines.Length];
|
||||
// TODO: Rent the candidate array from ArrayPool<CandidateResult> so chunk processing remains allocation-free
|
||||
// for hot reload paths.
|
||||
CandidateResult[] items = new CandidateResult[lines.Length];
|
||||
int count = 0;
|
||||
ulong pEmpty = CandidateResult.Empty.P;
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
@@ -495,11 +528,15 @@ internal static class Program
|
||||
return [];
|
||||
}
|
||||
|
||||
List<CandidateResult> sortedCandidates = [.. candidates];
|
||||
// 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];
|
||||
sortedCandidates.Sort(CandidateResultComparer.Instance);
|
||||
|
||||
List<CandidateResult> primeCandidates = [];
|
||||
using IEnumerator<ulong> primeEnumerator = Prime.Numbers.GetEnumerator();
|
||||
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();
|
||||
if (!primeEnumerator.MoveNext())
|
||||
{
|
||||
throw new InvalidOperationException("Prime generator did not provide any values.");
|
||||
@@ -538,7 +575,9 @@ internal static class Program
|
||||
return [];
|
||||
}
|
||||
|
||||
Dictionary<ulong, int> remainingCounts = new(primeCount);
|
||||
// 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);
|
||||
for (int i = 0; i < primeCount; i++)
|
||||
{
|
||||
CandidateResult candidate = sortedPrimeResults[i];
|
||||
@@ -551,7 +590,9 @@ internal static class Program
|
||||
remainingCounts.Add(candidate.P, 1);
|
||||
}
|
||||
|
||||
List<CandidateResult> rawPrimes = new(primeCount);
|
||||
// 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);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
CandidateResult candidate = candidates[i];
|
||||
@@ -597,10 +638,10 @@ internal static class Program
|
||||
return;
|
||||
}
|
||||
|
||||
if (threadCount <= 1 || count < threadCount * 4)
|
||||
{
|
||||
passedResults = new List<CandidateResult>(count);
|
||||
rejectedResults = new List<CandidateResult>(count);
|
||||
if (threadCount <= 1 || count < threadCount * 4)
|
||||
{
|
||||
passedResults = new List<CandidateResult>(count);
|
||||
rejectedResults = new List<CandidateResult>(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
CandidateResult candidate = sortedResults[i];
|
||||
@@ -617,13 +658,20 @@ internal static class Program
|
||||
}
|
||||
|
||||
int workerCount = Math.Min(threadCount, count);
|
||||
List<CandidateResult>[] passedSegments = new List<CandidateResult>[workerCount];
|
||||
List<CandidateResult>[] rejectedSegments = new List<CandidateResult>[workerCount];
|
||||
Task[] tasks = new Task[workerCount];
|
||||
// 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>[] rejectedSegments = new List<CandidateResult>[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 baseChunk = count / workerCount;
|
||||
int remainder = count % workerCount;
|
||||
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 remainder = count % workerCount;
|
||||
for (int worker = 0; worker < workerCount; worker++)
|
||||
{
|
||||
int length = baseChunk + (worker < remainder ? 1 : 0);
|
||||
@@ -639,10 +687,15 @@ internal static class Program
|
||||
}
|
||||
|
||||
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(() =>
|
||||
{
|
||||
List<CandidateResult> localPassed = 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;
|
||||
for (int i = chunkStart; i < end; i++)
|
||||
{
|
||||
@@ -663,10 +716,13 @@ internal static class Program
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
|
||||
passedResults = new List<CandidateResult>(count);
|
||||
rejectedResults = new List<CandidateResult>(count);
|
||||
for (int worker = 0; worker < workerCount; worker++)
|
||||
{
|
||||
passedResults = 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++)
|
||||
{
|
||||
if (passedSegments[worker].Count > 0)
|
||||
{
|
||||
passedResults.AddRange(passedSegments[worker]);
|
||||
|
||||
+262
-184
@@ -104,9 +104,11 @@ internal static class Program
|
||||
showHelp = true;
|
||||
break;
|
||||
}
|
||||
else if (arg.StartsWith("--prime=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
currentP = ulong.Parse(arg.AsSpan(arg.IndexOf('=') + 1));
|
||||
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));
|
||||
startPrimeProvided = true;
|
||||
}
|
||||
else if (arg.Equals("--increment=bit", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -117,9 +119,11 @@ internal static class Program
|
||||
{
|
||||
useBitTransform = false;
|
||||
}
|
||||
else if (arg.StartsWith("--threads=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
threadCount = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||
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)));
|
||||
}
|
||||
else if (arg.StartsWith("--mersenne=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -155,31 +159,37 @@ internal static class Program
|
||||
kernelType = GpuKernelType.Incremental;
|
||||
}
|
||||
}
|
||||
else if (arg.StartsWith("--divisor=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
divisor = UInt128.Parse(arg.AsSpan(arg.IndexOf('=') + 1));
|
||||
}
|
||||
else if (arg.StartsWith("--use-divisor-cycles=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (bool.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out bool parsedBool))
|
||||
{
|
||||
useDivisorCycles = parsedBool;
|
||||
}
|
||||
}
|
||||
else if (arg.StartsWith("--divisor-cycles-limit=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (ulong.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out parsedLimit))
|
||||
{
|
||||
divisorCyclesSearchLimit = parsedLimit;
|
||||
}
|
||||
}
|
||||
else if (arg.StartsWith("--residue-max-k=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (ulong.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out parsedResidueMax))
|
||||
{
|
||||
residueKMax = parsedResidueMax;
|
||||
}
|
||||
else if (arg.StartsWith("--divisor=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// TODO: Replace UInt128.Parse with the benchmarked fast-path parser once we expose the
|
||||
// TryParse-style span helper for UInt128 command-line arguments.
|
||||
divisor = UInt128.Parse(arg.AsSpan(arg.IndexOf('=') + 1));
|
||||
}
|
||||
else if (arg.StartsWith("--use-divisor-cycles=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (bool.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out bool parsedBool))
|
||||
{
|
||||
useDivisorCycles = parsedBool;
|
||||
}
|
||||
}
|
||||
else if (arg.StartsWith("--divisor-cycles-limit=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// 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))
|
||||
{
|
||||
divisorCyclesSearchLimit = parsedLimit;
|
||||
}
|
||||
}
|
||||
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))
|
||||
{
|
||||
residueKMax = parsedResidueMax;
|
||||
}
|
||||
}
|
||||
// Replaces --lucas=cpu|gpu; controls device for Lucas and for
|
||||
// incremental/pow2mod scanning
|
||||
else if (arg.StartsWith("--mersenne-device=", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -194,15 +204,17 @@ internal static class Program
|
||||
{
|
||||
useOrder = true;
|
||||
}
|
||||
else if (arg.StartsWith("--order-warmup-limit=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// stored and used below when initializing testers
|
||||
// Reusing parsedLimit to capture the --order-warmup-limit argument.
|
||||
if (ulong.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out parsedLimit))
|
||||
{
|
||||
_orderWarmupLimitOverride = parsedLimit;
|
||||
}
|
||||
}
|
||||
else if (arg.StartsWith("--order-warmup-limit=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// stored and used below when initializing testers
|
||||
// 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))
|
||||
{
|
||||
_orderWarmupLimitOverride = parsedLimit;
|
||||
}
|
||||
}
|
||||
else if (arg.Equals("--gcd-filter", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_useGcdFilter = true;
|
||||
@@ -255,42 +267,48 @@ internal static class Program
|
||||
{
|
||||
_rleBlacklistPath = arg[(arg.IndexOf('=') + 1)..];
|
||||
}
|
||||
else if (arg.StartsWith("--rle-hard-max=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ulong rleMaxP;
|
||||
if (ulong.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out rleMaxP))
|
||||
{
|
||||
_rleHardMaxP = rleMaxP;
|
||||
}
|
||||
}
|
||||
else if (arg.StartsWith("--rle-hard-max=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
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))
|
||||
{
|
||||
_rleHardMaxP = rleMaxP;
|
||||
}
|
||||
}
|
||||
else if (arg.StartsWith("--rle-only-last7=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ReadOnlySpan<char> value = arg.AsSpan(arg.IndexOf('=') + 1);
|
||||
_rleOnlyLast7 = !value.Equals("false", StringComparison.OrdinalIgnoreCase) && !value.Equals("0", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else if (arg.StartsWith("--zero-hard=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
double zeroFraction;
|
||||
if (double.TryParse(arg.AsSpan(arg.IndexOf('=') + 1), out zeroFraction))
|
||||
{
|
||||
_zeroFracHard = zeroFraction;
|
||||
}
|
||||
}
|
||||
else if (arg.StartsWith("--zero-hard=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
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))
|
||||
{
|
||||
_zeroFracHard = zeroFraction;
|
||||
}
|
||||
}
|
||||
else if (arg.StartsWith("--zero-conj=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// format: <zeroFrac>:<maxZeroBlock>
|
||||
ReadOnlySpan<char> value = arg.AsSpan(arg.IndexOf('=') + 1);
|
||||
int colon = value.IndexOf(':');
|
||||
if (colon > 0)
|
||||
{
|
||||
double zeroFrac;
|
||||
int maxZero;
|
||||
if (double.TryParse(value[..colon], out zeroFrac) && int.TryParse(value[(colon + 1)..], out maxZero))
|
||||
{
|
||||
_zeroFracConj = zeroFrac;
|
||||
_maxZeroConj = maxZero;
|
||||
}
|
||||
}
|
||||
int colon = value.IndexOf(':');
|
||||
if (colon > 0)
|
||||
{
|
||||
double zeroFrac;
|
||||
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))
|
||||
{
|
||||
_zeroFracConj = zeroFrac;
|
||||
_maxZeroConj = maxZero;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Backward-compat: accept deprecated device flags and map to new ones
|
||||
else if (arg.StartsWith("--lucas=", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -315,33 +333,44 @@ internal static class Program
|
||||
_resultsPrefix = arg[(arg.IndexOf('=') + 1)..];
|
||||
}
|
||||
|
||||
else if (arg.StartsWith("--gpu-prime-threads=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
gpuPrimeThreads = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||
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)));
|
||||
}
|
||||
else if (arg.StartsWith("--gpu-prime-batch=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
gpuPrimeBatch = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||
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)));
|
||||
}
|
||||
else if (arg.StartsWith("--ll-slice=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sliceSize = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||
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)));
|
||||
}
|
||||
else if (arg.StartsWith("--gpu-scan-batch=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
scanBatchSize = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||
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)));
|
||||
}
|
||||
else if (arg.StartsWith("--block-size=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
blockSize = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||
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)));
|
||||
}
|
||||
else if (arg.StartsWith("--filter-p=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
filterFile = arg[(arg.IndexOf('=') + 1)..];
|
||||
}
|
||||
else if (arg.StartsWith("--write-batch-size=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_writeBatchSize = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||
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)));
|
||||
}
|
||||
if (arg.StartsWith("--divisor-cycles="))
|
||||
{
|
||||
@@ -351,9 +380,11 @@ internal static class Program
|
||||
{
|
||||
useGpuCycles = !arg.AsSpan(arg.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else if (arg.StartsWith("--divisor-cycles-batch=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
cyclesBatchSize = Math.Max(1, int.Parse(arg.AsSpan(arg.IndexOf('=') + 1)));
|
||||
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)));
|
||||
}
|
||||
else if (arg.Equals("--divisor-cycles-continue", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -428,9 +459,15 @@ internal static class Program
|
||||
MersenneDivisorCycles.Shared.LoadFrom(cyclesPath);
|
||||
}
|
||||
|
||||
DivisorCycleCache.SetDivisorCyclesBatchSize(cyclesBatchSize);
|
||||
DivisorCycleCache.Shared.ConfigureGeneratorDevice(orderOnGpu);
|
||||
DivisorCycleCache.Shared.ReloadFromCurrentSnapshot();
|
||||
DivisorCycleCache.SetDivisorCyclesBatchSize(cyclesBatchSize);
|
||||
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();
|
||||
// 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");
|
||||
|
||||
@@ -581,8 +618,11 @@ internal static class Program
|
||||
}
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
bool useFilter = !string.IsNullOrEmpty(filterFile);
|
||||
HashSet<ulong> filter = [];
|
||||
@@ -599,6 +639,8 @@ internal static class Program
|
||||
}
|
||||
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];
|
||||
filterCount = 0;
|
||||
LoadResultsFile(filterFile, (p, detailedCheck, passedAllTests) =>
|
||||
@@ -672,7 +714,9 @@ internal static class Program
|
||||
int count, j;
|
||||
ulong p;
|
||||
bool passedAllTests, searchedMersenne, detailedCheck;
|
||||
ulong[] buffer = new ulong[blockSize];
|
||||
// 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];
|
||||
bool reachedMax = false;
|
||||
while (!Volatile.Read(ref _limitReached))
|
||||
{
|
||||
@@ -753,6 +797,8 @@ internal static class Program
|
||||
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;
|
||||
while (index < span.Length)
|
||||
{
|
||||
@@ -769,6 +815,8 @@ internal static class Program
|
||||
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))
|
||||
{
|
||||
candidates.Add(parsed);
|
||||
@@ -786,6 +834,8 @@ internal static class Program
|
||||
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(
|
||||
candidates,
|
||||
_byDivisorTester,
|
||||
@@ -832,7 +882,9 @@ internal static class Program
|
||||
continue;
|
||||
}
|
||||
|
||||
parsedP = ulong.Parse(span[..first]);
|
||||
// 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]);
|
||||
span = span[(first + 1)..];
|
||||
second = span.IndexOf(',');
|
||||
if (second < 0)
|
||||
@@ -850,7 +902,9 @@ internal static class Program
|
||||
detailedSpan = span[..third];
|
||||
passedAllTestsSpan = span[(third + 1)..];
|
||||
|
||||
if (bool.TryParse(detailedSpan, out detailed) && bool.TryParse(passedAllTestsSpan, out passedAllTests))
|
||||
// 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))
|
||||
{
|
||||
lineProcessorAction(parsedP, detailed, passedAllTests);
|
||||
}
|
||||
@@ -902,57 +956,62 @@ internal static class Program
|
||||
Console.WriteLine(" --help, -help, --?, -?, /? show this help message");
|
||||
}
|
||||
|
||||
private static string BuildResultsFileName(bool bitInc, int threads, int block, GpuKernelType kernelType, bool useLucasFlag, bool useDivisorFlag, bool useByDivisorFlag, bool mersenneOnGpu, bool useOrder, bool useModWorkaround, bool useGcd, NttBackend nttBackend, int gpuPrimeThreads, int llSlice, int gpuScanBatch, ulong warmupLimit, ModReductionMode reduction, string mersenneDevice, string primesDevice, string orderDevice)
|
||||
{
|
||||
string inc = bitInc ? "bit" : "add";
|
||||
string mers = useDivisorFlag
|
||||
? "divisor"
|
||||
: (useLucasFlag
|
||||
? "lucas"
|
||||
: (useByDivisorFlag
|
||||
? "bydivisor"
|
||||
: (kernelType == GpuKernelType.Pow2Mod ? "pow2mod" : "incremental")));
|
||||
string ntt = nttBackend == NttBackend.Staged ? "staged" : "reference";
|
||||
string red = reduction switch { ModReductionMode.Mont64 => "mont64", ModReductionMode.Barrett128 => "barrett128", ModReductionMode.GpuUInt128 => "uint128", _ => "auto" };
|
||||
string order = useOrder ? "order-on" : "order-off";
|
||||
string gcd = useGcd ? "gcd-on" : "gcd-off";
|
||||
string work = useModWorkaround ? "modfix-on" : "modfix-off";
|
||||
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";
|
||||
}
|
||||
private static string BuildResultsFileName(bool bitInc, int threads, int block, GpuKernelType kernelType, bool useLucasFlag, bool useDivisorFlag, bool useByDivisorFlag, bool mersenneOnGpu, bool useOrder, bool useModWorkaround, bool useGcd, NttBackend nttBackend, int gpuPrimeThreads, int llSlice, int gpuScanBatch, ulong warmupLimit, ModReductionMode reduction, string mersenneDevice, string primesDevice, string orderDevice)
|
||||
{
|
||||
string inc = bitInc ? "bit" : "add";
|
||||
string mers = useDivisorFlag
|
||||
? "divisor"
|
||||
: (useLucasFlag
|
||||
? "lucas"
|
||||
: (useByDivisorFlag
|
||||
? "bydivisor"
|
||||
: (kernelType == GpuKernelType.Pow2Mod ? "pow2mod" : "incremental")));
|
||||
string ntt = nttBackend == NttBackend.Staged ? "staged" : "reference";
|
||||
string red = reduction switch { ModReductionMode.Mont64 => "mont64", ModReductionMode.Barrett128 => "barrett128", ModReductionMode.GpuUInt128 => "uint128", _ => "auto" };
|
||||
string order = useOrder ? "order-on" : "order-off";
|
||||
string gcd = useGcd ? "gcd-on" : "gcd-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";
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static unsafe int ReserveBlock(ulong[] buffer, int blockSize)
|
||||
{
|
||||
Span<ulong> bufferSpan = new(buffer);
|
||||
ulong p, remainder;
|
||||
long state, newState, original;
|
||||
int count;
|
||||
while (true)
|
||||
{
|
||||
state = Volatile.Read(ref _state);
|
||||
p = (ulong)state >> 3;
|
||||
remainder = ((ulong)state) & 7UL;
|
||||
private static unsafe int ReserveBlock(ulong[] buffer, int blockSize)
|
||||
{
|
||||
Span<ulong> bufferSpan = new(buffer);
|
||||
ulong p, remainder;
|
||||
long state, newState, original;
|
||||
int count;
|
||||
while (true)
|
||||
{
|
||||
state = Volatile.Read(ref _state);
|
||||
p = (ulong)state >> 3;
|
||||
remainder = ((ulong)state) & 7UL;
|
||||
|
||||
count = 0;
|
||||
while (count < blockSize && !Volatile.Read(ref _limitReached))
|
||||
{
|
||||
bufferSpan[count++] = p;
|
||||
p = _transformP(p, ref remainder);
|
||||
}
|
||||
count = 0;
|
||||
while (count < blockSize && !Volatile.Read(ref _limitReached))
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
newState = ((long)p << 3) | (long)remainder;
|
||||
original = Interlocked.CompareExchange(ref _state, newState, state);
|
||||
if (original == state)
|
||||
{
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
newState = ((long)p << 3) | (long)remainder;
|
||||
original = Interlocked.CompareExchange(ref _state, newState, state);
|
||||
if (original == state)
|
||||
{
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool TryAcquireConsoleSlot(bool primeCandidate, out bool primeFlag)
|
||||
@@ -1014,7 +1073,9 @@ internal static class Program
|
||||
bool lastWasComposite = _lastCompositeP;
|
||||
bool primeCandidate = passedAllTests && !lastWasComposite && Volatile.Read(ref _primeFoundAfterInit);
|
||||
|
||||
StringBuilder localBuilder = StringBuilderPool.Rent();
|
||||
// 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();
|
||||
_ = localBuilder
|
||||
.Append(currentP).Append(',')
|
||||
.Append(searchedMersenne).Append(',')
|
||||
@@ -1071,13 +1132,15 @@ internal static class Program
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (FileWriteSync)
|
||||
{
|
||||
using FileStream stream = new(ResultsFileName, FileMode.Append, FileAccess.Write, FileShare.Read);
|
||||
using StreamWriter writer = new(stream) { AutoFlush = false };
|
||||
writer.Write(builderToFlush);
|
||||
writer.Flush();
|
||||
}
|
||||
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 StreamWriter writer = new(stream) { AutoFlush = false };
|
||||
writer.Write(builderToFlush);
|
||||
writer.Flush();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1087,10 +1150,12 @@ internal static class Program
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static ulong CountOnes(ulong value)
|
||||
{
|
||||
return (ulong)BitOperations.PopCount(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);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong GetNextAddDiff(ulong remainder)
|
||||
@@ -1117,11 +1182,13 @@ internal static class Program
|
||||
}
|
||||
|
||||
ulong next = (value << 1) | 1UL;
|
||||
remainder = (remainder << 1) + 1UL;
|
||||
while (remainder >= 6UL)
|
||||
{
|
||||
remainder -= 6UL;
|
||||
}
|
||||
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)
|
||||
{
|
||||
remainder -= 6UL;
|
||||
}
|
||||
|
||||
value = remainder switch
|
||||
{
|
||||
@@ -1139,11 +1206,13 @@ internal static class Program
|
||||
return original;
|
||||
}
|
||||
|
||||
remainder += value;
|
||||
while (remainder >= 6UL)
|
||||
{
|
||||
remainder -= 6UL;
|
||||
}
|
||||
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)
|
||||
{
|
||||
remainder -= 6UL;
|
||||
}
|
||||
|
||||
return next + value;
|
||||
}
|
||||
@@ -1160,11 +1229,13 @@ internal static class Program
|
||||
return next;
|
||||
}
|
||||
|
||||
remainder += diff;
|
||||
while (remainder >= 6UL)
|
||||
{
|
||||
remainder -= 6UL;
|
||||
}
|
||||
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)
|
||||
{
|
||||
remainder -= 6UL;
|
||||
}
|
||||
|
||||
return next + diff;
|
||||
}
|
||||
@@ -1193,12 +1264,14 @@ internal static class Program
|
||||
return candidate;
|
||||
}
|
||||
|
||||
candidate += diff;
|
||||
addRemainder += diff;
|
||||
while (addRemainder >= 6UL)
|
||||
{
|
||||
addRemainder -= 6UL;
|
||||
}
|
||||
candidate += 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)
|
||||
{
|
||||
addRemainder -= 6UL;
|
||||
}
|
||||
}
|
||||
|
||||
if (advancePrime)
|
||||
@@ -1332,8 +1405,10 @@ internal static class Program
|
||||
// Use ModResidueTracker with a small set of primes to pre-filter composite p.
|
||||
private static bool IsCompositeByResidues(ulong p)
|
||||
{
|
||||
var tracker = PResidue.Value!;
|
||||
tracker.BeginMerge(p);
|
||||
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);
|
||||
// Use the small prime list from PerfectNumbers.Core to the extent of sqrt(p)
|
||||
var primes = PrimesGenerator.SmallPrimes;
|
||||
var primesPow2 = PrimesGenerator.SmallPrimesPow2;
|
||||
@@ -1378,16 +1453,19 @@ internal static class Program
|
||||
int candidate = 0;
|
||||
int byteIndex = msbIndex;
|
||||
|
||||
for (; byteIndex >= 0; byteIndex--)
|
||||
{
|
||||
inspectedByte = (byte)(value >> (byteIndex * 8));
|
||||
if (byteIndex == msbIndex && bitsInTopByte < 8)
|
||||
{
|
||||
// Mask off leading unused bits by setting them to 1 (ignore)
|
||||
inspectedByte |= (byte)(0xFF << bitsInTopByte);
|
||||
}
|
||||
for (; byteIndex >= 0; byteIndex--)
|
||||
{
|
||||
inspectedByte = (byte)(value >> (byteIndex * 8));
|
||||
if (byteIndex == msbIndex && bitsInTopByte < 8)
|
||||
{
|
||||
// Mask off leading unused bits by setting them to 1 (ignore)
|
||||
inspectedByte |= (byte)(0xFF << bitsInTopByte);
|
||||
}
|
||||
|
||||
zeroCountInByte = ByteZeroCount[inspectedByte];
|
||||
// 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];
|
||||
zeroCount += zeroCountInByte;
|
||||
|
||||
if (zeroCountInByte == 8)
|
||||
|
||||
@@ -8,6 +8,8 @@ public sealed class AlphaCache
|
||||
|
||||
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))
|
||||
{
|
||||
ERational alphaPValue = AlphaCalculations.ComputeAlphaP(p, k);
|
||||
@@ -21,6 +23,8 @@ public sealed class AlphaCache
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ public static class AlphaCalculations
|
||||
{
|
||||
int exponent = checked(4 * k + 1);
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -15,6 +17,9 @@ public static class AlphaCalculations
|
||||
{
|
||||
EInteger num = PowerCache.Get(q, a2 + 1) - 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);
|
||||
}
|
||||
|
||||
@@ -22,7 +27,11 @@ public static class AlphaCalculations
|
||||
{
|
||||
ERational result = ERational.One;
|
||||
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));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ namespace PerfectNumbers.Core;
|
||||
|
||||
public enum AlphaMScannerCandidateStatus
|
||||
{
|
||||
// TODO: Store these statuses as byte-coded constants so hot-path evaluations can stay branchless when scanning candidates.
|
||||
UNKNOWN,
|
||||
PRODUCT_ABOVE_TARGET,
|
||||
PRODUCT_BELOW_TARGET,
|
||||
|
||||
@@ -2,8 +2,12 @@ namespace PerfectNumbers.Core.Cpu;
|
||||
|
||||
public static class CpuConstants
|
||||
{
|
||||
// Allowed q mod 10 sets depending on last digit of M_p
|
||||
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)
|
||||
public const int LastOneMask10 = (1 << 1) | (1 << 3) | (1 << 9);
|
||||
}
|
||||
// Allowed q mod 10 sets depending on last digit of M_p
|
||||
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)
|
||||
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)
|
||||
{
|
||||
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);
|
||||
// TODO: When divisorCycle is zero (cache miss), compute only that single cycle on the device
|
||||
// selected by the current settings, skip inserting the result into the shared cache, and keep
|
||||
// operating with the single on-disk block without requesting additional blocks.
|
||||
hit = CheckDivisor(prime, divisorCycle != 0UL, divisorCycle, divisorData);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Force this path to hydrate divisor cycles from disk once the cache guarantees
|
||||
// coverage so we can retire the slower Montgomery-only fallback highlighted by the CPU
|
||||
// divisor benchmarks.
|
||||
// TODO: Remove this no-cycle branch and require on-demand cycle computation (without
|
||||
// mutating the cache) so CPU scans never fall back to the slower Montgomery-only path
|
||||
// identified in the benchmarks.
|
||||
hit = CheckDivisor(prime, false, 0UL, divisorData);
|
||||
}
|
||||
|
||||
@@ -347,6 +353,9 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
||||
bool useDivisorCycles = _owner._useDivisorCycles;
|
||||
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}.");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,94 +2,101 @@ namespace PerfectNumbers.Core.Cpu;
|
||||
|
||||
public class MersenneNumberIncrementalCpuTester(GpuKernelType kernelType)
|
||||
{
|
||||
private static readonly MersenneDivisorCycles _cycles = MersenneDivisorCycles.Shared;
|
||||
private readonly GpuKernelType _kernelType = kernelType;
|
||||
private static readonly MersenneDivisorCycles _cycles = MersenneDivisorCycles.Shared;
|
||||
private readonly GpuKernelType _kernelType = kernelType;
|
||||
|
||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||
{
|
||||
UInt128 k, one = k = UInt128.One;
|
||||
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
|
||||
// Use residue automaton always on CPU
|
||||
var auto = new MersenneResidueAutomaton(exponent);
|
||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||
{
|
||||
UInt128 k = UInt128.One;
|
||||
UInt128 one = k;
|
||||
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
|
||||
// Use residue automaton always on CPU
|
||||
var auto = new MersenneResidueAutomaton(exponent);
|
||||
|
||||
bool reject, shouldCheck;
|
||||
UInt128 q = auto.CurrentQ(),
|
||||
divPow, halfPow, phi, qCycle;
|
||||
bool reject;
|
||||
bool shouldCheck;
|
||||
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))
|
||||
{
|
||||
qCycle = MersenneDivisorCycles.GetCycle(q);
|
||||
shouldCheck = lastIsSeven
|
||||
? (auto.Mod10R != 5UL)
|
||||
: (auto.Mod10R != 5UL);
|
||||
if (shouldCheck)
|
||||
{
|
||||
mod8 = auto.Mod8R;
|
||||
if (mod8 != 1UL && mod8 != 7UL)
|
||||
{
|
||||
shouldCheck = false;
|
||||
}
|
||||
else if (auto.Mod3R == 0UL || auto.Mod5R == 0UL)
|
||||
{
|
||||
shouldCheck = false;
|
||||
}
|
||||
}
|
||||
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);
|
||||
shouldCheck = auto.Mod10R != 5UL;
|
||||
if (shouldCheck)
|
||||
{
|
||||
mod8 = auto.Mod8R;
|
||||
if (mod8 != 1UL && mod8 != 7UL)
|
||||
{
|
||||
shouldCheck = false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCheck)
|
||||
{
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
if (shouldCheck)
|
||||
{
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
// TODO: Swap this PowModWithCycle call for the ProcessEightBitWindows helper once
|
||||
// the scalar implementation lands so CPU pow2mod scans match the GPU speedups
|
||||
// captured in GpuPow2ModBenchmarks.
|
||||
reject = exponent.PowModWithCycle(q, qCycle) == UInt128.One;
|
||||
}
|
||||
else
|
||||
{
|
||||
phi = q - one;
|
||||
reject = false;
|
||||
if (phi <= ulong.MaxValue)
|
||||
{
|
||||
phi64 = (ulong)phi;
|
||||
// TODO: Route the phi-based powmods through the upcoming windowed helper to
|
||||
// avoid the current square-and-multiply cost highlighted in the Pow2 bench
|
||||
// suite when scanning large divisors.
|
||||
if (phi64.PowModWithCycle(q, qCycle) == one)
|
||||
{
|
||||
// TODO: Once the windowed pow2 helper is available expose a version
|
||||
// that reuses cached windows here instead of recomputing via the slow
|
||||
// bit ladder for every divisor.
|
||||
halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - one;
|
||||
if (halfPow.BinaryGcd(q) == one)
|
||||
{
|
||||
div = phi64.FastDiv64(exponent, divMul);
|
||||
// TODO: Replace this divisor powmod with the shared windowed
|
||||
// implementation so each candidate uses the benchmarked fast
|
||||
// path instead of the current per-bit multiply chain.
|
||||
divPow = div.PowModWithCycle(q, qCycle) - one;
|
||||
if (divPow.BinaryGcd(q) == one)
|
||||
{
|
||||
// TODO: Swap this PowModWithCycle call for the ProcessEightBitWindows helper once
|
||||
// the scalar implementation lands so CPU pow2mod scans match the GPU speedups
|
||||
// captured in GpuPow2ModBenchmarks.
|
||||
reject = exponent.PowModWithCycle(q, qCycle) == UInt128.One;
|
||||
reject = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
phi = q - one;
|
||||
reject = false;
|
||||
if (phi <= ulong.MaxValue)
|
||||
{
|
||||
phi64 = (ulong)phi;
|
||||
// TODO: Route the phi-based powmods through the upcoming windowed helper to
|
||||
// avoid the current square-and-multiply cost highlighted in the Pow2 bench
|
||||
// suite when scanning large divisors.
|
||||
if (phi64.PowModWithCycle(q, qCycle) == one)
|
||||
{
|
||||
// TODO: Once the windowed pow2 helper is available expose a version
|
||||
// that reuses cached windows here instead of recomputing via the slow
|
||||
// bit ladder for every divisor.
|
||||
halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - one;
|
||||
if (halfPow.BinaryGcd(q) == one)
|
||||
{
|
||||
div = phi64.FastDiv64(exponent, divMul);
|
||||
// TODO: Replace this divisor powmod with the shared windowed
|
||||
// implementation so each candidate uses the benchmarked fast
|
||||
// path instead of the current per-bit multiply chain.
|
||||
divPow = div.PowModWithCycle(q, qCycle) - one;
|
||||
if (divPow.BinaryGcd(q) == one)
|
||||
{
|
||||
reject = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reject && q.IsPrimeCandidate())
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (reject && q.IsPrimeCandidate())
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
k += 1UL;
|
||||
auto.Next();
|
||||
q = auto.CurrentQ();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
k += 1UL;
|
||||
auto.Next();
|
||||
q = auto.CurrentQ();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,10 @@ public class MersenneNumberOrderCpuTester(GpuKernelType kernelType)
|
||||
|
||||
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;
|
||||
bool shouldCheck = ((allowMask2 >> (int)auto.Mod10R) & 1) != 0;
|
||||
if (shouldCheck)
|
||||
|
||||
@@ -10,9 +10,14 @@ public class MersenneNumberResidueCpuTester
|
||||
// Initialize/update Mersenne residue tracker and start a merge walk for ascending q divisors
|
||||
|
||||
_mersenneResidueTracker ??= new(ResidueModel.Mersenne);
|
||||
// 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.
|
||||
_mersenneResidueTracker.BeginMerge(exponent);
|
||||
// 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.
|
||||
// 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);
|
||||
|
||||
// Start at k = 1: q0 = 2*p*1 + 1
|
||||
UInt128 k = UInt128.One;
|
||||
@@ -80,12 +85,15 @@ public class MersenneNumberResidueCpuTester
|
||||
r5_3 = r5_2 + step5; if (r5_3 >= 10UL) r5_3 -= 10UL; if (r5_3 >= 5UL) r5_3 -= 5UL;
|
||||
|
||||
// lane 0
|
||||
if (lanes >= 1 &&
|
||||
(((allowMask >> (int)r10_0) & 1) != 0 && (r8_0 == 1UL || r8_0 == 7UL) && r3_0 != 0UL && r5_0 != 0UL))
|
||||
{
|
||||
// cycle-based quick check for small q
|
||||
if (localIsPrime && tracker.MergeOrAppend(exponent, q, out isDivider) && isDivider &&
|
||||
(q <= ulong.MaxValue ? ((ulong)q).IsPrimeCandidate() : q.IsPrimeCandidate()))
|
||||
if (lanes >= 1 &&
|
||||
(((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
|
||||
if (localIsPrime && tracker.MergeOrAppend(exponent, q, out isDivider) && isDivider &&
|
||||
(q <= ulong.MaxValue ? ((ulong)q).IsPrimeCandidate() : q.IsPrimeCandidate()))
|
||||
{
|
||||
localIsPrime = false;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ internal struct CycleRemainderStepper
|
||||
|
||||
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;
|
||||
_currentRemainder = 0UL;
|
||||
_hasState = false;
|
||||
@@ -37,7 +40,8 @@ internal struct CycleRemainderStepper
|
||||
if (remainder >= cycleLength)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
_previousPrime = prime;
|
||||
@@ -79,7 +83,8 @@ internal struct CycleRemainderStepper
|
||||
if (_currentRemainder >= cycleLength)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -88,6 +93,8 @@ internal struct CycleRemainderStepper
|
||||
|
||||
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;
|
||||
previousRemainder = _currentRemainder;
|
||||
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 ByteOne = 1;
|
||||
|
||||
private readonly object _sync = new();
|
||||
private readonly ConcurrentDictionary<int, Task<CycleBlock>> _pending = new();
|
||||
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 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(); // 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); // 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? _activeBlock;
|
||||
private CycleBlock? _prefetchedBlock;
|
||||
private CycleBlock? _activeBlock; // TODO: Remove dynamic blocks altogether once lookups route to the snapshot or compute an individual cycle without mutating shared state.
|
||||
private CycleBlock? _prefetchedBlock; // TODO: Delete prefetch support so the cache never materializes extra blocks and callers always compute the missing cycle inline.
|
||||
private bool _useGpuGeneration = true;
|
||||
private readonly int _divisorCyclesBatchSize;
|
||||
private static int _sharedDivisorCyclesBatchSize = GpuConstants.GpuCycleStepsPerInvocation;
|
||||
@@ -84,6 +86,8 @@ public sealed class DivisorCycleCache
|
||||
Initialize(snapshot);
|
||||
}
|
||||
|
||||
// TODO: Remove the ability to reload at runtime once the cache becomes a read-only snapshot plus
|
||||
// transient single-cycle generator; future callers should load the snapshot during startup only.
|
||||
public void ReloadFromCurrentSnapshot()
|
||||
{
|
||||
ulong[] snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
|
||||
@@ -96,6 +100,8 @@ public sealed class DivisorCycleCache
|
||||
{
|
||||
_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)
|
||||
@@ -116,6 +122,7 @@ public sealed class DivisorCycleCache
|
||||
}
|
||||
|
||||
_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);
|
||||
_activeBlock = null;
|
||||
_prefetchedBlock = null;
|
||||
@@ -129,6 +136,7 @@ public sealed class DivisorCycleCache
|
||||
public CycleBlock Acquire(ulong divisor)
|
||||
{
|
||||
CycleBlock block = divisor <= _baseBlock.End ? _baseBlock : AcquireDynamicBlock(divisor);
|
||||
// TODO: Replace this block-based acquisition with a direct GetCycle API that computes the missing cycle synchronously on the configured device so the cache never hands out mutable blocks or requires disposal.
|
||||
block.Retain();
|
||||
return block;
|
||||
}
|
||||
@@ -149,6 +157,9 @@ public sealed class DivisorCycleCache
|
||||
|
||||
if (!_pending.TryGetValue(blockIndex, out pendingTask))
|
||||
{
|
||||
// TODO: Replace Task.Run with an inline single-cycle computation that runs on the configured
|
||||
// device so we materialize exactly one missing cycle and stop scheduling thread-pool work for
|
||||
// entire blocks.
|
||||
pendingTask = Task.Run(() => GenerateBlock(blockIndex));
|
||||
_pending[blockIndex] = pendingTask;
|
||||
}
|
||||
@@ -165,6 +176,9 @@ public sealed class DivisorCycleCache
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -210,7 +224,8 @@ public sealed class DivisorCycleCache
|
||||
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)
|
||||
@@ -235,11 +250,14 @@ public sealed class DivisorCycleCache
|
||||
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)
|
||||
{
|
||||
// TODO: Remove prefetching entirely so the cache maintains a single snapshot block and computes
|
||||
// individual cycles on demand instead of triggering background generation for future indices.
|
||||
if (blockIndex <= 0)
|
||||
{
|
||||
return;
|
||||
@@ -262,6 +280,8 @@ public sealed class DivisorCycleCache
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Delete this once prefetching is removed; single-cycle generation will happen inline and
|
||||
// no asynchronous work should be scheduled.
|
||||
Task<CycleBlock> task = Task.Run(() => GenerateBlock(blockIndex));
|
||||
_pending[blockIndex] = task;
|
||||
task.ContinueWith(t => OnPrefetchCompleted(blockIndex, t), TaskScheduler.Default);
|
||||
@@ -269,6 +289,8 @@ public sealed class DivisorCycleCache
|
||||
|
||||
private void OnPrefetchCompleted(int blockIndex, Task<CycleBlock> task)
|
||||
{
|
||||
// TODO: Delete this callback when we remove prefetching. Single-cycle generation will run inline,
|
||||
// eliminating the need for asynchronous continuation handlers and concurrent dictionaries.
|
||||
if (!task.IsCompletedSuccessfully)
|
||||
{
|
||||
_pending.TryRemove(blockIndex, out _);
|
||||
@@ -284,6 +306,9 @@ public sealed class DivisorCycleCache
|
||||
|
||||
private void AssignPrefetchedBlockLocked(int blockIndex, CycleBlock block)
|
||||
{
|
||||
// TODO: Remove this entire method when prefetching goes away. The single-cycle flow should never
|
||||
// allocate or retain extra blocks beyond the startup snapshot, so prefetched block bookkeeping
|
||||
// becomes unnecessary.
|
||||
CycleBlock? active = _activeBlock;
|
||||
if (active is not null && blockIndex != active.Index + 1)
|
||||
{
|
||||
@@ -363,6 +388,9 @@ public sealed class DivisorCycleCache
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -393,6 +421,8 @@ public sealed class DivisorCycleCache
|
||||
ulong divisor = start;
|
||||
for (int i = 0; i < destination.Length; i++)
|
||||
{
|
||||
// TODO: Port this CPU fallback to the unrolled-hex cycle generator once it is shared so misses outside the snapshot
|
||||
// stop looping through CalculateCycleLength, which benchmarks showed is far slower than the GPU-assisted pipeline.
|
||||
destination[i] = MersenneDivisorCycles.CalculateCycleLength(divisor);
|
||||
divisor++;
|
||||
}
|
||||
@@ -407,6 +437,9 @@ public sealed class DivisorCycleCache
|
||||
{
|
||||
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);
|
||||
// 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 chunkCapacity = Math.Min(length, CycleGenerationBatchSize);
|
||||
@@ -568,3 +601,4 @@ public sealed class DivisorCycleCache
|
||||
public int GetHashCode(Accelerator obj) => RuntimeHelpers.GetHashCode(obj);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ public sealed class EulerPrimeTester
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsEulerPrime(ulong n)
|
||||
{
|
||||
// TODO: Inline this helper into callers so we skip the wrapper and pass the divisibility check directly to the optimized
|
||||
// prime tester path highlighted in the CPU benchmarks.
|
||||
return (n & 3UL) == 1UL && _primeTester.IsPrime(n, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,9 @@ internal struct ExponentRemainderStepper
|
||||
}
|
||||
|
||||
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
|
||||
// updates stop paying the single-bit cost highlighted in GpuPow2ModBenchmarks.
|
||||
ulong multiplier = delta.Pow2MontgomeryModMontgomery(_divisor);
|
||||
@@ -81,6 +84,8 @@ internal struct ExponentRemainderStepper
|
||||
}
|
||||
|
||||
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
|
||||
// currently lags behind the benchmarked implementation for huge divisor cycles.
|
||||
ulong multiplier = delta.Pow2MontgomeryModMontgomery(_divisor);
|
||||
@@ -125,6 +130,9 @@ internal struct ExponentRemainderStepper
|
||||
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);
|
||||
_previousExponent = exponent;
|
||||
isUnity = _currentMontgomery == _montgomeryOne;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
namespace PerfectNumbers.Core.Gpu
|
||||
{
|
||||
public static class GpuConstants
|
||||
{
|
||||
public const int ScanBatchSize = 2_097_152;
|
||||
public const int GpuCycleStepsPerInvocation = 256;
|
||||
public static class GpuConstants
|
||||
{
|
||||
public const int ScanBatchSize = 2_097_152;
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ namespace PerfectNumbers.Core.Gpu;
|
||||
|
||||
public static class GpuContextPool
|
||||
{
|
||||
private static readonly bool PoolingEnabled = true;
|
||||
// Default device preference for generic GPU kernels (prime scans, NTT, etc.)
|
||||
public static bool ForceCpu { get; set; } = false;
|
||||
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.)
|
||||
public static bool ForceCpu { get; set; } = false;
|
||||
|
||||
internal sealed class PooledContext
|
||||
{
|
||||
@@ -156,3 +158,4 @@ public static class GpuContextPool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,7 +694,7 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -712,14 +712,14 @@ public class GpuKernelPool
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
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);
|
||||
device.View.CopyFromCPU(host);
|
||||
kernels.SmallCycles = device;
|
||||
@@ -738,7 +738,7 @@ public class GpuKernelPool
|
||||
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 &&
|
||||
kernels.SmallPrimesLastSeven is { } existingLastSeven &&
|
||||
|
||||
@@ -8,6 +8,8 @@ public static class GpuPrimeWorkLimiter
|
||||
|
||||
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;
|
||||
sem.Wait();
|
||||
return new Releaser(sem);
|
||||
@@ -27,6 +29,8 @@ public static class GpuPrimeWorkLimiter
|
||||
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);
|
||||
_currentLimit = value;
|
||||
}
|
||||
|
||||
@@ -156,14 +156,14 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator >=(GpuUInt128 left, GpuUInt128 right) => left.CompareTo(right) >= 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static GpuUInt128 operator ++(GpuUInt128 value)
|
||||
{
|
||||
value.Add(1UL);
|
||||
return value;
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static GpuUInt128 operator ++(GpuUInt128 value)
|
||||
{
|
||||
value.Add(1UL);
|
||||
return value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly int CompareTo(GpuUInt128 other)
|
||||
{
|
||||
if (High < other.High)
|
||||
@@ -536,6 +536,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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)
|
||||
{
|
||||
return v;
|
||||
@@ -1044,11 +1047,15 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
|
||||
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 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)
|
||||
{
|
||||
if ((exponent.Low & 1UL) != 0UL)
|
||||
@@ -1066,11 +1073,13 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
|
||||
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 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)
|
||||
{
|
||||
if ((exponent & 1UL) != 0UL)
|
||||
@@ -1086,14 +1095,16 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
Low = result.Low;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ModInv(ulong modulus)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void ModInv(GpuUInt128 modulus)
|
||||
{
|
||||
if (modulus.High == 0UL)
|
||||
@@ -1114,7 +1125,9 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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 (h1, l1) = Mul64(left.Low, right.High);
|
||||
var (h2, l2) = Mul64(left.High, right.Low);
|
||||
|
||||
@@ -8,6 +8,9 @@ public static class GpuWorkLimiter
|
||||
|
||||
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;
|
||||
sem.Wait();
|
||||
return new Releaser(sem);
|
||||
@@ -30,6 +33,8 @@ public static class GpuWorkLimiter
|
||||
// Swap to a new semaphore for the new limit. We intentionally
|
||||
// do not dispose the old semaphore to avoid releasing handles
|
||||
// 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);
|
||||
_currentLimit = value;
|
||||
}
|
||||
|
||||
@@ -369,6 +369,9 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
|
||||
|
||||
DivisorCycleCache divisorCyclesCache = DivisorCycleCache.Shared;
|
||||
DivisorCycleCache.CycleBlock cycleLease = divisorCyclesCache.Acquire(currentDivisor);
|
||||
// TODO: Replace the block lease with a direct single-cycle lookup so the GPU path
|
||||
// follows the benchmarked single-block policy and computes the missing cycle inline on the
|
||||
// configured device without mutating or swapping cache blocks after startup.
|
||||
cycleLeaseEnd = cycleLease.End;
|
||||
|
||||
try
|
||||
@@ -389,6 +392,9 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
|
||||
{
|
||||
cycleLease.Dispose();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -414,6 +420,12 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
|
||||
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)
|
||||
{
|
||||
@@ -617,6 +629,9 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
|
||||
|
||||
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}.");
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,10 @@ public sealed class MersenneNumberDivisorGpuTester
|
||||
continue;
|
||||
}
|
||||
|
||||
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
|
||||
// UInt128 modulo on the CPU fallback highlighted by the Pow2Montgomery benchmarks.
|
||||
if ((UInt128)p % cycle128 != UInt128.Zero)
|
||||
|
||||
@@ -6,98 +6,99 @@ namespace PerfectNumbers.Core.Gpu;
|
||||
|
||||
public class MersenneNumberIncrementalGpuTester(GpuKernelType kernelType, bool useGpuOrder)
|
||||
{
|
||||
private readonly GpuKernelType _kernelType = kernelType;
|
||||
private readonly bool _useGpuOrder = useGpuOrder;
|
||||
private readonly GpuKernelType _kernelType = kernelType;
|
||||
private readonly bool _useGpuOrder = useGpuOrder;
|
||||
|
||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||
{
|
||||
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
|
||||
var execution = gpuLease.EnterExecutionScope();
|
||||
var accelerator = gpuLease.Accelerator;
|
||||
var stream = gpuLease.Stream;
|
||||
int batchSize = GpuConstants.ScanBatchSize; // large batch improves GPU occupancy
|
||||
UInt128 kStart = 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
|
||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||
{
|
||||
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
|
||||
var execution = gpuLease.EnterExecutionScope();
|
||||
var accelerator = gpuLease.Accelerator;
|
||||
var stream = gpuLease.Stream;
|
||||
int batchSize = GpuConstants.ScanBatchSize;
|
||||
UInt128 kStart = 1UL;
|
||||
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
|
||||
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 incKernel = gpuLease.IncrementalKernel;
|
||||
exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||
GpuUInt128 twoPGpu = (GpuUInt128)twoP;
|
||||
var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
|
||||
ResiduePrimeViews primeViews = default;
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
primeViews = GpuKernelPool.EnsureSmallPrimesOnDevice(accelerator);
|
||||
}
|
||||
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;
|
||||
exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||
GpuUInt128 twoPGpu = (GpuUInt128)twoP;
|
||||
var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
|
||||
ResiduePrimeViews primeViews = default;
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
primeViews = GpuKernelPool.EnsureSmallPrimesOnDevice(accelerator);
|
||||
}
|
||||
|
||||
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);
|
||||
UInt128 remaining;
|
||||
int currentSize;
|
||||
int i;
|
||||
UInt128 q = UInt128.Zero;
|
||||
try
|
||||
{
|
||||
while (kStart <= maxK && Volatile.Read(ref isPrime))
|
||||
{
|
||||
remaining = maxK - kStart + UInt128.One;
|
||||
currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining;
|
||||
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
||||
// Precompute residue automaton bases for this batch
|
||||
UInt128 q0 = twoP * kStart + UInt128.One;
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
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);
|
||||
pow2Kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven,
|
||||
primeViews.LastOnePow2, primeViews.LastSevenPow2);
|
||||
}
|
||||
else
|
||||
{
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
incKernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
q0m10, q0m8, q0m3, q0m5, orderBuffer.View, smallCyclesView);
|
||||
}
|
||||
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
|
||||
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||
UInt128 remaining;
|
||||
int currentSize;
|
||||
int i;
|
||||
UInt128 q = UInt128.Zero;
|
||||
|
||||
stream.Synchronize();
|
||||
orderBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(orders), currentSize);
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
for (i = 0; i < currentSize; i++)
|
||||
{
|
||||
if (orders[i] != 0UL)
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
q = twoP * kStart + UInt128.One;
|
||||
for (i = 0; i < currentSize && Volatile.Read(ref isPrime); i++, q += twoP)
|
||||
{
|
||||
if (orders[i] != 0UL && q.IsPrimeCandidate())
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
while (kStart <= maxK && Volatile.Read(ref isPrime))
|
||||
{
|
||||
remaining = maxK - kStart + UInt128.One;
|
||||
currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining;
|
||||
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
||||
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.
|
||||
|
||||
kStart += (UInt128)currentSize;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<ulong>.Shared.Return(orderArray);
|
||||
orderBuffer.Dispose();
|
||||
execution.Dispose();
|
||||
gpuLease.Dispose();
|
||||
}
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
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);
|
||||
pow2Kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven,
|
||||
primeViews.LastOnePow2, primeViews.LastSevenPow2);
|
||||
}
|
||||
else
|
||||
{
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
incKernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
q0m10, q0m8, q0m3, q0m5, orderBuffer.View, smallCyclesView);
|
||||
}
|
||||
|
||||
}
|
||||
stream.Synchronize();
|
||||
orderBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(orders), currentSize);
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
for (i = 0; i < currentSize; i++)
|
||||
{
|
||||
if (orders[i] != 0UL)
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (orders[i] != 0UL && q.IsPrimeCandidate())
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kStart += (UInt128)currentSize;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<ulong>.Shared.Return(orderArray);
|
||||
orderBuffer.Dispose();
|
||||
execution.Dispose();
|
||||
gpuLease.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
{
|
||||
// Default to global kernel preference for backward compatibility.
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -120,6 +123,8 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
GpuUInt128[] modulusArray = ArrayPool<GpuUInt128>.Shared.Rent(count);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -292,11 +297,15 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
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)
|
||||
{
|
||||
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 modLow = ulong.Parse(parts[2]);
|
||||
ulong rootHigh = ulong.Parse(parts[3]);
|
||||
@@ -315,6 +324,9 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
{
|
||||
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);
|
||||
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();
|
||||
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) =>
|
||||
{
|
||||
ulong k = (ulong)worker + 1UL;
|
||||
@@ -377,6 +392,8 @@ public class MersenneNumberLucasLehmerGpuTester
|
||||
private static ulong FindPrimitiveRoot(ulong modulus, ulong order)
|
||||
{
|
||||
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);
|
||||
for (ulong g = 2UL; g < modulus; g++)
|
||||
{
|
||||
|
||||
@@ -23,11 +23,12 @@ public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuO
|
||||
GpuKernelType.Pow2Mod => gpuLease.Pow2ModOrderKernel,
|
||||
_ => 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: 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);
|
||||
exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||
try
|
||||
{
|
||||
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);
|
||||
try
|
||||
{
|
||||
UInt128 remaining;
|
||||
int currentSize;
|
||||
int found = 0;
|
||||
@@ -37,9 +38,12 @@ public class MersenneNumberOrderGpuTester(GpuKernelType kernelType, bool useGpuO
|
||||
currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining;
|
||||
foundBuffer.MemSetToZero();
|
||||
// Precompute residue automaton bases for this batch
|
||||
UInt128 q0 = twoP * kStart + 1UL;
|
||||
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);
|
||||
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);
|
||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||
|
||||
|
||||
// Ensure device has small cycles table for early rejections in order kernels
|
||||
|
||||
@@ -1,106 +1,110 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using ILGPU.Runtime;
|
||||
|
||||
namespace PerfectNumbers.Core.Gpu;
|
||||
|
||||
public class MersenneNumberResidueGpuTester(bool useGpuOrder)
|
||||
{
|
||||
private readonly bool _useGpuOrder = useGpuOrder;
|
||||
// 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
|
||||
// via GpuKernelPool to avoid host round-trips.
|
||||
private readonly bool _useGpuOrder = useGpuOrder;
|
||||
// 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
|
||||
// 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.
|
||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||
{
|
||||
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
|
||||
var accelerator = gpuLease.Accelerator;
|
||||
var stream = gpuLease.Stream;
|
||||
// Ensure device has small cycles and primes tables for in-kernel lookup
|
||||
var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
|
||||
ResiduePrimeViews primeViews = GpuKernelPool.EnsureSmallPrimesOnDevice(accelerator);
|
||||
int batchSize = GpuConstants.ScanBatchSize;
|
||||
UInt128 kStart = 1UL;
|
||||
UInt128 limit = maxK + UInt128.One;
|
||||
byte last = lastIsSeven ? (byte)1 : (byte)0;
|
||||
var kernel = gpuLease.Pow2ModKernel; // TODO: Swap this to the ProcessEightBitWindows kernel once GpuUInt128.Pow2Minus1Mod adopts the shared windowed helper measured fastest in GpuPow2ModBenchmarks.
|
||||
twoP.Mod10_8_5_3(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||
step10 = step10.Mod10();
|
||||
// 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)
|
||||
{
|
||||
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
|
||||
var accelerator = gpuLease.Accelerator;
|
||||
var stream = gpuLease.Stream;
|
||||
// Ensure device has small cycles and primes tables for in-kernel lookup
|
||||
var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
|
||||
ResiduePrimeViews primeViews = GpuKernelPool.EnsureSmallPrimesOnDevice(accelerator);
|
||||
int batchSize = GpuConstants.ScanBatchSize;
|
||||
UInt128 kStart = 1UL;
|
||||
UInt128 limit = maxK + UInt128.One;
|
||||
byte last = lastIsSeven ? (byte)1 : (byte)0;
|
||||
var kernel = gpuLease.Pow2ModKernel; // TODO: Swap this to the ProcessEightBitWindows kernel once GpuUInt128.Pow2Minus1Mod adopts the shared windowed helper measured fastest in GpuPow2ModBenchmarks.
|
||||
twoP.Mod10_8_5_3(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||
step10 = step10.Mod10();
|
||||
|
||||
GpuUInt128 twoPGpu = (GpuUInt128)twoP;
|
||||
GpuUInt128 twoPGpu = (GpuUInt128)twoP;
|
||||
|
||||
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
|
||||
// Device memory returned by Allocate1D is not guaranteed to be zeroed.
|
||||
// Ensure the buffer starts with a known state so that lanes skipped by the
|
||||
// kernel (for example due to early rejections) do not leave stale garbage
|
||||
// values that would be interpreted as composite witnesses when copied back
|
||||
// to the host. This mirrors the explicit zeroing performed by other GPU
|
||||
// testers such as the order kernels.
|
||||
orderBuffer.MemSetToZero();
|
||||
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||
UInt128 batchSize128 = (UInt128)batchSize;
|
||||
UInt128 fullBatchStep = twoP * batchSize128;
|
||||
UInt128 q = twoP * kStart + UInt128.One;
|
||||
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
|
||||
// Device memory returned by Allocate1D is not guaranteed to be zeroed.
|
||||
// Ensure the buffer starts with a known state so that lanes skipped by the
|
||||
// kernel (for example due to early rejections) do not leave stale garbage
|
||||
// values that would be interpreted as composite witnesses when copied back
|
||||
// to the host. This mirrors the explicit zeroing performed by other GPU
|
||||
// testers such as the order kernels.
|
||||
orderBuffer.MemSetToZero();
|
||||
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||
UInt128 batchSize128 = (UInt128)batchSize;
|
||||
UInt128 fullBatchStep = twoP * batchSize128;
|
||||
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
|
||||
{
|
||||
while (kStart < limit && Volatile.Read(ref isPrime))
|
||||
{
|
||||
UInt128 remaining = limit - kStart;
|
||||
int currentSize = remaining > batchSize128 ? batchSize : (int)remaining;
|
||||
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
||||
ref ulong ordersRef = ref MemoryMarshal.GetReference(orders);
|
||||
try
|
||||
{
|
||||
while (kStart < limit && Volatile.Read(ref isPrime))
|
||||
{
|
||||
UInt128 remaining = limit - kStart;
|
||||
int currentSize = remaining > batchSize128 ? batchSize : (int)remaining;
|
||||
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
||||
ref ulong ordersRef = ref MemoryMarshal.GetReference(orders);
|
||||
|
||||
q.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);
|
||||
q.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);
|
||||
|
||||
kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, 0UL,
|
||||
kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven, primeViews.LastOnePow2, primeViews.LastSevenPow2);
|
||||
kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, 0UL,
|
||||
kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven, primeViews.LastOnePow2, primeViews.LastSevenPow2);
|
||||
|
||||
accelerator.Synchronize();
|
||||
orderBuffer.View.CopyToCPU(ref ordersRef, currentSize);
|
||||
if (!Volatile.Read(ref isPrime))
|
||||
{
|
||||
break;
|
||||
}
|
||||
accelerator.Synchronize();
|
||||
orderBuffer.View.CopyToCPU(ref ordersRef, currentSize);
|
||||
if (!Volatile.Read(ref isPrime))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
bool compositeFound = false;
|
||||
for (int i = 0; i < currentSize; i++)
|
||||
{
|
||||
if (orders[i] != 0UL)
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
compositeFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
bool compositeFound = false;
|
||||
for (int i = 0; i < currentSize; i++)
|
||||
{
|
||||
if (orders[i] != 0UL)
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
compositeFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (compositeFound)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (compositeFound)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
UInt128 processed = (UInt128)currentSize;
|
||||
kStart += processed;
|
||||
if (kStart < limit)
|
||||
{
|
||||
if (processed == batchSize128)
|
||||
{
|
||||
q += fullBatchStep;
|
||||
}
|
||||
else
|
||||
{
|
||||
q += twoP * processed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<ulong>.Shared.Return(orderArray);
|
||||
orderBuffer.Dispose();
|
||||
gpuLease.Dispose();
|
||||
}
|
||||
}
|
||||
UInt128 processed = (UInt128)currentSize;
|
||||
kStart += processed;
|
||||
if (kStart < limit)
|
||||
{
|
||||
if (processed == batchSize128)
|
||||
{
|
||||
q += fullBatchStep;
|
||||
}
|
||||
else
|
||||
{
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<ulong>.Shared.Return(orderArray);
|
||||
orderBuffer.Dispose();
|
||||
gpuLease.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,23 +29,23 @@ public static class NttGpuMath
|
||||
public static NttBackend GpuTransformBackend { get; set; } = NttBackend.Reference;
|
||||
// Controls modular reduction strategy inside staged NTT paths.
|
||||
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>, int, int, int, ArrayView<GpuUInt128>, GpuUInt128>> StageKernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, GpuUInt128, GpuUInt128>> ScaleKernelCache = new();
|
||||
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, ulong, ulong>> StageBarrett128KernelCache = new();
|
||||
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>, ulong, ulong, ulong, ulong>> SquareBarrett128KernelCache = new();
|
||||
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>> FromMont64KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong>> SquareMont64KernelCache = new();
|
||||
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, ulong, ulong, ulong>> ScaleMont64KernelCache = 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(); // 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(); // 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(); // 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(); // 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(); // 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(); // 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(); // 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(); // TODO: As above – drop ConcurrentDictionary once startup prewarms every kernel.
|
||||
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(); // 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
|
||||
{
|
||||
@@ -111,6 +111,9 @@ public static class NttGpuMath
|
||||
int half = len >> 1;
|
||||
StageOffsets[stage - 1] = offset;
|
||||
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);
|
||||
wLen.ModPow((ulong)exp, modulus);
|
||||
var w = new GpuUInt128(0UL, 1UL);
|
||||
@@ -119,6 +122,8 @@ public static class NttGpuMath
|
||||
forward[offset + j] = w;
|
||||
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);
|
||||
wLenInv.ModPow((ulong)exp, modulus);
|
||||
var wi = new GpuUInt128(0UL, 1UL);
|
||||
@@ -140,6 +145,8 @@ public static class NttGpuMath
|
||||
{
|
||||
UseMontgomery64 = true;
|
||||
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);
|
||||
var mBI = new BigInteger(ModulusLow);
|
||||
MontRMod64 = (ulong)((BigInteger.One << 64) % mBI);
|
||||
|
||||
@@ -4,25 +4,25 @@ namespace PerfectNumbers.Core;
|
||||
|
||||
public interface IMersenneNumberDivisorByDivisorTester
|
||||
{
|
||||
bool UseDivisorCycles { get; set; } // TODO: Remove the setter once divisor cycle usage becomes mandatory so all implementations always leverage the cached cycles.
|
||||
bool UseDivisorCycles { get; set; } // TODO: Remove the setter once divisor cycle usage becomes mandatory so all implementations always leverage the cached cycles.
|
||||
|
||||
int BatchSize { get; set; }
|
||||
int BatchSize { get; set; }
|
||||
|
||||
void ConfigureFromMaxPrime(ulong maxPrime);
|
||||
void ConfigureFromMaxPrime(ulong maxPrime);
|
||||
|
||||
ulong DivisorLimit { get; }
|
||||
ulong DivisorLimit { get; }
|
||||
|
||||
ulong GetAllowedMaxDivisor(ulong prime);
|
||||
ulong GetAllowedMaxDivisor(ulong prime);
|
||||
|
||||
bool IsPrime(ulong prime, out bool divisorsExhausted);
|
||||
bool IsPrime(ulong prime, out bool divisorsExhausted);
|
||||
|
||||
IDivisorScanSession CreateDivisorSession();
|
||||
IDivisorScanSession CreateDivisorSession();
|
||||
|
||||
void PrepareCandidates(ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues);
|
||||
void PrepareCandidates(ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues);
|
||||
|
||||
public interface IDivisorScanSession : IDisposable
|
||||
{
|
||||
void CheckDivisor(ulong divisor, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits);
|
||||
}
|
||||
public interface IDivisorScanSession : IDisposable
|
||||
{
|
||||
void CheckDivisor(ulong divisor, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,16 +10,22 @@ public static class InputParser
|
||||
if (value.Contains('^'))
|
||||
{
|
||||
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);
|
||||
int e = int.Parse(parts[1], CultureInfo.InvariantCulture);
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@ public static class KRangeFinder
|
||||
int? max = null;
|
||||
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);
|
||||
// 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);
|
||||
int cmp = prod.CompareTo(RationalNumbers.Two);
|
||||
if (cmp == 0)
|
||||
@@ -46,6 +50,8 @@ public static class KRangeFinder
|
||||
int? max = null;
|
||||
EInteger[] eulerPrimes = primes.GetEulerPrimes(pMin, pMax);
|
||||
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(
|
||||
0,
|
||||
eulerPrimes.Length,
|
||||
|
||||
@@ -115,6 +115,9 @@ public class MersenneDivisorCycles
|
||||
// TODO: Replace this naive doubling fallback with the unrolled-hex generator from
|
||||
// MersenneDivisorCycleLengthGpuBenchmarks so large divisors avoid the millions of
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -444,19 +447,22 @@ public class MersenneDivisorCycles
|
||||
return pow == 1UL;
|
||||
}
|
||||
|
||||
public static ulong CalculateCycleLength(ulong divisor)
|
||||
{
|
||||
// For divisor = 2^k, cycle is 1
|
||||
if ((divisor & (divisor - 1UL)) == 0UL)
|
||||
{
|
||||
return 1UL;
|
||||
}
|
||||
public static ulong CalculateCycleLength(ulong divisor)
|
||||
{
|
||||
// For divisor = 2^k, cycle is 1
|
||||
if ((divisor & (divisor - 1UL)) == 0UL)
|
||||
{
|
||||
return 1UL;
|
||||
}
|
||||
|
||||
// Otherwise, find order of 2 mod divisor
|
||||
// Otherwise, find order of 2 mod divisor
|
||||
// TODO: Switch this scalar fallback to the unrolled-hex stepping sequence once
|
||||
// the generator is shared with CPU callers; the benchmark shows the unrolled
|
||||
// variant winning decisively for divisors >= 131,071.
|
||||
ulong order = 1UL, pow = 2UL;
|
||||
// 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;
|
||||
while (pow != 1)
|
||||
{
|
||||
pow <<= 1;
|
||||
|
||||
@@ -40,6 +40,8 @@ public static class MersenneNumberDivisorByDivisorTester
|
||||
ulong maxPrime = 0UL;
|
||||
|
||||
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();
|
||||
ulong currentPrime = hasPrime ? primeEnumerator.Current : 0UL;
|
||||
|
||||
@@ -214,6 +216,9 @@ public static class MersenneNumberDivisorByDivisorTester
|
||||
for (int workerIndex = 0; workerIndex < workers.Length; workerIndex++)
|
||||
{
|
||||
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(() =>
|
||||
{
|
||||
using var session = tester.CreateDivisorSession();
|
||||
@@ -254,7 +259,14 @@ public static class MersenneNumberDivisorByDivisorTester
|
||||
if (useDivisorCycles)
|
||||
{
|
||||
cycleBlock = DivisorCycleCache.Shared.Acquire(divisor);
|
||||
// TODO: Remove this block leasing once the cache exposes a direct
|
||||
// single-cycle retrieval API so coordinator threads never mutate the
|
||||
// cache state beyond the snapshot loaded at startup.
|
||||
divisorCycle = cycleBlock.GetCycle(divisor);
|
||||
// TODO: When divisorCycle returns zero, compute only that single cycle on the
|
||||
// device selected by the current configuration, keep the result transient, and
|
||||
// continue operating with the single cached block instead of scheduling additional
|
||||
// batches.
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -406,6 +418,8 @@ public static class MersenneNumberDivisorByDivisorTester
|
||||
ref long nextDivisorBits = ref Unsafe.As<ulong, long>(ref nextDivisor);
|
||||
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))
|
||||
{
|
||||
exhausted = false;
|
||||
@@ -481,6 +495,8 @@ public static class MersenneNumberDivisorByDivisorTester
|
||||
cursor += 2UL;
|
||||
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))
|
||||
{
|
||||
divisor = candidate;
|
||||
@@ -502,6 +518,8 @@ public static class MersenneNumberDivisorByDivisorTester
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ public sealed class MersenneNumberTester(
|
||||
private readonly bool _useGpuScan = useGpuScan; // device for pow2mod/incremental scanning
|
||||
private readonly bool _useGpuOrder = useGpuOrder; // device for order computations
|
||||
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 MersenneNumberIncrementalCpuTester _incrementalCpuTester = new(kernelType);
|
||||
private readonly MersenneNumberOrderGpuTester _orderGpuTester = new(kernelType, useGpuOrder);
|
||||
@@ -56,32 +59,34 @@ public sealed class MersenneNumberTester(
|
||||
return;
|
||||
}
|
||||
|
||||
UInt128 twoP = (UInt128)exponent << 1; // 2 * p
|
||||
bool lastIsSeven = (exponent & 3UL) == 3UL;
|
||||
// CPU path: compute orders directly and cache
|
||||
if (!_useGpuOrder)
|
||||
{
|
||||
UInt128 k = UInt128.One;
|
||||
UInt128 twoP = (UInt128)exponent << 1; // 2 * p
|
||||
bool lastIsSeven = (exponent & 3UL) == 3UL;
|
||||
// CPU path: compute orders directly and cache
|
||||
if (!_useGpuOrder)
|
||||
{
|
||||
UInt128 k = UInt128.One;
|
||||
UInt128 q;
|
||||
ulong remainder;
|
||||
for (; k <= limit; k++)
|
||||
{
|
||||
q = twoP * k + UInt128.One;
|
||||
remainder = q.Mod10();
|
||||
bool shouldCheck = lastIsSeven
|
||||
? remainder == 7UL || remainder == 9UL || remainder == 3UL
|
||||
: remainder == 1UL || remainder == 3UL || remainder == 9UL;
|
||||
{
|
||||
q = twoP * k + UInt128.One;
|
||||
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
|
||||
? remainder == 7UL || remainder == 9UL || remainder == 3UL
|
||||
: remainder == 1UL || remainder == 3UL || remainder == 9UL;
|
||||
|
||||
// Early modular filters for odd order and small-prime rejections
|
||||
// Early modular filters for odd order and small-prime rejections
|
||||
if (shouldCheck)
|
||||
{
|
||||
ulong mod8 = q.Mod8();
|
||||
if (mod8 != 1UL && mod8 != 7UL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (mod8 != 1UL && mod8 != 7UL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (q.Mod3() == 0UL || q.Mod5() == 0UL)
|
||||
{
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -107,13 +112,15 @@ public sealed class MersenneNumberTester(
|
||||
ulong remainder2;
|
||||
for (; k2 <= limit; k2++)
|
||||
{
|
||||
q2 = twoP * k2 + 1UL;
|
||||
remainder2 = q2.Mod10();
|
||||
bool shouldCheck2 = lastIsSeven
|
||||
? remainder2 == 7UL || remainder2 == 9UL || remainder2 == 3UL
|
||||
: remainder2 == 1UL || remainder2 == 3UL || remainder2 == 9UL;
|
||||
q2 = twoP * k2 + 1UL;
|
||||
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
|
||||
? remainder2 == 7UL || remainder2 == 9UL || remainder2 == 3UL
|
||||
: remainder2 == 1UL || remainder2 == 3UL || remainder2 == 9UL;
|
||||
|
||||
// Early modular filters for odd order and small-prime rejections
|
||||
// Early modular filters for odd order and small-prime rejections
|
||||
if (shouldCheck2)
|
||||
{
|
||||
ulong mod8 = q2.Mod8();
|
||||
|
||||
@@ -36,6 +36,8 @@ public static class MersennePrimeFactorTester
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return false;
|
||||
@@ -45,6 +47,8 @@ public static class MersennePrimeFactorTester
|
||||
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 = [];
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -64,6 +68,9 @@ public static class MersennePrimeFactorTester
|
||||
{
|
||||
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))
|
||||
{
|
||||
return cached;
|
||||
@@ -74,6 +81,8 @@ public static class MersennePrimeFactorTester
|
||||
UInt128[] factors = phi <= ulong.MaxValue
|
||||
? Array.ConvertAll(Factor64((ulong)phi, ct), x => (UInt128)x)
|
||||
: 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 candidate, prime;
|
||||
@@ -86,6 +95,8 @@ public static class MersennePrimeFactorTester
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
lock (_orderCache)
|
||||
@@ -209,7 +223,7 @@ public static class MersennePrimeFactorTester
|
||||
private static void ReturnDictionary(Dictionary<ulong, byte> dict)
|
||||
{
|
||||
dict.Clear();
|
||||
DictPool.Add(dict);
|
||||
DictPool.Add(dict);
|
||||
}
|
||||
|
||||
private static readonly ConcurrentBag<Dictionary<UInt128, byte>> DictPool128 = new();
|
||||
@@ -239,7 +253,9 @@ public static class MersennePrimeFactorTester
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
while (queue.Count != 0)
|
||||
@@ -277,6 +293,8 @@ public static class MersennePrimeFactorTester
|
||||
}
|
||||
|
||||
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)
|
||||
// {
|
||||
// FactorCache[n] = result;
|
||||
@@ -294,7 +312,9 @@ public static class MersennePrimeFactorTester
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
while (queue.Count != 0)
|
||||
@@ -331,7 +351,9 @@ public static class MersennePrimeFactorTester
|
||||
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;
|
||||
foreach (var key in dict.Keys)
|
||||
{
|
||||
@@ -349,7 +371,9 @@ public static class MersennePrimeFactorTester
|
||||
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;
|
||||
while (true)
|
||||
@@ -371,19 +395,24 @@ public static class MersennePrimeFactorTester
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
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
|
||||
}
|
||||
|
||||
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;
|
||||
b = t;
|
||||
}
|
||||
@@ -493,7 +523,8 @@ public static class MersennePrimeFactorTester
|
||||
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;
|
||||
while (true)
|
||||
@@ -515,26 +546,30 @@ public static class MersennePrimeFactorTester
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
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)
|
||||
@@ -555,7 +590,8 @@ public static class MersennePrimeFactorTester
|
||||
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;
|
||||
b = t;
|
||||
}
|
||||
|
||||
@@ -10,36 +10,54 @@ public class MersenneResidueStepper
|
||||
public MersenneResidueStepper(UInt128 modulus, ulong p0, UInt128 M_mod_m_at_p0)
|
||||
{
|
||||
if (modulus == 0)
|
||||
{
|
||||
throw new ArgumentException("Modulus must be positive.");
|
||||
}
|
||||
|
||||
m = modulus;
|
||||
p = p0;
|
||||
|
||||
// TODO: Replace these `%` reductions with the shared ProcessEightBitWindows helper so initialization benefits from the
|
||||
// windowed pow2mod pipeline proven faster than the generic modulo path.
|
||||
M_mod = M_mod_m_at_p0 % 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) Step(long delta)
|
||||
{
|
||||
if (delta == 0)
|
||||
{
|
||||
return State();
|
||||
}
|
||||
|
||||
UInt128 g = UInt128.One;
|
||||
ulong absDelta = (ulong)Math.Abs(delta);
|
||||
|
||||
for (ulong i = 0; i < absDelta; i++)
|
||||
{
|
||||
// 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
|
||||
// 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;
|
||||
|
||||
if (g >= m)
|
||||
{
|
||||
g -= m;
|
||||
}
|
||||
}
|
||||
|
||||
if (delta > 0)
|
||||
{
|
||||
// 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.
|
||||
pow2_mod = (pow2_mod * g) % m;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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.
|
||||
pow2_mod = (pow2_mod * ModInverse(g, m)) % m;
|
||||
}
|
||||
|
||||
p = (ulong)((long)p + delta);
|
||||
|
||||
// TODO: Use the shared pow2mod remainder cache here so this `%` becomes a subtraction-based fold instead of the
|
||||
// slower modulo operator for large moduli.
|
||||
M_mod = (pow2_mod + m - 1) % m;
|
||||
|
||||
return State();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return Step(delta);
|
||||
}
|
||||
|
||||
private static UInt128 ModInverse(UInt128 a, UInt128 m)
|
||||
{
|
||||
UInt128 m0 = m, t, q;
|
||||
UInt128 x0 = 0, x1 = 1;
|
||||
if (m == 1) return 0;
|
||||
UInt128 m0 = m;
|
||||
UInt128 t;
|
||||
UInt128 q;
|
||||
UInt128 x0 = 0;
|
||||
UInt128 x1 = 1;
|
||||
|
||||
if (m == 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
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;
|
||||
t = m;
|
||||
m = a % m; a = t;
|
||||
m = a % m;
|
||||
a = t;
|
||||
t = x0;
|
||||
x0 = x1 - q * x0;
|
||||
x1 = t;
|
||||
}
|
||||
if (x1 < 0) x1 += m0;
|
||||
|
||||
if (x1 < 0)
|
||||
{
|
||||
x1 += m0;
|
||||
}
|
||||
|
||||
return x1;
|
||||
}
|
||||
}
|
||||
@@ -179,22 +179,25 @@ public sealed class ModResidueTracker
|
||||
UInt128 d, value;
|
||||
List<UInt128> residues = _residues;
|
||||
|
||||
if (_model == ResidueModel.Identity)
|
||||
{
|
||||
// Avoid 128-bit division: delta is small in scanner (<= 6) so use add/sub.
|
||||
for (; i < divisorsCount; i++)
|
||||
{
|
||||
d = divisors[i];
|
||||
value = residues[i] + delta;
|
||||
// For tiny moduli (2..6) we may need a few subtractions; keep the loop tight.
|
||||
while (value >= d)
|
||||
{
|
||||
value -= d;
|
||||
}
|
||||
if (_model == ResidueModel.Identity)
|
||||
{
|
||||
// Avoid 128-bit division: delta is small in scanner (<= 6) so use add/sub.
|
||||
for (; i < divisorsCount; i++)
|
||||
{
|
||||
d = divisors[i];
|
||||
value = residues[i] + delta;
|
||||
// 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)
|
||||
{
|
||||
value -= d;
|
||||
}
|
||||
|
||||
residues[i] = value;
|
||||
}
|
||||
}
|
||||
residues[i] = value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (; i < divisors.Count; i++)
|
||||
@@ -259,7 +262,8 @@ public sealed class ModResidueTracker
|
||||
// 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
|
||||
// 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);
|
||||
|
||||
// q divides M_p ⇔ 2^p ≡ 1 (mod q) AND ord_q(2) | p
|
||||
|
||||
@@ -9,14 +9,14 @@ namespace PerfectNumbers.Core;
|
||||
/// </summary>
|
||||
public sealed class MersenneResidueAutomaton
|
||||
{
|
||||
private readonly UInt128 _step; // 2*p
|
||||
private UInt128 _currentQ; // current q value
|
||||
private readonly UInt128 _step; // 2*p
|
||||
private UInt128 _currentQ; // current q value
|
||||
|
||||
// Cached residues for current q
|
||||
public ulong Mod10R;
|
||||
public ulong Mod8R;
|
||||
public ulong Mod3R;
|
||||
public ulong Mod5R;
|
||||
// Cached residues for current q
|
||||
public ulong Mod10R;
|
||||
public ulong Mod8R;
|
||||
public ulong Mod3R;
|
||||
public ulong Mod5R;
|
||||
|
||||
// Cached residues for step (2*p mod m)
|
||||
private readonly ulong _step10;
|
||||
@@ -27,21 +27,23 @@ public sealed class MersenneResidueAutomaton
|
||||
public MersenneResidueAutomaton(ulong exponent)
|
||||
{
|
||||
_step = (UInt128)exponent << 1; // 2 * p
|
||||
_currentQ = _step + UInt128.One; // start at k = 1
|
||||
_currentQ = _step + UInt128.One; // start at k = 1
|
||||
|
||||
// init residues
|
||||
// init residues
|
||||
Mod10R = _currentQ.Mod10();
|
||||
Mod8R = _currentQ.Mod8();
|
||||
Mod3R = _currentQ.Mod3();
|
||||
Mod5R = _currentQ.Mod5();
|
||||
Mod8R = _currentQ.Mod8();
|
||||
Mod3R = _currentQ.Mod3();
|
||||
Mod5R = _currentQ.Mod5();
|
||||
|
||||
_step10 = _step.Mod10().Mod10();
|
||||
_step8 = _step.Mod8();
|
||||
_step3 = _step.Mod3();
|
||||
_step5 = _step.Mod5();
|
||||
_step8 = _step.Mod8();
|
||||
_step3 = _step.Mod3();
|
||||
_step5 = _step.Mod5();
|
||||
}
|
||||
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
@@ -65,11 +67,16 @@ public sealed class MersenneResidueAutomaton
|
||||
Mod3R = r3;
|
||||
|
||||
ulong r5 = Mod5R + _step5;
|
||||
|
||||
if (r5 >= 5UL)
|
||||
{
|
||||
r5 -= 5UL;
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +89,8 @@ public sealed class Ending7Automaton
|
||||
private ulong _n; // current candidate n, ends with 7
|
||||
|
||||
private readonly ulong[] _moduli;
|
||||
private readonly ulong[] _steps; // 10 % m
|
||||
private readonly ulong[] _res; // n % m
|
||||
private readonly ulong[] _steps; // 10 % m
|
||||
private readonly ulong[] _res; // n % m
|
||||
private readonly bool[] _reachable;
|
||||
|
||||
public Ending7Automaton(ulong start, params ulong[] moduli)
|
||||
@@ -95,6 +102,7 @@ public sealed class Ending7Automaton
|
||||
_res = Array.Empty<ulong>();
|
||||
_reachable = Array.Empty<bool>();
|
||||
_n = AlignTo7(start);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -104,20 +112,33 @@ public sealed class Ending7Automaton
|
||||
_steps = new ulong[len];
|
||||
_res = new ulong[len];
|
||||
_reachable = new bool[len];
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
ulong m = moduli[i];
|
||||
_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;
|
||||
_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
|
||||
|
||||
// Zero is reachable iff gcd(step, m) | res
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
[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;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -140,6 +161,7 @@ public sealed class Ending7Automaton
|
||||
public void Next()
|
||||
{
|
||||
_n += 10UL;
|
||||
|
||||
for (int i = 0; i < _moduli.Length; i++)
|
||||
{
|
||||
if (!_reachable[i])
|
||||
@@ -149,19 +171,25 @@ public sealed class Ending7Automaton
|
||||
|
||||
ulong m = _moduli[i];
|
||||
ulong r = _res[i] + _steps[i];
|
||||
|
||||
if (r >= m)
|
||||
{
|
||||
r -= m;
|
||||
}
|
||||
|
||||
_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)]
|
||||
private static ulong AlignTo7(ulong start)
|
||||
{
|
||||
ulong r = start.Mod10();
|
||||
ulong add = (7UL + 10UL - r).Mod10();
|
||||
ulong add = (7UL + 10UL - r).Mod10();
|
||||
|
||||
return start + add;
|
||||
}
|
||||
|
||||
@@ -176,6 +204,7 @@ public sealed class Ending7Automaton
|
||||
a = b;
|
||||
b = t;
|
||||
}
|
||||
|
||||
return a;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,10 @@ internal static class MontgomeryDivisorDataCache
|
||||
{
|
||||
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();
|
||||
|
||||
public static MontgomeryDivisorData Get(ulong modulus)
|
||||
@@ -30,11 +33,16 @@ internal static class MontgomeryDivisorDataCache
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
internal static void ReleaseBlock(DivisorCycleCache.CycleBlock block)
|
||||
{
|
||||
// TODO: Remove this entire release hook once the cycle cache no longer hands out dynamic blocks;
|
||||
// the single-cycle pipeline will never transfer ownership back to Montgomery metadata.
|
||||
if (block.Index <= 0)
|
||||
{
|
||||
return;
|
||||
@@ -86,10 +94,15 @@ internal static class MontgomeryDivisorDataCache
|
||||
return created;
|
||||
}
|
||||
}
|
||||
// TODO: Collapse this retry loop once the cache becomes a simple snapshot dictionary. With the
|
||||
// single-cycle policy we can compute the requested cycle, populate the dictionary exactly once,
|
||||
// and return without spinning through concurrent retries.
|
||||
}
|
||||
|
||||
private static void RegisterBlockMembership(ulong modulus, int blockIndex)
|
||||
{
|
||||
// TODO: Delete this method once block membership goes away; the single snapshot model must avoid
|
||||
// tracking additional indices or allocating concurrent dictionaries for Montgomery lookups.
|
||||
if (blockIndex <= 0)
|
||||
{
|
||||
return;
|
||||
@@ -106,6 +119,9 @@ internal static class MontgomeryDivisorDataCache
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@ namespace PerfectNumbers.Core;
|
||||
|
||||
public static class PerfectNumberConstants
|
||||
{
|
||||
public const ulong BiggestKnownEvenPerfectP = 136279841UL;
|
||||
public const int ConsoleInterval = 100_000;
|
||||
public const ulong ExtraDivisorCycleSearchLimit = 64UL;
|
||||
public const int MaxQForDivisorCycles = 4_000_000;
|
||||
public static readonly uint PrimesLimit = 1_000_000; //(ulong)Array.MaxLength;// 1_000_000;
|
||||
|
||||
public const ulong BiggestKnownEvenPerfectP = 136279841UL;
|
||||
public const int ConsoleInterval = 100_000;
|
||||
public const ulong ExtraDivisorCycleSearchLimit = 64UL;
|
||||
public const int MaxQForDivisorCycles = 4_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))
|
||||
{
|
||||
// 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[0] = BigInteger.One;
|
||||
BigCache[baseVal] = powers;
|
||||
@@ -21,6 +24,8 @@ public static class PowerCache
|
||||
if (powers.Length <= exponent)
|
||||
{
|
||||
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);
|
||||
for (int i = oldLength; i <= exponent; i++)
|
||||
powers[i] = BigInteger.Zero;
|
||||
@@ -35,7 +40,7 @@ public static class PowerCache
|
||||
if (powers[i].IsZero)
|
||||
{
|
||||
// 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
|
||||
// scan when p >= 138M.
|
||||
powers[i] = powers[i - 1] * baseVal;
|
||||
|
||||
@@ -25,6 +25,8 @@ public sealed class PrimeCache
|
||||
last = current;
|
||||
// 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.
|
||||
// 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);
|
||||
_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
|
||||
// conversions; walking the enumerator element-by-element is noticeably slower in the
|
||||
// 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)
|
||||
{
|
||||
yield return val;
|
||||
|
||||
@@ -25,6 +25,8 @@ public sealed class PrimeTester(bool useInternal = false)
|
||||
|
||||
Span<ulong> one = stackalloc ulong[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;
|
||||
IsPrimeBatchGpu(one, outFlags);
|
||||
|
||||
@@ -116,6 +118,9 @@ public sealed class PrimeTester(bool useInternal = false)
|
||||
var input = scratch.Input;
|
||||
var output = scratch.Output;
|
||||
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
|
||||
{
|
||||
@@ -156,6 +161,9 @@ public sealed class PrimeTester(bool useInternal = false)
|
||||
public MemoryBuffer1D<uint, Stride1D.Dense> DevicePrimes { get; }
|
||||
private readonly Accelerator _accel;
|
||||
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)
|
||||
{
|
||||
@@ -226,6 +234,9 @@ public sealed class PrimeTester(bool useInternal = false)
|
||||
{
|
||||
// Map accelerator to cached state; use Lazy to serialize kernel creation
|
||||
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)]
|
||||
public static KernelState GetOrCreate(Accelerator accelerator)
|
||||
@@ -308,12 +319,18 @@ public sealed class PrimeTester(bool useInternal = false)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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);
|
||||
return BinaryGcd(n, m) != 1UL;
|
||||
}
|
||||
|
||||
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 accelerator = gpu.Accelerator;
|
||||
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<byte>>(SharesFactorKernel);
|
||||
@@ -337,6 +354,9 @@ public sealed class PrimeTester(bool useInternal = false)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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)
|
||||
{
|
||||
return v;
|
||||
@@ -375,6 +395,9 @@ public sealed class PrimeTester(bool useInternal = false)
|
||||
|
||||
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)
|
||||
{
|
||||
return v;
|
||||
|
||||
@@ -4,122 +4,130 @@ namespace PerfectNumbers.Core;
|
||||
|
||||
public static class PrimesGenerator
|
||||
{
|
||||
public static readonly uint[] SmallPrimes;
|
||||
public static readonly ulong[] SmallPrimesPow2;
|
||||
public static readonly uint[] SmallPrimesLastOne;
|
||||
public static readonly ulong[] SmallPrimesPow2LastOne;
|
||||
public static readonly uint[] SmallPrimesLastSeven;
|
||||
public static readonly ulong[] SmallPrimesPow2LastSeven;
|
||||
public static readonly uint[] SmallPrimes;
|
||||
public static readonly ulong[] SmallPrimesPow2;
|
||||
public static readonly uint[] SmallPrimesLastOne;
|
||||
public static readonly ulong[] SmallPrimesPow2LastOne;
|
||||
public static readonly uint[] SmallPrimesLastSeven;
|
||||
public static readonly ulong[] SmallPrimesPow2LastSeven;
|
||||
|
||||
static PrimesGenerator()
|
||||
{
|
||||
BuildSmallPrimes(
|
||||
out SmallPrimes,
|
||||
out SmallPrimesPow2,
|
||||
out SmallPrimesLastOne,
|
||||
out SmallPrimesPow2LastOne,
|
||||
out SmallPrimesLastSeven,
|
||||
out SmallPrimesPow2LastSeven);
|
||||
}
|
||||
static PrimesGenerator()
|
||||
{
|
||||
BuildSmallPrimes(
|
||||
out SmallPrimes,
|
||||
out SmallPrimesPow2,
|
||||
out SmallPrimesLastOne,
|
||||
out SmallPrimesPow2LastOne,
|
||||
out SmallPrimesLastSeven,
|
||||
out SmallPrimesPow2LastSeven);
|
||||
}
|
||||
|
||||
private static void BuildSmallPrimes(
|
||||
out uint[] all,
|
||||
out ulong[] allPow2,
|
||||
out uint[] lastOne,
|
||||
out ulong[] lastOnePow2,
|
||||
out uint[] lastSeven,
|
||||
out ulong[] lastSevenPow2)
|
||||
{
|
||||
uint target = PerfectNumberConstants.PrimesLimit;
|
||||
int targetInt = checked((int)target);
|
||||
all = new uint[targetInt];
|
||||
allPow2 = new ulong[targetInt];
|
||||
lastOne = new uint[targetInt];
|
||||
lastOnePow2 = new ulong[targetInt];
|
||||
lastSeven = new uint[targetInt];
|
||||
lastSevenPow2 = new ulong[targetInt];
|
||||
private static void BuildSmallPrimes(
|
||||
out uint[] all,
|
||||
out ulong[] allPow2,
|
||||
out uint[] lastOne,
|
||||
out ulong[] lastOnePow2,
|
||||
out uint[] lastSeven,
|
||||
out ulong[] lastSevenPow2)
|
||||
{
|
||||
uint target = PerfectNumberConstants.PrimesLimit;
|
||||
int targetInt = checked((int)target);
|
||||
all = new uint[targetInt];
|
||||
allPow2 = new ulong[targetInt];
|
||||
lastOne = new uint[targetInt];
|
||||
lastOnePow2 = new ulong[targetInt];
|
||||
lastSeven = new uint[targetInt];
|
||||
lastSevenPow2 = new ulong[targetInt];
|
||||
|
||||
List<uint> primes = new(targetInt * 4 / 3 + 16);
|
||||
uint candidate = 2U;
|
||||
int allCount = 0;
|
||||
int lastOneCount = 0;
|
||||
int lastSevenCount = 0;
|
||||
List<uint> primes = new(targetInt * 4 / 3 + 16);
|
||||
uint candidate = 2U;
|
||||
int allCount = 0;
|
||||
int lastOneCount = 0;
|
||||
int lastSevenCount = 0;
|
||||
|
||||
while (allCount < targetInt || lastOneCount < targetInt || lastSevenCount < targetInt)
|
||||
{
|
||||
bool isPrime = true;
|
||||
int primesCount = primes.Count;
|
||||
for (int i = 0; i < primesCount; i++)
|
||||
{
|
||||
uint p = primes[i];
|
||||
if (p * p > candidate)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO: Replace this trial-division `%` with the sieve-based generator that avoids
|
||||
// per-candidate modulo work so building the small-prime tables stops dominating
|
||||
// startup time for large scans.
|
||||
if (candidate % p == 0U)
|
||||
{
|
||||
isPrime = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPrime)
|
||||
{
|
||||
primes.Add(candidate);
|
||||
if (allCount < targetInt)
|
||||
{
|
||||
all[allCount] = candidate;
|
||||
allPow2[allCount] = checked((ulong)candidate * candidate);
|
||||
allCount++;
|
||||
}
|
||||
|
||||
if (candidate != 2U)
|
||||
{
|
||||
if (lastOneCount < targetInt && IsAllowedForLastOne(candidate))
|
||||
{
|
||||
lastOne[lastOneCount] = candidate;
|
||||
lastOnePow2[lastOneCount] = checked((ulong)candidate * candidate);
|
||||
lastOneCount++;
|
||||
}
|
||||
|
||||
if (lastSevenCount < targetInt && IsAllowedForLastSeven(candidate))
|
||||
{
|
||||
lastSeven[lastSevenCount] = candidate;
|
||||
lastSevenPow2[lastSevenCount] = checked((ulong)candidate * candidate);
|
||||
lastSevenCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidate = candidate == 2U ? 3U : candidate + 2U;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsAllowedForLastOne(uint prime)
|
||||
while (allCount < targetInt || lastOneCount < targetInt || lastSevenCount < targetInt)
|
||||
{
|
||||
// TODO: Swap the `% 10` usage for ULongExtensions.Mod10 so the hot classification path
|
||||
// reuses the benchmarked residue helper instead of repeated divisions.
|
||||
return (prime % 10U) switch
|
||||
bool isPrime = true;
|
||||
int primesCount = primes.Count;
|
||||
for (int i = 0; i < primesCount; i++)
|
||||
{
|
||||
uint p = primes[i];
|
||||
// TODO: Reuse a single squared local for `p` within this loop so we avoid recalculating
|
||||
// `p * p` repeatedly without storing every squared prime globally (the cache must remain
|
||||
// limited to the small-divisor tables to honor the memory constraints).
|
||||
if (p * p > candidate)
|
||||
{
|
||||
1U or 3U or 9U => true,
|
||||
_ => prime == 7U || prime == 11U,
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsAllowedForLastSeven(uint prime)
|
||||
{
|
||||
// TODO: Route this `% 10` classification through ULongExtensions.Mod10 to match the faster
|
||||
// residue helper used elsewhere in the scanner.
|
||||
return (prime % 10U) switch
|
||||
// TODO: Replace this trial-division `%` with the sieve-based generator that avoids
|
||||
// per-candidate modulo work so building the small-prime tables stops dominating
|
||||
// startup time for large scans.
|
||||
if (candidate % p == 0U)
|
||||
{
|
||||
3U or 7U or 9U => true,
|
||||
_ => prime == 11U,
|
||||
};
|
||||
isPrime = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPrime)
|
||||
{
|
||||
primes.Add(candidate);
|
||||
if (allCount < targetInt)
|
||||
{
|
||||
// TODO: Reuse a single cached `candidateSquared` per iteration so we avoid issuing
|
||||
// three separate 64-bit multiplications when populating the pow2 arrays here and
|
||||
// in the last-one/last-seven branches below.
|
||||
all[allCount] = candidate;
|
||||
allPow2[allCount] = checked((ulong)candidate * candidate);
|
||||
allCount++;
|
||||
}
|
||||
|
||||
if (candidate != 2U)
|
||||
{
|
||||
if (lastOneCount < targetInt && IsAllowedForLastOne(candidate))
|
||||
{
|
||||
lastOne[lastOneCount] = candidate;
|
||||
lastOnePow2[lastOneCount] = checked((ulong)candidate * candidate);
|
||||
lastOneCount++;
|
||||
}
|
||||
|
||||
if (lastSevenCount < targetInt && IsAllowedForLastSeven(candidate))
|
||||
{
|
||||
lastSeven[lastSevenCount] = candidate;
|
||||
lastSevenPow2[lastSevenCount] = checked((ulong)candidate * candidate);
|
||||
lastSevenCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Switch this increment to the Mod6 stepping table identified in the Mod6ComparisonBenchmarks
|
||||
// so we skip the composite residues without relying on per-iteration `%` checks.
|
||||
candidate = candidate == 2U ? 3U : candidate + 2U;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsAllowedForLastOne(uint prime)
|
||||
{
|
||||
// TODO: Swap the `% 10` usage for ULongExtensions.Mod10 so the hot classification path
|
||||
// reuses the benchmarked residue helper instead of repeated divisions.
|
||||
return (prime % 10U) switch
|
||||
{
|
||||
1U or 3U or 9U => true,
|
||||
_ => prime == 7U || prime == 11U,
|
||||
};
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsAllowedForLastSeven(uint prime)
|
||||
{
|
||||
// TODO: Route this `% 10` classification through ULongExtensions.Mod10 to match the faster
|
||||
// residue helper used elsewhere in the scanner.
|
||||
return (prime % 10U) switch
|
||||
{
|
||||
3U or 7U or 9U => true,
|
||||
_ => prime == 11U,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,17 +10,24 @@ public static class RationalHelper
|
||||
long numerator = checked((long)Math.Round(value * scale));
|
||||
long denominator = scale;
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public static ulong FloorToUInt64(ERational value)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -29,6 +36,9 @@ public static class RationalHelper
|
||||
EInteger ei = value.ToEInteger();
|
||||
if (value.CompareTo(ERational.Create(ei, EInteger.One)) > 0)
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,7 @@ namespace PerfectNumbers.Core;
|
||||
public static class RationalNumbers
|
||||
{
|
||||
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);
|
||||
// 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))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
@@ -242,6 +244,8 @@ public static class RleBlacklist
|
||||
|
||||
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)
|
||||
{
|
||||
tmp[count++] = v;
|
||||
@@ -323,6 +327,8 @@ public static class RleBlacklist
|
||||
|
||||
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)
|
||||
{
|
||||
tmp[count++] = v;
|
||||
|
||||
@@ -7,11 +7,17 @@ public static class StringBuilderPool
|
||||
{
|
||||
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)
|
||||
{
|
||||
_ = sb.Clear();
|
||||
// TODO: Preserve the builder's capacity when returning it so the pool hands the same buffer back without shrinkage.
|
||||
_stringBuilderPool.Enqueue(sb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,16 @@ public sealed class TextFileWriter : IDisposable
|
||||
|
||||
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);
|
||||
@@ -36,6 +45,9 @@ public sealed class TextFileWriter : IDisposable
|
||||
{
|
||||
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.Position = 0;
|
||||
_textWriter?.Flush();
|
||||
@@ -65,6 +77,8 @@ public sealed class TextFileWriter : IDisposable
|
||||
{
|
||||
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);
|
||||
BinaryWriter.Flush();
|
||||
}
|
||||
@@ -72,6 +86,8 @@ public sealed class TextFileWriter : IDisposable
|
||||
|
||||
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();
|
||||
_textWriter?.Dispose();
|
||||
_stream.Dispose();
|
||||
|
||||
@@ -5,320 +5,337 @@ namespace PerfectNumbers.Core;
|
||||
|
||||
public static class UInt128Extensions
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 BinaryGcd(this UInt128 u, UInt128 v)
|
||||
{
|
||||
UInt128 zero = UInt128.Zero;
|
||||
if (u == zero)
|
||||
{
|
||||
return v;
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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;
|
||||
if (u == zero)
|
||||
{
|
||||
return v;
|
||||
}
|
||||
|
||||
if (v == zero)
|
||||
{
|
||||
return u;
|
||||
}
|
||||
if (v == zero)
|
||||
{
|
||||
return u;
|
||||
}
|
||||
|
||||
int shift = CountTrailingZeros(u | v);
|
||||
u >>= CountTrailingZeros(u);
|
||||
int shift = CountTrailingZeros(u | v);
|
||||
u >>= CountTrailingZeros(u);
|
||||
|
||||
do
|
||||
{
|
||||
v >>= CountTrailingZeros(v);
|
||||
if (u > v)
|
||||
{
|
||||
(u, v) = (v, u);
|
||||
}
|
||||
do
|
||||
{
|
||||
v >>= CountTrailingZeros(v);
|
||||
if (u > v)
|
||||
{
|
||||
(u, v) = (v, u);
|
||||
}
|
||||
|
||||
v -= u;
|
||||
}
|
||||
while (v != zero);
|
||||
v -= u;
|
||||
}
|
||||
while (v != zero);
|
||||
|
||||
return u << shift;
|
||||
}
|
||||
return u << shift;
|
||||
}
|
||||
|
||||
public static ulong CalculateOrder(this UInt128 q)
|
||||
{
|
||||
if (q <= UInt128Numbers.Two)
|
||||
{
|
||||
return 0UL;
|
||||
}
|
||||
public static ulong CalculateOrder(this UInt128 q)
|
||||
{
|
||||
if (q <= UInt128Numbers.Two)
|
||||
{
|
||||
return 0UL;
|
||||
}
|
||||
|
||||
UInt128 one = UInt128.One;
|
||||
UInt128 phi = q - one;
|
||||
if (phi > ulong.MaxValue)
|
||||
{
|
||||
throw new NotImplementedException("Such big values are not yet supported");
|
||||
}
|
||||
UInt128 one = UInt128.One;
|
||||
UInt128 phi = q - one;
|
||||
if (phi > ulong.MaxValue)
|
||||
{
|
||||
throw new NotImplementedException("Such big values are not yet supported");
|
||||
}
|
||||
|
||||
ulong order = (ulong)phi;
|
||||
uint[] smallPrimes = PrimesGenerator.SmallPrimes;
|
||||
ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2;
|
||||
ulong order = (ulong)phi;
|
||||
uint[] smallPrimes = PrimesGenerator.SmallPrimes;
|
||||
ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2;
|
||||
|
||||
int i = 0, primesLength = smallPrimes.Length;
|
||||
UInt128 cycle = MersenneDivisorCycles.GetCycle(q);
|
||||
ulong prime, temp;
|
||||
for (; i < primesLength; i++)
|
||||
{
|
||||
if (smallPrimesPow2[i] > order)
|
||||
{
|
||||
break;
|
||||
}
|
||||
int i = 0, primesLength = smallPrimes.Length;
|
||||
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;
|
||||
for (; i < primesLength; i++)
|
||||
{
|
||||
if (smallPrimesPow2[i] > order)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
prime = smallPrimes[i];
|
||||
while (order % prime == 0UL)
|
||||
{
|
||||
temp = order / prime;
|
||||
// TODO: Switch this divisor-order powmod to the ProcessEightBitWindows helper so the
|
||||
// cycle factoring loop benefits from the faster windowed pow2 ladder measured in CPU benchmarks.
|
||||
if (temp.PowModWithCycle(q, cycle) == one)
|
||||
{
|
||||
order = temp;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int CountTrailingZeros(this UInt128 value)
|
||||
{
|
||||
ulong valuePart = (ulong)value;
|
||||
if (valuePart != 0UL)
|
||||
{
|
||||
return BitOperations.TrailingZeroCount(valuePart);
|
||||
}
|
||||
|
||||
return 64 + BitOperations.TrailingZeroCount((ulong)(value >> 64));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsPrimeCandidate(this UInt128 n)
|
||||
{
|
||||
UInt128 p, zero = UInt128.Zero;
|
||||
|
||||
uint[] smallPrimes = PrimesGenerator.SmallPrimes;
|
||||
ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2;
|
||||
ulong i, smallPrimesCount = (ulong)smallPrimes.Length;
|
||||
for (i = 0UL; i < smallPrimesCount; i++)
|
||||
prime = smallPrimes[i];
|
||||
while (order % prime == 0UL)
|
||||
{
|
||||
temp = order / prime;
|
||||
// TODO: Switch this divisor-order powmod to the ProcessEightBitWindows helper so the
|
||||
// cycle factoring loop benefits from the faster windowed pow2 ladder measured in CPU benchmarks.
|
||||
if (temp.PowModWithCycle(q, cycle) == one)
|
||||
{
|
||||
order = temp;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (smallPrimesPow2[i] > n)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
p = smallPrimes[i];
|
||||
// TODO: Replace this direct `%` test with the shared divisor-cycle filter once the
|
||||
// UInt128 path is wired into the cached cycle tables so wide candidates skip the slow
|
||||
// modulo checks during primality pre-filtering.
|
||||
if (n % p == zero)
|
||||
{
|
||||
return n == p;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod3(this UInt128 value)
|
||||
{
|
||||
ulong remainder = ((ulong)value) % 3UL + ((ulong)(value >> 64)) % 3UL;
|
||||
return remainder >= 3UL ? remainder - 3UL : remainder;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod5(this UInt128 value)
|
||||
{
|
||||
ulong remainder = (((ulong)value) % 5UL) + (((ulong)(value >> 64)) % 5UL);
|
||||
return remainder >= 5UL ? remainder - 5UL : remainder;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod6(this UInt128 value) => ((value.Mod3() << 1) | ((ulong)value & 1UL)) switch
|
||||
{
|
||||
0UL => 0UL,
|
||||
1UL => 3UL,
|
||||
2UL => 4UL,
|
||||
3UL => 1UL,
|
||||
4UL => 2UL,
|
||||
_ => 5UL,
|
||||
};
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod7(this UInt128 value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int CountTrailingZeros(this UInt128 value)
|
||||
{
|
||||
ulong valuePart = (ulong)value;
|
||||
if (valuePart != 0UL)
|
||||
{
|
||||
ulong low = (ulong)value % 7UL;
|
||||
ulong high = (ulong)(value >> 64) % 7UL;
|
||||
ulong remainder = low + (high * 2UL);
|
||||
if (remainder >= 7UL)
|
||||
return BitOperations.TrailingZeroCount(valuePart);
|
||||
}
|
||||
|
||||
return 64 + BitOperations.TrailingZeroCount((ulong)(value >> 64));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsPrimeCandidate(this UInt128 n)
|
||||
{
|
||||
UInt128 p, zero = UInt128.Zero;
|
||||
|
||||
uint[] smallPrimes = PrimesGenerator.SmallPrimes;
|
||||
ulong[] smallPrimesPow2 = PrimesGenerator.SmallPrimesPow2;
|
||||
ulong i, smallPrimesCount = (ulong)smallPrimes.Length;
|
||||
for (i = 0UL; i < smallPrimesCount; i++)
|
||||
{
|
||||
if (smallPrimesPow2[i] > n)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
p = smallPrimes[i];
|
||||
// TODO: Replace this direct `%` test with the shared divisor-cycle filter once the
|
||||
// UInt128 path is wired into the cached cycle tables so wide candidates skip the slow
|
||||
// modulo checks during primality pre-filtering.
|
||||
if (n % p == zero)
|
||||
{
|
||||
return n == p;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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;
|
||||
return remainder >= 3UL ? remainder - 3UL : remainder;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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);
|
||||
return remainder >= 5UL ? remainder - 5UL : remainder;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod6(this UInt128 value) => ((value.Mod3() << 1) | ((ulong)value & 1UL)) switch
|
||||
{
|
||||
0UL => 0UL,
|
||||
1UL => 3UL,
|
||||
2UL => 4UL,
|
||||
3UL => 1UL,
|
||||
4UL => 2UL,
|
||||
_ => 5UL,
|
||||
};
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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 high = (ulong)(value >> 64) % 7UL;
|
||||
ulong remainder = low + (high * 2UL);
|
||||
if (remainder >= 7UL)
|
||||
{
|
||||
remainder -= 7UL;
|
||||
if (remainder >= 7UL)
|
||||
{
|
||||
remainder -= 7UL;
|
||||
}
|
||||
}
|
||||
|
||||
return remainder;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod8(this UInt128 value) => (ulong)value & 7UL;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod10(this UInt128 value128)
|
||||
{
|
||||
// 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 high = (ulong)(value128 >> 64);
|
||||
return ((low % 10UL) + ((high % 10UL) * 6UL)) % 10UL;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Mod10_8_5_3(this UInt128 value, out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3)
|
||||
{
|
||||
byte current;
|
||||
uint byteSum = 0U;
|
||||
mod8 = (ulong)value & 7UL;
|
||||
UInt128 zero = UInt128.Zero;
|
||||
do
|
||||
{
|
||||
current = (byte)value;
|
||||
byteSum += current;
|
||||
value >>= 8;
|
||||
}
|
||||
while (value != zero);
|
||||
|
||||
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
|
||||
? mod5 switch
|
||||
{
|
||||
0U => 0UL,
|
||||
1U => 6UL,
|
||||
2U => 2UL,
|
||||
3U => 8UL,
|
||||
_ => 4UL,
|
||||
}
|
||||
: mod5 switch
|
||||
{
|
||||
0U => 5UL,
|
||||
1U => 1UL,
|
||||
2U => 7UL,
|
||||
3U => 3UL,
|
||||
_ => 9UL,
|
||||
};
|
||||
|
||||
mod3 = byteSum % 3U;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod11(this UInt128 value)
|
||||
{
|
||||
ulong remainder = (((ulong)value) % 11UL) + (((ulong)(value >> 64)) % 11UL) * 5UL;
|
||||
while (remainder >= 11UL)
|
||||
{
|
||||
remainder -= 11UL;
|
||||
}
|
||||
|
||||
return remainder;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod128(this UInt128 value) => (ulong)value & 127UL;
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 ModPow(this UInt128 value, UInt128 exponent, UInt128 modulus)
|
||||
{
|
||||
UInt128 zero = UInt128.Zero;
|
||||
UInt128 one = UInt128.One;
|
||||
UInt128 result = UInt128.One;
|
||||
value %= modulus;
|
||||
|
||||
while (exponent != zero)
|
||||
{
|
||||
if ((exponent & one) != zero)
|
||||
{
|
||||
result = MulMod(result, value, modulus);
|
||||
}
|
||||
|
||||
value = MulMod(value, value, modulus);
|
||||
exponent >>= 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 Mul64(this UInt128 a, UInt128 b)
|
||||
{
|
||||
ulong aLow = (ulong)a;
|
||||
ulong bLow = (ulong)b;
|
||||
ulong bHigh = (ulong)(b >> 64);
|
||||
|
||||
ulong low = aLow * bLow;
|
||||
|
||||
// Keep the high-word accumulation in locals so the JIT does not rebuild the
|
||||
// expression tree around the shift. Mirroring the MulHigh layout lets RyuJIT
|
||||
// keep the intermediate sum in registers instead of reloading the partial
|
||||
// product from the stack.
|
||||
ulong high = aLow.MulHigh(bLow);
|
||||
high += aLow * bHigh;
|
||||
|
||||
return ((UInt128)high << 64) | low;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 MulMod(this UInt128 a, UInt128 b, UInt128 modulus)
|
||||
{
|
||||
UInt128 one = UInt128.One,
|
||||
result = UInt128.Zero,
|
||||
zero = UInt128.Zero;
|
||||
|
||||
while (b != zero)
|
||||
{
|
||||
if ((b & one) != zero)
|
||||
{
|
||||
result += a;
|
||||
if (result >= modulus)
|
||||
{
|
||||
remainder -= 7UL;
|
||||
if (remainder >= 7UL)
|
||||
{
|
||||
remainder -= 7UL;
|
||||
}
|
||||
result -= modulus;
|
||||
}
|
||||
}
|
||||
|
||||
return remainder;
|
||||
a <<= 1;
|
||||
if (a >= modulus)
|
||||
{
|
||||
a -= modulus;
|
||||
}
|
||||
|
||||
b >>= 1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod8(this UInt128 value) => (ulong)value & 7UL;
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod10(this UInt128 value128)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 Pow(this UInt128 value, ulong exponent)
|
||||
{
|
||||
UInt128 result = UInt128.One;
|
||||
while (exponent != 0UL)
|
||||
{
|
||||
// Split and fold under mod 10: 2^64 ≡ 6 (mod 10)
|
||||
ulong low = (ulong)value128;
|
||||
ulong high = (ulong)(value128 >> 64);
|
||||
return ((low % 10UL) + ((high % 10UL) * 6UL)) % 10UL;
|
||||
if ((exponent & 1UL) != 0UL)
|
||||
{
|
||||
result *= value;
|
||||
}
|
||||
|
||||
value *= value;
|
||||
exponent >>= 1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Mod10_8_5_3(this UInt128 value, out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3)
|
||||
{
|
||||
byte current;
|
||||
uint byteSum = 0U;
|
||||
mod8 = (ulong)value & 7UL;
|
||||
UInt128 zero = UInt128.Zero;
|
||||
do
|
||||
{
|
||||
current = (byte)value;
|
||||
byteSum += current;
|
||||
value >>= 8;
|
||||
}
|
||||
while (value != zero);
|
||||
|
||||
mod5 = byteSum.Mod5();
|
||||
mod10 = (mod8 & 1UL) == 0UL
|
||||
? mod5 switch
|
||||
{
|
||||
0U => 0UL,
|
||||
1U => 6UL,
|
||||
2U => 2UL,
|
||||
3U => 8UL,
|
||||
_ => 4UL,
|
||||
}
|
||||
: mod5 switch
|
||||
{
|
||||
0U => 5UL,
|
||||
1U => 1UL,
|
||||
2U => 7UL,
|
||||
3U => 3UL,
|
||||
_ => 9UL,
|
||||
};
|
||||
|
||||
mod3 = byteSum % 3U;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod11(this UInt128 value)
|
||||
{
|
||||
ulong remainder = (((ulong)value) % 11UL) + (((ulong)(value >> 64)) % 11UL) * 5UL;
|
||||
while (remainder >= 11UL)
|
||||
{
|
||||
remainder -= 11UL;
|
||||
}
|
||||
|
||||
return remainder;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong Mod128(this UInt128 value) => (ulong)value & 127UL;
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 ModPow(this UInt128 value, UInt128 exponent, UInt128 modulus)
|
||||
{
|
||||
UInt128 zero = UInt128.Zero;
|
||||
UInt128 one = UInt128.One;
|
||||
UInt128 result = UInt128.One;
|
||||
value %= modulus;
|
||||
|
||||
while (exponent != zero)
|
||||
{
|
||||
if ((exponent & one) != zero)
|
||||
{
|
||||
result = MulMod(result, value, modulus);
|
||||
}
|
||||
|
||||
value = MulMod(value, value, modulus);
|
||||
exponent >>= 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 Mul64(this UInt128 a, UInt128 b)
|
||||
{
|
||||
ulong aLow = (ulong)a;
|
||||
ulong bLow = (ulong)b;
|
||||
ulong bHigh = (ulong)(b >> 64);
|
||||
|
||||
ulong low = aLow * bLow;
|
||||
|
||||
// Keep the high-word accumulation in locals so the JIT does not rebuild the
|
||||
// expression tree around the shift. Mirroring the MulHigh layout lets RyuJIT
|
||||
// keep the intermediate sum in registers instead of reloading the partial
|
||||
// product from the stack.
|
||||
ulong high = aLow.MulHigh(bLow);
|
||||
high += aLow * bHigh;
|
||||
|
||||
return ((UInt128)high << 64) | low;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 MulMod(this UInt128 a, UInt128 b, UInt128 modulus)
|
||||
{
|
||||
UInt128 one = UInt128.One,
|
||||
result = UInt128.Zero,
|
||||
zero = UInt128.Zero;
|
||||
|
||||
while (b != zero)
|
||||
{
|
||||
if ((b & one) != zero)
|
||||
{
|
||||
result += a;
|
||||
if (result >= modulus)
|
||||
{
|
||||
result -= modulus;
|
||||
}
|
||||
}
|
||||
|
||||
a <<= 1;
|
||||
if (a >= modulus)
|
||||
{
|
||||
a -= modulus;
|
||||
}
|
||||
|
||||
b >>= 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UInt128 Pow(this UInt128 value, ulong exponent)
|
||||
{
|
||||
UInt128 result = UInt128.One;
|
||||
while (exponent != 0UL)
|
||||
{
|
||||
if ((exponent & 1UL) != 0UL)
|
||||
{
|
||||
result *= value;
|
||||
}
|
||||
|
||||
value *= value;
|
||||
exponent >>= 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
namespace PerfectNumbers.Core
|
||||
{
|
||||
public static class UInt128Numbers
|
||||
{
|
||||
public static readonly UInt128 Two = 2UL;
|
||||
public static readonly UInt128 Four = 4UL;
|
||||
public static readonly UInt128 SixtyFour = 64UL;
|
||||
public static readonly UInt128 OneHundredTwentySeven = 127UL;
|
||||
public static class UInt128Numbers
|
||||
{
|
||||
public static readonly UInt128 Two = 2UL;
|
||||
public static readonly UInt128 Four = 4UL;
|
||||
public static readonly UInt128 SixtyFour = 64UL;
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,18 @@ namespace PerfectNumbers.Core;
|
||||
|
||||
public static class UIntExtensions
|
||||
{
|
||||
private const ulong Mod3Mask = 0xAAAAAAABUL;
|
||||
public const ulong Mod5Mask = 0xCCCCCCCDUL;
|
||||
private const ulong Mod3Mask = 0xAAAAAAABUL;
|
||||
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)]
|
||||
public static uint Mod3(uint value) => value - (uint)((value * Mod3Mask) >> 33) * 3U;
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint Mod3(uint value) => value - (uint)((value * Mod3Mask) >> 33) * 3U;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint Mod5(this uint value) => value - (uint)((value * Mod5Mask) >> 34) * 5U;
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint Mod5(this uint value) => value - (uint)((value * Mod5Mask) >> 34) * 5U;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint Mod8(this uint value) => value & 7U;
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static uint Mod8(this uint value) => value & 7U;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ public static class ULongExtensions
|
||||
int i = 0, primesLength = smallPrimes.Length;
|
||||
UInt128 q128 = q,
|
||||
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++)
|
||||
{
|
||||
@@ -235,6 +239,8 @@ public static class ULongExtensions
|
||||
public static ulong ModPow64(this ulong value, ulong exponent, ulong modulus)
|
||||
{
|
||||
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;
|
||||
|
||||
while (exponent != 0UL)
|
||||
@@ -367,6 +373,8 @@ public static class ULongExtensions
|
||||
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;
|
||||
return rotationCount.Pow2MontgomeryModFromCycleRemainder(divisor);
|
||||
}
|
||||
@@ -474,6 +482,8 @@ public static class ULongExtensions
|
||||
exponentLoopIndex = 0UL;
|
||||
// Reusing result after resetting it for the rotation-accumulation pass.
|
||||
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;
|
||||
for (; exponentLoopIndex < rotationCount; exponentLoopIndex++)
|
||||
{
|
||||
@@ -525,6 +535,8 @@ public static class ULongExtensions
|
||||
|
||||
// Reusing result after resetting it for the rotation-driven accumulation phase.
|
||||
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 rotationIndex = UInt128.Zero;
|
||||
while (rotationIndex < rotationCount)
|
||||
@@ -581,6 +593,8 @@ public static class ULongExtensions
|
||||
|
||||
// Reusing result after resetting it for the rotation-driven accumulation phase.
|
||||
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;
|
||||
|
||||
// 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:
|
||||
// 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);
|
||||
currentModulus *= modulusCandidate;
|
||||
|
||||
@@ -633,6 +649,8 @@ public static class ULongExtensions
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user