diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs index e9fdd0c..96e5d12 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System; using System.Runtime.CompilerServices; namespace PerfectNumbers.Core.Cpu; @@ -6,7 +6,9 @@ namespace PerfectNumbers.Core.Cpu; public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDivisorByDivisorTester { private readonly object _sync = new(); - private readonly ConcurrentBag _sessionPool = []; + [ThreadStatic] + private static DivisorScanSession? _threadLocalSession; // The nested session type stays private, so pool it here. + private ulong _divisorLimit; private ulong _lastStatusDivisor; private int _batchSize = 1_024; @@ -20,23 +22,23 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv public void ConfigureFromMaxPrime(ulong maxPrime) { - _divisorLimit = ComputeDivisorLimitFromMaxPrime(maxPrime); - _lastStatusDivisor = 0UL; + _divisorLimit = ComputeDivisorLimitFromMaxPrime(maxPrime); + _lastStatusDivisor = 0UL; } public ulong DivisorLimit - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - return _divisorLimit; + return _divisorLimit; } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ulong GetAllowedMaxDivisor(ulong prime) => ComputeAllowedMaxDivisor(prime, _divisorLimit); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ulong GetAllowedMaxDivisor(ulong prime) => ComputeAllowedMaxDivisor(prime, _divisorLimit); - public bool IsPrime(ulong prime, out bool divisorsExhausted) + public bool IsPrime(ulong prime, out bool divisorsExhausted) { ulong allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit); @@ -76,8 +78,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv public void PrepareCandidates(in ReadOnlySpan primes, Span allowedMaxValues) { ulong divisorLimit = _divisorLimit; - int length = primes.Length; - for (int index = 0; index < length; index++) + int length = primes.Length; + for (int index = 0; index < length; index++) { allowedMaxValues[index] = ComputeAllowedMaxDivisor(primes[index], divisorLimit); } @@ -85,18 +87,49 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession() { - if (_sessionPool.TryTake(out DivisorScanSession? session)) - { - session.Reset(); - return session; - } + DivisorScanSession? session = _threadLocalSession; + if (session is not null) + { + _threadLocalSession = null; + session.Reset(); + return session; + } - return new DivisorScanSession(this); + return new DivisorScanSession(this); } private void ReturnSession(DivisorScanSession session) { - _sessionPool.Add(session); + _threadLocalSession = session; + } + + private struct FactorCacheLease + { + private Dictionary? _cache; + + public void EnsureInitialized(ref Dictionary? cache) + { + Dictionary? current = cache; + if (current is null) + { + current = ThreadStaticPools.TakeMersenneFactorCacheDictionary(); + cache = current; + } + + _cache = current; + } + + public void Dispose() + { + Dictionary? cache = _cache; + if (cache is null) + { + return; + } + + ThreadStaticPools.ReturnMersenneFactorCacheDictionary(cache); + _cache = null; + } } private bool CheckDivisors( @@ -108,10 +141,12 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv processedCount = 0UL; processedAll = false; - if (allowedMax < 3UL) - { - return false; - } + // The EvenPerfectBitScanner feeds primes >= 138,000,000 here, so allowedMax >= 3 in production runs. + // Keeping the guard commented out documents the reasoning for benchmarks and tests. + // if (allowedMax < 3UL) + // { + // return false; + // } UInt128 step = (UInt128)prime << 1; if (step == UInt128.Zero) @@ -128,8 +163,9 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv return false; } + FactorCacheLease factorCacheLease = new FactorCacheLease(); Dictionary? factorCache = null; - DivisorCycleCache cycleCache = DivisorCycleCache.Shared; + bool foundDivisor = false; byte step10 = (byte)(step % 10UL); byte step8 = (byte)(step % 8UL); @@ -161,28 +197,22 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv if (admissible && (remainder8 == 1 || remainder8 == 7) && remainder3 != 0 && remainder5 != 0 && remainder7 != 0 && remainder11 != 0) { MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(candidate); - ulong divisorCycle; - - if (candidate <= PerfectNumberConstants.MaxQForDivisorCycles) + Dictionary? cacheForCycle = null; + if (candidate > PerfectNumberConstants.MaxQForDivisorCycles) { - divisorCycle = cycleCache.GetCycleLength(candidate); + factorCacheLease.EnsureInitialized(ref factorCache); + cacheForCycle = factorCache; } - else + + if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponent(candidate, prime, divisorData, cacheForCycle, out ulong divisorCycle)) { - factorCache ??= new Dictionary(8); - if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponent(candidate, prime, divisorData, factorCache, out divisorCycle) || divisorCycle == 0UL) - { - divisorCycle = cycleCache.GetCycleLength(candidate); - } + divisorCycle = 0UL; } if (divisorCycle == prime) { - // A cycle equal to the tested exponent (which is prime in this path) - // guarantees that the candidate divides the corresponding Mersenne - // number because the order of 2 modulo the divisor is exactly p. - processedAll = true; - return true; + foundDivisor = true; + break; } if (divisorCycle == 0UL) @@ -200,6 +230,15 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv remainder11 = AddMod(remainder11, step11, (byte)11); } + factorCacheLease.Dispose(); + factorCache = null; // The pooled dictionary was returned to the thread-static slot. + + if (foundDivisor) + { + processedAll = true; + return true; + } + processedAll = divisor > limit; return false; } diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index 410978a..eab0187 100644 --- a/PerfectNumbers.Core/MersenneDivisorCycles.cs +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -17,13 +17,32 @@ public class MersenneDivisorCycles private List<(ulong divisor, ulong cycleLength)> _table = []; // Lightweight read-mostly cache for small divisors (<= 4,000,000). 0 => unknown private ulong[]? _smallCycles; - public static MersenneDivisorCycles Shared { get; } = new MersenneDivisorCycles(); public MersenneDivisorCycles() { } + private static Dictionary RentFactorCountDictionary() + { + return ThreadStaticPools.TakeFactorCountDictionary(); + } + + private static void ReturnFactorCountDictionary(Dictionary dictionary) + { + ThreadStaticPools.ReturnFactorCountDictionary(dictionary); + } + + private static Dictionary RentFactorScratchDictionary() + { + return ThreadStaticPools.TakeFactorScratchDictionary(); + } + + private static void ReturnFactorScratchDictionary(Dictionary dictionary) + { + ThreadStaticPools.ReturnFactorScratchDictionary(dictionary); + } + public void LoadFrom(string path) { EnsureSmallBuffer(); @@ -316,19 +335,27 @@ public class MersenneDivisorCycles return false; } - Dictionary factorCounts = new(8); - if (twoCount > 0) - { - factorCounts[2UL] = twoCount; - } + Dictionary factorCounts = RentFactorCountDictionary(); - if (!AccumulateFactors(exponent, factorCache, factorCounts, cacheResult: true) || !AccumulateFactors(k, factorCache, factorCounts, cacheResult: false)) + try { - return false; - } + if (twoCount > 0) + { + factorCounts[2UL] = twoCount; + } - cycleLength = ReduceOrder(divisorData, phi, factorCounts); - return true; + if (!AccumulateFactors(exponent, factorCache, factorCounts, cacheResult: true) || !AccumulateFactors(k, factorCache, factorCounts, cacheResult: false)) + { + return false; + } + + cycleLength = ReduceOrder(divisorData, phi, factorCounts); + return true; + } + finally + { + ReturnFactorCountDictionary(factorCounts); + } } private static bool AccumulateFactors( @@ -370,37 +397,46 @@ public class MersenneDivisorCycles return true; } - Dictionary scratch = new(8); - if (!TryFactorIntoCountsInternal(value, scratch)) + Dictionary scratch = RentFactorScratchDictionary(); + + try { - factorization = default!; - return false; - } + if (!TryFactorIntoCountsInternal(value, scratch)) + { + factorization = default!; + return false; + } - int count = scratch.Count; - ulong[] primes = new ulong[count]; - byte[] exponents = new byte[count]; - int index = 0; - foreach (KeyValuePair entry in scratch) + int count = scratch.Count; + ulong[] primes = new ulong[count]; + byte[] exponents = new byte[count]; + int index = 0; + foreach (KeyValuePair entry in scratch) + { + primes[index] = entry.Key; + exponents[index] = checked((byte)entry.Value); + index++; + } + + Array.Sort(primes, exponents); + factorization = new FactorCacheEntry(primes, exponents, count); + + if (cache is not null && cacheResult) + { + cache[value] = factorization; + } + + return true; + } + finally { - primes[index] = entry.Key; - exponents[index] = checked((byte)entry.Value); - index++; + ReturnFactorScratchDictionary(scratch); } - - Array.Sort(primes, exponents); - factorization = new FactorCacheEntry(primes, exponents, count); - - if (cache is not null && cacheResult) - { - cache[value] = factorization; - } - - return true; } private static bool TryFactorIntoCountsInternal(ulong value, Dictionary counts) { + // This guard terminates the recursion once Pollard-Rho factorization has reduced the input to 1. if (value <= 1UL) { return true; diff --git a/PerfectNumbers.Core/ThreadStaticPools.cs b/PerfectNumbers.Core/ThreadStaticPools.cs index cd02a0d..a953ec7 100644 --- a/PerfectNumbers.Core/ThreadStaticPools.cs +++ b/PerfectNumbers.Core/ThreadStaticPools.cs @@ -1,41 +1,118 @@ +using System; using System.Buffers; +using System.Collections.Generic; using System.Runtime.CompilerServices; namespace PerfectNumbers.Core { public static class ThreadStaticPools { - [ThreadStatic] - private static ArrayPool? _factorEntryPool; - public static ArrayPool FactorEntryPool - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - return _factorEntryPool ??= ArrayPool.Create(); - } - } + [ThreadStatic] + private static ArrayPool? _factorEntryPool; - [ThreadStatic] - private static ArrayPool? _factorEntry128Pool; - public static ArrayPool FactorEntry128Pool - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - return _factorEntry128Pool ??= ArrayPool.Create(); - } - } + public static ArrayPool FactorEntryPool + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return _factorEntryPool ??= ArrayPool.Create(); + } + } - [ThreadStatic] - private static ArrayPool? _ulongPool; - public static ArrayPool UlongPool - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - return _ulongPool ??= ArrayPool.Create(); - } - } - } -} \ No newline at end of file + [ThreadStatic] + private static ArrayPool? _factorEntry128Pool; + + public static ArrayPool FactorEntry128Pool + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return _factorEntry128Pool ??= ArrayPool.Create(); + } + } + + [ThreadStatic] + private static ArrayPool? _ulongPool; + + public static ArrayPool UlongPool + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return _ulongPool ??= ArrayPool.Create(); + } + } + + [ThreadStatic] + private static Dictionary? _mersenneFactorCacheDictionary; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Dictionary TakeMersenneFactorCacheDictionary() + { + Dictionary? cache = _mersenneFactorCacheDictionary; + if (cache is null) + { + return new Dictionary(8); + } + + _mersenneFactorCacheDictionary = null; + cache.Clear(); + return cache; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReturnMersenneFactorCacheDictionary(Dictionary cache) + { + cache.Clear(); + _mersenneFactorCacheDictionary = cache; + } + + [ThreadStatic] + private static Dictionary? _factorCountDictionary; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Dictionary TakeFactorCountDictionary() + { + Dictionary? dictionary = _factorCountDictionary; + if (dictionary is null) + { + return new Dictionary(8); + } + + _factorCountDictionary = null; + dictionary.Clear(); + return dictionary; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReturnFactorCountDictionary(Dictionary dictionary) + { + dictionary.Clear(); + _factorCountDictionary = dictionary; + } + + [ThreadStatic] + private static Dictionary? _factorScratchDictionary; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Dictionary TakeFactorScratchDictionary() + { + Dictionary? dictionary = _factorScratchDictionary; + if (dictionary is null) + { + return new Dictionary(8); + } + + _factorScratchDictionary = null; + dictionary.Clear(); + return dictionary; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReturnFactorScratchDictionary(Dictionary dictionary) + { + dictionary.Clear(); + _factorScratchDictionary = dictionary; + } + } +}