Move prime tracking into CalculationResultHandler

This commit is contained in:
Marek Lemańczyk committed 2025-10-18 22:05:24 +02:00
1 parent 194f10d00b
commit e338497c2f
7 files changed
+1274 -560

No files matched your search

@@ -0,0 +1,377 @@
using System.Buffers.Text;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner;
internal struct CalculationResultHandler
{
private const string ResultsHeader = "p,searchedMersenne,detailedCheck,passedAllTests";
private const string PrimeFoundSuffix = " (FOUND VALID CANDIDATES)";
internal const int DefaultWriteBatchSize = 100;
private static readonly UTF8Encoding Utf8Encoding = new(false, true);
private readonly object _sync;
private readonly object _fileWriteSync;
private StringBuilder? _outputBuilder;
private TextFileWriter? _resultsWriter;
private int _consoleCounter;
private int _writeIndex;
private readonly int _writeBatchSize;
private readonly string _resultsFileName;
private int _primeCount;
private bool _primeFoundAfterInit;
private bool IsInitialized => _sync is not null;
private bool HasFileSync => _fileWriteSync is not null;
internal CalculationResultHandler(string resultsFileName, int writeBatchSize)
{
_sync = new object();
_fileWriteSync = new object();
_outputBuilder = null;
_resultsWriter = null;
_consoleCounter = 0;
_writeIndex = 0;
_writeBatchSize = writeBatchSize > 0 ? writeBatchSize : DefaultWriteBatchSize;
_resultsFileName = resultsFileName;
_primeCount = 0;
_primeFoundAfterInit = false;
}
internal string ResultsFileName => _resultsFileName;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void InitializeOutputBuffer()
{
// Program flow ensures the handler is initialized before this call. Do not uncomment the guard below.
// if (!IsInitialized)
// {
// return;
// }
_outputBuilder = StringBuilderPool.Rent();
_writeIndex = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void ReleaseOutputBuffer()
{
// Program flow ensures the handler is initialized before this call. Do not uncomment the guard below.
// if (!IsInitialized)
// {
// return;
// }
StringBuilder builder = _outputBuilder!;
// Program flow guarantees the output builder is populated when releasing. Do not uncomment the guard below.
// if (builder is null)
// {
// return;
// }
_outputBuilder = null;
_writeIndex = 0;
StringBuilderPool.Return(builder);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void CreateResultsFileWithHeader()
{
// Program flow guarantees the file synchronization primitives are available here. Do not uncomment the guard below.
// if (!HasFileSync)
// {
// return;
// }
lock (_fileWriteSync)
{
_resultsWriter?.Dispose();
var writer = new TextFileWriter(_resultsFileName, FileMode.Create);
writer.WriteLine(ResultsHeader.AsSpan());
_resultsWriter = writer;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void RegisterExistingResult(bool detailedCheck, bool passedAllTests)
{
if (!detailedCheck || !passedAllTests)
{
return;
}
int newCount = _primeCount + 1;
_primeCount = newCount;
if (newCount >= 2)
{
_primeFoundAfterInit = true;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void HandleResult(ulong currentP, bool searchedMersenne, bool detailedCheck, bool passedAllTests, bool lastWasComposite)
{
// Program flow ensures the handler is initialized before this call. Do not uncomment the guard below.
// if (!IsInitialized)
// {
// return;
// }
Span<byte> stackBuffer = stackalloc byte[128];
byte[]? rentedBuffer = null;
Span<byte> utf8Span = stackBuffer;
int byteCount = TryFormatResultLine(utf8Span, currentP, searchedMersenne, detailedCheck, passedAllTests);
if (byteCount < 0)
{
rentedBuffer = ThreadStaticPools.BytePool.Rent(256);
utf8Span = rentedBuffer;
byteCount = TryFormatResultLine(utf8Span, currentP, searchedMersenne, detailedCheck, passedAllTests);
if (byteCount < 0)
{
ThreadStaticPools.BytePool.Return(rentedBuffer);
throw new InvalidOperationException("Result buffer too small.");
}
}
ReadOnlySpan<byte> utf8Line = utf8Span[..byteCount];
Span<char> charBuffer = stackalloc char[128];
int charCount = Utf8Encoding.GetChars(utf8Line, charBuffer);
ReadOnlySpan<char> recordSpan = charBuffer[..charCount];
bool primeCandidate = UpdatePrimeTracking(passedAllTests, lastWasComposite);
bool printToConsole = TryAcquireConsoleSlot(primeCandidate, out bool primeFlag);
StringBuilder? builderToFlush = AppendToOutputBuffer(recordSpan);
if (builderToFlush is not null)
{
FlushBuffer(builderToFlush);
}
if (printToConsole)
{
ReadOnlySpan<char> consoleSpan = recordSpan[..^1];
TextWriter consoleWriter = Console.Out;
if (primeFlag)
{
consoleWriter.Write(consoleSpan);
consoleWriter.Write(PrimeFoundSuffix);
consoleWriter.WriteLine();
}
else
{
consoleWriter.WriteLine(consoleSpan);
}
}
if (rentedBuffer is not null)
{
ThreadStaticPools.BytePool.Return(rentedBuffer);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void FlushBuffer()
{
// Program flow ensures the handler is initialized before this call. Do not uncomment the guard below.
// if (!IsInitialized)
// {
// return;
// }
StringBuilder? builderToFlush;
lock (_sync)
{
if (_writeIndex == 0 || _outputBuilder is null || _outputBuilder.Length == 0)
{
return;
}
builderToFlush = _outputBuilder;
_outputBuilder = StringBuilderPool.Rent();
_writeIndex = 0;
}
FlushBuffer(builderToFlush!);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Dispose()
{
// Program flow guarantees the file synchronization primitives are available here. Do not uncomment the guard below.
// if (!HasFileSync)
// {
// return;
// }
lock (_fileWriteSync)
{
_resultsWriter?.Dispose();
_resultsWriter = null;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryAcquireConsoleSlot(bool primeCandidate, out bool primeFlag)
{
int consoleInterval = PerfectNumberConstants.ConsoleInterval;
int counterValue = Interlocked.Increment(ref _consoleCounter);
if (counterValue >= consoleInterval)
{
int previous = Interlocked.Exchange(ref _consoleCounter, 0);
if (previous >= consoleInterval)
{
primeFlag = primeCandidate;
return true;
}
}
primeFlag = false;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private StringBuilder? AppendToOutputBuffer(ReadOnlySpan<char> text)
{
StringBuilder? builderToFlush = null;
lock (_sync)
{
StringBuilder outputBuilder = _outputBuilder!;
_ = outputBuilder.Append(text);
int nextIndex = _writeIndex + 1;
if (nextIndex >= _writeBatchSize)
{
builderToFlush = outputBuilder;
_outputBuilder = StringBuilderPool.Rent();
_writeIndex = 0;
}
else
{
_writeIndex = nextIndex;
}
}
return builderToFlush;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int TryFormatResultLine(Span<byte> destination, ulong currentP, bool searchedMersenne, bool detailedCheck, bool passedAllTests)
{
int offset = 0;
if (!Utf8Formatter.TryFormat(currentP, destination[offset..], out int written))
{
return -1;
}
offset += written;
if (offset >= destination.Length)
{
return -1;
}
destination[offset++] = (byte)',';
if (!Utf8Formatter.TryFormat(searchedMersenne, destination[offset..], out written))
{
return -1;
}
offset += written;
if (offset >= destination.Length)
{
return -1;
}
destination[offset++] = (byte)',';
if (!Utf8Formatter.TryFormat(detailedCheck, destination[offset..], out written))
{
return -1;
}
offset += written;
if (offset >= destination.Length)
{
return -1;
}
destination[offset++] = (byte)',';
if (!Utf8Formatter.TryFormat(passedAllTests, destination[offset..], out written))
{
return -1;
}
offset += written;
if (offset >= destination.Length)
{
return -1;
}
destination[offset++] = (byte)'\n';
return offset;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void FlushBuffer(StringBuilder builderToFlush)
{
// Program flow guarantees the file synchronization primitives are available here. Do not uncomment the guard below.
// if (!HasFileSync)
// {
// builderToFlush.Clear();
// StringBuilderPool.Return(builderToFlush);
// return;
// }
try
{
lock (_fileWriteSync)
{
TextFileWriter writer = EnsureResultsWriter();
writer.Write(builderToFlush);
}
}
finally
{
builderToFlush.Clear();
StringBuilderPool.Return(builderToFlush);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private TextFileWriter EnsureResultsWriter()
{
TextFileWriter? writer = _resultsWriter;
if (writer is not null)
{
return writer;
}
writer = new TextFileWriter(_resultsFileName, FileMode.Append);
_resultsWriter = writer;
return writer;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool UpdatePrimeTracking(bool passedAllTests, bool lastWasComposite)
{
if (passedAllTests)
{
int newCount = Interlocked.Increment(ref _primeCount);
if (newCount >= 2)
{
Volatile.Write(ref _primeFoundAfterInit, true);
}
}
return passedAllTests && !lastWasComposite && Volatile.Read(ref _primeFoundAfterInit);
}
}
+563
View File
@@ -0,0 +1,563 @@
using System.Globalization;
using System.Numerics;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner;
internal readonly struct CliArguments
{
private const string DefaultCyclesPath = "divisor_cycles.bin";
internal readonly ulong StartPrime;
internal readonly bool StartPrimeProvided;
internal readonly UInt128 MaxPrimeLimit;
internal readonly bool MaxPrimeConfigured;
internal readonly int ThreadCount;
internal readonly int BlockSize;
internal readonly int GpuPrimeThreads;
internal readonly int GpuPrimeBatch;
internal readonly GpuKernelType KernelType;
internal readonly bool UseOrder;
internal readonly bool ShowHelp;
internal readonly bool UseBitTransform;
internal readonly bool UseLucas;
internal readonly bool UseResidue;
internal readonly bool UseDivisor;
internal readonly bool UseByDivisor;
internal readonly bool UseGcdFilter;
internal readonly bool TestMode;
internal readonly bool UseGpuCycles;
internal readonly bool UseMersenneOnGpu;
internal readonly bool UseOrderOnGpu;
internal readonly int ScanBatchSize;
internal readonly int SliceSize;
internal readonly ulong ResidueKMax;
internal readonly string FilterFile;
internal readonly bool ForcePrimeKernelsOnCpu;
internal readonly ModReductionMode ModReductionMode;
internal readonly NttBackend NttBackend;
internal readonly string? ResultsDirectory;
internal readonly string? ResultsPrefix;
internal readonly int WriteBatchSize;
internal readonly string CyclesPath;
internal readonly bool ContinueCyclesGeneration;
internal readonly int CyclesBatchSize;
internal readonly ulong DivisorCyclesSearchLimit;
internal readonly ulong? OrderWarmupLimitOverride;
internal readonly string? RleBlacklistPath;
internal readonly ulong RleHardMaxP;
internal readonly bool RleOnlyLast7;
internal readonly double ZeroFractionHard;
internal readonly double ZeroFractionConjecture;
internal readonly int MaxZeroConjecture;
private CliArguments(
ulong startPrime,
bool startPrimeProvided,
UInt128 maxPrimeLimit,
bool maxPrimeConfigured,
int threadCount,
int blockSize,
int gpuPrimeThreads,
int gpuPrimeBatch,
GpuKernelType kernelType,
bool useOrder,
bool showHelp,
bool useBitTransform,
bool useLucas,
bool useResidue,
bool useDivisor,
bool useByDivisor,
bool useGcdFilter,
bool testMode,
bool useGpuCycles,
bool useMersenneOnGpu,
bool useOrderOnGpu,
int scanBatchSize,
int sliceSize,
ulong residueKMax,
string filterFile,
bool forcePrimeKernelsOnCpu,
ModReductionMode modReductionMode,
NttBackend nttBackend,
string? resultsDirectory,
string? resultsPrefix,
int writeBatchSize,
string cyclesPath,
bool continueCyclesGeneration,
int cyclesBatchSize,
ulong divisorCyclesSearchLimit,
ulong? orderWarmupLimitOverride,
string? rleBlacklistPath,
ulong rleHardMaxP,
bool rleOnlyLast7,
double zeroFractionHard,
double zeroFractionConjecture,
int maxZeroConjecture)
{
StartPrime = startPrime;
StartPrimeProvided = startPrimeProvided;
MaxPrimeLimit = maxPrimeLimit;
MaxPrimeConfigured = maxPrimeConfigured;
ThreadCount = threadCount;
BlockSize = blockSize;
GpuPrimeThreads = gpuPrimeThreads;
GpuPrimeBatch = gpuPrimeBatch;
KernelType = kernelType;
UseOrder = useOrder;
ShowHelp = showHelp;
UseBitTransform = useBitTransform;
UseLucas = useLucas;
UseResidue = useResidue;
UseDivisor = useDivisor;
UseByDivisor = useByDivisor;
UseGcdFilter = useGcdFilter;
TestMode = testMode;
UseGpuCycles = useGpuCycles;
UseMersenneOnGpu = useMersenneOnGpu;
UseOrderOnGpu = useOrderOnGpu;
ScanBatchSize = scanBatchSize;
SliceSize = sliceSize;
ResidueKMax = residueKMax;
FilterFile = filterFile;
ForcePrimeKernelsOnCpu = forcePrimeKernelsOnCpu;
ModReductionMode = modReductionMode;
NttBackend = nttBackend;
ResultsDirectory = resultsDirectory;
ResultsPrefix = resultsPrefix;
WriteBatchSize = writeBatchSize;
CyclesPath = cyclesPath;
ContinueCyclesGeneration = continueCyclesGeneration;
CyclesBatchSize = cyclesBatchSize;
DivisorCyclesSearchLimit = divisorCyclesSearchLimit;
OrderWarmupLimitOverride = orderWarmupLimitOverride;
RleBlacklistPath = rleBlacklistPath;
RleHardMaxP = rleHardMaxP;
RleOnlyLast7 = rleOnlyLast7;
ZeroFractionHard = zeroFractionHard;
ZeroFractionConjecture = zeroFractionConjecture;
MaxZeroConjecture = maxZeroConjecture;
}
internal bool UseFilter => !TestMode && !string.IsNullOrEmpty(FilterFile);
internal static CliArguments Parse(string[] args)
{
ulong startPrime = PerfectNumberConstants.BiggestKnownEvenPerfectP;
bool startPrimeProvided = false;
UInt128 maxPrimeLimit = UInt128.MaxValue;
bool maxPrimeConfigured = false;
int threadCount = Environment.ProcessorCount;
int blockSize = 1;
int gpuPrimeThreads = 1;
int gpuPrimeBatch = 262_144;
GpuKernelType kernelType = GpuKernelType.Incremental;
bool useOrder = false;
bool showHelp = false;
bool useBitTransform = false;
bool useLucas = false;
bool useResidue = false;
bool useDivisor = false;
bool useByDivisor = false;
bool useGcdFilter = false;
bool testMode = false;
bool useGpuCycles = true;
bool useMersenneOnGpu = true;
bool useOrderOnGpu = true;
int scanBatchSize = 2_097_152;
int sliceSize = 32;
ulong residueKMax = 5_000_000UL;
string filterFile = string.Empty;
bool forcePrimeKernelsOnCpu = false;
ModReductionMode modReductionMode = ModReductionMode.Auto;
NttBackend nttBackend = NttBackend.Reference;
string? resultsDirectory = null;
string? resultsPrefix = null;
int writeBatchSize = CalculationResultHandler.DefaultWriteBatchSize;
string cyclesPath = DefaultCyclesPath;
bool continueCyclesGeneration = false;
int cyclesBatchSize = 512;
ulong divisorCyclesSearchLimit = PerfectNumberConstants.ExtraDivisorCycleSearchLimit;
ulong? orderWarmupLimitOverride = null;
string? rleBlacklistPath = null;
ulong rleHardMaxP = ulong.MaxValue;
bool rleOnlyLast7 = true;
double zeroFractionHard = -1.0;
double zeroFractionConjecture = -1.0;
int maxZeroConjecture = -1;
foreach (string argument in args)
{
if (argument.Equals("--?", StringComparison.OrdinalIgnoreCase) ||
argument.Equals("-?", StringComparison.OrdinalIgnoreCase) ||
argument.Equals("--help", StringComparison.OrdinalIgnoreCase) ||
argument.Equals("-help", StringComparison.OrdinalIgnoreCase) ||
argument.Equals("/?", StringComparison.OrdinalIgnoreCase))
{
showHelp = true;
break;
}
if (argument.StartsWith("--prime=", StringComparison.OrdinalIgnoreCase))
{
startPrime = Utf8CliParser.ParseUInt64(argument.AsSpan(argument.IndexOf('=') + 1));
startPrimeProvided = true;
continue;
}
if (argument.StartsWith("--max-prime=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = argument.AsSpan(argument.IndexOf('=') + 1);
if (UInt128.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out UInt128 parsedMaxPrime))
{
maxPrimeLimit = parsedMaxPrime;
maxPrimeConfigured = true;
}
else
{
Console.WriteLine("Invalid value for --max-prime.");
return default;
}
continue;
}
if (argument.Equals("--increment=bit", StringComparison.OrdinalIgnoreCase))
{
useBitTransform = true;
continue;
}
if (argument.Equals("--increment=add", StringComparison.OrdinalIgnoreCase))
{
useBitTransform = false;
continue;
}
if (argument.StartsWith("--threads=", StringComparison.OrdinalIgnoreCase))
{
threadCount = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan(argument.IndexOf('=') + 1)));
continue;
}
if (argument.StartsWith("--mersenne=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = argument.AsSpan(argument.IndexOf('=') + 1);
if (value.Equals("pow2mod", StringComparison.OrdinalIgnoreCase))
{
kernelType = GpuKernelType.Pow2Mod;
}
else if (value.Equals("lucas", StringComparison.OrdinalIgnoreCase))
{
useLucas = true;
}
else if (value.Equals("residue", StringComparison.OrdinalIgnoreCase))
{
useResidue = true;
useLucas = false;
}
else if (value.Equals("divisor", StringComparison.OrdinalIgnoreCase))
{
useDivisor = true;
useLucas = false;
useResidue = false;
}
else if (value.Equals("bydivisor", StringComparison.OrdinalIgnoreCase))
{
useByDivisor = true;
useLucas = false;
useResidue = false;
useDivisor = false;
}
else
{
kernelType = GpuKernelType.Incremental;
}
continue;
}
if (argument.StartsWith("--divisor-cycles-limit=", StringComparison.OrdinalIgnoreCase))
{
if (Utf8CliParser.TryParseUInt64(argument.AsSpan(argument.IndexOf('=') + 1), out ulong parsedLimit))
{
divisorCyclesSearchLimit = parsedLimit;
}
continue;
}
if (argument.StartsWith("--residue-max-k=", StringComparison.OrdinalIgnoreCase))
{
if (Utf8CliParser.TryParseUInt64(argument.AsSpan(argument.IndexOf('=') + 1), out ulong parsedResidueMax))
{
residueKMax = parsedResidueMax;
}
continue;
}
if (argument.StartsWith("--mersenne-device=", StringComparison.OrdinalIgnoreCase))
{
useMersenneOnGpu = !argument.AsSpan(argument.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.Equals("--test", StringComparison.OrdinalIgnoreCase))
{
testMode = true;
continue;
}
if (argument.Equals("--use-order", StringComparison.OrdinalIgnoreCase))
{
useOrder = true;
continue;
}
if (argument.StartsWith("--order-warmup-limit=", StringComparison.OrdinalIgnoreCase))
{
if (Utf8CliParser.TryParseUInt64(argument.AsSpan(argument.IndexOf('=') + 1), out ulong parsedLimit))
{
orderWarmupLimitOverride = parsedLimit;
}
continue;
}
if (argument.Equals("--gcd-filter", StringComparison.OrdinalIgnoreCase))
{
useGcdFilter = true;
continue;
}
if (argument.StartsWith("--ntt=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = argument.AsSpan(argument.IndexOf('=') + 1);
nttBackend = value.Equals("staged", StringComparison.OrdinalIgnoreCase) ? NttBackend.Staged : NttBackend.Reference;
continue;
}
if (argument.StartsWith("--mod-reduction=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = argument.AsSpan(argument.IndexOf('=') + 1);
if (value.Equals("mont64", StringComparison.OrdinalIgnoreCase))
{
modReductionMode = ModReductionMode.Mont64;
}
else if (value.Equals("barrett128", StringComparison.OrdinalIgnoreCase))
{
modReductionMode = ModReductionMode.Barrett128;
}
else if (value.Equals("uint128", StringComparison.OrdinalIgnoreCase))
{
modReductionMode = ModReductionMode.GpuUInt128;
}
else
{
modReductionMode = ModReductionMode.Auto;
}
continue;
}
if (argument.StartsWith("--primes-device=", StringComparison.OrdinalIgnoreCase))
{
forcePrimeKernelsOnCpu = argument.AsSpan(argument.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.StartsWith("--order-device=", StringComparison.OrdinalIgnoreCase))
{
useOrderOnGpu = !argument.AsSpan(argument.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.StartsWith("--rle-blacklist=", StringComparison.OrdinalIgnoreCase))
{
rleBlacklistPath = argument[(argument.IndexOf('=') + 1)..];
continue;
}
if (argument.StartsWith("--rle-hard-max=", StringComparison.OrdinalIgnoreCase))
{
if (Utf8CliParser.TryParseUInt64(argument.AsSpan(argument.IndexOf('=') + 1), out ulong parsedMax))
{
rleHardMaxP = parsedMax;
}
continue;
}
if (argument.StartsWith("--rle-only-last7=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = argument.AsSpan(argument.IndexOf('=') + 1);
rleOnlyLast7 = !value.Equals("false", StringComparison.OrdinalIgnoreCase) && !value.Equals("0", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.StartsWith("--zero-hard=", StringComparison.OrdinalIgnoreCase))
{
if (Utf8CliParser.TryParseDouble(argument.AsSpan(argument.IndexOf('=') + 1), out double zeroFraction))
{
zeroFractionHard = zeroFraction;
}
continue;
}
if (argument.StartsWith("--zero-conj=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = argument.AsSpan(argument.IndexOf('=') + 1);
int colon = value.IndexOf(':');
if (colon > 0)
{
if (Utf8CliParser.TryParseDouble(value[..colon], out double zeroFrac) &&
Utf8CliParser.TryParseInt32(value[(colon + 1)..], out int maxZero))
{
zeroFractionConjecture = zeroFrac;
maxZeroConjecture = maxZero;
}
}
continue;
}
if (argument.StartsWith("--lucas=", StringComparison.OrdinalIgnoreCase))
{
useMersenneOnGpu = !argument.AsSpan(argument.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.StartsWith("--primes=", StringComparison.OrdinalIgnoreCase) ||
argument.StartsWith("--gpu-kernels=", StringComparison.OrdinalIgnoreCase) ||
argument.StartsWith("--accelerator=", StringComparison.OrdinalIgnoreCase))
{
forcePrimeKernelsOnCpu = argument.AsSpan(argument.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.StartsWith("--results-dir=", StringComparison.OrdinalIgnoreCase))
{
resultsDirectory = argument[(argument.IndexOf('=') + 1)..];
continue;
}
if (argument.StartsWith("--results-prefix=", StringComparison.OrdinalIgnoreCase))
{
resultsPrefix = argument[(argument.IndexOf('=') + 1)..];
continue;
}
if (argument.StartsWith("--gpu-prime-threads=", StringComparison.OrdinalIgnoreCase))
{
gpuPrimeThreads = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan(argument.IndexOf('=') + 1)));
continue;
}
if (argument.StartsWith("--gpu-prime-batch=", StringComparison.OrdinalIgnoreCase))
{
gpuPrimeBatch = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan(argument.IndexOf('=') + 1)));
continue;
}
if (argument.StartsWith("--ll-slice=", StringComparison.OrdinalIgnoreCase))
{
sliceSize = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan(argument.IndexOf('=') + 1)));
continue;
}
if (argument.StartsWith("--gpu-scan-batch=", StringComparison.OrdinalIgnoreCase))
{
scanBatchSize = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan(argument.IndexOf('=') + 1)));
continue;
}
if (argument.StartsWith("--block-size=", StringComparison.OrdinalIgnoreCase))
{
blockSize = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan(argument.IndexOf('=') + 1)));
continue;
}
if (argument.StartsWith("--filter-p=", StringComparison.OrdinalIgnoreCase))
{
filterFile = argument[(argument.IndexOf('=') + 1)..];
continue;
}
if (argument.StartsWith("--write-batch-size=", StringComparison.OrdinalIgnoreCase))
{
writeBatchSize = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan(argument.IndexOf('=') + 1)));
continue;
}
if (argument.StartsWith("--divisor-cycles=", StringComparison.OrdinalIgnoreCase))
{
cyclesPath = argument[(argument.IndexOf('=') + 1)..];
continue;
}
if (argument.StartsWith("--divisor-cycles-device=", StringComparison.OrdinalIgnoreCase))
{
useGpuCycles = !argument.AsSpan(argument.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
continue;
}
if (argument.StartsWith("--divisor-cycles-batch=", StringComparison.OrdinalIgnoreCase))
{
cyclesBatchSize = Math.Max(1, Utf8CliParser.ParseInt32(argument.AsSpan(argument.IndexOf('=') + 1)));
continue;
}
if (argument.Equals("--divisor-cycles-continue", StringComparison.OrdinalIgnoreCase))
{
continueCyclesGeneration = true;
}
}
return new CliArguments(
startPrime,
startPrimeProvided,
maxPrimeLimit,
maxPrimeConfigured,
threadCount,
blockSize,
gpuPrimeThreads,
gpuPrimeBatch,
kernelType,
useOrder,
showHelp,
useBitTransform,
useLucas,
useResidue,
useDivisor,
useByDivisor,
useGcdFilter,
testMode,
useGpuCycles,
useMersenneOnGpu,
useOrderOnGpu,
scanBatchSize,
sliceSize,
residueKMax,
filterFile,
forcePrimeKernelsOnCpu,
modReductionMode,
nttBackend,
resultsDirectory,
resultsPrefix,
writeBatchSize,
cyclesPath,
continueCyclesGeneration,
cyclesBatchSize,
divisorCyclesSearchLimit,
orderWarmupLimitOverride,
rleBlacklistPath,
rleHardMaxP,
rleOnlyLast7,
zeroFractionHard,
zeroFractionConjecture,
maxZeroConjecture);
}
}
+163 -554
View File
@@ -1,10 +1,8 @@
using System.Buffers;
using System.Buffers.Text;
using System.Diagnostics;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
using Open.Collections;
using Open.Numeric.Primes;
using PerfectNumbers.Core;
@@ -19,26 +17,18 @@ internal static class Program
private static ThreadLocal<MersenneNumberTester> MersenneTesters = null!;
private static ThreadLocal<ModResidueTracker> PResidue = null!; // p mod d tracker (per-thread)
private const ulong InitialP = PerfectNumberConstants.BiggestKnownEvenPerfectP;
private const int WriteBatchSize = 100;
private const string DefaultCyclesPath = "divisor_cycles.bin";
private static string ResultsFileName = "even_perfect_bit_scan_results.csv";
private const string PrimeFoundSuffix = " (FOUND VALID CANDIDATES)";
private static StringBuilder? _outputBuilder;
private static readonly object Sync = new();
private static readonly object FileWriteSync = new();
private static int _consoleCounter;
private static int _writeIndex;
private static int _primeCount;
private static bool _primeFoundAfterInit;
private static bool _useGcdFilter;
private static bool _useDivisor;
private static bool _useResidueMode;
private static bool _useByDivisorMode;
private static MersenneNumberDivisorGpuTester? _divisorTester;
private static IMersenneNumberDivisorByDivisorTester? _byDivisorTester;
private static Dictionary<ulong, (bool DetailedCheck, bool PassedAllTests)>? _byDivisorPreviousResults;
private static ulong? _orderWarmupLimitOverride;
private static PrimeTransformMode _transformMode;
private static long _state;
private static bool _limitReached;
private static readonly Optimized PrimeIterator = new();
private static ulong _byDivisorStartPrime;
private static CliArguments _cliArguments;
private static CalculationResultHandler _resultHandler;
private static int _testTargetPrimeCount;
private static int _testProcessedPrimeCount;
private const string TestTimeFileName = "even_perfect_test_time.txt";
private enum PrimeTransformMode
{
@@ -46,343 +36,74 @@ internal static class Program
Add,
AddPrimes,
}
private static long _state;
private static bool _limitReached;
private static string? _resultsDir;
private static string? _resultsPrefix;
private static readonly Optimized PrimeIterator = new();
private static ulong _byDivisorStartPrime;
private static bool _testMode;
private static int _testTargetPrimeCount;
private static int _testProcessedPrimeCount;
private const string TestTimeFileName = "even_perfect_test_time.txt";
private static PrimeTransformMode TransformMode => _cliArguments.UseBitTransform
? PrimeTransformMode.Bit
: (_cliArguments.UseResidue
? PrimeTransformMode.Add
: PrimeTransformMode.AddPrimes);
[ThreadStatic]
private static bool _lastCompositeP;
// RLE/bit-pattern filtering options
private static string? _rleBlacklistPath;
private static ulong _rleHardMaxP = ulong.MaxValue;
private static bool _rleOnlyLast7 = true;
private static int _writeBatchSize = WriteBatchSize;
private static double _zeroFracHard = -1.0; // disabled when < 0
private static double _zeroFracConj = -1.0; // disabled when < 0
private static int _maxZeroConj = -1; // disabled when < 0
private static void Main(string[] args)
{
ulong currentP = InitialP;
int threadCount = Environment.ProcessorCount;
int blockSize = 1;
int gpuPrimeThreads = 1;
int gpuPrimeBatch = 262_144;
GpuKernelType kernelType = GpuKernelType.Incremental;
// removed: useModAutomaton
bool useOrder = false;
bool showHelp = false;
bool useBitTransform = false;
bool useLucas = false;
bool useResidue = false; // M_p test via residue divisors (replaces LL/incremental)
bool useDivisor = false; // M_p divisibility by specific divisor
bool useByDivisor = false; // Iterative divisor scan across primes UInt128 divisor = UInt128.Zero;
// Device routing
bool useGpuCycles = true;
bool mersenneOnGpu = true; // controls Lucas/incremental/pow2mod device
bool orderOnGpu = true; // controls order computations device
int scanBatchSize = 2_097_152, sliceSize = 32;
ulong residueKMax = 5_000_000UL;
string filterFile = string.Empty;
UInt128 maxPrimeLimit = UInt128.MaxValue;
bool maxPrimeConfigured = false;
string cyclesPath = DefaultCyclesPath;
int cyclesBatchSize = 512;
bool continueCyclesGeneration = false;
ulong divisorCyclesSearchLimit = PerfectNumberConstants.ExtraDivisorCycleSearchLimit;
int argIndex = 0;
string arg = string.Empty;
ReadOnlySpan<char> mersenneOption = default;
ulong parsedLimit = 0UL;
ulong parsedResidueMax = 0UL;
ulong remainder = 0UL;
bool startPrimeProvided = false;
try
{
CliArguments parsedArguments = CliArguments.Parse(args);
if (parsedArguments.ThreadCount == 0 && !parsedArguments.ShowHelp)
{
return;
}
_testMode = false;
_cliArguments = parsedArguments;
if (_cliArguments.ShowHelp)
{
PrintHelp();
return;
}
NttGpuMath.GpuTransformBackend = _cliArguments.NttBackend;
NttGpuMath.ReductionMode = _cliArguments.ModReductionMode;
GpuContextPool.ForceCpu = _cliArguments.ForcePrimeKernelsOnCpu;
ulong currentP = _cliArguments.StartPrime;
ulong remainder = currentP % 6UL;
bool startPrimeProvided = _cliArguments.StartPrimeProvided;
int threadCount = Math.Max(1, _cliArguments.ThreadCount);
int blockSize = Math.Max(1, _cliArguments.BlockSize);
int gpuPrimeThreads = Math.Max(1, _cliArguments.GpuPrimeThreads);
int gpuPrimeBatch = Math.Max(1, _cliArguments.GpuPrimeBatch);
GpuKernelType kernelType = _cliArguments.KernelType;
bool useBitTransform = _cliArguments.UseBitTransform;
bool useOrder = _cliArguments.UseOrder;
bool useResidue = _cliArguments.UseResidue;
bool useDivisor = _cliArguments.UseDivisor;
bool useByDivisor = _cliArguments.UseByDivisor;
bool useLucas = _cliArguments.UseLucas;
bool testMode = _cliArguments.TestMode;
bool useGpuCycles = _cliArguments.UseGpuCycles;
bool mersenneOnGpu = _cliArguments.UseMersenneOnGpu;
bool orderOnGpu = _cliArguments.UseOrderOnGpu;
int scanBatchSize = Math.Max(1, _cliArguments.ScanBatchSize);
int sliceSize = Math.Max(1, _cliArguments.SliceSize);
ulong residueKMax = _cliArguments.ResidueKMax;
string filterFile = _cliArguments.FilterFile;
UInt128 maxPrimeLimit = _cliArguments.MaxPrimeLimit;
bool maxPrimeConfigured = _cliArguments.MaxPrimeConfigured;
string cyclesPath = _cliArguments.CyclesPath;
int cyclesBatchSize = Math.Max(1, _cliArguments.CyclesBatchSize);
bool continueCyclesGeneration = _cliArguments.ContinueCyclesGeneration;
ulong divisorCyclesSearchLimit = _cliArguments.DivisorCyclesSearchLimit;
ulong? orderWarmupLimitOverride = _cliArguments.OrderWarmupLimitOverride;
int testPrimeCandidateLimit = 0;
_limitReached = false;
_testTargetPrimeCount = 0;
_testProcessedPrimeCount = 0;
// NTT backend selection (GPU): reference vs staged
for (; argIndex < args.Length; argIndex++)
{
arg = args[argIndex];
if (arg.Equals("--?", StringComparison.OrdinalIgnoreCase) || arg.Equals("-?", StringComparison.OrdinalIgnoreCase) || arg.Equals("--help", StringComparison.OrdinalIgnoreCase) || arg.Equals("-help", StringComparison.OrdinalIgnoreCase) || arg.Equals("/?", StringComparison.OrdinalIgnoreCase))
{
showHelp = true;
break;
}
else if (arg.StartsWith("--prime=", StringComparison.OrdinalIgnoreCase))
{
currentP = Utf8CliParser.ParseUInt64(arg.AsSpan(arg.IndexOf('=') + 1));
startPrimeProvided = true;
}
else if (arg.StartsWith("--max-prime=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = arg.AsSpan(arg.IndexOf('=') + 1);
if (UInt128.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out UInt128 parsedMaxPrime))
{
maxPrimeLimit = parsedMaxPrime;
maxPrimeConfigured = true;
}
else
{
Console.WriteLine("Invalid value for --max-prime.");
return;
}
}
else if (arg.Equals("--increment=bit", StringComparison.OrdinalIgnoreCase))
{
useBitTransform = true;
}
else if (arg.Equals("--increment=add", StringComparison.OrdinalIgnoreCase))
{
useBitTransform = false;
}
else if (arg.StartsWith("--threads=", StringComparison.OrdinalIgnoreCase))
{
threadCount = Math.Max(1, Utf8CliParser.ParseInt32(arg.AsSpan(arg.IndexOf('=') + 1)));
}
else if (arg.StartsWith("--mersenne=", StringComparison.OrdinalIgnoreCase))
{
mersenneOption = arg.AsSpan(arg.IndexOf('=') + 1);
if (mersenneOption.Equals("pow2mod", StringComparison.OrdinalIgnoreCase))
{
kernelType = GpuKernelType.Pow2Mod; // Dispatch to the ProcessEightBitWindows pow2mod kernel.
}
else if (mersenneOption.Equals("lucas", StringComparison.OrdinalIgnoreCase))
{
useLucas = true;
}
else if (mersenneOption.Equals("residue", StringComparison.OrdinalIgnoreCase))
{
useResidue = true;
useLucas = false;
}
else if (mersenneOption.Equals("divisor", StringComparison.OrdinalIgnoreCase))
{
useDivisor = true;
useLucas = false;
useResidue = false;
}
else if (mersenneOption.Equals("bydivisor", StringComparison.OrdinalIgnoreCase))
{
useByDivisor = true;
useLucas = false;
useResidue = false;
useDivisor = false;
}
else
{
kernelType = GpuKernelType.Incremental;
}
}
else if (arg.StartsWith("--divisor-cycles-limit=", StringComparison.OrdinalIgnoreCase))
{
if (Utf8CliParser.TryParseUInt64(arg.AsSpan(arg.IndexOf('=') + 1), out parsedLimit))
{
divisorCyclesSearchLimit = parsedLimit;
}
}
else if (arg.StartsWith("--residue-max-k=", StringComparison.OrdinalIgnoreCase))
{
if (Utf8CliParser.TryParseUInt64(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))
{
mersenneOnGpu = !arg.AsSpan(arg.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
}
else if (arg.Equals("--test", StringComparison.OrdinalIgnoreCase))
{
_testMode = true;
}
else if (arg.Equals("--use-order", StringComparison.OrdinalIgnoreCase))
{
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 (Utf8CliParser.TryParseUInt64(arg.AsSpan(arg.IndexOf('=') + 1), out parsedLimit))
{
_orderWarmupLimitOverride = parsedLimit;
}
}
else if (arg.Equals("--gcd-filter", StringComparison.OrdinalIgnoreCase))
{
_useGcdFilter = true;
}
else if (arg.StartsWith("--ntt=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = arg.AsSpan(arg.IndexOf('=') + 1);
if (value.Equals("staged", StringComparison.OrdinalIgnoreCase))
{
NttGpuMath.GpuTransformBackend = NttBackend.Staged;
}
else
{
NttGpuMath.GpuTransformBackend = NttBackend.Reference;
}
}
else if (arg.StartsWith("--mod-reduction=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = arg.AsSpan(arg.IndexOf('=') + 1);
if (value.Equals("mont64", StringComparison.OrdinalIgnoreCase))
{
NttGpuMath.ReductionMode = ModReductionMode.Mont64;
}
else if (value.Equals("barrett128", StringComparison.OrdinalIgnoreCase))
{
NttGpuMath.ReductionMode = ModReductionMode.Barrett128;
}
else if (value.Equals("uint128", StringComparison.OrdinalIgnoreCase))
{
NttGpuMath.ReductionMode = ModReductionMode.GpuUInt128;
}
else
{
NttGpuMath.ReductionMode = ModReductionMode.Auto;
}
}
else if (arg.StartsWith("--primes-device=", StringComparison.OrdinalIgnoreCase))
{
// Controls default device for library prime-related GPU kernels
// that are not explicitly parameterized (backward compatibility).
GpuContextPool.ForceCpu = arg.AsSpan(arg.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
}
// New: choose device for order computations (warm-ups and order scans)
else if (arg.StartsWith("--order-device=", StringComparison.OrdinalIgnoreCase))
{
orderOnGpu = !arg.AsSpan(arg.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
}
// Optional: RLE blacklist for p
else if (arg.StartsWith("--rle-blacklist=", StringComparison.OrdinalIgnoreCase))
{
_rleBlacklistPath = arg[(arg.IndexOf('=') + 1)..];
}
else if (arg.StartsWith("--rle-hard-max=", StringComparison.OrdinalIgnoreCase))
{
ulong rleMaxP;
if (Utf8CliParser.TryParseUInt64(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 (Utf8CliParser.TryParseDouble(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 (Utf8CliParser.TryParseDouble(value[..colon], out zeroFrac) && Utf8CliParser.TryParseInt32(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))
{
mersenneOnGpu = !arg.AsSpan(arg.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
}
else if (arg.StartsWith("--primes=", StringComparison.OrdinalIgnoreCase))
{
GpuContextPool.ForceCpu = arg.AsSpan(arg.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
}
else if (arg.StartsWith("--gpu-kernels=", StringComparison.OrdinalIgnoreCase) || arg.StartsWith("--accelerator=", StringComparison.OrdinalIgnoreCase))
{
// Legacy alias: treat as primes-device for backward compat
GpuContextPool.ForceCpu = arg.AsSpan(arg.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
}
else if (arg.StartsWith("--results-dir=", StringComparison.OrdinalIgnoreCase))
{
_resultsDir = arg[(arg.IndexOf('=') + 1)..];
}
else if (arg.StartsWith("--results-prefix=", StringComparison.OrdinalIgnoreCase))
{
_resultsPrefix = arg[(arg.IndexOf('=') + 1)..];
}
else if (arg.StartsWith("--gpu-prime-threads=", StringComparison.OrdinalIgnoreCase))
{
gpuPrimeThreads = Math.Max(1, Utf8CliParser.ParseInt32(arg.AsSpan(arg.IndexOf('=') + 1)));
}
else if (arg.StartsWith("--gpu-prime-batch=", StringComparison.OrdinalIgnoreCase))
{
gpuPrimeBatch = Math.Max(1, Utf8CliParser.ParseInt32(arg.AsSpan(arg.IndexOf('=') + 1)));
}
else if (arg.StartsWith("--ll-slice=", StringComparison.OrdinalIgnoreCase))
{
sliceSize = Math.Max(1, Utf8CliParser.ParseInt32(arg.AsSpan(arg.IndexOf('=') + 1)));
}
else if (arg.StartsWith("--gpu-scan-batch=", StringComparison.OrdinalIgnoreCase))
{
scanBatchSize = Math.Max(1, Utf8CliParser.ParseInt32(arg.AsSpan(arg.IndexOf('=') + 1)));
}
else if (arg.StartsWith("--block-size=", StringComparison.OrdinalIgnoreCase))
{
blockSize = Math.Max(1, Utf8CliParser.ParseInt32(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, Utf8CliParser.ParseInt32(arg.AsSpan(arg.IndexOf('=') + 1)));
}
if (arg.StartsWith("--divisor-cycles="))
{
cyclesPath = arg[(arg.IndexOf('=') + 1)..];
}
else if (arg.StartsWith("--divisor-cycles-device=", StringComparison.OrdinalIgnoreCase))
{
useGpuCycles = !arg.AsSpan(arg.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
}
else if (arg.StartsWith("--divisor-cycles-batch=", StringComparison.OrdinalIgnoreCase))
{
cyclesBatchSize = Math.Max(1, Utf8CliParser.ParseInt32(arg.AsSpan(arg.IndexOf('=') + 1)));
}
else if (arg.Equals("--divisor-cycles-continue", StringComparison.OrdinalIgnoreCase))
{
continueCyclesGeneration = true;
}
}
int testPrimeCandidateLimit = 0;
if (_testMode)
if (testMode)
{
currentP = 31UL;
remainder = currentP % 6UL;
@@ -402,18 +123,12 @@ internal static class Program
_byDivisorStartPrime = 0UL;
}
if (useByDivisor && !_testMode && string.IsNullOrEmpty(filterFile))
if (useByDivisor && !testMode && string.IsNullOrEmpty(filterFile))
{
Console.WriteLine("--mersenne=bydivisor requires --filter-p=<path>.");
return;
}
if (showHelp)
{
PrintHelp();
return;
}
// Apply GPU prime sieve runtime configuration
GpuPrimeWorkLimiter.SetLimit(gpuPrimeThreads);
PrimeTester.GpuBatchSize = Math.Max(1, gpuPrimeBatch);
@@ -477,26 +192,11 @@ internal static class Program
blockSize = 1;
}
_useDivisor = useDivisor;
_useResidueMode = useResidue;
_useByDivisorMode = useByDivisor;
if (useBitTransform)
{
_transformMode = PrimeTransformMode.Bit;
}
else if (useResidue)
{
_transformMode = PrimeTransformMode.Add;
}
else
{
_transformMode = PrimeTransformMode.AddPrimes;
}
PResidue = new ThreadLocal<ModResidueTracker>(() => new ModResidueTracker(ResidueModel.Identity, initialNumber: currentP, initialized: true), trackAllValues: true);
PrimeTesters = new ThreadLocal<PrimeTester>(() => new PrimeTester(), trackAllValues: true);
// Note: --primes-device controls default device for library kernels; p primality remains CPU here.
// Initialize per-thread p residue tracker (Identity model) at currentP
if (!useDivisor && !useByDivisor)
if (!useDivisor && !_cliArguments.UseByDivisor)
{
MersenneTesters = new ThreadLocal<MersenneNumberTester>(() =>
{
@@ -514,7 +214,7 @@ internal static class Program
if (!useLucas)
{
Console.WriteLine("Warming up orders");
tester.WarmUpOrders(currentP, _orderWarmupLimitOverride ?? 5_000_000UL);
tester.WarmUpOrders(currentP, orderWarmupLimitOverride ?? 5_000_000UL);
}
return tester;
@@ -535,9 +235,9 @@ internal static class Program
}
// Load RLE blacklist (optional)
if (!string.IsNullOrEmpty(_rleBlacklistPath))
if (!string.IsNullOrEmpty(_cliArguments.RleBlacklistPath))
{
RleBlacklist.Load(_rleBlacklistPath!);
RleBlacklist.Load(_cliArguments.RleBlacklistPath!);
}
// Mod6 lookup turned out slower (see Mod6ComparisonBenchmarks), so keep `%` here for large candidates.
@@ -568,23 +268,28 @@ internal static class Program
useByDivisor,
mersenneOnGpu,
useOrder,
_useGcdFilter,
_cliArguments.UseGcdFilter,
NttGpuMath.GpuTransformBackend,
gpuPrimeThreads,
sliceSize,
scanBatchSize,
_orderWarmupLimitOverride ?? 5_000_000UL,
orderWarmupLimitOverride ?? 5_000_000UL,
NttGpuMath.ReductionMode,
mersenneOnGpu ? "gpu" : "cpu",
(GpuContextPool.ForceCpu ? "cpu" : "gpu"),
orderOnGpu ? "gpu" : "cpu");
if (!string.IsNullOrEmpty(_resultsPrefix))
if (!string.IsNullOrEmpty(_cliArguments.ResultsPrefix))
{
builtName = _resultsPrefix + "_" + builtName;
builtName = _cliArguments.ResultsPrefix + "_" + builtName;
}
ResultsFileName = string.IsNullOrEmpty(_resultsDir) ? builtName : Path.Combine(_resultsDir!, builtName);
var dir = Path.GetDirectoryName(ResultsFileName);
string resultsFileName = string.IsNullOrEmpty(_cliArguments.ResultsDirectory)
? builtName
: Path.Combine(_cliArguments.ResultsDirectory!, builtName);
_resultHandler = new CalculationResultHandler(resultsFileName, _cliArguments.WriteBatchSize);
var dir = Path.GetDirectoryName(resultsFileName);
if (!string.IsNullOrEmpty(dir))
{
Directory.CreateDirectory(dir!);
@@ -594,15 +299,12 @@ internal static class Program
Dictionary<ulong, (bool DetailedCheck, bool PassedAllTests)>? previousResults = useByDivisor ? new Dictionary<ulong, (bool DetailedCheck, bool PassedAllTests)>() : null;
_byDivisorPreviousResults = previousResults;
if (File.Exists(ResultsFileName))
if (File.Exists(resultsFileName))
{
Console.WriteLine("Processing previous results...");
LoadResultsFile(ResultsFileName, (p, detailedCheck, passedAllTests) =>
LoadResultsFile(resultsFileName, (p, detailedCheck, passedAllTests) =>
{
if (detailedCheck && passedAllTests)
{
_primeCount++;
}
_resultHandler.RegisterExistingResult(detailedCheck, passedAllTests);
if (previousResults is not null)
{
@@ -612,22 +314,19 @@ 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}");
_resultHandler.CreateResultsFileWithHeader();
}
bool useFilter = !_testMode && !string.IsNullOrEmpty(filterFile);
bool useFilter = !testMode && !string.IsNullOrEmpty(filterFile);
HashSet<ulong> filter = [];
List<ulong> byDivisorCandidates = [];
ulong maxP = 0UL;
ulong[] localFilter = Array.Empty<ulong>();
int filterCount = 0;
if (_useByDivisorMode)
if (useByDivisor)
{
if (_testMode)
if (testMode)
{
byDivisorCandidates = BuildTestPrimeCandidates(testPrimeCandidateLimit);
}
@@ -709,17 +408,11 @@ internal static class Program
}
if (_testMode)
if (testMode)
{
_testTargetPrimeCount = testPrimeCandidateLimit;
}
if (_primeCount >= 2)
{
_primeFoundAfterInit = true;
}
_outputBuilder = StringBuilderPool.Rent();
_resultHandler.InitializeOutputBuffer();
// Limit GPU concurrency only for prime checks (LL/NTT & GPU order scans).
GpuPrimeWorkLimiter.SetLimit(gpuPrimeThreads);
@@ -727,14 +420,14 @@ internal static class Program
PrimeTester.GpuBatchSize = gpuPrimeBatch;
Stopwatch? stopwatch = null;
if (_testMode)
if (testMode)
{
stopwatch = Stopwatch.StartNew();
}
Console.WriteLine("Starting scan...");
if (_useByDivisorMode)
if (useByDivisor)
{
MersenneNumberDivisorByDivisorTester.Run(
byDivisorCandidates,
@@ -745,8 +438,8 @@ internal static class Program
static () => _lastCompositeP = false,
PrintResult,
threadCount);
FlushBuffer();
StringBuilderPool.Return(_outputBuilder!);
_resultHandler.FlushBuffer();
_resultHandler.ReleaseOutputBuffer();
if (stopwatch is not null)
{
stopwatch.Stop();
@@ -756,18 +449,18 @@ internal static class Program
return;
}
if (!useDivisor && !useByDivisor)
if (!useDivisor && !_cliArguments.UseByDivisor)
{
threadCount = Math.Max(1, threadCount);
_ = MersenneTesters.Value;
if (!useLucas)
{
Console.WriteLine("Warming up orders...");
MersenneTesters.Value!.WarmUpOrders(currentP, _orderWarmupLimitOverride ?? 5_000_000UL);
MersenneTesters.Value!.WarmUpOrders(currentP, orderWarmupLimitOverride ?? 5_000_000UL);
}
}
if (_testMode)
if (testMode)
{
testPrimeCandidateLimit = Math.Max(1, threadCount) * 3;
_testTargetPrimeCount = testPrimeCandidateLimit;
@@ -854,14 +547,19 @@ internal static class Program
reportTestTime = true;
}
FlushBuffer();
StringBuilderPool.Return(_outputBuilder!);
_resultHandler.FlushBuffer();
_resultHandler.ReleaseOutputBuffer();
if (reportTestTime)
{
ReportTestTime(testElapsed);
}
}
finally
{
_resultHandler.Dispose();
}
}
private static List<ulong> BuildTestPrimeCandidates(int targetCount)
{
@@ -1073,14 +771,51 @@ internal static class Program
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";
// 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}_gputh-{gpuPrimeThreads}_llslice-{llSlice}_scanb-{gpuScanBatch}_warm-{warmupLimit}.csv";
Span<char> initialBuffer = stackalloc char[256];
var builder = new PooledValueStringBuilder(initialBuffer);
try
{
builder.Append("even_perfect_bit_scan_inc-");
builder.Append(inc);
builder.Append("_thr-");
builder.Append(threads);
builder.Append("_blk-");
builder.Append(block);
builder.Append("_mers-");
builder.Append(mers);
builder.Append("_mersdev-");
builder.Append(mersenneDevice);
builder.Append("_ntt-");
builder.Append(ntt);
builder.Append("_red-");
builder.Append(red);
builder.Append("_primesdev-");
builder.Append(primesDevice);
builder.Append('_');
builder.Append(order);
builder.Append("_orderdev-");
builder.Append(orderDevice);
builder.Append('_');
builder.Append(gcd);
builder.Append("_gputh-");
builder.Append(gpuPrimeThreads);
builder.Append("_llslice-");
builder.Append(llSlice);
builder.Append("_scanb-");
builder.Append(gpuScanBatch);
builder.Append("_warm-");
builder.Append(warmupLimit);
builder.Append(".csv");
return builder.ToString();
}
finally
{
builder.Dispose();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong AdvancePrime(ulong value, ref ulong remainder) => _transformMode switch
private static ulong AdvancePrime(ulong value, ref ulong remainder) => TransformMode switch
{
PrimeTransformMode.Bit => TransformPBit(value, ref remainder),
PrimeTransformMode.Add => TransformPAdd(value, ref remainder),
@@ -1101,7 +836,7 @@ internal static class Program
remainder = ((ulong)state) & 7UL;
count = 0;
switch (_transformMode)
switch (TransformMode)
{
case PrimeTransformMode.Bit:
while (count < blockSize && !Volatile.Read(ref _limitReached))
@@ -1148,7 +883,7 @@ internal static class Program
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void OnPrimeCandidateConfirmed()
{
if (!_testMode)
if (!_cliArguments.TestMode)
{
return;
}
@@ -1160,140 +895,14 @@ internal static class Program
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryAcquireConsoleSlot(bool primeCandidate, out bool primeFlag)
{
int consoleInterval = PerfectNumberConstants.ConsoleInterval;
int counterValue = Interlocked.Increment(ref _consoleCounter);
if (counterValue >= consoleInterval)
{
int previous = Interlocked.Exchange(ref _consoleCounter, 0);
if (previous >= consoleInterval)
{
primeFlag = primeCandidate;
return true;
}
}
primeFlag = false;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static StringBuilder? AppendToOutputBuffer(StringBuilder source)
{
StringBuilder? builderToFlush = null;
lock (Sync)
{
StringBuilder outputBuilder = _outputBuilder!;
_ = outputBuilder.Append(source);
int nextIndex = _writeIndex + 1;
if (nextIndex >= _writeBatchSize)
{
builderToFlush = outputBuilder;
_outputBuilder = StringBuilderPool.Rent();
_writeIndex = 0;
}
else
{
_writeIndex = nextIndex;
}
}
return builderToFlush;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void PrintResult(ulong currentP, bool searchedMersenne, bool detailedCheck, bool passedAllTests)
{
if (passedAllTests)
{
int newCount = Interlocked.Increment(ref _primeCount);
if (newCount >= 2)
{
Volatile.Write(ref _primeFoundAfterInit, true);
}
}
bool lastWasComposite = _lastCompositeP;
bool primeCandidate = passedAllTests && !lastWasComposite && Volatile.Read(ref _primeFoundAfterInit);
// TODO: Switch this StringBuilder-based formatter to a span-friendly Utf8Formatter pipeline so
// console/file output avoids intermediate builders in the hot logging loop.
StringBuilder localBuilder = StringBuilderPool.Rent();
_ = localBuilder
.Append(currentP).Append(',')
.Append(searchedMersenne).Append(',')
.Append(detailedCheck).Append(',')
.Append(passedAllTests).Append('\n');
bool printToConsole = TryAcquireConsoleSlot(primeCandidate, out bool primeFlag);
StringBuilder? builderToFlush = AppendToOutputBuffer(localBuilder);
if (builderToFlush is not null)
{
FlushBuffer(builderToFlush);
}
if (printToConsole)
{
if (primeFlag)
{
_ = localBuilder
.Remove(localBuilder.Length - 1, 1)
.Append(PrimeFoundSuffix)
.Append('\n');
}
Console.WriteLine(localBuilder.Remove(localBuilder.Length - 1, 1).ToString());
}
localBuilder.Clear();
StringBuilderPool.Return(localBuilder);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void FlushBuffer()
{
StringBuilder? builderToFlush = null;
lock (Sync)
{
if (_writeIndex == 0 || _outputBuilder is null || _outputBuilder.Length == 0)
{
return;
}
builderToFlush = _outputBuilder;
_outputBuilder = StringBuilderPool.Rent();
_writeIndex = 0;
}
FlushBuffer(builderToFlush!);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void FlushBuffer(StringBuilder builderToFlush)
{
try
{
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
{
builderToFlush.Clear();
StringBuilderPool.Return(builderToFlush);
}
_resultHandler.HandleResult(currentP, searchedMersenne, detailedCheck, passedAllTests, lastWasComposite);
}
private static void ReportTestTime(TimeSpan elapsed)
@@ -1327,12 +936,12 @@ internal static class Program
private static string GetTestTimeFilePath()
{
if (string.IsNullOrEmpty(_resultsDir))
if (string.IsNullOrEmpty(_cliArguments.ResultsDirectory))
{
return TestTimeFileName;
}
return Path.Combine(_resultsDir!, TestTimeFileName);
return Path.Combine(_cliArguments.ResultsDirectory!, TestTimeFileName);
}
private static bool TryReadTestTime(string filePath, out double milliseconds)
@@ -1527,16 +1136,16 @@ internal static class Program
detailedCheck = false;
_lastCompositeP = false;
if (_useGcdFilter && IsCompositeByGcd(p))
if (_cliArguments.UseGcdFilter && IsCompositeByGcd(p))
{
_lastCompositeP = true;
return false;
}
// Optional: RLE blacklist and binary-threshold filters on p (safe only when configured)
if (p <= _rleHardMaxP)
if (p <= _cliArguments.RleHardMaxP)
{
if (!_rleOnlyLast7 || p.Mod10() == 7UL)
if (!_cliArguments.RleOnlyLast7 || p.Mod10() == 7UL)
{
if (RleBlacklist.IsLoaded() && RleBlacklist.Matches(p))
{
@@ -1545,7 +1154,7 @@ internal static class Program
}
}
if (_zeroFracHard >= 0 || (_zeroFracConj >= 0 && _maxZeroConj >= 0))
if (_cliArguments.ZeroFractionHard >= 0 || (_cliArguments.ZeroFractionConjecture >= 0 && _cliArguments.MaxZeroConjecture >= 0))
{
double zf;
int bitLength;
@@ -1553,17 +1162,17 @@ internal static class Program
int maxZeroBlockValue;
ComputeBitStats(p, out bitLength, out zeroCountValue, out maxZeroBlockValue);
zf = (double)zeroCountValue / bitLength;
if (_zeroFracHard >= 0)
if (_cliArguments.ZeroFractionHard >= 0)
{
if (zf > _zeroFracHard)
if (zf > _cliArguments.ZeroFractionHard)
{
return false;
}
}
if (_zeroFracConj >= 0 && _maxZeroConj >= 0)
if (_cliArguments.ZeroFractionConjecture >= 0 && _cliArguments.MaxZeroConjecture >= 0)
{
if (zf > _zeroFracConj && maxZeroBlockValue >= _maxZeroConj)
if (zf > _cliArguments.ZeroFractionConjecture && maxZeroBlockValue >= _cliArguments.MaxZeroConjecture)
{
return false;
}
@@ -1571,7 +1180,7 @@ internal static class Program
}
// Fast residue-based composite check for p using small primes
if (!_useDivisor && !_useByDivisorMode && IsCompositeByResidues(p))
if (!_cliArguments.UseDivisor && !_cliArguments.UseByDivisor && IsCompositeByResidues(p))
{
searchedMersenne = false;
detailedCheck = false;
@@ -1580,7 +1189,7 @@ internal static class Program
}
// If primes-device=gpu, route p primality through GPU-assisted sieve with deterministic MR validation.
if (!_useByDivisorMode)
if (!_cliArguments.UseByDivisor)
{
if (!(GpuContextPool.ForceCpu
? PrimeTesters.Value!.IsPrime(p, CancellationToken.None)
@@ -1594,7 +1203,7 @@ internal static class Program
}
searchedMersenne = true;
if (_useByDivisorMode)
if (_cliArguments.UseByDivisor)
{
// Windowed pow2mod kernel handles by-divisor scans.
return _byDivisorTester!.IsPrime(p, out detailedCheck);
@@ -0,0 +1,121 @@
using System;
using System.Globalization;
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core;
public ref struct PooledValueStringBuilder
{
private Span<char> _span;
private char[]? _arrayFromPool;
private int _pos;
public PooledValueStringBuilder(Span<char> initialBuffer)
{
_span = initialBuffer;
_arrayFromPool = null;
_pos = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(char value)
{
int position = _pos;
if (position < _span.Length)
{
_span[position] = value;
_pos = position + 1;
return;
}
Grow(1);
_span[_pos++] = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(string? value)
{
if (!string.IsNullOrEmpty(value))
{
Append(value.AsSpan());
}
}
public void Append(ReadOnlySpan<char> value)
{
if (value.Length == 0)
{
return;
}
int remaining = _span.Length - _pos;
if (value.Length <= remaining)
{
value.CopyTo(_span[_pos..]);
_pos += value.Length;
return;
}
Grow(value.Length);
value.CopyTo(_span[_pos..]);
_pos += value.Length;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(int value)
{
AppendSpanFormattable(value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(ulong value)
{
AppendSpanFormattable(value);
}
public ReadOnlySpan<char> AsSpan() => _span[.._pos];
public override string ToString()
{
return new string(_span[.._pos]);
}
public void Dispose()
{
if (_arrayFromPool is { } array)
{
ThreadStaticPools.CharPool.Return(array);
_arrayFromPool = null;
}
_span = Span<char>.Empty;
_pos = 0;
}
private void AppendSpanFormattable<T>(T value) where T : ISpanFormattable
{
int charsWritten;
while (!value.TryFormat(_span[_pos..], out charsWritten, default, CultureInfo.InvariantCulture))
{
Grow(Math.Max(4, _span.Length));
}
_pos += charsWritten;
}
private void Grow(int additionalCapacity)
{
int required = _pos + additionalCapacity;
int currentLength = _span.Length;
int newCapacity = currentLength == 0 ? Math.Max(4, required) : Math.Max(currentLength * 2, required);
char[] newArray = ThreadStaticPools.CharPool.Rent(newCapacity);
_span[.._pos].CopyTo(newArray.AsSpan());
if (_arrayFromPool is { } toReturn)
{
ThreadStaticPools.CharPool.Return(toReturn);
}
_span = newArray;
_arrayFromPool = newArray;
}
}
+38
View File
@@ -1,3 +1,6 @@
using System;
using System.Text;
namespace PerfectNumbers.Core;
/// <summary>
@@ -70,6 +73,41 @@ public sealed class TextFileWriter : IDisposable
}
}
public void WriteLine(ReadOnlySpan<char> line)
{
lock (_lock)
{
StreamWriter writer = TextWriter;
writer.Write(line);
writer.WriteLine();
writer.Flush();
}
}
public void Write(ReadOnlySpan<char> text)
{
lock (_lock)
{
StreamWriter writer = TextWriter;
writer.Write(text);
writer.Flush();
}
}
public void Write(StringBuilder builder)
{
lock (_lock)
{
StreamWriter writer = TextWriter;
foreach (ReadOnlyMemory<char> chunk in builder.GetChunks())
{
writer.Write(chunk.Span);
}
writer.Flush();
}
}
/// <summary>
/// Executes a binary writing action within the internal lock.
/// </summary>
+12
View File
@@ -101,6 +101,18 @@ namespace PerfectNumbers.Core
}
}
[ThreadStatic]
private static ArrayPool<char>? _charPool;
public static ArrayPool<char> CharPool
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return _charPool ??= ArrayPool<char>.Create();
}
}
[ThreadStatic]
private static List<List<ulong>>? _ulongListPool;
-6
View File
@@ -1,9 +1,3 @@
Results & Output Pipelines
- EvenPerfectBitScanner/Program.cs:615: Replace File.WriteAllText with the pooled TextFileWriter pipeline for the results header.
- EvenPerfectBitScanner/Program.cs:1218: Swap the results filename interpolation for the pooled ValueStringBuilder pipeline.
- EvenPerfectBitScanner/Program.cs:1335: Move PrintResult to a Utf8Formatter-based pipeline instead of renting StringBuilders per line.
- EvenPerfectBitScanner/Program.cs:1396: Replace per-flush FileStream/StreamWriter allocations with a pooled TextFileWriter-style helper.
Arithmetic Helpers & Bit Statistics
- EvenPerfectBitScanner/Program.cs:1483: Remove the CountOnes wrapper once callers invoke BitOperations.PopCount directly.
- EvenPerfectBitScanner/Program.cs:1785: Replace the byte-by-byte zero statistics scan with the lookup-table collector validated in the BitStats benchmarks.