diff --git a/EvenPerfectBitScanner/Program.cs b/EvenPerfectBitScanner/Program.cs index d11fb46..7cefd12 100644 --- a/EvenPerfectBitScanner/Program.cs +++ b/EvenPerfectBitScanner/Program.cs @@ -44,6 +44,7 @@ internal static class Program private static string? _resultsPrefix; private static readonly Optimized PrimeIterator = new(); private static ulong _byDivisorStartPrime; + private static TimeSpan? _primeTestLimit; [ThreadStatic] private static bool _lastCompositeP; @@ -181,6 +182,17 @@ internal static class Program { mersenneOnGpu = !arg.AsSpan(arg.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase); } + else if (arg.StartsWith("--prime-test-limit=", StringComparison.OrdinalIgnoreCase)) + { + ReadOnlySpan value = arg.AsSpan(arg.IndexOf('=') + 1); + if (!TryParsePrimeTestLimit(value, out TimeSpan parsedLimitValue)) + { + Console.WriteLine("Failed to parse --prime-test-limit value."); + return; + } + + _primeTestLimit = parsedLimitValue; + } else if (arg.Equals("--workaround-mod", StringComparison.OrdinalIgnoreCase)) { useModuloWorkaround = true; @@ -667,7 +679,7 @@ internal static class Program if (_useByDivisorMode) { - RunByDivisorMode(byDivisorCandidates, threadCount); + RunByDivisorMode(byDivisorCandidates, threadCount, _primeTestLimit); FlushBuffer(); StringBuilderPool.Return(_outputBuilder!); return; @@ -808,7 +820,7 @@ internal static class Program return candidates; } - private static void RunByDivisorMode(List candidates, int threadCount) + private static void RunByDivisorMode(List candidates, int threadCount, TimeSpan? primeTestLimit) { if (_byDivisorTester is null) { @@ -825,7 +837,8 @@ internal static class Program static () => _lastCompositeP = true, static () => _lastCompositeP = false, PrintResult, - threadCount); + threadCount, + primeTestLimit); } private static unsafe void LoadResultsFile(string resultsFileName, Action lineProcessorAction) @@ -892,6 +905,51 @@ internal static class Program } } + private static bool TryParsePrimeTestLimit(ReadOnlySpan value, out TimeSpan result) + { + result = TimeSpan.Zero; + if (value.IsEmpty) + { + return false; + } + + ReadOnlySpan magnitudeSpan = value; + double magnitude; + + if (value.Length >= 2 && value.EndsWith("ms", StringComparison.OrdinalIgnoreCase)) + { + magnitudeSpan = value[..^2]; + if (!double.TryParse(magnitudeSpan, NumberStyles.Float, CultureInfo.InvariantCulture, out magnitude)) + { + return false; + } + + result = TimeSpan.FromMilliseconds(magnitude); + return true; + } + + char suffix = char.ToLowerInvariant(value[^1]); + if (suffix == 's' || suffix == 'm') + { + magnitudeSpan = value[..^1]; + if (!double.TryParse(magnitudeSpan, NumberStyles.Float, CultureInfo.InvariantCulture, out magnitude)) + { + return false; + } + + result = suffix == 's' ? TimeSpan.FromSeconds(magnitude) : TimeSpan.FromMinutes(magnitude); + return true; + } + + if (!double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out magnitude)) + { + return false; + } + + result = TimeSpan.FromMilliseconds(magnitude); + return true; + } + private static void PrintHelp() { Console.WriteLine("Usage: EvenPerfectBitScanner [options]"); @@ -925,6 +983,7 @@ internal static class Program Console.WriteLine(" --divisor-cycles-batch= batch size for cycles generation (default 512)"); Console.WriteLine(" --divisor-cycles-continue continue divisor cycles generation"); Console.WriteLine(" --divisor-cycles-limit= cycle search iterations when --mersenne=divisor"); + Console.WriteLine(" --prime-test-limit= Time limit for --mersenne=bydivisor prime checks (e.g. 5s, 500ms)"); Console.WriteLine(" --use-order test primality via q order"); Console.WriteLine(" --workaround-mod avoid '%' operator on the GPU"); // mod-automaton removed diff --git a/PerfectNumbers.Core.Tests/MersenneNumberDivisorByDivisorTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberDivisorByDivisorTesterTests.cs index 42b4068..176d34b 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberDivisorByDivisorTesterTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberDivisorByDivisorTesterTests.cs @@ -26,7 +26,8 @@ public class MersenneNumberDivisorByDivisorTesterTests markComposite: () => compositeMarks++, clearComposite: () => clearedMarks++, printResult: (prime, searched, detailed, passed) => results.Add((prime, searched, detailed, passed)), - threadCount: 1); + threadCount: 1, + primeTestLimit: null); tester.ConfiguredMaxPrime.Should().Be(13UL); compositeMarks.Should().Be(2); @@ -59,7 +60,7 @@ public class MersenneNumberDivisorByDivisorTesterTests return DivisorLimit; } - public bool IsPrime(ulong prime, out bool divisorsExhausted) + public bool IsPrime(ulong prime, out bool divisorsExhausted, TimeSpan? timeLimit = null) { divisorsExhausted = true; return prime % 7UL != 0UL && prime % 11UL != 0UL; diff --git a/PerfectNumbers.Core.Tests/MersenneNumberDivisorCpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberDivisorCpuTesterTests.cs index a3a34a3..43850f8 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberDivisorCpuTesterTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberDivisorCpuTesterTests.cs @@ -7,78 +7,88 @@ namespace PerfectNumbers.Core.Tests; public class MersenneNumberDivisorCpuTesterTests { - [Fact] - [Trait("Category", "Fast")] - public void ByDivisor_tester_tracks_divisors_across_primes() - { - var tester = new MersenneNumberDivisorByDivisorCpuTester(); - tester.ConfigureFromMaxPrime(11UL); + [Fact] + [Trait("Category", "Fast")] + public void ByDivisor_tester_tracks_divisors_across_primes() + { + var tester = new MersenneNumberDivisorByDivisorCpuTester(); + tester.ConfigureFromMaxPrime(11UL); - tester.IsPrime(5UL, out bool divisorsExhausted).Should().BeTrue(); - divisorsExhausted.Should().BeTrue(); + tester.IsPrime(5UL, out bool divisorsExhausted).Should().BeTrue(); + divisorsExhausted.Should().BeTrue(); - tester.IsPrime(7UL, out divisorsExhausted).Should().BeTrue(); - divisorsExhausted.Should().BeTrue(); + tester.IsPrime(7UL, out divisorsExhausted).Should().BeTrue(); + divisorsExhausted.Should().BeTrue(); - tester.IsPrime(11UL, out divisorsExhausted).Should().BeFalse(); - divisorsExhausted.Should().BeTrue(); - } + tester.IsPrime(11UL, out divisorsExhausted).Should().BeFalse(); + divisorsExhausted.Should().BeTrue(); + } - [Fact] - [Trait("Category", "Fast")] - public void ByDivisor_session_checks_divisors_across_primes() - { - var tester = new MersenneNumberDivisorByDivisorCpuTester(); - tester.ConfigureFromMaxPrime(13UL); + [Fact] + [Trait("Category", "Fast")] + public void ByDivisor_prime_test_limit_disables_divisor_exhaustion() + { + var tester = new MersenneNumberDivisorByDivisorCpuTester(); + tester.ConfigureFromMaxPrime(11UL); - using var session = tester.CreateDivisorSession(); - ulong[] primes = { 5UL, 7UL, 11UL, 13UL }; - byte[] hits = new byte[primes.Length]; + tester.IsPrime(5UL, out bool divisorsExhausted, TimeSpan.Zero).Should().BeTrue(); + divisorsExhausted.Should().BeFalse(); + } - ulong cycle23 = MersenneDivisorCycles.CalculateCycleLength(23UL, MontgomeryDivisorData.FromModulus(23UL)); - session.CheckDivisor(23UL, MontgomeryDivisorData.FromModulus(23UL), cycle23, primes, hits); - hits.Should().ContainInOrder(new byte[] { 0, 0, 1, 0 }); + [Fact] + [Trait("Category", "Fast")] + public void ByDivisor_session_checks_divisors_across_primes() + { + var tester = new MersenneNumberDivisorByDivisorCpuTester(); + tester.ConfigureFromMaxPrime(13UL); - Array.Fill(hits, (byte)0); - ulong cycle31 = MersenneDivisorCycles.CalculateCycleLength(31UL, MontgomeryDivisorData.FromModulus(31UL)); - session.CheckDivisor(31UL, MontgomeryDivisorData.FromModulus(31UL), cycle31, primes, hits); - hits.Should().ContainInOrder(new byte[] { 1, 0, 0, 0 }); - } + using var session = tester.CreateDivisorSession(); + ulong[] primes = { 5UL, 7UL, 11UL, 13UL }; + byte[] hits = new byte[primes.Length]; - [Fact] - [Trait("Category", "Fast")] - public void ByDivisor_session_marks_mersenne_numbers_divisible_by_seven_as_composite() - { - var tester = new MersenneNumberDivisorByDivisorCpuTester(); - tester.ConfigureFromMaxPrime(43UL); + ulong cycle23 = MersenneDivisorCycles.CalculateCycleLength(23UL, MontgomeryDivisorData.FromModulus(23UL)); + session.CheckDivisor(23UL, MontgomeryDivisorData.FromModulus(23UL), cycle23, primes, hits); + hits.Should().ContainInOrder(new byte[] { 0, 0, 1, 0 }); - using var session = tester.CreateDivisorSession(); - ulong[] exponents = { 6UL, 7UL, 9UL, 10UL, 12UL }; - byte[] hits = new byte[exponents.Length]; + Array.Fill(hits, (byte)0); + ulong cycle31 = MersenneDivisorCycles.CalculateCycleLength(31UL, MontgomeryDivisorData.FromModulus(31UL)); + session.CheckDivisor(31UL, MontgomeryDivisorData.FromModulus(31UL), cycle31, primes, hits); + hits.Should().ContainInOrder(new byte[] { 1, 0, 0, 0 }); + } - MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(7UL); - ulong cycle = MersenneDivisorCycles.CalculateCycleLength(7UL, divisorData); - session.CheckDivisor(7UL, divisorData, cycle, exponents, hits); + [Fact] + [Trait("Category", "Fast")] + public void ByDivisor_session_marks_mersenne_numbers_divisible_by_seven_as_composite() + { + var tester = new MersenneNumberDivisorByDivisorCpuTester(); + tester.ConfigureFromMaxPrime(43UL); - hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 }); - } + using var session = tester.CreateDivisorSession(); + ulong[] exponents = { 6UL, 7UL, 9UL, 10UL, 12UL }; + byte[] hits = new byte[exponents.Length]; - [Fact] - [Trait("Category", "Fast")] - public void ByDivisor_session_marks_mersenne_numbers_divisible_by_eleven_as_composite() - { - var tester = new MersenneNumberDivisorByDivisorCpuTester(); - tester.ConfigureFromMaxPrime(61UL); + MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(7UL); + ulong cycle = MersenneDivisorCycles.CalculateCycleLength(7UL, divisorData); + session.CheckDivisor(7UL, divisorData, cycle, exponents, hits); - using var session = tester.CreateDivisorSession(); - ulong[] exponents = { 10UL, 11UL, 20UL, 21UL, 30UL }; - byte[] hits = new byte[exponents.Length]; + hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 }); + } - MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(11UL); - ulong cycle = MersenneDivisorCycles.CalculateCycleLength(11UL, divisorData); - session.CheckDivisor(11UL, divisorData, cycle, exponents, hits); + [Fact] + [Trait("Category", "Fast")] + public void ByDivisor_session_marks_mersenne_numbers_divisible_by_eleven_as_composite() + { + var tester = new MersenneNumberDivisorByDivisorCpuTester(); + tester.ConfigureFromMaxPrime(61UL); - hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 }); - } + using var session = tester.CreateDivisorSession(); + ulong[] exponents = { 10UL, 11UL, 20UL, 21UL, 30UL }; + byte[] hits = new byte[exponents.Length]; + + MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(11UL); + ulong cycle = MersenneDivisorCycles.CalculateCycleLength(11UL, divisorData); + session.CheckDivisor(11UL, divisorData, cycle, exponents, hits); + + hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 }); + } } - diff --git a/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs b/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs index 3d36431..aceaea7 100644 --- a/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneNumberDivisorGpuTesterTests.cs @@ -86,6 +86,17 @@ public class MersenneNumberDivisorGpuTesterTests exhausted.Should().BeTrue(); } + [Fact] + [Trait("Category", "Fast")] + public void ByDivisor_gpu_prime_test_limit_stops_scan_immediately() + { + var tester = new MersenneNumberDivisorByDivisorGpuTester(); + tester.ConfigureFromMaxPrime(43UL); + + tester.IsPrime(31UL, out bool divisorsExhausted, TimeSpan.Zero).Should().BeTrue(); + divisorsExhausted.Should().BeFalse(); + } + [Fact] [Trait("Category", "Fast")] public void ByDivisor_tester_requires_configuration() diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs index d4d74cd..d269982 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs @@ -61,7 +61,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv } } - public bool IsPrime(ulong prime, out bool divisorsExhausted) + public bool IsPrime(ulong prime, out bool divisorsExhausted, TimeSpan? timeLimit = null) { ulong allowedMax; lock (_sync) @@ -89,7 +89,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv allowedMax, out lastProcessed, out processedAll, - out processedCount); + out processedCount, + timeLimit); if (processedCount > 0UL) { @@ -163,12 +164,22 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv ulong allowedMax, out ulong lastProcessed, out bool processedAll, - out ulong processedCount) + out ulong processedCount, + TimeSpan? timeLimit) { lastProcessed = 0UL; processedCount = 0UL; processedAll = false; + PrimeTestTimeLimit limitGuard; + if (!PrimeTestTimeLimit.TryCreate(timeLimit, out limitGuard)) + { + return false; + } + + bool enforceLimit = limitGuard.IsActive; + ulong iterationCounter = 0UL; + if (allowedMax < 3UL) { return false; @@ -216,6 +227,13 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv while (divisor <= limit) { + if (enforceLimit && (iterationCounter == 0UL || (iterationCounter & 63UL) == 0UL) && limitGuard.HasExpired()) + { + processedAll = false; + return false; + } + + iterationCounter++; ulong candidate = (ulong)divisor; processedCount++; lastProcessed = candidate; diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs index a67ce8f..947b95e 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs @@ -71,7 +71,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv } } - public bool IsPrime(ulong prime, out bool divisorsExhausted) + public bool IsPrime(ulong prime, out bool divisorsExhausted, TimeSpan? timeLimit = null) { ulong allowedMax; int batchCapacity; @@ -108,6 +108,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv composite = CheckDivisors( prime, allowedMax, + timeLimit, accelerator, kernel, resources.DivisorsBuffer, @@ -175,6 +176,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv private static bool CheckDivisors( ulong prime, ulong allowedMax, + TimeSpan? timeLimit, Accelerator accelerator, Action, ArrayView, ArrayView> kernel, MemoryBuffer1D divisorsBuffer, @@ -202,6 +204,15 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv bool processedAll = false; processedCount = 0UL; lastProcessed = 0UL; + coveredRange = false; + + PrimeTestTimeLimit limitGuard; + if (!PrimeTestTimeLimit.TryCreate(timeLimit, out limitGuard)) + { + return false; + } + + bool enforceLimit = limitGuard.IsActive; Span divisorDataSpan; Span divisorSpan; @@ -277,6 +288,12 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv { while (currentK <= maxK) { + if (enforceLimit && limitGuard.HasExpired()) + { + processedAll = false; + return false; + } + int chunkCount = Math.Min(chunkCapacity, batchCapacity); ulong remainingK = maxK - currentK + 1UL; if ((ulong)chunkCount > remainingK) diff --git a/PerfectNumbers.Core/IMersenneNumberDivisorByDivisorTester.cs b/PerfectNumbers.Core/IMersenneNumberDivisorByDivisorTester.cs index e700843..d452476 100644 --- a/PerfectNumbers.Core/IMersenneNumberDivisorByDivisorTester.cs +++ b/PerfectNumbers.Core/IMersenneNumberDivisorByDivisorTester.cs @@ -12,7 +12,7 @@ public interface IMersenneNumberDivisorByDivisorTester ulong GetAllowedMaxDivisor(ulong prime); - bool IsPrime(ulong prime, out bool divisorsExhausted); + bool IsPrime(ulong prime, out bool divisorsExhausted, TimeSpan? timeLimit = null); IDivisorScanSession CreateDivisorSession(); diff --git a/PerfectNumbers.Core/MersenneNumberDivisorByDivisorTester.cs b/PerfectNumbers.Core/MersenneNumberDivisorByDivisorTester.cs index f5e07a9..82b9e18 100644 --- a/PerfectNumbers.Core/MersenneNumberDivisorByDivisorTester.cs +++ b/PerfectNumbers.Core/MersenneNumberDivisorByDivisorTester.cs @@ -18,7 +18,8 @@ public static class MersenneNumberDivisorByDivisorTester Action markComposite, Action clearComposite, Action printResult, - int threadCount) + int threadCount, + TimeSpan? primeTestLimit) { if (candidates.Count == 0) { @@ -243,7 +244,7 @@ public static class MersenneNumberDivisorByDivisorTester void ProcessPrime(ulong prime) { - bool isPrime = tester.IsPrime(prime, out bool divisorsExhausted); + bool isPrime = tester.IsPrime(prime, out bool divisorsExhausted, primeTestLimit); if (!isPrime) { diff --git a/PerfectNumbers.Core/PrimeTestTimeLimit.cs b/PerfectNumbers.Core/PrimeTestTimeLimit.cs new file mode 100644 index 0000000..29bbcc5 --- /dev/null +++ b/PerfectNumbers.Core/PrimeTestTimeLimit.cs @@ -0,0 +1,72 @@ +using System; +using System.Diagnostics; + +namespace PerfectNumbers.Core; + +internal readonly struct PrimeTestTimeLimit +{ + private readonly long _deadlineTimestamp; + + private PrimeTestTimeLimit(bool isActive, long deadlineTimestamp) + { + IsActive = isActive; + _deadlineTimestamp = deadlineTimestamp; + } + + internal bool IsActive { get; } + + internal static bool TryCreate(TimeSpan? timeLimit, out PrimeTestTimeLimit limit) + { + if (!timeLimit.HasValue) + { + limit = new PrimeTestTimeLimit(isActive: false, deadlineTimestamp: long.MaxValue); + return true; + } + + TimeSpan limitValue = timeLimit.GetValueOrDefault(); + if (limitValue <= TimeSpan.Zero) + { + limit = new PrimeTestTimeLimit(isActive: true, deadlineTimestamp: long.MinValue); + return false; + } + + double seconds = limitValue.TotalSeconds; + if (double.IsInfinity(seconds)) + { + limit = new PrimeTestTimeLimit(isActive: true, deadlineTimestamp: long.MaxValue); + return true; + } + + double scaled = seconds * Stopwatch.Frequency; + long limitTicks = scaled >= long.MaxValue ? long.MaxValue : (long)scaled; + long startTimestamp = Stopwatch.GetTimestamp(); + long deadline = startTimestamp <= long.MaxValue - limitTicks ? startTimestamp + limitTicks : long.MaxValue; + if (deadline < startTimestamp) + { + deadline = long.MaxValue; + } + + limit = new PrimeTestTimeLimit(isActive: true, deadlineTimestamp: deadline); + return true; + } + + internal bool HasExpired() + { + if (!IsActive) + { + return false; + } + + if (_deadlineTimestamp == long.MaxValue) + { + return false; + } + + if (_deadlineTimestamp == long.MinValue) + { + return true; + } + + return Stopwatch.GetTimestamp() >= _deadlineTimestamp; + } +}