diff --git a/AGENTS.md b/AGENTS.md index 07fc98b..35271f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -112,6 +112,10 @@ Keep the divisor cycle cache limited to a single block in memory (only the on-di All code, comments, commit messages, branch names and PR descriptions must be written in American English. +- When modifying existing files, preserve all relevant TODO notes and explanatory comments unless they are no longer valid. +- Prefer defining data holders as structs or classes with public fields over auto-properties when performance on the hot path is a concern. +- Do not add or keep methods that simply wrap another method call without adding any logic; inline those calls or extend the wrapper with meaningful work. + ## Performance implementation checklist - Prefer dedicated kernels over flag-controlled or parameterized kernels. Avoid conditional branches in kernels and tight loops by creating separate kernels when necessary. diff --git a/PerfectNumbers.Core/Cpu/FactorCacheLease.cs b/PerfectNumbers.Core/Cpu/FactorCacheLease.cs new file mode 100644 index 0000000..21c1447 --- /dev/null +++ b/PerfectNumbers.Core/Cpu/FactorCacheLease.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace PerfectNumbers.Core.Cpu; + +internal struct FactorCacheLease +{ + private Dictionary? _cache; + + public void EnsureInitialized(ref Dictionary? cache) + { + Dictionary? current = cache; + if (current is null) + { + current = ThreadStaticPools.RentMersenneFactorCacheDictionary(); + cache = current; + } + + _cache = current; + } + + public void Dispose() + { + Dictionary? cache = _cache; + if (cache is null) + { + return; + } + + ThreadStaticPools.ReturnMersenneFactorCacheDictionary(cache); + _cache = null; + } +} diff --git a/PerfectNumbers.Core/Cpu/MersenneCpuDivisorScanSession.cs b/PerfectNumbers.Core/Cpu/MersenneCpuDivisorScanSession.cs new file mode 100644 index 0000000..355a63b --- /dev/null +++ b/PerfectNumbers.Core/Cpu/MersenneCpuDivisorScanSession.cs @@ -0,0 +1,82 @@ +using System; + +namespace PerfectNumbers.Core.Cpu; + +internal sealed class MersenneCpuDivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession +{ + private bool _disposed; + + public void Reset() + { + _disposed = false; + } + + public void CheckDivisor( + ulong divisor, + in MontgomeryDivisorData divisorData, + ulong divisorCycle, + in ReadOnlySpan primes, + Span hits) + { + int length = primes.Length; + if (length == 0) + { + return; + } + + MontgomeryDivisorData cachedData = divisorData; + if (cachedData.Modulus != divisor) + { + cachedData = MontgomeryDivisorDataCache.Get(divisor); + } + + if (divisorCycle == 0UL) + { + divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor); + if (divisorCycle == 0UL) + { + hits.Clear(); + return; + } + } + + // Keep these remainder steppers in place so future updates continue reusing the previously computed residues. + // They are critical for avoiding repeated full Montgomery exponentiation work when scanning divisors. + var exponentStepper = new ExponentRemainderStepper(cachedData); + if (!exponentStepper.IsValidModulus) + { + hits.Clear(); + return; + } + + var cycleStepper = new CycleRemainderStepper(divisorCycle); + + ulong remainder = cycleStepper.Initialize(primes[0]); + hits[0] = remainder == 0UL + ? (exponentStepper.ComputeNextIsUnity(primes[0]) ? (byte)1 : (byte)0) + : (byte)0; + + for (int i = 1; i < length; i++) + { + remainder = cycleStepper.ComputeNext(primes[i]); + if (remainder != 0UL) + { + hits[i] = 0; + continue; + } + + hits[i] = exponentStepper.ComputeNextIsUnity(primes[i]) ? (byte)1 : (byte)0; + } + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + ThreadStaticPools.ReturnMersenneCpuDivisorSession(this); + } +} diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs index 5d379cc..0bd75fa 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; namespace PerfectNumbers.Core.Cpu; @@ -6,8 +7,6 @@ namespace PerfectNumbers.Core.Cpu; public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDivisorByDivisorTester { private readonly object _sync = new(); - [ThreadStatic] - private static DivisorScanSession? _threadLocalSession; // The nested session type stays private, so pool it here. private ulong _divisorLimit; private ulong _lastStatusDivisor; @@ -44,6 +43,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv if (allowedMax < 3UL) { + // EvenPerfectBitScanner routes primes below the small-divisor cutoff to the GPU path, so the CPU path still sees + // trivial candidates during targeted tests. Short-circuit here to keep those runs aligned with the production flow. divisorsExhausted = true; return true; } @@ -87,49 +88,13 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession() { - DivisorScanSession? session = _threadLocalSession; + MersenneCpuDivisorScanSession? session = ThreadStaticPools.RentMersenneCpuDivisorSession(); if (session is not null) { - _threadLocalSession = null; - session.Reset(); return session; } - return new DivisorScanSession(this); - } - - private void ReturnSession(DivisorScanSession session) - { - _threadLocalSession = session; - } - - private struct FactorCacheLease - { - private Dictionary? _cache; - - public void EnsureInitialized(ref Dictionary? cache) - { - Dictionary? current = cache; - if (current is null) - { - current = ThreadStaticPools.RentMersenneFactorCacheDictionary(); - cache = current; - } - - _cache = current; - } - - public void Dispose() - { - Dictionary? cache = _cache; - if (cache is null) - { - return; - } - - ThreadStaticPools.ReturnMersenneFactorCacheDictionary(cache); - _cache = null; - } + return new MersenneCpuDivisorScanSession(); } private bool CheckDivisors( @@ -188,7 +153,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv byte remainder7 = (byte)(divisor % 7UL); byte remainder11 = (byte)(divisor % 11UL); - // DivisorCycleCache cycleCache = DivisorCycleCache.Shared; // Preserve the reference for easy restoration if the fallback cache is re-enabled. + // DivisorCycleCache cycleCache = DivisorCycleCache.Shared; // Do not restore the fallback cache; divisors do not repeat on this path. // Keep the divisibility filters aligned with the divisor-cycle generator so the // CPU path never requests cycles that were skipped during cache creation. @@ -320,85 +285,5 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv return Math.Min((1UL << (int)(prime - 1UL)) - 1UL, divisorLimit); } - private sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession - { - private readonly MersenneNumberDivisorByDivisorCpuTester _owner; - private bool _disposed; - internal DivisorScanSession(MersenneNumberDivisorByDivisorCpuTester owner) - { - _owner = owner; - } - - internal void Reset() - { - _disposed = false; - } - - public void CheckDivisor(ulong divisor, in MontgomeryDivisorData divisorData, ulong divisorCycle, in ReadOnlySpan primes, Span hits) - { - int length = primes.Length; - if (length == 0) - { - return; - } - - MontgomeryDivisorData cachedData = divisorData; - if (cachedData.Modulus != divisor) - { - cachedData = MontgomeryDivisorDataCache.Get(divisor); - } - - if (divisorCycle == 0UL) - { - divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor); - if (divisorCycle == 0UL) - { - hits.Clear(); - return; - } - } - - // Keep these remainder steppers in place so future updates continue reusing the previously computed residues. - // They are critical for avoiding repeated full Montgomery exponentiation work when scanning divisors. - var exponentStepper = new ExponentRemainderStepper(cachedData); - if (!exponentStepper.IsValidModulus) - { - hits.Clear(); - return; - } - - var cycleStepper = new CycleRemainderStepper(divisorCycle); - - ulong remainder = cycleStepper.Initialize(primes[0]); - hits[0] = remainder == 0UL - ? (exponentStepper.ComputeNextIsUnity(primes[0]) ? (byte)1 : (byte)0) - : (byte)0; - - for (int i = 1; i < length; i++) - { - remainder = cycleStepper.ComputeNext(primes[i]); - if (remainder != 0UL) - { - hits[i] = 0; - continue; - } - - hits[i] = exponentStepper.ComputeNextIsUnity(primes[i]) ? (byte)1 : (byte)0; - } - } - - public void Dispose() - { - if (_disposed) - { - return; - } - - _disposed = true; - _owner.ReturnSession(this); - } - } } - - diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index 6839d52..0258347 100644 --- a/PerfectNumbers.Core/MersenneDivisorCycles.cs +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -96,7 +96,7 @@ public class MersenneDivisorCycles factorCache = cache; } - if (TryCalculateCycleLengthForExponent(divisor, exponent, divisorData, cache, out ulong cycleLength) && cycleLength != 0UL) + if (TryCalculateCycleLengthForExponent(divisor, exponent, divisorData, cache, usePooledCache: false, out ulong cycleLength) && cycleLength != 0UL) { return cycleLength == exponent; } @@ -263,22 +263,6 @@ public class MersenneDivisorCycles return order; } - public static bool TryCalculateCycleLengthForExponent( - ulong divisor, - ulong exponent, - in MontgomeryDivisorData divisorData, - Dictionary? factorCache, - out ulong cycleLength) - { - return TryCalculateCycleLengthForExponent( - divisor, - exponent, - divisorData, - factorCache, - usePooledCache: false, - out cycleLength); - } - public static bool TryCalculateCycleLengthForExponent( ulong divisor, ulong exponent, @@ -517,7 +501,14 @@ public class MersenneDivisorCycles } ulong quotient = remaining / factor; - if (factor > 1UL && !TryFactorIntoCountsInternal(factor, counts)) + // Pollard-Rho never returns factors <= 1 on the EvenPerfectBitScanner path; keep the old guard documented + // for completeness without branching in the hot path. + // if (factor > 1UL && !TryFactorIntoCountsInternal(factor, counts)) + // { + // return false; + // } + + if (!TryFactorIntoCountsInternal(factor, counts)) { return false; } @@ -714,6 +705,15 @@ public class MersenneDivisorCycles internal int Count; private bool _fromPool; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void Reset() + { + Entries = null; + Count = 0; + _fromPool = false; + Next = null; + } + internal void Initialize(FactorEntry[]? entries, int count, bool fromPool) { Entries = entries; @@ -731,10 +731,7 @@ public class MersenneDivisorCycles ThreadStaticPools.FactorEntryPool.Return(entries, clearArray: false); } - Entries = null; - Count = 0; - _fromPool = false; - Next = null; + Reset(); ThreadStaticPools.ReturnFactorCacheEntry(this); } } diff --git a/PerfectNumbers.Core/ThreadStaticPools.cs b/PerfectNumbers.Core/ThreadStaticPools.cs index 7c94732..07a9081 100644 --- a/PerfectNumbers.Core/ThreadStaticPools.cs +++ b/PerfectNumbers.Core/ThreadStaticPools.cs @@ -2,10 +2,11 @@ using System; using System.Buffers; using System.Collections.Generic; using System.Runtime.CompilerServices; +using PerfectNumbers.Core.Cpu; namespace PerfectNumbers.Core { - public static class ThreadStaticPools + public readonly struct ThreadStaticPools { [ThreadStatic] private static ArrayPool? _factorEntryPool; @@ -31,6 +32,29 @@ namespace PerfectNumbers.Core } } + [ThreadStatic] + private static MersenneCpuDivisorScanSession? _mersenneCpuDivisorSession; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static MersenneCpuDivisorScanSession? RentMersenneCpuDivisorSession() + { + MersenneCpuDivisorScanSession? session = _mersenneCpuDivisorSession; + if (session is null) + { + return null; + } + + _mersenneCpuDivisorSession = null; + session.Reset(); + return session; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void ReturnMersenneCpuDivisorSession(MersenneCpuDivisorScanSession session) + { + _mersenneCpuDivisorSession = session; + } + [ThreadStatic] private static ArrayPool? _ulongPool; @@ -81,11 +105,14 @@ namespace PerfectNumbers.Core MersenneDivisorCycles.FactorCacheEntry? entry = _factorCacheEntryPoolHead; if (entry is null) { - return new MersenneDivisorCycles.FactorCacheEntry(); + entry = new MersenneDivisorCycles.FactorCacheEntry(); + } + else + { + _factorCacheEntryPoolHead = entry.Next; } - _factorCacheEntryPoolHead = entry.Next; - entry.Next = null; + entry.Reset(); return entry; }