Merge pull request #29 from mlemanczyk/codex/optimize-threading-and-reduce-synchronization

Parallelize results parser pipeline
This commit is contained in:
Marek Lemańczyk authored and GitHub committed 2025-09-19 23:39:54 +02:00
commit b3633d0a5c
1 file changed
+369 -329
+369 -329
View File
@@ -1,6 +1,4 @@
using System.Collections.Concurrent;
using System.Globalization; using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Open.Numeric.Primes; using Open.Numeric.Primes;
@@ -51,7 +49,12 @@ internal static class Program
return 1; return 1;
} }
if (!TryParseRangeArguments(args.AsSpan(1), out ulong pMin, out ulong pMax)) if (!TryParseProcessingArguments(
args.AsSpan(1),
out ulong pMin,
out ulong pMax,
out int threadCount,
out int bufferSize))
{ {
return 1; return 1;
} }
@@ -62,9 +65,21 @@ internal static class Program
return 1; return 1;
} }
if (threadCount < 1)
{
Console.Error.WriteLine("The --threads value must be a positive integer.");
return 1;
}
if (bufferSize < 1)
{
Console.Error.WriteLine("The --buffer-size value must be a positive integer.");
return 1;
}
try try
{ {
ProcessFile(inputPath, pMin, pMax); ProcessFile(inputPath, pMin, pMax, threadCount, bufferSize);
return 0; return 0;
} }
catch (Exception exception) catch (Exception exception)
@@ -76,31 +91,32 @@ internal static class Program
private static bool IsHelpOption(string value) => HelpOptions.Contains(value); private static bool IsHelpOption(string value) => HelpOptions.Contains(value);
private static bool TryParseRangeArguments(ReadOnlySpan<string> arguments, out ulong pMin, out ulong pMax) private static bool TryParseProcessingArguments(
ReadOnlySpan<string> arguments,
out ulong pMin,
out ulong pMax,
out int threadCount,
out int bufferSize)
{ {
pMin = 0UL; pMin = 0UL;
pMax = ulong.MaxValue; pMax = ulong.MaxValue;
threadCount = Environment.ProcessorCount;
bufferSize = 4096;
if (arguments.Length == 0) if (arguments.Length == 0)
{ {
return true; return true;
} }
if ((arguments.Length & 1) == 1) int index = 0;
while (index < arguments.Length)
{
string argument = arguments[index];
if (!TryReadOptionValue(arguments, ref index, out string option, out string value))
{ {
Console.Error.WriteLine("Each range option must be followed by a numeric value.");
PrintHelp();
return false; return false;
} }
int index;
string option, optionAndValue, value;
for (int i = 0; i < arguments.Length; i += 2)
{
optionAndValue = arguments[i];
index = optionAndValue.IndexOf('=');
option = optionAndValue[..index];
value = optionAndValue[(index + 1)..];
try try
{ {
switch (option) switch (option)
@@ -111,6 +127,12 @@ internal static class Program
case "--p-max": case "--p-max":
pMax = ParseRangeValue(option, value); pMax = ParseRangeValue(option, value);
break; break;
case "--threads":
threadCount = ParsePositiveInt(option, value);
break;
case "--buffer-size":
bufferSize = ParsePositiveInt(option, value);
break;
default: default:
Console.Error.WriteLine($"Unsupported option '{option}'."); Console.Error.WriteLine($"Unsupported option '{option}'.");
PrintHelp(); PrintHelp();
@@ -138,20 +160,59 @@ internal static class Program
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.");
}
return parsed;
}
private static bool TryReadOptionValue(
ReadOnlySpan<string> arguments,
ref int index,
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 = current[..equalsIndex];
value = current[(equalsIndex + 1)..];
index++;
return true;
}
private static void PrintHelp() private static void PrintHelp()
{ {
Console.WriteLine("EvenPerfectBitScanner.ResultsParser"); Console.WriteLine("EvenPerfectBitScanner.ResultsParser");
Console.WriteLine("Parses CSV results produced by the candidate scanner."); Console.WriteLine("Parses CSV results produced by the candidate scanner.");
Console.WriteLine(); Console.WriteLine();
Console.WriteLine("Usage:"); Console.WriteLine("Usage:");
Console.WriteLine(" EvenPerfectBitScanner.ResultsParser <results.csv> [--p-min <value>] [--p-max <value>]"); Console.WriteLine(
" EvenPerfectBitScanner.ResultsParser <results.csv> [--p-min=<value>] [--p-max=<value>] [--threads=<value>] [--buffer-size=<value>]");
Console.WriteLine(); Console.WriteLine();
Console.WriteLine("The program accepts the following help switches:"); Console.WriteLine("The program accepts the following help switches:");
Console.WriteLine(" --?, -?, /?, -help, --help, --h, -h, /h, /help"); Console.WriteLine(" --?, -?, /?, -help, --help, --h, -h, /h, /help");
Console.WriteLine(); Console.WriteLine();
Console.WriteLine("Optional range arguments allow filtering primes by exponent:"); Console.WriteLine("Optional range arguments allow filtering primes by exponent:");
Console.WriteLine(" --p-min <value> - include entries with p >= value"); Console.WriteLine(" --p-min=<value> - include entries with p >= value");
Console.WriteLine(" --p-max <value> - include entries with p <= value"); Console.WriteLine(" --p-max=<value> - include entries with p <= value");
Console.WriteLine();
Console.WriteLine("Parallel processing options:");
Console.WriteLine(" --threads=<value> - number of worker threads used for parsing and splitting (default: logical cores)");
Console.WriteLine(" --buffer-size=<value> - number of records read into each parsing batch (default: 4096)");
Console.WriteLine("Values must be provided using the --option=value syntax.");
Console.WriteLine(); Console.WriteLine();
Console.WriteLine("Outputs prime results into four files placed next to the source file:"); Console.WriteLine("Outputs prime results into four files placed next to the source file:");
Console.WriteLine(" raw-primes-<name> - prime entries in the original order"); Console.WriteLine(" raw-primes-<name> - prime entries in the original order");
@@ -160,15 +221,14 @@ internal static class Program
Console.WriteLine(" sorted-primes-rejected-<name> - sorted prime entries with passedAllTests = false"); Console.WriteLine(" sorted-primes-rejected-<name> - sorted prime entries with passedAllTests = false");
Console.WriteLine(); Console.WriteLine();
Console.WriteLine("Example:"); Console.WriteLine("Example:");
Console.WriteLine(" EvenPerfectBitScanner.ResultsParser results.csv --p-min 89 --p-max 107"); Console.WriteLine(" EvenPerfectBitScanner.ResultsParser results.csv --p-min=89 --p-max=107");
} }
private const int CandidateBufferCapacity = 512; private static void ProcessFile(in string inputPath, ulong pMin, ulong pMax, int threadCount, int bufferSize)
private static void ProcessFile(in string inputPath, ulong pMin, ulong pMax)
{ {
using IEnumerator<string> enumerator = File.ReadLines(inputPath).GetEnumerator(); using StreamReader reader = new(inputPath);
if (!enumerator.MoveNext()) string? headerLine = reader.ReadLine();
if (headerLine is null)
{ {
Console.WriteLine("Input file did not contain any data. Created empty outputs using the default header."); Console.WriteLine("Input file did not contain any data. Created empty outputs using the default header.");
string fallbackHeader = DefaultHeader; string fallbackHeader = DefaultHeader;
@@ -178,66 +238,18 @@ internal static class Program
return; return;
} }
string headerLine = enumerator.Current;
string header = string.IsNullOrWhiteSpace(headerLine) ? DefaultHeader : headerLine.Trim(); string header = string.IsNullOrWhiteSpace(headerLine) ? DefaultHeader : headerLine.Trim();
using IEnumerator<ulong> primeEnumerator = Prime.Numbers.GetEnumerator(); List<CandidateResult> candidates = LoadCandidates(reader, pMin, pMax, threadCount, bufferSize);
if (!primeEnumerator.MoveNext())
{
throw new InvalidOperationException("Prime generator did not provide any values.");
}
ulong currentPrime = primeEnumerator.Current; List<CandidateResult> sortedPrimeResults = ExtractSortedPrimeCandidates(candidates);
PendingCandidateList pendingResults = new(); List<CandidateResult> primeResults = BuildRawPrimeResults(candidates, sortedPrimeResults);
List<CandidateResult> primeResults = [];
int consoleProgress = 0;
ulong lastReportedCandidate = 0UL;
CandidateResult[] candidateBuffer = new CandidateResult[CandidateBufferCapacity];
int bufferedCount = 0;
ulong pEmpty = CandidateResult.Empty.P;
while (enumerator.MoveNext())
{
string currentLine = enumerator.Current;
if (string.IsNullOrWhiteSpace(currentLine))
{
continue;
}
CandidateResult result = ParseLine(currentLine, pMin, pMax);
if (result.P == pEmpty)
{
continue;
}
candidateBuffer[bufferedCount++] = result;
lastReportedCandidate = result.P;
if (bufferedCount == CandidateBufferCapacity)
{
ProcessCandidateBuffer(candidateBuffer.AsSpan(0, bufferedCount), pendingResults);
DrainPrimeMatches(pendingResults, primeResults, ref currentPrime, primeEnumerator, false);
bufferedCount = 0;
}
if (++consoleProgress == 10_000)
{
Console.WriteLine($"Processed {lastReportedCandidate}");
consoleProgress = 0;
}
}
if (bufferedCount > 0)
{
ProcessCandidateBuffer(candidateBuffer.AsSpan(0, bufferedCount), pendingResults);
DrainPrimeMatches(pendingResults, primeResults, ref currentPrime, primeEnumerator, false);
}
DrainPrimeMatches(pendingResults, primeResults, ref currentPrime, primeEnumerator, true);
string rawOutputPath = BuildOutputPath(inputPath, "raw-primes-"); string rawOutputPath = BuildOutputPath(inputPath, "raw-primes-");
bool append = File.Exists(rawOutputPath); bool append = File.Exists(rawOutputPath);
WriteCsv(rawOutputPath, header, append, primeResults); WriteCsv(rawOutputPath, header, append, primeResults);
if (primeResults.Count == 0) if (sortedPrimeResults.Count == 0)
{ {
Console.WriteLine("No prime entries were found in the input file."); Console.WriteLine("No prime entries were found in the input file.");
Console.WriteLine($"Created an empty prime snapshot at: {rawOutputPath}"); Console.WriteLine($"Created an empty prime snapshot at: {rawOutputPath}");
@@ -245,12 +257,11 @@ internal static class Program
return; return;
} }
List<CandidateResult> sortedResults = [.. primeResults];
Console.WriteLine("Sorting candidates..."); Console.WriteLine("Sorting candidates...");
SortByPrime(sortedResults); SortByPrime(sortedPrimeResults);
Console.WriteLine("Splitting results..."); Console.WriteLine("Splitting results...");
SplitResults(sortedResults, out List<CandidateResult> passedResults, out List<CandidateResult> rejectedResults); SplitResults(sortedPrimeResults, threadCount, out List<CandidateResult> passedResults, out List<CandidateResult> rejectedResults);
Console.WriteLine("Saving files..."); Console.WriteLine("Saving files...");
string sortedOutputPath = BuildOutputPath(inputPath, "sorted-primes-"); string sortedOutputPath = BuildOutputPath(inputPath, "sorted-primes-");
@@ -258,13 +269,13 @@ internal static class Program
string rejectedOutputPath = BuildOutputPath(inputPath, "sorted-primes-rejected-"); string rejectedOutputPath = BuildOutputPath(inputPath, "sorted-primes-rejected-");
append = File.Exists(sortedOutputPath); append = File.Exists(sortedOutputPath);
WriteCsv(sortedOutputPath, header, append, sortedResults); WriteCsv(sortedOutputPath, header, append, sortedPrimeResults);
append = File.Exists(passedOutputPath); append = File.Exists(passedOutputPath);
WriteCsv(passedOutputPath, header, append, passedResults); WriteCsv(passedOutputPath, header, append, passedResults);
append = File.Exists(rejectedOutputPath); append = File.Exists(rejectedOutputPath);
WriteCsv(rejectedOutputPath, header, append, rejectedResults); WriteCsv(rejectedOutputPath, header, append, rejectedResults);
Console.WriteLine($"Processed {primeResults.Count} prime entr{(primeResults.Count == 1 ? "y" : "ies")}."); Console.WriteLine($"Processed {sortedPrimeResults.Count} prime entr{(sortedPrimeResults.Count == 1 ? "y" : "ies")}.");
Console.WriteLine($"Prime results (raw order): {rawOutputPath}"); Console.WriteLine($"Prime results (raw order): {rawOutputPath}");
Console.WriteLine($"Prime results (sorted): {sortedOutputPath}"); Console.WriteLine($"Prime results (sorted): {sortedOutputPath}");
Console.WriteLine($"Prime results (passed): {passedOutputPath}"); Console.WriteLine($"Prime results (passed): {passedOutputPath}");
@@ -329,36 +340,237 @@ internal static class Program
} }
} }
private static void ProcessCandidateBuffer(Span<CandidateResult> buffer, PendingCandidateList pendingResults) private static List<CandidateResult> LoadCandidates(
StreamReader reader,
ulong pMin,
ulong pMax,
int threadCount,
int bufferSize)
{ {
if (buffer.Length == 0) List<CandidateResult> candidates = [];
if (bufferSize < 1)
{
return candidates;
}
int workerLimit = Math.Max(1, threadCount);
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];
int buffered = 0;
int chunkIndex = 0;
void FlushChunk(in ChunkResult chunk)
{
if (chunk.Count == 0)
{ {
return; return;
} }
buffer.Sort(CandidateResultComparer.Instance); CandidateResult[] items = chunk.Items;
int count = chunk.Count;
int start = 0; candidates.EnsureCapacity(candidates.Count + count);
int length = buffer.Length; for (int i = 0; i < count; i++)
while (start < length)
{ {
int end = start + 1; CandidateResult candidate = items[i];
ulong previous = buffer[start].P; candidates.Add(candidate);
while (end < length) lastReportedCandidate = candidate.P;
if (++consoleProgress == 10_000)
{ {
ulong current = buffer[end].P; Console.WriteLine($"Processed {lastReportedCandidate}");
if (current > previous + 1) consoleProgress = 0;
{ }
break; }
} }
previous = current; void TryFlush()
end++; {
while (completedChunks.Remove(nextChunkToFlush, out ChunkResult ready))
{
FlushChunk(in ready);
nextChunkToFlush++;
}
} }
pendingResults.AddRange(buffer[start..end]); void WaitForAny()
start = end; {
Task<ChunkResult> completed = Task.WhenAny(activeChunks).GetAwaiter().GetResult();
activeChunks.Remove(completed);
ChunkResult chunk = completed.GetAwaiter().GetResult();
completedChunks.Add(chunk.Index, chunk);
TryFlush();
} }
void ScheduleChunk(string[] chunkLines)
{
int currentIndex = chunkIndex++;
Task<ChunkResult> task = Task.Run(() => ProcessChunk(chunkLines, currentIndex, pMin, pMax));
activeChunks.Add(task);
if (activeChunks.Count == workerLimit)
{
WaitForAny();
}
}
void DispatchBuffer(int length)
{
string[] chunkLines;
if (length == buffer.Length)
{
chunkLines = buffer;
buffer = new string[bufferSize];
}
else
{
chunkLines = new string[length];
Array.Copy(buffer, chunkLines, length);
}
buffered = 0;
ScheduleChunk(chunkLines);
}
string? line;
while ((line = reader.ReadLine()) is not null)
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
buffer[buffered++] = line;
if (buffered == bufferSize)
{
DispatchBuffer(buffered);
}
}
if (buffered > 0)
{
DispatchBuffer(buffered);
}
while (activeChunks.Count > 0)
{
WaitForAny();
}
TryFlush();
return candidates;
}
private static ChunkResult ProcessChunk(string[] lines, int index, ulong pMin, ulong pMax)
{
CandidateResult[] items = new CandidateResult[lines.Length];
int count = 0;
ulong pEmpty = CandidateResult.Empty.P;
for (int i = 0; i < lines.Length; i++)
{
string line = lines[i];
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
CandidateResult candidate = ParseLine(line, pMin, pMax);
if (candidate.P == pEmpty)
{
continue;
}
items[count++] = candidate;
}
return new ChunkResult(index, items, count);
}
private static List<CandidateResult> ExtractSortedPrimeCandidates(List<CandidateResult> candidates)
{
if (candidates.Count == 0)
{
return [];
}
List<CandidateResult> sortedCandidates = [.. candidates];
sortedCandidates.Sort(CandidateResultComparer.Instance);
List<CandidateResult> primeCandidates = [];
using IEnumerator<ulong> primeEnumerator = Prime.Numbers.GetEnumerator();
if (!primeEnumerator.MoveNext())
{
throw new InvalidOperationException("Prime generator did not provide any values.");
}
ulong currentPrime = primeEnumerator.Current;
foreach (CandidateResult candidate in sortedCandidates)
{
ulong candidateP = candidate.P;
while (currentPrime < candidateP)
{
if (!primeEnumerator.MoveNext())
{
return primeCandidates;
}
currentPrime = primeEnumerator.Current;
}
if (currentPrime == candidateP)
{
primeCandidates.Add(candidate);
}
}
return primeCandidates;
}
private static List<CandidateResult> BuildRawPrimeResults(
List<CandidateResult> candidates,
List<CandidateResult> sortedPrimeResults)
{
int primeCount = sortedPrimeResults.Count;
if (primeCount == 0)
{
return [];
}
Dictionary<ulong, int> remainingCounts = new(primeCount);
for (int i = 0; i < primeCount; i++)
{
CandidateResult candidate = sortedPrimeResults[i];
if (remainingCounts.TryGetValue(candidate.P, out int count))
{
remainingCounts[candidate.P] = count + 1;
continue;
}
remainingCounts.Add(candidate.P, 1);
}
List<CandidateResult> rawPrimes = new(primeCount);
for (int i = 0; i < candidates.Count; i++)
{
CandidateResult candidate = candidates[i];
if (!remainingCounts.TryGetValue(candidate.P, out int count))
{
continue;
}
rawPrimes.Add(candidate);
if (count == 1)
{
remainingCounts.Remove(candidate.P);
continue;
}
remainingCounts[candidate.P] = count - 1;
}
return rawPrimes;
} }
private static void SortByPrime(List<CandidateResult> results) private static void SortByPrime(List<CandidateResult> results)
@@ -373,6 +585,7 @@ internal static class Program
private static void SplitResults( private static void SplitResults(
List<CandidateResult> sortedResults, List<CandidateResult> sortedResults,
int threadCount,
out List<CandidateResult> passedResults, out List<CandidateResult> passedResults,
out List<CandidateResult> rejectedResults) out List<CandidateResult> rejectedResults)
{ {
@@ -384,7 +597,7 @@ internal static class Program
return; return;
} }
if (count < Environment.ProcessorCount * 4) if (threadCount <= 1 || count < threadCount * 4)
{ {
passedResults = new List<CandidateResult>(count); passedResults = new List<CandidateResult>(count);
rejectedResults = new List<CandidateResult>(count); rejectedResults = new List<CandidateResult>(count);
@@ -403,18 +616,34 @@ internal static class Program
return; return;
} }
List<CandidateResult> sharedPassed = new(count); int workerCount = Math.Min(threadCount, count);
List<CandidateResult> sharedRejected = new(count); List<CandidateResult>[] passedSegments = new List<CandidateResult>[workerCount];
object passedLock = new(); List<CandidateResult>[] rejectedSegments = new List<CandidateResult>[workerCount];
object rejectedLock = new(); Task[] tasks = new Task[workerCount];
Parallel.ForEach(
Partitioner.Create(0, count), int start = 0;
() => new SplitAccumulator(), int baseChunk = count / workerCount;
(range, _, local) => int remainder = count % workerCount;
for (int worker = 0; worker < workerCount; worker++)
{ {
List<CandidateResult> localPassed = local.Passed; int length = baseChunk + (worker < remainder ? 1 : 0);
List<CandidateResult> localRejected = local.Rejected; int chunkStart = start;
for (int i = range.Item1; i < range.Item2; i++) start += length;
if (length == 0)
{
passedSegments[worker] = [];
rejectedSegments[worker] = [];
tasks[worker] = Task.CompletedTask;
continue;
}
tasks[worker] = Task.Run(() =>
{
List<CandidateResult> localPassed = new(length);
List<CandidateResult> localRejected = new(length);
int end = chunkStart + length;
for (int i = chunkStart; i < end; i++)
{ {
CandidateResult candidate = sortedResults[i]; CandidateResult candidate = sortedResults[i];
if (candidate.PassedAllTests) if (candidate.PassedAllTests)
@@ -426,29 +655,27 @@ internal static class Program
localRejected.Add(candidate); localRejected.Add(candidate);
} }
return local; passedSegments[worker] = localPassed;
}, rejectedSegments[worker] = localRejected;
local =>
{
if (local.Passed.Count > 0)
{
lock (passedLock)
{
sharedPassed.AddRange(local.Passed);
}
}
if (local.Rejected.Count > 0)
{
lock (rejectedLock)
{
sharedRejected.AddRange(local.Rejected);
}
}
}); });
}
passedResults = sharedPassed; Task.WaitAll(tasks);
rejectedResults = sharedRejected;
passedResults = new List<CandidateResult>(count);
rejectedResults = new List<CandidateResult>(count);
for (int worker = 0; worker < workerCount; worker++)
{
if (passedSegments[worker].Count > 0)
{
passedResults.AddRange(passedSegments[worker]);
}
if (rejectedSegments[worker].Count > 0)
{
rejectedResults.AddRange(rejectedSegments[worker]);
}
}
} }
private sealed class CandidateResultComparer : IComparer<CandidateResult> private sealed class CandidateResultComparer : IComparer<CandidateResult>
@@ -466,204 +693,17 @@ internal static class Program
} }
} }
private static void DrainPrimeMatches( private readonly struct ChunkResult
PendingCandidateList pendingResults,
List<CandidateResult> primeResults,
ref ulong currentPrime,
IEnumerator<ulong> primeEnumerator,
bool inputCompleted)
{ {
if (pendingResults.Count == 0) public readonly int Index;
{ public readonly CandidateResult[] Items;
return; public readonly int Count;
}
pendingResults.RemoveLessThan(currentPrime); public ChunkResult(int index, CandidateResult[] items, int count)
bool extractedPrime;
while (pendingResults.Count > 0)
{ {
extractedPrime = ExtractPrimeEntries(pendingResults, primeResults, currentPrime); Index = index;
if (extractedPrime) Items = items;
{ Count = count;
if (!TryAdvancePrime(primeEnumerator, ref currentPrime, pendingResults))
{
pendingResults.Clear();
return;
}
pendingResults.RemoveLessThan(currentPrime);
continue;
}
if (!inputCompleted)
{
return;
}
if (!TryAdvancePrime(primeEnumerator, ref currentPrime, pendingResults))
{
pendingResults.Clear();
return;
}
pendingResults.RemoveLessThan(currentPrime);
}
}
private static bool ExtractPrimeEntries(
PendingCandidateList pendingResults,
List<CandidateResult> primeResults,
ulong prime)
{
if (pendingResults.Count == 0)
{
return false;
}
if (!pendingResults.TryPeek(out CandidateResult next) || next.P != prime)
{
return false;
}
do
{
primeResults.Add(pendingResults.Dequeue());
}
while (pendingResults.TryPeek(out CandidateResult candidate) && candidate.P == prime);
return true;
}
private static bool TryAdvancePrime(
IEnumerator<ulong> primeEnumerator,
ref ulong currentPrime,
PendingCandidateList pendingResults)
{
ulong minimumPending = pendingResults.Count > 0 && pendingResults.TryPeek(out CandidateResult pending)
? pending.P
: 0UL;
do
{
if (!primeEnumerator.MoveNext())
{
currentPrime = ulong.MaxValue;
return false;
}
currentPrime = primeEnumerator.Current;
}
while (pendingResults.Count > 0 && currentPrime < minimumPending);
return true;
}
private sealed class PendingCandidateList
{
private readonly List<CandidateResult> _items = [];
public int Count => _items.Count;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddRange(ReadOnlySpan<CandidateResult> candidates)
{
if (candidates.Length == 0)
{
return;
}
List<CandidateResult> items = _items;
items.EnsureCapacity(items.Count + candidates.Length);
int insertIndex = FindInsertIndex(items, candidates[0].P);
int candidateIndex = 0;
int candidateCount = candidates.Length;
while (candidateIndex < candidateCount)
{
CandidateResult candidate = candidates[candidateIndex];
while (insertIndex < items.Count && items[insertIndex].P <= candidate.P)
{
insertIndex++;
}
items.Insert(insertIndex, candidate);
insertIndex++;
candidateIndex++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryPeek(out CandidateResult result)
{
if (_items.Count == 0)
{
result = default;
return false;
}
result = _items[0];
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CandidateResult Dequeue()
{
CandidateResult value = _items[0];
_items.RemoveAt(0);
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveLessThan(ulong prime)
{
int removeCount = 0;
List<CandidateResult> items = _items;
int count = items.Count;
while (removeCount < count && items[removeCount].P < prime)
{
removeCount++;
}
if (removeCount > 0)
{
items.RemoveRange(0, removeCount);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Clear()
{
_items.Clear();
}
private static int FindInsertIndex(List<CandidateResult> items, ulong value)
{
int low = 0;
int high = items.Count;
while (low < high)
{
int mid = (low + high) >> 1;
if (items[mid].P < value)
{
low = mid + 1;
continue;
}
high = mid;
}
return low;
}
}
private readonly struct SplitAccumulator
{
public readonly List<CandidateResult> Passed;
public readonly List<CandidateResult> Rejected;
public SplitAccumulator()
{
Passed = [];
Rejected = [];
} }
} }