From ca0cadc8a3bc9abec3642a55effa2823208a8d74 Mon Sep 17 00:00:00 2001 From: mlemanczyk Date: Fri, 10 Oct 2025 09:26:25 +0200 Subject: [PATCH] Various optimizations and cleanups. "in" removed from GpuUInt128 to avoid defensive copies from IL. --- .../GpuPow2ModBenchmarks.cs | 2 +- .../Pow2MontgomeryModBenchmarks.cs | 42 +- ...MontgomeryModCycleComputationBenchmarks.cs | 85 +++- .../Pow2MontgomeryModUInt128Benchmarks.cs | 6 +- ...MersenneNumberDivisorByDivisorCpuTester.cs | 27 +- .../ExponentRemainderStepper.cs | 43 +-- PerfectNumbers.Core/FactorEntry.cs | 8 + PerfectNumbers.Core/Gpu/GpuUInt128.cs | 24 +- ...MersenneNumberDivisorByDivisorGpuTester.cs | 4 +- .../Gpu/PrimeOrderGpuHeuristics.cs | 8 +- PerfectNumbers.Core/HeuristicFailureLog.cs | 98 +++++ PerfectNumbers.Core/HeuristicFailureReason.cs | 11 + PerfectNumbers.Core/MersenneDivisorCycles.cs | 24 +- PerfectNumbers.Core/PartialFactorResult.cs | 76 ++++ PerfectNumbers.Core/PerfectNumberConstants.cs | 1 + PerfectNumbers.Core/PrimeOrderCalculator.cs | 362 ++++-------------- PerfectNumbers.Core/UInt128Extensions.cs | 14 +- PerfectNumbers.Core/ULongExtensions.cs | 125 +++++- 18 files changed, 565 insertions(+), 395 deletions(-) create mode 100644 PerfectNumbers.Core/FactorEntry.cs create mode 100644 PerfectNumbers.Core/HeuristicFailureLog.cs create mode 100644 PerfectNumbers.Core/HeuristicFailureReason.cs create mode 100644 PerfectNumbers.Core/PartialFactorResult.cs diff --git a/EvenPerfectBitScanner.Benchmarks/GpuPow2ModBenchmarks.cs b/EvenPerfectBitScanner.Benchmarks/GpuPow2ModBenchmarks.cs index 090c163..238943e 100644 --- a/EvenPerfectBitScanner.Benchmarks/GpuPow2ModBenchmarks.cs +++ b/EvenPerfectBitScanner.Benchmarks/GpuPow2ModBenchmarks.cs @@ -50,7 +50,7 @@ public class GpuPow2ModBenchmarks return Pow2ModSingleBit(Input.Exponent, Input.Modulus); } - private static GpuUInt128 Pow2ModSingleBit(ulong exponent, in GpuUInt128 modulus) + private static GpuUInt128 Pow2ModSingleBit(ulong exponent, GpuUInt128 modulus) { if (modulus.IsZero || modulus == GpuUInt128.One) { diff --git a/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModBenchmarks.cs b/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModBenchmarks.cs index 48c2050..2504a3e 100644 --- a/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModBenchmarks.cs +++ b/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModBenchmarks.cs @@ -52,14 +52,31 @@ public class Pow2MontgomeryModBenchmarks /// Baseline right-to-left Montgomery ladder; measured 36.36 μs on the small sample set and 117.32 μs on the large one. /// [Benchmark(Baseline = true)] - public ulong Baseline() + public ulong BaselineCpu() { GetData(out var exponents, out var divisors, out _); ulong checksum = 0UL; for (int i = 0; i < SampleCount; i++) { - checksum ^= exponents[i].Pow2MontgomeryModWindowed(divisors[i], keepMontgomery: false); + checksum ^= exponents[i].Pow2MontgomeryModWindowedCpu(divisors[i], keepMontgomery: false); + } + + return checksum; + } + + /// + /// Baseline right-to-left Montgomery ladder; measured ??.?? μs on the small sample set and ??.?? μs on the large one. + /// + [Benchmark] + public ulong BaselineGpu() + { + GetData(out var exponents, out var divisors, out _); + ulong checksum = 0UL; + + for (int i = 0; i < SampleCount; i++) + { + checksum ^= exponents[i].Pow2MontgomeryModWindowedGpu(divisors[i], keepMontgomery: false); } return checksum; @@ -69,14 +86,31 @@ public class Pow2MontgomeryModBenchmarks /// Uses a known divisor cycle to fold the exponent first, dropping runtimes to 26.04 μs on small inputs (0.72× baseline) and 9.53 μs on large inputs (0.08× baseline). /// [Benchmark] - public ulong BaselineWithKnownCycle() + public ulong BaselineWithKnownCycleCpu() { GetData(out var exponents, out var divisors, out var cycles); ulong checksum = 0UL; for (int i = 0; i < SampleCount; i++) { - checksum ^= exponents[i].Pow2MontgomeryModWithCycle(cycles[i], divisors[i]); + checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycles[i], divisors[i]); + } + + return checksum; + } + + /// + /// Uses a known divisor cycle to fold the exponent first, dropping runtimes to 26.04 μs on small inputs (0.72× baseline) and 9.53 μs on large inputs (0.08× baseline). + /// + [Benchmark] + public ulong BaselineWithKnownCycleGpu() + { + GetData(out var exponents, out var divisors, out var cycles); + ulong checksum = 0UL; + + for (int i = 0; i < SampleCount; i++) + { + checksum ^= exponents[i].Pow2MontgomeryModWithCycleGpu(cycles[i], divisors[i]); } return checksum; diff --git a/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModCycleComputationBenchmarks.cs b/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModCycleComputationBenchmarks.cs index 7bee614..f4f8aef 100644 --- a/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModCycleComputationBenchmarks.cs +++ b/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModCycleComputationBenchmarks.cs @@ -89,14 +89,32 @@ public class Pow2MontgomeryModCycleComputationBenchmarks /// large set. /// // [Benchmark(Baseline = true)] - public ulong MontgomeryWithoutCycle() + public ulong MontgomeryWithoutCycleCpu() { GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _); ulong checksum = 0UL; for (int i = 0; i < SampleCount; i++) { - checksum ^= exponents[i].Pow2MontgomeryModWindowed(divisors[i], keepMontgomery: false); + checksum ^= exponents[i].Pow2MontgomeryModWindowedCpu(divisors[i], keepMontgomery: false); + } + + return checksum; + } + + /// + /// Baseline Montgomery reduction without precomputed cycle data; measured 35.41 μs on the small sample and 117.33 μs on the + /// large set. + /// + // [Benchmark(Baseline = true)] + public ulong MontgomeryWithoutCycleGpu() + { + GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _); + ulong checksum = 0UL; + + for (int i = 0; i < SampleCount; i++) + { + checksum ^= exponents[i].Pow2MontgomeryModWindowedGpu(divisors[i], keepMontgomery: false); } return checksum; @@ -107,14 +125,32 @@ public class Pow2MontgomeryModCycleComputationBenchmarks /// on the large one (0.08×). /// [Benchmark] - public ulong MontgomeryWithPrecomputedCycle() + public ulong MontgomeryWithPrecomputedCycleCpu() { GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out ulong[] cycles); ulong checksum = 0UL; for (int i = 0; i < SampleCount; i++) { - checksum ^= exponents[i].Pow2MontgomeryModWithCycle(cycles[i], divisors[i]); + checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycles[i], divisors[i]); + } + + return checksum; + } + + /// + /// Montgomery reduction with a known cycle length (cycle lookup only); 25.81 μs on the small set (0.73× baseline) and 9.12 μs + /// on the large one (0.08×). + /// + [Benchmark] + public ulong MontgomeryWithPrecomputedCycleGpu() + { + GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out ulong[] cycles); + ulong checksum = 0UL; + + for (int i = 0; i < SampleCount; i++) + { + checksum ^= exponents[i].Pow2MontgomeryModWithCycleGpu(cycles[i], divisors[i]); } return checksum; @@ -125,7 +161,7 @@ public class Pow2MontgomeryModCycleComputationBenchmarks /// cycle discovery, but drops to 12.91 μs on the large set (0.11× baseline). /// // [Benchmark] - public ulong MontgomeryWithGpuCycleComputation() + public ulong MontgomeryWithGpuCycleComputationCpu() { GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _); ulong checksum = 0UL; @@ -133,15 +169,33 @@ public class Pow2MontgomeryModCycleComputationBenchmarks for (int i = 0; i < SampleCount; i++) { ulong cycle = MersenneDivisorCycles.CalculateCycleLengthGpu(divisors[i].Modulus); - checksum ^= exponents[i].Pow2MontgomeryModWithCycle(cycle, divisors[i]); + checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycle, divisors[i]); } return checksum; } + /// + /// Montgomery reduction with the cycle computed on the fly using the GPU helper; costs 54.09 ms on the small benchmark due to + /// cycle discovery, but drops to 12.91 μs on the large set (0.11× baseline). + /// + // [Benchmark] + public ulong MontgomeryWithGpuCycleComputationGpu() + { + GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _); + ulong checksum = 0UL; + + for (int i = 0; i < SampleCount; i++) + { + ulong cycle = MersenneDivisorCycles.CalculateCycleLengthGpu(divisors[i].Modulus); + checksum ^= exponents[i].Pow2MontgomeryModWithCycleGpu(cycle, divisors[i]); + } + + return checksum; + } [Benchmark] - public ulong MontgomeryWithHeuristicCycleComputation() + public ulong MontgomeryWithHeuristicCycleComputationCpu() { GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _); ulong checksum = 0UL; @@ -149,7 +203,22 @@ public class Pow2MontgomeryModCycleComputationBenchmarks for (int i = 0; i < SampleCount; i++) { ulong cycle = CalculateCycleLengthWithHeuristics(divisors[i].Modulus); - checksum ^= exponents[i].Pow2MontgomeryModWithCycle(cycle, divisors[i]); + checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycle, divisors[i]); + } + + return checksum; + } + + [Benchmark] + public ulong MontgomeryWithHeuristicCycleComputationGpu() + { + GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _); + ulong checksum = 0UL; + + for (int i = 0; i < SampleCount; i++) + { + ulong cycle = CalculateCycleLengthWithHeuristics(divisors[i].Modulus); + checksum ^= exponents[i].Pow2MontgomeryModWithCycleGpu(cycle, divisors[i]); } return checksum; diff --git a/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModUInt128Benchmarks.cs b/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModUInt128Benchmarks.cs index 440cd96..d3ad626 100644 --- a/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModUInt128Benchmarks.cs +++ b/EvenPerfectBitScanner.Benchmarks/Pow2MontgomeryModUInt128Benchmarks.cs @@ -176,7 +176,7 @@ public class Pow2MontgomeryModUInt128Benchmarks return (UInt128)result; } - private static bool ShouldUseSingleBit(in GpuUInt128 exponent) + private static bool ShouldUseSingleBit(GpuUInt128 exponent) { if (exponent.High == 0UL && exponent.Low <= Pow2WindowFallbackThreshold) { @@ -257,7 +257,7 @@ public class Pow2MontgomeryModUInt128Benchmarks return result; } - private static bool IsBitSet(in GpuUInt128 value, int bitIndex) + private static bool IsBitSet(GpuUInt128 value, int bitIndex) { if (bitIndex >= 64) { @@ -267,7 +267,7 @@ public class Pow2MontgomeryModUInt128Benchmarks return ((value.Low >> bitIndex) & 1UL) != 0UL; } - private static ulong ExtractWindowValue(in GpuUInt128 exponent, int windowStart, int windowBitCount) + private static ulong ExtractWindowValue(GpuUInt128 exponent, int windowStart, int windowBitCount) { if (windowStart != 0) { diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs index 41da801..101f215 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs @@ -1,15 +1,12 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Numerics; -using PerfectNumbers.Core; namespace PerfectNumbers.Core.Cpu; public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDivisorByDivisorTester { private readonly object _sync = new(); - private readonly ConcurrentBag _sessionPool = new(); + private readonly ConcurrentBag _sessionPool = []; private ulong _divisorLimit; private ulong _lastStatusDivisor; private bool _isConfigured; @@ -213,7 +210,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv ulong mersenne; if (prime <= 63UL) { - mersenne = (1UL << (int)prime) - 1UL; + mersenne = (1UL << (int)prime) - 1UL; hasExactMersenne = true; } else if (prime == 64UL) @@ -249,7 +246,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv else { factorCache ??= new Dictionary(8); - if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponent(candidate, prime, divisorData, factorCache, out divisorCycle) || divisorCycle == 0UL) + if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponentCpu(candidate, prime, divisorData, factorCache, out divisorCycle) || divisorCycle == 0UL) { divisorCycle = cycleCache.GetCycleLength(candidate); } @@ -287,12 +284,12 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv UInt128 incrementAmount = step * increments; divisor += incrementAmount; - remainder10 = AdvanceRemainder(remainder10, step10, (byte)10, increments); - remainder8 = AdvanceRemainder(remainder8, step8, (byte)8, increments); - remainder5 = AdvanceRemainder(remainder5, step5, (byte)5, increments); - remainder3 = AdvanceRemainder(remainder3, step3, (byte)3, increments); - remainder7 = AdvanceRemainder(remainder7, step7, (byte)7, increments); - remainder11 = AdvanceRemainder(remainder11, step11, (byte)11, increments); + remainder10 = AdvanceRemainder(remainder10, step10, 10, increments); + remainder8 = AdvanceRemainder(remainder8, step8, 8, increments); + remainder5 = AdvanceRemainder(remainder5, step5, 5, increments); + remainder3 = AdvanceRemainder(remainder3, step3, 3, increments); + remainder7 = AdvanceRemainder(remainder7, step7, 7, increments); + remainder11 = AdvanceRemainder(remainder11, step11, 11, increments); } processedAll = divisor > limit; @@ -301,7 +298,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv private static byte CheckDivisor(ulong prime, ulong divisorCycle, in MontgomeryDivisorData divisorData) { - ulong residue = prime.Pow2MontgomeryModWithCycle(divisorCycle, divisorData); + ulong residue = prime.Pow2MontgomeryModWithCycleCpu(divisorCycle, divisorData); return residue == 1UL ? (byte)1 : (byte)0; } @@ -323,8 +320,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv return remainder; } - ulong modulusValue = (ulong)modulus; - ulong scaledDelta = ((ulong)delta * (increments % modulusValue)) % modulusValue; + ulong modulusValue = modulus; + ulong scaledDelta = (delta * (increments % modulusValue)) % modulusValue; byte reducedDelta = (byte)scaledDelta; int sum = remainder + reducedDelta; if (sum >= modulus) diff --git a/PerfectNumbers.Core/ExponentRemainderStepper.cs b/PerfectNumbers.Core/ExponentRemainderStepper.cs index 39fb669..4d84ec3 100644 --- a/PerfectNumbers.Core/ExponentRemainderStepper.cs +++ b/PerfectNumbers.Core/ExponentRemainderStepper.cs @@ -1,31 +1,20 @@ namespace PerfectNumbers.Core; -internal struct ExponentRemainderStepper +internal struct ExponentRemainderStepper(in MontgomeryDivisorData divisor) { - private readonly MontgomeryDivisorData _divisor; - private readonly ulong _modulus; - private readonly ulong _nPrime; - private readonly ulong _montgomeryOne; - private ulong _previousExponent; - private ulong _currentMontgomery; - private bool _hasState; + private readonly MontgomeryDivisorData _divisor = divisor; + private readonly ulong _modulus = divisor.Modulus; + private readonly ulong _nPrime = divisor.NPrime; + private readonly ulong _montgomeryOne = divisor.MontgomeryOne; + private ulong _previousExponent = 0UL; + private ulong _currentMontgomery = divisor.MontgomeryOne; + private bool _hasState = false; - public ExponentRemainderStepper(in MontgomeryDivisorData divisor) - { - _divisor = divisor; - _modulus = divisor.Modulus; - _nPrime = divisor.NPrime; - _montgomeryOne = divisor.MontgomeryOne; - _previousExponent = 0UL; - _currentMontgomery = divisor.MontgomeryOne; - _hasState = false; - } + public readonly bool IsValidModulus => _modulus > 1UL && (_modulus & 1UL) != 0UL; - public bool IsValidModulus => _modulus > 1UL && (_modulus & 1UL) != 0UL; + public readonly bool HasState => _hasState; - public bool HasState => _hasState; - - public ulong PreviousExponent => _previousExponent; + public readonly ulong PreviousExponent => _previousExponent; public void Reset() { @@ -47,7 +36,7 @@ internal struct ExponentRemainderStepper // TODO: Route these state resets through the ProcessEightBitWindows helper once the scalar // Pow2MontgomeryMod implementation adopts it so delta stepping inherits the benchmarked // 2× gains for large exponents. - _currentMontgomery = exponent.Pow2MontgomeryModWindowed(_divisor, keepMontgomery: true); + _currentMontgomery = exponent.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true); _previousExponent = exponent; _hasState = true; return ReduceCurrent(); @@ -59,7 +48,7 @@ internal struct ExponentRemainderStepper // cached Montgomery residue ladder highlighted in MersenneDivisorCycleLengthGpuBenchmarks. // TODO: Replace this per-delta powmod with the upcoming windowed ladder so incremental // updates stop paying the single-bit cost highlighted in GpuPow2ModBenchmarks. - ulong multiplier = delta.Pow2MontgomeryModWindowed(_divisor, keepMontgomery: true); + ulong multiplier = delta.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true); _currentMontgomery = _currentMontgomery.MontgomeryMultiply(multiplier, _modulus, _nPrime); _previousExponent = exponent; return ReduceCurrent(); @@ -77,7 +66,7 @@ internal struct ExponentRemainderStepper { // TODO: Switch this reload to the shared windowed pow2 helper once available so CPU // residue checks align with the optimized ProcessEightBitWindows timings. - _currentMontgomery = exponent.Pow2MontgomeryModWindowed(_divisor, keepMontgomery: true); + _currentMontgomery = exponent.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true); _previousExponent = exponent; _hasState = true; return _currentMontgomery == _montgomeryOne; @@ -88,7 +77,7 @@ internal struct ExponentRemainderStepper // lookups so this branch also avoids recomputing powmods when the snapshot lacks the divisor. // TODO: Use the windowed delta pow2 helper here as well to avoid the single-bit ladder that // currently lags behind the benchmarked implementation for huge divisor cycles. - ulong multiplier = delta.Pow2MontgomeryModWindowed(_divisor, keepMontgomery: true); + ulong multiplier = delta.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true); _currentMontgomery = _currentMontgomery.MontgomeryMultiply(multiplier, _modulus, _nPrime); _previousExponent = exponent; return _currentMontgomery == _montgomeryOne; @@ -123,7 +112,7 @@ internal struct ExponentRemainderStepper { // TODO: Replace this fallback path with the upcoming ProcessEightBitWindows helper so // fresh Montgomery states also benefit from the faster pow2 ladder measured on CPUs. - _currentMontgomery = exponent.Pow2MontgomeryModWindowed(_divisor, keepMontgomery: true); + _currentMontgomery = exponent.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true); _previousExponent = exponent; _hasState = true; isUnity = _currentMontgomery == _montgomeryOne; diff --git a/PerfectNumbers.Core/FactorEntry.cs b/PerfectNumbers.Core/FactorEntry.cs new file mode 100644 index 0000000..e4ba3be --- /dev/null +++ b/PerfectNumbers.Core/FactorEntry.cs @@ -0,0 +1,8 @@ +namespace PerfectNumbers.Core +{ + public readonly struct FactorEntry(ulong value, int exponent) + { + public readonly ulong Value = value; + public readonly int Exponent = exponent; + } +} \ No newline at end of file diff --git a/PerfectNumbers.Core/Gpu/GpuUInt128.cs b/PerfectNumbers.Core/Gpu/GpuUInt128.cs index cee3beb..e3e64f8 100644 --- a/PerfectNumbers.Core/Gpu/GpuUInt128.cs +++ b/PerfectNumbers.Core/Gpu/GpuUInt128.cs @@ -267,7 +267,7 @@ public struct GpuUInt128 : IComparable, IEquatable public void Add(in ReadOnlyGpuUInt128 other) => AddInternal(other.High, other.Low); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(in GpuUInt128 other) => AddInternal(other.High, other.Low); + public void Add(GpuUInt128 other) => AddInternal(other.High, other.Low); [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddInternal(ulong otherHigh, ulong otherLow) @@ -294,17 +294,17 @@ public struct GpuUInt128 : IComparable, IEquatable public void AddMod(in ReadOnlyGpuUInt128 value, in ReadOnlyGpuUInt128 modulus) => AddModInternal(value.High, value.Low, in modulus); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddMod(in GpuUInt128 value, in ReadOnlyGpuUInt128 modulus) => AddModInternal(value.High, value.Low, in modulus); + public void AddMod(GpuUInt128 value, in ReadOnlyGpuUInt128 modulus) => AddModInternal(value.High, value.Low, in modulus); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddMod(in ReadOnlyGpuUInt128 value, in GpuUInt128 modulus) + public void AddMod(in ReadOnlyGpuUInt128 value, GpuUInt128 modulus) { ReadOnlyGpuUInt128 readOnlyMod = modulus.AsReadOnly(); AddModInternal(value.High, value.Low, in readOnlyMod); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddMod(in GpuUInt128 value, in GpuUInt128 modulus) + public void AddMod(GpuUInt128 value, GpuUInt128 modulus) { ReadOnlyGpuUInt128 readOnlyMod = modulus.AsReadOnly(); AddModInternal(value.High, value.Low, in readOnlyMod); @@ -390,7 +390,7 @@ public struct GpuUInt128 : IComparable, IEquatable } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddMod(in GpuUInt128 value, ulong modulus) => AddMod(value.Low, modulus); + public void AddMod(GpuUInt128 value, ulong modulus) => AddMod(value.Low, modulus); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddMod(ulong value, ulong modulus) @@ -414,7 +414,7 @@ public struct GpuUInt128 : IComparable, IEquatable public void Sub(in ReadOnlyGpuUInt128 other) => SubInternal(other.High, other.Low); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Sub(in GpuUInt128 other) => SubInternal(other.High, other.Low); + public void Sub(GpuUInt128 other) => SubInternal(other.High, other.Low); [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SubInternal(ulong otherHigh, ulong otherLow) @@ -461,7 +461,7 @@ public struct GpuUInt128 : IComparable, IEquatable public void Mul64(in ReadOnlyGpuUInt128 other) => Mul64Internal(other.High, other.Low); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Mul64(in GpuUInt128 other) => Mul64Internal(other.High, other.Low); + public void Mul64(GpuUInt128 other) => Mul64Internal(other.High, other.Low); [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Mul64Internal(ulong otherHigh, ulong otherLow) @@ -588,7 +588,7 @@ public struct GpuUInt128 : IComparable, IEquatable } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int TrailingZeroCount(in GpuUInt128 value) + public static int TrailingZeroCount(GpuUInt128 value) { if (value.Low != 0UL) { @@ -646,17 +646,17 @@ public struct GpuUInt128 : IComparable, IEquatable public void SubMod(in ReadOnlyGpuUInt128 value, in ReadOnlyGpuUInt128 modulus) => SubModInternal(value.High, value.Low, in modulus); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SubMod(in GpuUInt128 value, in ReadOnlyGpuUInt128 modulus) => SubModInternal(value.High, value.Low, in modulus); + public void SubMod(GpuUInt128 value, in ReadOnlyGpuUInt128 modulus) => SubModInternal(value.High, value.Low, in modulus); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SubMod(in ReadOnlyGpuUInt128 value, in GpuUInt128 modulus) + public void SubMod(in ReadOnlyGpuUInt128 value, GpuUInt128 modulus) { ReadOnlyGpuUInt128 readOnlyMod = modulus.AsReadOnly(); SubModInternal(value.High, value.Low, in readOnlyMod); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SubMod(in GpuUInt128 value, in GpuUInt128 modulus) + public void SubMod(GpuUInt128 value, GpuUInt128 modulus) { ReadOnlyGpuUInt128 readOnlyMod = modulus.AsReadOnly(); SubModInternal(value.High, value.Low, in readOnlyMod); @@ -727,7 +727,7 @@ public struct GpuUInt128 : IComparable, IEquatable } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Mul(in GpuUInt128 other) + public void Mul(GpuUInt128 other) { ReadOnlyGpuUInt128 readOnlyThis = AsReadOnly(); ReadOnlyGpuUInt128 readOnlyOther = other.AsReadOnly(); diff --git a/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs b/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs index 5a41f3f..2220711 100644 --- a/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs +++ b/PerfectNumbers.Core/Gpu/MersenneNumberDivisorByDivisorGpuTester.cs @@ -551,7 +551,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv } ulong exponent = exponents[index]; - hits[index] = exponent.Pow2MontgomeryModWindowed(divisor, keepMontgomery: false) == 1UL ? (byte)1 : (byte)0; + hits[index] = exponent.Pow2MontgomeryModWindowedGpu(divisor, keepMontgomery: false) == 1UL ? (byte)1 : (byte)0; } public sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession @@ -770,7 +770,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv ulong exponent = exponents[index]; - results[index] = exponent.Pow2MontgomeryModWindowed(divisor, keepMontgomery: true); + results[index] = exponent.Pow2MontgomeryModWindowedGpu(divisor, keepMontgomery: true); } public ulong DivisorLimit diff --git a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs index cb920cc..d38c0cc 100644 --- a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs +++ b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs @@ -811,7 +811,7 @@ internal static partial class PrimeOrderGpuHeuristics } ulong reduced = phi / factor; - if (reduced.Pow2MontgomeryModWindowed(divisor, keepMontgomery: false) == 1UL) + if (reduced.Pow2MontgomeryModWindowedGpu(divisor, keepMontgomery: false) == 1UL) { return false; } @@ -1595,7 +1595,7 @@ internal static partial class PrimeOrderGpuHeuristics private static bool Pow2EqualsOneKernel(ulong exponent, in MontgomeryDivisorData divisor) { - return exponent.Pow2MontgomeryModWindowed(divisor, keepMontgomery: false) == 1UL; + return exponent.Pow2MontgomeryModWindowedGpu(divisor, keepMontgomery: false) == 1UL; } private static ulong CalculateByDoublingKernel(ulong prime) @@ -1795,7 +1795,7 @@ internal static partial class PrimeOrderGpuHeuristics return 0UL; } - return exponent.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false); + return exponent.Pow2MontgomeryModWindowedGpu(divisorData, keepMontgomery: false); } private static Action, MontgomeryDivisorData, ArrayView1D> GetPow2ModKernel(Accelerator accelerator) @@ -1811,7 +1811,7 @@ internal static partial class PrimeOrderGpuHeuristics private static void Pow2ModKernel(Index1D index, ArrayView1D exponents, MontgomeryDivisorData divisor, ArrayView1D remainders) { ulong exponent = exponents[index]; - remainders[index] = exponent.Pow2MontgomeryModWindowed(divisor, keepMontgomery: false); + remainders[index] = exponent.Pow2MontgomeryModWindowedGpu(divisor, keepMontgomery: false); } private static Action, GpuUInt128, ArrayView1D> GetPow2ModWideKernel(Accelerator accelerator) diff --git a/PerfectNumbers.Core/HeuristicFailureLog.cs b/PerfectNumbers.Core/HeuristicFailureLog.cs new file mode 100644 index 0000000..aa1b48d --- /dev/null +++ b/PerfectNumbers.Core/HeuristicFailureLog.cs @@ -0,0 +1,98 @@ +using System.Collections.Concurrent; +using System.Globalization; +using System.Text; + +namespace PerfectNumbers.Core +{ + internal static class HeuristicFailureLog + { + private const string LogFileName = "prime-order-heuristic-fallbacks.log"; + private const int FlushThreshold = 4096; + private const int MaxBufferedEntries = 16; + private static readonly ThreadLocal s_threadBuilder = new(() => StringBuilderPool.Rent(), trackAllValues: true); + private static readonly ThreadLocal s_threadEntryCount = new(() => 0, trackAllValues: true); + private static readonly object s_fileLock = new(); + private static readonly string s_logPath = Path.Combine(AppContext.BaseDirectory, LogFileName); + private static readonly UTF8Encoding s_utf8NoBom = new(false); + + static HeuristicFailureLog() + { + AppDomain.CurrentDomain.ProcessExit += static (_, _) => FlushAll(force: true); + } + + public static void Record(ulong prime, ulong? candidateOrder, HeuristicFailureReason reason) + { + string primeText = prime.ToString(CultureInfo.InvariantCulture); + string? candidateText = candidateOrder.HasValue ? candidateOrder.Value.ToString(CultureInfo.InvariantCulture) : null; + RecordInternal(primeText, candidateText, reason); + } + + public static void Record(UInt128 prime, UInt128? candidateOrder, HeuristicFailureReason reason) + { + string primeText = prime.ToString(CultureInfo.InvariantCulture); + string? candidateText = candidateOrder.HasValue ? candidateOrder.Value.ToString(CultureInfo.InvariantCulture) : null; + RecordInternal(primeText, candidateText, reason); + } + + private static void RecordInternal(string primeText, string? candidateText, HeuristicFailureReason reason) + { + StringBuilder? existing = s_threadBuilder.Value; + StringBuilder builder = existing ?? StringBuilderPool.Rent(); + int count = s_threadEntryCount.Value + 1; + bool forceFlush = count >= MaxBufferedEntries; + builder.Append(DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)); + builder.Append(" | prime="); + builder.Append(primeText); + builder.Append(" | reason="); + builder.Append(reason); + if (candidateText is not null) + { + builder.Append(" | candidateOrder="); + builder.Append(candidateText); + } + + builder.AppendLine(); + FlushBuilderIfNeeded(builder, forceFlush); + if (builder.Length == 0) + { + count = 0; + } + + s_threadEntryCount.Value = count; + s_threadBuilder.Value = builder; + } + + private static void FlushAll(bool force) + { + foreach (StringBuilder? builder in s_threadBuilder.Values) + { + if (builder is not null) + { + FlushBuilderIfNeeded(builder, force); + } + } + } + + private static void FlushBuilderIfNeeded(StringBuilder builder, bool force) + { + if (builder.Length == 0) + { + return; + } + + if (!force && builder.Length < FlushThreshold) + { + return; + } + + string text = builder.ToString(); + builder.Clear(); + lock (s_fileLock) + { + using FileStream stream = new FileStream(s_logPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); + using StreamWriter writer = new StreamWriter(stream, s_utf8NoBom); + writer.Write(text); + } + } + } +} \ No newline at end of file diff --git a/PerfectNumbers.Core/HeuristicFailureReason.cs b/PerfectNumbers.Core/HeuristicFailureReason.cs new file mode 100644 index 0000000..6c4c4b7 --- /dev/null +++ b/PerfectNumbers.Core/HeuristicFailureReason.cs @@ -0,0 +1,11 @@ +namespace PerfectNumbers.Core +{ + internal enum HeuristicFailureReason + { + PhiPartialFactorizationFailed, + StrictModeRequested, + HeuristicPipelineUnresolved, + HeuristicPipelineAssumedFullOrder, + } + +} \ No newline at end of file diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index 1d1b1f3..d8eb5ed 100644 --- a/PerfectNumbers.Core/MersenneDivisorCycles.cs +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -54,15 +54,15 @@ public class MersenneDivisorCycles return snapshot; } - public static bool CycleEqualsExponent(ulong divisor, in MontgomeryDivisorData divisorData, ulong exponent) + public static bool CycleEqualsExponentCpu(ulong divisor, in MontgomeryDivisorData divisorData, ulong exponent) { Dictionary? cache = null; - return CycleEqualsExponent(divisor, exponent, divisorData, ref cache); + return CycleEqualsExponentCpu(divisor, exponent, divisorData, ref cache); } - public static bool CycleEqualsExponent(ulong divisor, ulong exponent, in MontgomeryDivisorData divisorData, ref Dictionary? factorCache) + public static bool CycleEqualsExponentCpu(ulong divisor, ulong exponent, in MontgomeryDivisorData divisorData, ref Dictionary? factorCache) { - if (CycleEqualsExponentForMersenneCandidate(divisor, divisorData, exponent)) + if (CycleEqualsExponentForMersenneCandidateCpu(divisor, divisorData, exponent)) { return true; } @@ -97,15 +97,15 @@ public class MersenneDivisorCycles factorCache = cache; } - if (TryCalculateCycleLengthForExponent(divisor, exponent, divisorData, cache, out ulong cycleLength) && cycleLength != 0UL) + if (TryCalculateCycleLengthForExponentCpu(divisor, exponent, divisorData, cache, out ulong cycleLength) && cycleLength != 0UL) { return cycleLength == exponent; } - return exponent.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL; + return exponent.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL; } - public static bool CycleEqualsExponentForMersenneCandidate(ulong divisor, in MontgomeryDivisorData divisorData, ulong exponent) + public static bool CycleEqualsExponentForMersenneCandidateCpu(ulong divisor, in MontgomeryDivisorData divisorData, ulong exponent) { if (!IsValidMersenneDivisorCandidate(divisor, exponent)) { @@ -126,7 +126,7 @@ public class MersenneDivisorCycles } } - return exponent.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL; + return exponent.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL; } private static bool IsValidMersenneDivisorCandidate(ulong divisor, ulong exponent) @@ -264,7 +264,7 @@ public class MersenneDivisorCycles return order; } - public static bool TryCalculateCycleLengthForExponent( + public static bool TryCalculateCycleLengthForExponentCpu( ulong divisor, ulong exponent, in MontgomeryDivisorData divisorData, @@ -327,7 +327,7 @@ public class MersenneDivisorCycles return false; } - cycleLength = ReduceOrder(divisorData, phi, factorCounts); + cycleLength = ReduceOrderCpu(divisorData, phi, factorCounts); return true; } @@ -471,7 +471,7 @@ public class MersenneDivisorCycles } } - private static ulong ReduceOrder(in MontgomeryDivisorData divisorData, ulong initialOrder, Dictionary factorCounts) + private static ulong ReduceOrderCpu(in MontgomeryDivisorData divisorData, ulong initialOrder, Dictionary factorCounts) { if (factorCounts.Count == 0) { @@ -505,7 +505,7 @@ public class MersenneDivisorCycles } ulong candidate = order / prime; - if (candidate.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL) + if (candidate.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL) { order = candidate; continue; diff --git a/PerfectNumbers.Core/PartialFactorResult.cs b/PerfectNumbers.Core/PartialFactorResult.cs new file mode 100644 index 0000000..d47363b --- /dev/null +++ b/PerfectNumbers.Core/PartialFactorResult.cs @@ -0,0 +1,76 @@ +using System.Buffers; + +namespace PerfectNumbers.Core +{ + public sealed class PartialFactorResult + { + [ThreadStatic] + private static PartialFactorResult? s_poolHead; + + public FactorEntry[]? Factors; + private PartialFactorResult? _next; + public ulong Cofactor; + public bool FullyFactored; + public int Count; + + private PartialFactorResult() + { + } + + public static PartialFactorResult Rent(in FactorEntry[]? factors, ulong cofactor, bool fullyFactored, int count) + { + PartialFactorResult? instance = s_poolHead; + if (instance is null) + { + instance = new(); + } + else + { + s_poolHead = instance._next; + } + + instance._next = null; + instance.Factors = factors; + instance.Cofactor = cofactor; + instance.FullyFactored = fullyFactored; + instance.Count = count; + return instance; + } + + public static readonly PartialFactorResult Empty = new() { Cofactor = 1UL, FullyFactored = true }; + + public PartialFactorResult WithAdditionalPrime(ulong prime) + { + FactorEntry[]? source = Factors; + if (source is null || Count == 0) + { + FactorEntry[] local = ArrayPool.Shared.Rent(1); + local[0] = new FactorEntry(prime, 1); + return Rent(local, 1UL, true, 1); + } + + FactorEntry[] extended = ArrayPool.Shared.Rent(Count + 1); + Array.Copy(source, 0, extended, 0, Count); + extended[Count] = new FactorEntry(prime, 1); + Span span = extended.AsSpan(0, Count + 1); + span.Sort(static (a, b) => a.Value.CompareTo(b.Value)); + return Rent(extended, 1UL, true, Count + 1); + } + + public void Dispose() + { + if (this != Empty) + { + FactorEntry[]? factors = Factors; + if (factors is not null) + { + Factors = null; + ArrayPool.Shared.Return(factors, clearArray: false); + } + + _next = s_poolHead; + s_poolHead = this; + } + } + } +} \ No newline at end of file diff --git a/PerfectNumbers.Core/PerfectNumberConstants.cs b/PerfectNumbers.Core/PerfectNumberConstants.cs index 4eb6e93..3e37bd3 100644 --- a/PerfectNumbers.Core/PerfectNumberConstants.cs +++ b/PerfectNumbers.Core/PerfectNumberConstants.cs @@ -8,6 +8,7 @@ public static class PerfectNumberConstants public const int MaxQForDivisorCycles = 4_000_000; public static readonly uint PrimesLimit = 1_000_000; //(ulong)Array.MaxLength;// 1_000_000; public const int MaxOddPowersCount = 128; + public const int PooledArrayThreshold = 64; // TODO: Load these limits from the benchmark-driven configuration so CPU and GPU scans stay aligned with the optimal // divisor-cycle datasets we generate offline. // TODO: Promote these magic numbers into a runtime profile derived from EvenPerfectBitScanner.Benchmarks so we can retune diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.cs b/PerfectNumbers.Core/PrimeOrderCalculator.cs index 5124839..f96168c 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.cs @@ -1,60 +1,31 @@ -using System; using System.Buffers; using System.Buffers.Binary; -using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; -using System.IO; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security.Cryptography; -using System.Text; using PerfectNumbers.Core.Gpu; -using System.Threading; namespace PerfectNumbers.Core; internal static partial class PrimeOrderCalculator { - - internal enum PrimeOrderMode { Heuristic, Strict, } - private enum HeuristicFailureReason - { - PhiPartialFactorizationFailed, - StrictModeRequested, - HeuristicPipelineUnresolved, - HeuristicPipelineAssumedFullOrder, - } - - internal readonly struct PrimeOrderSearchConfig - { - public PrimeOrderSearchConfig(uint smallFactorLimit, int pollardRhoMilliseconds, int maxPowChecks, PrimeOrderMode mode) - { - SmallFactorLimit = smallFactorLimit; - PollardRhoMilliseconds = pollardRhoMilliseconds; - MaxPowChecks = maxPowChecks; - Mode = mode; - } - - public uint SmallFactorLimit { get; } - - public int PollardRhoMilliseconds { get; } - - public int MaxPowChecks { get; } - - public PrimeOrderMode Mode { get; } - - public static PrimeOrderSearchConfig HeuristicDefault => new(smallFactorLimit: 100_000, pollardRhoMilliseconds: 128, maxPowChecks: 24, PrimeOrderMode.Heuristic); - + internal readonly struct PrimeOrderSearchConfig(uint smallFactorLimit, int pollardRhoMilliseconds, int maxPowChecks, PrimeOrderMode mode) + { + public static PrimeOrderSearchConfig HeuristicDefault => new(smallFactorLimit: 100_000, pollardRhoMilliseconds: 128, maxPowChecks: 24, PrimeOrderMode.Heuristic); public static PrimeOrderSearchConfig StrictDefault => new(smallFactorLimit: 1_000_000, pollardRhoMilliseconds: 0, maxPowChecks: 0, PrimeOrderMode.Strict); + + public readonly uint SmallFactorLimit = smallFactorLimit; + public readonly int PollardRhoMilliseconds = pollardRhoMilliseconds; + public readonly int MaxPowChecks = maxPowChecks; + public readonly PrimeOrderMode Mode = mode; } internal enum PrimeOrderHeuristicDevice @@ -88,29 +59,30 @@ internal static partial class PrimeOrderCalculator private static Dictionary? s_partialFactorCounts; private static bool IsGpuHeuristicDevice => s_pow2ModeInitialized && s_deviceMode == PrimeOrderHeuristicDevice.Gpu; - private readonly struct Pow2ModeScope - { - private readonly bool _previousInitialized; - private readonly bool _previousAllow; - private readonly PrimeOrderHeuristicDevice _previousDevice; - public Pow2ModeScope(PrimeOrderHeuristicDevice device) - { - _previousInitialized = s_pow2ModeInitialized; - _previousAllow = s_allowGpuPow2; - _previousDevice = s_deviceMode; - s_pow2ModeInitialized = true; - s_allowGpuPow2 = device == PrimeOrderHeuristicDevice.Gpu; - s_deviceMode = device; - } + private readonly struct Pow2ModeScope + { + private readonly bool _previousInitialized; + private readonly bool _previousAllow; + private readonly PrimeOrderHeuristicDevice _previousDevice; - public void Dispose() - { - s_allowGpuPow2 = _previousAllow; - s_pow2ModeInitialized = _previousInitialized; - s_deviceMode = _previousDevice; - } - } + public Pow2ModeScope(PrimeOrderHeuristicDevice device) + { + _previousInitialized = s_pow2ModeInitialized; + _previousAllow = s_allowGpuPow2; + _previousDevice = s_deviceMode; + s_pow2ModeInitialized = true; + s_allowGpuPow2 = device == PrimeOrderHeuristicDevice.Gpu; + s_deviceMode = device; + } + + public void Dispose() + { + s_allowGpuPow2 = _previousAllow; + s_pow2ModeInitialized = _previousInitialized; + s_deviceMode = _previousDevice; + } + } private readonly struct DebugLoggingScope { @@ -184,17 +156,22 @@ internal static partial class PrimeOrderCalculator return gpuOrder; } - using PartialFactorResult phiFactors = PartialFactor(phi, config); + PartialFactorResult phiFactors = PartialFactor(phi, config); - if (phiFactors.Factors is null) - { - return FinishStrictly(prime, divisorData); - } + ulong result; + if (phiFactors.Factors is null) + { + result = FinishStrictlyCpu(prime, divisorData); + phiFactors.Dispose(); + return result; + } - return RunHeuristicPipeline(prime, previousOrder, config, divisorData, phi, phiFactors); + result = RunHeuristicPipelineCpu(prime, previousOrder, config, divisorData, phi, phiFactors); + phiFactors.Dispose(); + return result; } - private static ulong RunHeuristicPipeline( + private static ulong RunHeuristicPipelineCpu( ulong prime, ulong? previousOrder, in PrimeOrderSearchConfig config, @@ -202,25 +179,25 @@ internal static partial class PrimeOrderCalculator ulong phi, PartialFactorResult phiFactors) { - if (phiFactors.FullyFactored && TrySpecialMax(phi, prime, phiFactors, divisorData)) + if (phiFactors.FullyFactored && TrySpecialMaxCpu(phi, prime, phiFactors, divisorData)) { return phi; } - ulong candidateOrder = InitializeStartingOrder(prime, phi, divisorData); - candidateOrder = ExponentLowering(candidateOrder, prime, phiFactors, divisorData); + ulong candidateOrder = InitializeStartingOrderCpu(prime, phi, divisorData); + candidateOrder = ExponentLoweringCpu(candidateOrder, prime, phiFactors, divisorData); - if (TryConfirmOrder(prime, candidateOrder, divisorData, config)) + if (TryConfirmOrderCpu(prime, candidateOrder, divisorData, config)) { return candidateOrder; } if (config.Mode == PrimeOrderMode.Strict) { - return FinishStrictly(prime, divisorData); + return FinishStrictlyCpu(prime, divisorData); } - if (TryHeuristicFinish(prime, candidateOrder, previousOrder, divisorData, config, phiFactors, out ulong order)) + if (TryHeuristicFinishCpu(prime, candidateOrder, previousOrder, divisorData, config, phiFactors, out ulong order)) { return order; } @@ -228,12 +205,12 @@ internal static partial class PrimeOrderCalculator return candidateOrder; } - private static ulong FinishStrictly(ulong prime, in MontgomeryDivisorData divisorData) + private static ulong FinishStrictlyCpu(ulong prime, in MontgomeryDivisorData divisorData) { - return CalculateByFactorization(prime, divisorData); + return CalculateByFactorizationCpu(prime, divisorData); } - private static bool TrySpecialMax(ulong phi, ulong prime, PartialFactorResult factors, in MontgomeryDivisorData divisorData) + private static bool TrySpecialMaxCpu(ulong phi, ulong prime, PartialFactorResult factors, in MontgomeryDivisorData divisorData) { ReadOnlySpan factorSpan = factors.Factors; int length = factors.Count; @@ -241,7 +218,7 @@ internal static partial class PrimeOrderCalculator { ulong factor = factorSpan[i].Value; ulong reduced = phi / factor; - if (Pow2EqualsOne(reduced, prime, divisorData)) + if (Pow2EqualsOneCpu(reduced, prime, divisorData)) { return false; } @@ -250,13 +227,13 @@ internal static partial class PrimeOrderCalculator return true; } - private static ulong InitializeStartingOrder(ulong prime, ulong phi, in MontgomeryDivisorData divisorData) + private static ulong InitializeStartingOrderCpu(ulong prime, ulong phi, in MontgomeryDivisorData divisorData) { ulong order = phi; if ((prime & 7UL) == 1UL || (prime & 7UL) == 7UL) { ulong half = phi >> 1; - if (Pow2EqualsOne(half, prime, divisorData)) + if (Pow2EqualsOneCpu(half, prime, divisorData)) { order = half; } @@ -265,7 +242,7 @@ internal static partial class PrimeOrderCalculator return order; } - private static ulong ExponentLowering(ulong order, ulong prime, PartialFactorResult factors, in MontgomeryDivisorData divisorData) + private static ulong ExponentLoweringCpu(ulong order, ulong prime, PartialFactorResult factors, in MontgomeryDivisorData divisorData) { FactorEntry[]? tempArray = null; try @@ -298,7 +275,7 @@ internal static partial class PrimeOrderCalculator } ulong reduced = order / primeFactor; - if (Pow2EqualsOne(reduced, prime, divisorData)) + if (Pow2EqualsOneCpu(reduced, prime, divisorData)) { order = reduced; continue; @@ -319,7 +296,7 @@ internal static partial class PrimeOrderCalculator } } - private static bool TryConfirmOrder(ulong prime, ulong order, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config) + private static bool TryConfirmOrderCpu(ulong prime, ulong order, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config) { if (order == 0UL) { @@ -328,7 +305,7 @@ internal static partial class PrimeOrderCalculator // Calculating `a^order ≡ 1 (mod p)` is a prerequisite for `order` being the actual order of 2 modulo `p`. // DebugLog("Verifying a^order ≡ 1 (mod p)"); - if (!Pow2EqualsOne(order, prime, divisorData)) + if (!Pow2EqualsOneCpu(order, prime, divisorData)) { return false; } @@ -380,7 +357,7 @@ internal static partial class PrimeOrderCalculator } reduced /= primeFactor; - if (Pow2EqualsOne(reduced, prime, divisorData)) + if (Pow2EqualsOneCpu(reduced, prime, divisorData)) { return false; } @@ -395,7 +372,7 @@ internal static partial class PrimeOrderCalculator } } - private static bool TryHeuristicFinish( + private static bool TryHeuristicFinishCpu( ulong prime, ulong order, ulong? previousOrder, @@ -528,14 +505,14 @@ internal static partial class PrimeOrderCalculator } else { - equalsOne = Pow2EqualsOne(candidate, prime, divisorData); + equalsOne = Pow2EqualsOneCpu(candidate, prime, divisorData); } if (!equalsOne) { continue; } - if (!TryConfirmCandidate(prime, candidate, divisorData, config, ref powUsed, powBudget)) + if (!TryConfirmCandidateCpu(prime, candidate, divisorData, config, ref powUsed, powBudget)) { continue; } @@ -733,7 +710,7 @@ internal static partial class PrimeOrderCalculator } } - private static bool TryConfirmCandidate(ulong prime, ulong candidate, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config, ref int powUsed, int powBudget) + private static bool TryConfirmCandidateCpu(ulong prime, ulong candidate, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config, ref int powUsed, int powBudget) { PartialFactorResult factorization = PartialFactor(candidate, config); try @@ -782,7 +759,7 @@ internal static partial class PrimeOrderCalculator } powUsed++; - if (Pow2EqualsOne(reduced, prime, divisorData)) + if (Pow2EqualsOneCpu(reduced, prime, divisorData)) { return false; } @@ -797,7 +774,7 @@ internal static partial class PrimeOrderCalculator } } - private static bool Pow2EqualsOne(ulong exponent, ulong prime, in MontgomeryDivisorData divisorData) + private static bool Pow2EqualsOneCpu(ulong exponent, ulong prime, in MontgomeryDivisorData divisorData) { if (IsGpuPow2Allowed) { @@ -808,111 +785,7 @@ internal static partial class PrimeOrderCalculator } } - return exponent.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL; - } - - private static class HeuristicFailureLog - { - private const string LogFileName = "prime-order-heuristic-fallbacks.log"; - private const int FlushThreshold = 4096; - private const int MaxBufferedEntries = 16; - private static readonly ConcurrentQueue s_builderPool = new(); - private static readonly ThreadLocal s_threadBuilder = new(() => AcquireBuilder(), trackAllValues: true); - private static readonly ThreadLocal s_threadEntryCount = new(() => 0, trackAllValues: true); - private static readonly object s_fileLock = new(); - private static readonly string s_logPath = Path.Combine(AppContext.BaseDirectory, LogFileName); - private static readonly UTF8Encoding s_utf8NoBom = new(false); - - static HeuristicFailureLog() - { - AppDomain.CurrentDomain.ProcessExit += static (_, _) => FlushAll(force: true); - } - - public static void Record(ulong prime, ulong? candidateOrder, HeuristicFailureReason reason) - { - string primeText = prime.ToString(CultureInfo.InvariantCulture); - string? candidateText = candidateOrder.HasValue ? candidateOrder.Value.ToString(CultureInfo.InvariantCulture) : null; - RecordInternal(primeText, candidateText, reason); - } - - public static void Record(UInt128 prime, UInt128? candidateOrder, HeuristicFailureReason reason) - { - string primeText = prime.ToString(CultureInfo.InvariantCulture); - string? candidateText = candidateOrder.HasValue ? candidateOrder.Value.ToString(CultureInfo.InvariantCulture) : null; - RecordInternal(primeText, candidateText, reason); - } - - private static void RecordInternal(string primeText, string? candidateText, HeuristicFailureReason reason) - { - StringBuilder? existing = s_threadBuilder.Value; - StringBuilder builder = existing ?? AcquireBuilder(); - int count = s_threadEntryCount.Value + 1; - bool forceFlush = count >= MaxBufferedEntries; - builder.Append(DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)); - builder.Append(" | prime="); - builder.Append(primeText); - builder.Append(" | reason="); - builder.Append(reason); - if (candidateText is not null) - { - builder.Append(" | candidateOrder="); - builder.Append(candidateText); - } - - builder.AppendLine(); - FlushBuilderIfNeeded(builder, forceFlush); - if (builder.Length == 0) - { - count = 0; - } - - s_threadEntryCount.Value = count; - s_threadBuilder.Value = builder; - } - - private static void FlushAll(bool force) - { - foreach (StringBuilder? builder in s_threadBuilder.Values) - { - if (builder is not null) - { - FlushBuilderIfNeeded(builder, force); - } - } - } - - private static void FlushBuilderIfNeeded(StringBuilder builder, bool force) - { - if (builder.Length == 0) - { - return; - } - - if (!force && builder.Length < FlushThreshold) - { - return; - } - - string text = builder.ToString(); - builder.Clear(); - lock (s_fileLock) - { - using FileStream stream = new FileStream(s_logPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); - using StreamWriter writer = new StreamWriter(stream, s_utf8NoBom); - writer.Write(text); - } - } - - private static StringBuilder AcquireBuilder() - { - if (s_builderPool.TryDequeue(out StringBuilder? builder)) - { - builder.Clear(); - return builder; - } - - return new StringBuilder(FlushThreshold); - } + return exponent.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL; } private static List AcquirePendingCompositeList(int capacityHint) @@ -952,16 +825,15 @@ internal static partial class PrimeOrderCalculator private static Dictionary AcquireFactorCountDictionary(int capacityHint) { Dictionary? dictionary = s_partialFactorCounts; - int adjustedCapacity = Math.Max(capacityHint, 8); if (dictionary is null) { - dictionary = new Dictionary(adjustedCapacity); + dictionary = new Dictionary(capacityHint); s_partialFactorCounts = dictionary; return dictionary; } dictionary.Clear(); - dictionary.EnsureCapacity(adjustedCapacity); + dictionary.EnsureCapacity(capacityHint); return dictionary; } @@ -969,7 +841,7 @@ internal static partial class PrimeOrderCalculator { if (value <= 1UL) { - return PartialFactorResult.RentEmpty(); + return PartialFactorResult.Empty; } const int FactorSlotCount = GpuSmallPrimeFactorSlots; @@ -1459,7 +1331,7 @@ internal static partial class PrimeOrderCalculator } } - private static ulong CalculateByFactorization(ulong prime, in MontgomeryDivisorData divisorData) + private static ulong CalculateByFactorizationCpu(ulong prime, in MontgomeryDivisorData divisorData) { ulong phi = prime - 1UL; Dictionary counts = new(capacity: 8); @@ -1486,7 +1358,7 @@ internal static partial class PrimeOrderCalculator } ulong candidate = order / primeFactor; - if (Pow2EqualsOne(candidate, prime, divisorData)) + if (Pow2EqualsOneCpu(candidate, prime, divisorData)) { order = candidate; continue; @@ -1557,96 +1429,4 @@ internal static partial class PrimeOrderCalculator public readonly long Secondary = secondary; public readonly long Tertiary = tertiary; } - - private readonly struct FactorEntry(ulong value, int exponent) - { - public readonly ulong Value = value; - public readonly int Exponent = exponent; - } - - private sealed class PartialFactorResult : IDisposable - { - [ThreadStatic] - private static PartialFactorResult? s_poolHead; - - private PartialFactorResult? _next; - private bool _returned; - - public FactorEntry[]? Factors; - public ulong Cofactor; - public bool FullyFactored; - public int Count; - - private PartialFactorResult() - { - } - - public static PartialFactorResult Rent(FactorEntry[]? factors, ulong cofactor, bool fullyFactored, int count) - { - PartialFactorResult? instance = s_poolHead; - if (instance is null) - { - instance = new PartialFactorResult(); - } - else - { - s_poolHead = instance._next; - } - - instance._next = null; - instance._returned = false; - instance.Factors = factors; - instance.Cofactor = cofactor; - instance.FullyFactored = fullyFactored; - instance.Count = count; - return instance; - } - - public static PartialFactorResult RentEmpty() - { - return Rent(null, 1UL, true, 0); - } - - public PartialFactorResult WithAdditionalPrime(ulong prime) - { - FactorEntry[]? source = Factors; - if (source is null || Count == 0) - { - FactorEntry[] local = ArrayPool.Shared.Rent(1); - local[0] = new FactorEntry(prime, 1); - return Rent(local, 1UL, true, 1); - } - - FactorEntry[] extended = ArrayPool.Shared.Rent(Count + 1); - Array.Copy(source, 0, extended, 0, Count); - extended[Count] = new FactorEntry(prime, 1); - Span span = extended.AsSpan(0, Count + 1); - span.Sort(static (a, b) => a.Value.CompareTo(b.Value)); - return Rent(extended, 1UL, true, Count + 1); - } - - public void Dispose() - { - if (_returned) - { - return; - } - - _returned = true; - - FactorEntry[]? factors = Factors; - if (factors is not null) - { - Factors = null; - ArrayPool.Shared.Return(factors, clearArray: false); - } - - Cofactor = 0UL; - FullyFactored = false; - Count = 0; - - _next = s_poolHead; - s_poolHead = this; - } - } } diff --git a/PerfectNumbers.Core/UInt128Extensions.cs b/PerfectNumbers.Core/UInt128Extensions.cs index 7f62518..e7bf9c3 100644 --- a/PerfectNumbers.Core/UInt128Extensions.cs +++ b/PerfectNumbers.Core/UInt128Extensions.cs @@ -218,7 +218,7 @@ public static class UInt128Extensions } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsBitSet(in GpuUInt128 value, int bitIndex) + private static bool IsBitSet(GpuUInt128 value, int bitIndex) { if (bitIndex >= 64) { @@ -229,18 +229,16 @@ public static class UInt128Extensions } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong ExtractWindowValue(in GpuUInt128 exponent, int windowStart, int windowBitCount) + private static ulong ExtractWindowValue(GpuUInt128 exponent, int windowStart, int windowBitCount) { + ulong mask = (1UL << windowBitCount) - 1UL; if (windowStart != 0) { - GpuUInt128 shifted = exponent; - shifted.ShiftRight(windowStart); - ulong mask = (1UL << windowBitCount) - 1UL; - return shifted.Low & mask; + exponent.ShiftRight(windowStart); + return exponent.Low & mask; } - ulong directMask = (1UL << windowBitCount) - 1UL; - return exponent.Low & directMask; + return exponent.Low & mask; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/PerfectNumbers.Core/ULongExtensions.cs b/PerfectNumbers.Core/ULongExtensions.cs index f46b61c..f9ae011 100644 --- a/PerfectNumbers.Core/ULongExtensions.cs +++ b/PerfectNumbers.Core/ULongExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; using ILGPU.Algorithms; @@ -76,7 +77,7 @@ public static class ULongExtensions public static ulong MultiplyShiftRightShiftFirst(ulong value, ulong multiplier, int shift) { ulong high = value >> shift; - ulong mask = (1UL << shift) - 1UL; + ulong mask = (1UL << shift) - 1UL; ulong low = value & mask; UInt128 highContribution = (UInt128)high * multiplier; @@ -326,7 +327,7 @@ public static class ULongExtensions } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ulong Pow2MontgomeryModWindowed(this ulong exponent, in MontgomeryDivisorData divisor, bool keepMontgomery) + public static ulong Pow2MontgomeryModWindowedCpu(this ulong exponent, in MontgomeryDivisorData divisor, bool keepMontgomery) { ulong modulus = divisor.Modulus; if (exponent == 0UL) @@ -345,7 +346,78 @@ public static class ULongExtensions ulong result = divisor.MontgomeryOne; ulong nPrime = divisor.NPrime; - ulong[] oddPowers = InitializeMontgomeryOddPowers(divisor, modulus, nPrime, oddPowerCount); + ulong[] oddPowers = InitializeMontgomeryOddPowersCpu(divisor, modulus, nPrime, oddPowerCount); + + int index = bitLength - 1; + while (index >= 0) + { + if (((exponent >> index) & 1UL) == 0UL) + { + result = result.MontgomeryMultiply(result, modulus, nPrime); + index--; + continue; + } + + int windowStart = index - windowSize + 1; + if (windowStart < 0) + { + windowStart = 0; + } + + while (((exponent >> windowStart) & 1UL) == 0UL) + { + windowStart++; + } + + int windowLength = index - windowStart + 1; + for (int square = 0; square < windowLength; square++) + { + result = result.MontgomeryMultiply(result, modulus, nPrime); + } + + ulong mask = (1UL << windowLength) - 1UL; + ulong windowValue = (exponent >> windowStart) & mask; + int tableIndex = (int)((windowValue - 1UL) >> 1); + ulong multiplier = oddPowers[tableIndex]; + result = result.MontgomeryMultiply(multiplier, modulus, nPrime); + + index = windowStart - 1; + } + + if (oddPowerCount >= PerfectNumberConstants.PooledArrayThreshold) + { + ArrayPool.Shared.Return(oddPowers); + } + + if (keepMontgomery) + { + return result; + } + + return result.MontgomeryMultiply(1UL, modulus, nPrime); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Pow2MontgomeryModWindowedGpu(this ulong exponent, in MontgomeryDivisorData divisor, bool keepMontgomery) + { + ulong modulus = divisor.Modulus; + if (exponent == 0UL) + { + return keepMontgomery ? divisor.MontgomeryOne : 1UL % modulus; + } + + if (exponent <= Pow2WindowFallbackThreshold) + { + return Pow2MontgomeryModSingleBit(exponent, divisor, keepMontgomery); + } + + int bitLength = GetPortableBitLength(exponent); + int windowSize = GetWindowSize(bitLength); + int oddPowerCount = 1 << (windowSize - 1); + + ulong result = divisor.MontgomeryOne; + ulong nPrime = divisor.NPrime; + ulong[] oddPowers = InitializeMontgomeryOddPowersGpu(divisor, modulus, nPrime, oddPowerCount); int index = bitLength - 1; while (index >= 0) @@ -461,7 +533,30 @@ public static class ULongExtensions } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ulong[] InitializeMontgomeryOddPowers(in MontgomeryDivisorData divisor, ulong modulus, ulong nPrime, int oddPowerCount) + private static ulong[] InitializeMontgomeryOddPowersCpu(in MontgomeryDivisorData divisor, ulong modulus, ulong nPrime, int oddPowerCount) + { + ulong[] oddPowers = oddPowerCount < PerfectNumberConstants.PooledArrayThreshold ? new ulong[oddPowerCount] : ArrayPool.Shared.Rent(oddPowerCount); + + oddPowers[0] = divisor.MontgomeryTwo; + if (oddPowerCount == 1) + { + return oddPowers; + } + + ulong previous, + square = divisor.MontgomeryTwoSquared; + + for (int i = 1; i < oddPowerCount; i++) + { + previous = oddPowers[i - 1]; + oddPowers[i] = previous.MontgomeryMultiply(square, modulus, nPrime); + } + + return oddPowers; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong[] InitializeMontgomeryOddPowersGpu(in MontgomeryDivisorData divisor, ulong modulus, ulong nPrime, int oddPowerCount) { ulong[] oddPowers = new ulong[PerfectNumberConstants.MaxOddPowersCount]; oddPowers[0] = divisor.MontgomeryTwo; @@ -481,17 +576,31 @@ public static class ULongExtensions } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ulong Pow2MontgomeryModWithCycle(this ulong exponent, ulong cycleLength, in MontgomeryDivisorData divisor) + public static ulong Pow2MontgomeryModWithCycleCpu(this ulong exponent, ulong cycleLength, in MontgomeryDivisorData divisor) { ulong rotationCount = exponent % cycleLength; - return Pow2MontgomeryModWindowed(rotationCount, divisor, keepMontgomery: false); + return Pow2MontgomeryModWindowedCpu(rotationCount, divisor, keepMontgomery: false); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Pow2MontgomeryModFromCycleRemainderCpu(this ulong reducedExponent, in MontgomeryDivisorData divisor) + { + return Pow2MontgomeryModWindowedCpu(reducedExponent, divisor, keepMontgomery: false); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ulong Pow2MontgomeryModFromCycleRemainder(this ulong reducedExponent, in MontgomeryDivisorData divisor) + public static ulong Pow2MontgomeryModWithCycleGpu(this ulong exponent, ulong cycleLength, in MontgomeryDivisorData divisor) { - return Pow2MontgomeryModWindowed(reducedExponent, divisor, keepMontgomery: false); + ulong rotationCount = exponent % cycleLength; + return Pow2MontgomeryModWindowedGpu(rotationCount, divisor, keepMontgomery: false); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Pow2MontgomeryModFromCycleRemainderGpu(this ulong reducedExponent, in MontgomeryDivisorData divisor) + { + return Pow2MontgomeryModWindowedGpu(reducedExponent, divisor, keepMontgomery: false); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static UInt128 PowMod(this ulong exponent, UInt128 modulus) {