From 18b65052815c8f700043ae9b8c86d454bc698f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Fri, 19 Sep 2025 23:37:20 +0200 Subject: [PATCH 1/2] Parallelize results parser pipeline --- .../Program.cs | 923 ++++++++++-------- 1 file changed, 489 insertions(+), 434 deletions(-) diff --git a/EvenPerfectBitScanner.ResultsParser/Program.cs b/EvenPerfectBitScanner.ResultsParser/Program.cs index 9615064..12e9205 100644 --- a/EvenPerfectBitScanner.ResultsParser/Program.cs +++ b/EvenPerfectBitScanner.ResultsParser/Program.cs @@ -1,6 +1,4 @@ -using System.Collections.Concurrent; using System.Globalization; -using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using Open.Numeric.Primes; @@ -24,11 +22,11 @@ internal static class Program private const string DefaultHeader = "p,searchedMersenne,detailedCheck,passedAllTests"; - private static int Main(string[] args) - { - if (args.Length == 0) - { - PrintHelp(); + private static int Main(string[] args) + { + if (args.Length == 0) + { + PrintHelp(); return 1; } @@ -51,92 +49,164 @@ internal static class Program return 1; } - if (!TryParseRangeArguments(args.AsSpan(1), out ulong pMin, out ulong pMax)) - { - return 1; - } + if (!TryParseProcessingArguments( + args.AsSpan(1), + out ulong pMin, + out ulong pMax, + out int threadCount, + out int bufferSize)) + { + return 1; + } - if (pMin > pMax) - { - Console.Error.WriteLine("The --p-min value cannot be greater than --p-max."); - return 1; - } + if (pMin > pMax) + { + Console.Error.WriteLine("The --p-min value cannot be greater than --p-max."); + return 1; + } - try - { - ProcessFile(inputPath, pMin, pMax); - return 0; - } - catch (Exception exception) - { - Console.Error.WriteLine($"Processing failed: {exception.Message}"); + 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 + { + ProcessFile(inputPath, pMin, pMax, threadCount, bufferSize); + return 0; + } + catch (Exception exception) + { + Console.Error.WriteLine($"Processing failed: {exception.Message}"); return 1; } } private static bool IsHelpOption(string value) => HelpOptions.Contains(value); - private static bool TryParseRangeArguments(ReadOnlySpan arguments, out ulong pMin, out ulong pMax) - { - pMin = 0UL; - pMax = ulong.MaxValue; - if (arguments.Length == 0) - { - return true; - } + private static bool TryParseProcessingArguments( + ReadOnlySpan arguments, + out ulong pMin, + out ulong pMax, + out int threadCount, + out int bufferSize) + { + pMin = 0UL; + pMax = ulong.MaxValue; + threadCount = Environment.ProcessorCount; + bufferSize = 4096; - if ((arguments.Length & 1) == 1) - { - Console.Error.WriteLine("Each range option must be followed by a numeric value."); - PrintHelp(); - return false; - } + if (arguments.Length == 0) + { + return true; + } - 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)..]; + int index = 0; + while (index < arguments.Length) + { + string argument = arguments[index]; + if (!TryReadOptionValue(arguments, ref index, out string option, out string value)) + { + return false; + } - try - { - switch (option) - { - case "--p-min": - pMin = ParseRangeValue(option, value); - break; - case "--p-max": - pMax = ParseRangeValue(option, value); - break; - default: - Console.Error.WriteLine($"Unsupported option '{option}'."); - PrintHelp(); - return false; - } - } - catch (FormatException exception) - { - Console.Error.WriteLine(exception.Message); - PrintHelp(); - return false; - } - } + try + { + switch (option) + { + case "--p-min": + pMin = ParseRangeValue(option, value); + break; + case "--p-max": + pMax = ParseRangeValue(option, value); + break; + case "--threads": + threadCount = ParsePositiveInt(option, value); + break; + case "--buffer-size": + bufferSize = ParsePositiveInt(option, value); + break; + default: + Console.Error.WriteLine($"Unsupported option '{option}'."); + PrintHelp(); + return false; + } + } + catch (FormatException exception) + { + Console.Error.WriteLine(exception.Message); + PrintHelp(); + return false; + } + } - return true; - } + 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) + { + 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."); + } + + return parsed; + } + + private static bool TryReadOptionValue( + ReadOnlySpan 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) + { + option = current[..equalsIndex]; + value = current[(equalsIndex + 1)..]; + index++; + if (value.Length == 0) + { + Console.Error.WriteLine($"Option '{option}' must specify a value."); + PrintHelp(); + return false; + } + + return true; + } + + option = current; + if (index + 1 >= arguments.Length) + { + Console.Error.WriteLine($"Option '{option}' must be followed by a value."); + PrintHelp(); + return false; + } + + value = arguments[index + 1]; + index += 2; + return true; + } private static void PrintHelp() { @@ -144,16 +214,22 @@ internal static class Program Console.WriteLine("Parses CSV results produced by the candidate scanner."); Console.WriteLine(); Console.WriteLine("Usage:"); - Console.WriteLine(" EvenPerfectBitScanner.ResultsParser [--p-min ] [--p-max ]"); - Console.WriteLine(); - Console.WriteLine("The program accepts the following help switches:"); - Console.WriteLine(" --?, -?, /?, -help, --help, --h, -h, /h, /help"); - Console.WriteLine(); - Console.WriteLine("Optional range arguments allow filtering primes by exponent:"); - Console.WriteLine(" --p-min - include entries with p >= value"); - Console.WriteLine(" --p-max - include entries with p <= value"); - Console.WriteLine(); - Console.WriteLine("Outputs prime results into four files placed next to the source file:"); + Console.WriteLine( + " EvenPerfectBitScanner.ResultsParser [--p-min ] [--p-max ] [--threads ] [--buffer-size ]"); + Console.WriteLine(); + Console.WriteLine("The program accepts the following help switches:"); + Console.WriteLine(" --?, -?, /?, -help, --help, --h, -h, /h, /help"); + Console.WriteLine(); + Console.WriteLine("Optional range arguments allow filtering primes by exponent:"); + Console.WriteLine(" --p-min - include entries with p >= value"); + Console.WriteLine(" --p-max - include entries with p <= value"); + Console.WriteLine(); + Console.WriteLine("Parallel processing options:"); + Console.WriteLine(" --threads - number of worker threads used for parsing and splitting (default: logical cores)"); + Console.WriteLine(" --buffer-size - number of records read into each parsing batch (default: 4096)"); + Console.WriteLine("Values can also be provided using the --option=value syntax."); + Console.WriteLine(); + Console.WriteLine("Outputs prime results into four files placed next to the source file:"); Console.WriteLine(" raw-primes- - prime entries in the original order"); Console.WriteLine(" sorted-primes- - prime entries sorted by p"); Console.WriteLine(" sorted-primes-passed- - sorted prime entries with passedAllTests = true"); @@ -163,113 +239,63 @@ internal static class Program 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) + private static void ProcessFile(in string inputPath, ulong pMin, ulong pMax, int threadCount, int bufferSize) { - using IEnumerator enumerator = File.ReadLines(inputPath).GetEnumerator(); - if (!enumerator.MoveNext()) - { - Console.WriteLine("Input file did not contain any data. Created empty outputs using the default header."); - string fallbackHeader = DefaultHeader; - string rawFallbackPath = BuildOutputPath(inputPath, "raw-primes-"); - WriteCsv(rawFallbackPath, fallbackHeader, File.Exists(rawFallbackPath), []); - CreateEmptyOutputs(inputPath, fallbackHeader); - return; - } - - string headerLine = enumerator.Current; - string header = string.IsNullOrWhiteSpace(headerLine) ? DefaultHeader : headerLine.Trim(); - - using IEnumerator primeEnumerator = Prime.Numbers.GetEnumerator(); - if (!primeEnumerator.MoveNext()) - { - throw new InvalidOperationException("Prime generator did not provide any values."); - } - - ulong currentPrime = primeEnumerator.Current; - PendingCandidateList pendingResults = new(); - List primeResults = []; - int consoleProgress = 0; - ulong lastReportedCandidate = 0UL; - CandidateResult[] candidateBuffer = new CandidateResult[CandidateBufferCapacity]; - int bufferedCount = 0; - ulong pEmpty = CandidateResult.Empty.P; - while (enumerator.MoveNext()) + using StreamReader reader = new(inputPath); + string? headerLine = reader.ReadLine(); + if (headerLine is null) { - 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; - } + Console.WriteLine("Input file did not contain any data. Created empty outputs using the default header."); + string fallbackHeader = DefaultHeader; + string rawFallbackPath = BuildOutputPath(inputPath, "raw-primes-"); + WriteCsv(rawFallbackPath, fallbackHeader, File.Exists(rawFallbackPath), []); + CreateEmptyOutputs(inputPath, fallbackHeader); + return; } - if (bufferedCount > 0) + string header = string.IsNullOrWhiteSpace(headerLine) ? DefaultHeader : headerLine.Trim(); + + List candidates = LoadCandidates(reader, pMin, pMax, threadCount, bufferSize); + + List sortedPrimeResults = ExtractSortedPrimeCandidates(candidates); + List primeResults = BuildRawPrimeResults(candidates, sortedPrimeResults); + + string rawOutputPath = BuildOutputPath(inputPath, "raw-primes-"); + bool append = File.Exists(rawOutputPath); + WriteCsv(rawOutputPath, header, append, primeResults); + + if (sortedPrimeResults.Count == 0) { - ProcessCandidateBuffer(candidateBuffer.AsSpan(0, bufferedCount), pendingResults); - DrainPrimeMatches(pendingResults, primeResults, ref currentPrime, primeEnumerator, false); + Console.WriteLine("No prime entries were found in the input file."); + Console.WriteLine($"Created an empty prime snapshot at: {rawOutputPath}"); + CreateEmptyOutputs(inputPath, header); + return; } - DrainPrimeMatches(pendingResults, primeResults, ref currentPrime, primeEnumerator, true); - - string rawOutputPath = BuildOutputPath(inputPath, "raw-primes-"); - bool append = File.Exists(rawOutputPath); - WriteCsv(rawOutputPath, header, append, primeResults); - - if (primeResults.Count == 0) - { - Console.WriteLine("No prime entries were found in the input file."); - Console.WriteLine($"Created an empty prime snapshot at: {rawOutputPath}"); - CreateEmptyOutputs(inputPath, header); - return; - } - - List sortedResults = [.. primeResults]; Console.WriteLine("Sorting candidates..."); - SortByPrime(sortedResults); + SortByPrime(sortedPrimeResults); Console.WriteLine("Splitting results..."); - SplitResults(sortedResults, out List passedResults, out List rejectedResults); + SplitResults(sortedPrimeResults, threadCount, out List passedResults, out List rejectedResults); - Console.WriteLine("Saving files..."); - string sortedOutputPath = BuildOutputPath(inputPath, "sorted-primes-"); - string passedOutputPath = BuildOutputPath(inputPath, "sorted-primes-passed-"); - string rejectedOutputPath = BuildOutputPath(inputPath, "sorted-primes-rejected-"); + Console.WriteLine("Saving files..."); + string sortedOutputPath = BuildOutputPath(inputPath, "sorted-primes-"); + string passedOutputPath = BuildOutputPath(inputPath, "sorted-primes-passed-"); + string rejectedOutputPath = BuildOutputPath(inputPath, "sorted-primes-rejected-"); - append = File.Exists(sortedOutputPath); - WriteCsv(sortedOutputPath, header, append, sortedResults); - append = File.Exists(passedOutputPath); - WriteCsv(passedOutputPath, header, append, passedResults); - append = File.Exists(rejectedOutputPath); - WriteCsv(rejectedOutputPath, header, append, rejectedResults); + append = File.Exists(sortedOutputPath); + WriteCsv(sortedOutputPath, header, append, sortedPrimeResults); + append = File.Exists(passedOutputPath); + WriteCsv(passedOutputPath, header, append, passedResults); + append = File.Exists(rejectedOutputPath); + WriteCsv(rejectedOutputPath, header, append, rejectedResults); - Console.WriteLine($"Processed {primeResults.Count} prime entr{(primeResults.Count == 1 ? "y" : "ies")}."); - Console.WriteLine($"Prime results (raw order): {rawOutputPath}"); - Console.WriteLine($"Prime results (sorted): {sortedOutputPath}"); - Console.WriteLine($"Prime results (passed): {passedOutputPath}"); - Console.WriteLine($"Prime results (rejected): {rejectedOutputPath}"); - } + Console.WriteLine($"Processed {sortedPrimeResults.Count} prime entr{(sortedPrimeResults.Count == 1 ? "y" : "ies")}."); + Console.WriteLine($"Prime results (raw order): {rawOutputPath}"); + Console.WriteLine($"Prime results (sorted): {sortedOutputPath}"); + Console.WriteLine($"Prime results (passed): {passedOutputPath}"); + Console.WriteLine($"Prime results (rejected): {rejectedOutputPath}"); + } private static void CreateEmptyOutputs(in string inputPath, in string header) { @@ -329,36 +355,237 @@ internal static class Program } } - private static void ProcessCandidateBuffer(Span buffer, PendingCandidateList pendingResults) + private static List LoadCandidates( + StreamReader reader, + ulong pMin, + ulong pMax, + int threadCount, + int bufferSize) { - if (buffer.Length == 0) + List candidates = []; + if (bufferSize < 1) { - return; + return candidates; } - buffer.Sort(CandidateResultComparer.Instance); + int workerLimit = Math.Max(1, threadCount); + List> activeChunks = new(workerLimit); + Dictionary completedChunks = new(); + int nextChunkToFlush = 0; + int consoleProgress = 0; + ulong lastReportedCandidate = 0UL; - int start = 0; - int length = buffer.Length; - while (start < length) + string[] buffer = new string[bufferSize]; + int buffered = 0; + int chunkIndex = 0; + + void FlushChunk(in ChunkResult chunk) { - int end = start + 1; - ulong previous = buffer[start].P; - while (end < length) + if (chunk.Count == 0) { - ulong current = buffer[end].P; - if (current > previous + 1) - { - break; - } - - previous = current; - end++; + return; } - pendingResults.AddRange(buffer[start..end]); - start = end; + CandidateResult[] items = chunk.Items; + int count = chunk.Count; + candidates.EnsureCapacity(candidates.Count + count); + for (int i = 0; i < count; i++) + { + CandidateResult candidate = items[i]; + candidates.Add(candidate); + lastReportedCandidate = candidate.P; + if (++consoleProgress == 10_000) + { + Console.WriteLine($"Processed {lastReportedCandidate}"); + consoleProgress = 0; + } + } } + + void TryFlush() + { + while (completedChunks.Remove(nextChunkToFlush, out ChunkResult ready)) + { + FlushChunk(in ready); + nextChunkToFlush++; + } + } + + void WaitForAny() + { + Task 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 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 ExtractSortedPrimeCandidates(List candidates) + { + if (candidates.Count == 0) + { + return []; + } + + List sortedCandidates = [.. candidates]; + sortedCandidates.Sort(CandidateResultComparer.Instance); + + List primeCandidates = []; + using IEnumerator 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 BuildRawPrimeResults( + List candidates, + List sortedPrimeResults) + { + int primeCount = sortedPrimeResults.Count; + if (primeCount == 0) + { + return []; + } + + Dictionary 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 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 results) @@ -373,6 +600,7 @@ internal static class Program private static void SplitResults( List sortedResults, + int threadCount, out List passedResults, out List rejectedResults) { @@ -384,7 +612,7 @@ internal static class Program return; } - if (count < Environment.ProcessorCount * 4) + if (threadCount <= 1 || count < threadCount * 4) { passedResults = new List(count); rejectedResults = new List(count); @@ -403,18 +631,34 @@ internal static class Program return; } - List sharedPassed = new(count); - List sharedRejected = new(count); - object passedLock = new(); - object rejectedLock = new(); - Parallel.ForEach( - Partitioner.Create(0, count), - () => new SplitAccumulator(), - (range, _, local) => + int workerCount = Math.Min(threadCount, count); + List[] passedSegments = new List[workerCount]; + List[] rejectedSegments = new List[workerCount]; + Task[] tasks = new Task[workerCount]; + + int start = 0; + int baseChunk = count / workerCount; + int remainder = count % workerCount; + for (int worker = 0; worker < workerCount; worker++) + { + int length = baseChunk + (worker < remainder ? 1 : 0); + int chunkStart = start; + start += length; + + if (length == 0) { - List localPassed = local.Passed; - List localRejected = local.Rejected; - for (int i = range.Item1; i < range.Item2; i++) + passedSegments[worker] = []; + rejectedSegments[worker] = []; + tasks[worker] = Task.CompletedTask; + continue; + } + + tasks[worker] = Task.Run(() => + { + List localPassed = new(length); + List localRejected = new(length); + int end = chunkStart + length; + for (int i = chunkStart; i < end; i++) { CandidateResult candidate = sortedResults[i]; if (candidate.PassedAllTests) @@ -426,29 +670,27 @@ internal static class Program localRejected.Add(candidate); } - return local; - }, - local => - { - if (local.Passed.Count > 0) - { - lock (passedLock) - { - sharedPassed.AddRange(local.Passed); - } - } - - if (local.Rejected.Count > 0) - { - lock (rejectedLock) - { - sharedRejected.AddRange(local.Rejected); - } - } + passedSegments[worker] = localPassed; + rejectedSegments[worker] = localRejected; }); + } - passedResults = sharedPassed; - rejectedResults = sharedRejected; + Task.WaitAll(tasks); + + passedResults = new List(count); + rejectedResults = new List(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 @@ -466,212 +708,25 @@ internal static class Program } } - private static void DrainPrimeMatches( - PendingCandidateList pendingResults, - List primeResults, - ref ulong currentPrime, - IEnumerator primeEnumerator, - bool inputCompleted) + private readonly struct ChunkResult { - if (pendingResults.Count == 0) + public readonly int Index; + public readonly CandidateResult[] Items; + public readonly int Count; + + public ChunkResult(int index, CandidateResult[] items, int count) { - return; - } - - pendingResults.RemoveLessThan(currentPrime); - - bool extractedPrime; - while (pendingResults.Count > 0) - { - extractedPrime = ExtractPrimeEntries(pendingResults, primeResults, currentPrime); - if (extractedPrime) - { - 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 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 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 _items = []; - - public int Count => _items.Count; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddRange(ReadOnlySpan candidates) - { - if (candidates.Length == 0) - { - return; - } - - List 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 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 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 Passed; - public readonly List Rejected; - - public SplitAccumulator() - { - Passed = []; - Rejected = []; + Index = index; + Items = items; + Count = count; } } private readonly record struct CandidateResult(ulong P, bool PassedAllTests, string Csv) { public static readonly CandidateResult Empty = new(0UL, false, ""); - - public readonly string Csv = Csv; + + public readonly string Csv = Csv; public readonly ulong P = P; public readonly bool PassedAllTests = PassedAllTests; } From 7fbc3d3b497115b4a3c86b7944f5192ffd6733df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Fri, 19 Sep 2025 23:39:30 +0200 Subject: [PATCH 2/2] Enforce equals syntax for parser options --- .../Program.cs | 39 ++++++------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/EvenPerfectBitScanner.ResultsParser/Program.cs b/EvenPerfectBitScanner.ResultsParser/Program.cs index 12e9205..28ba604 100644 --- a/EvenPerfectBitScanner.ResultsParser/Program.cs +++ b/EvenPerfectBitScanner.ResultsParser/Program.cs @@ -180,31 +180,16 @@ internal static class Program value = string.Empty; string current = arguments[index]; int equalsIndex = current.IndexOf('='); - if (equalsIndex > 0) + if (equalsIndex <= 0 || equalsIndex == current.Length - 1) { - option = current[..equalsIndex]; - value = current[(equalsIndex + 1)..]; - index++; - if (value.Length == 0) - { - Console.Error.WriteLine($"Option '{option}' must specify a value."); - PrintHelp(); - return false; - } - - return true; - } - - option = current; - if (index + 1 >= arguments.Length) - { - Console.Error.WriteLine($"Option '{option}' must be followed by a value."); + Console.Error.WriteLine($"Option '{current}' must use the --name=value syntax."); PrintHelp(); return false; } - value = arguments[index + 1]; - index += 2; + option = current[..equalsIndex]; + value = current[(equalsIndex + 1)..]; + index++; return true; } @@ -215,19 +200,19 @@ internal static class Program Console.WriteLine(); Console.WriteLine("Usage:"); Console.WriteLine( - " EvenPerfectBitScanner.ResultsParser [--p-min ] [--p-max ] [--threads ] [--buffer-size ]"); + " EvenPerfectBitScanner.ResultsParser [--p-min=] [--p-max=] [--threads=] [--buffer-size=]"); Console.WriteLine(); Console.WriteLine("The program accepts the following help switches:"); Console.WriteLine(" --?, -?, /?, -help, --help, --h, -h, /h, /help"); Console.WriteLine(); Console.WriteLine("Optional range arguments allow filtering primes by exponent:"); - Console.WriteLine(" --p-min - include entries with p >= value"); - Console.WriteLine(" --p-max - include entries with p <= value"); + Console.WriteLine(" --p-min= - include entries with p >= value"); + Console.WriteLine(" --p-max= - include entries with p <= value"); Console.WriteLine(); Console.WriteLine("Parallel processing options:"); - Console.WriteLine(" --threads - number of worker threads used for parsing and splitting (default: logical cores)"); - Console.WriteLine(" --buffer-size - number of records read into each parsing batch (default: 4096)"); - Console.WriteLine("Values can also be provided using the --option=value syntax."); + Console.WriteLine(" --threads= - number of worker threads used for parsing and splitting (default: logical cores)"); + Console.WriteLine(" --buffer-size= - 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("Outputs prime results into four files placed next to the source file:"); Console.WriteLine(" raw-primes- - prime entries in the original order"); @@ -236,7 +221,7 @@ internal static class Program Console.WriteLine(" sorted-primes-rejected- - sorted prime entries with passedAllTests = false"); Console.WriteLine(); 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 static void ProcessFile(in string inputPath, ulong pMin, ulong pMax, int threadCount, int bufferSize)