diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index 5efc86a..e208ec1 100644 --- a/PerfectNumbers.Core/MersenneDivisorCycles.cs +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -1,3 +1,4 @@ +using System; using System.Buffers; using System.Buffers.Binary; using System.Collections.Generic; @@ -18,6 +19,12 @@ public class MersenneDivisorCycles // Lightweight read-mostly cache for small divisors (<= 4,000,000). 0 => unknown private ulong[]? _smallCycles; + [ThreadStatic] + private static Dictionary? s_factorCountsPool; + + [ThreadStatic] + private static Dictionary? s_factorScratchPool; + public static MersenneDivisorCycles Shared { get; } = new MersenneDivisorCycles(); public MersenneDivisorCycles() @@ -330,21 +337,28 @@ public class MersenneDivisorCycles // return false; // } - Dictionary factorCounts = new(8); - // The divisor candidates are odd, so phi is even and this branch always records at least one factor of two. - // if (twoCount > 0) - // { - // factorCounts[2UL] = twoCount; - // } - factorCounts[2UL] = twoCount; - - if (!AccumulateFactors(exponent, factorCache, factorCounts, cacheResult: true) || !AccumulateFactors(k, factorCache, factorCounts, cacheResult: false)) + Dictionary factorCounts = AcquireFactorCountDictionary(8); + try { - return false; - } + // The divisor candidates are odd, so phi is even and this branch always records at least one factor of two. + // if (twoCount > 0) + // { + // factorCounts[2UL] = twoCount; + // } + 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 + { + ReleaseFactorCountDictionary(factorCounts); + } } private static bool AccumulateFactors( @@ -358,61 +372,119 @@ public class MersenneDivisorCycles return true; } - if (!TryGetFactorization(value, cache, cacheResult, out FactorCacheEntry factorization)) + FactorCacheEntry cachedFactorization; + PrimeFactorPartialResult? lease = null; + bool fromCache; + + try { - return false; + if (!TryGetFactorization(value, cache, cacheResult, out cachedFactorization, out lease, out fromCache)) + { + return false; + } + + if (fromCache) + { + ulong[] primes = cachedFactorization.Primes; + byte[] exponents = cachedFactorization.Exponents; + int length = cachedFactorization.Count; + + for (int i = 0; i < length; i++) + { + AddFactor(counts, primes[i], exponents[i]); + } + + return true; + } + + if (lease is null || lease.Count == 0) + { + return true; + } + + ReadOnlySpan primesSpan = lease.Primes; + ReadOnlySpan exponentsSpan = lease.Exponents; + int spanLength = lease.Count; + + for (int i = 0; i < spanLength; i++) + { + AddFactor(counts, primesSpan[i], exponentsSpan[i]); + } + + return true; } - - ulong[] primes = factorization.Primes; - byte[] exponents = factorization.Exponents; - int length = factorization.Count; - - for (int i = 0; i < length; i++) + finally { - AddFactor(counts, primes[i], exponents[i]); + lease?.Dispose(); } - - return true; } private static bool TryGetFactorization( ulong value, Dictionary? cache, bool cacheResult, - out FactorCacheEntry factorization) + out FactorCacheEntry factorization, + out PrimeFactorPartialResult? lease, + out bool fromCache) { - if (cache is not null && cache.TryGetValue(value, out factorization)) + factorization = default; + lease = null; + fromCache = false; + + if (value <= 1UL) { return true; } - Dictionary scratch = new(8); - if (!TryFactorIntoCountsInternal(value, scratch)) + if (cache is not null && cache.TryGetValue(value, out factorization)) { - factorization = default!; - return false; + fromCache = true; + return true; } - int count = scratch.Count; - ulong[] primes = new ulong[count]; - byte[] exponents = new byte[count]; - int index = 0; - foreach (KeyValuePair entry in scratch) + Dictionary scratch = AcquireFactorScratchDictionary(8); + try { - primes[index] = entry.Key; - exponents[index] = checked((byte)entry.Value); - index++; + if (!TryFactorIntoCountsInternal(value, scratch)) + { + factorization = default; + return false; + } + + int count = scratch.Count; + if (count == 0) + { + return true; + } + + PrimeFactorPartialResult result = PrimeFactorPartialResult.Rent(count); + lease = result; + + Span primes = result.PrimeWriteSpan; + Span exponents = result.ExponentWriteSpan; + int index = 0; + foreach (KeyValuePair entry in scratch) + { + primes[index] = entry.Key; + exponents[index] = checked((byte)entry.Value); + index++; + } + + result.CommitCount(index); + result.Sort(); + + if (cache is not null && cacheResult) + { + FactorCacheEntry cacheEntry = result.ToCacheEntry(); + cache[value] = cacheEntry; + } + + return true; } - - Array.Sort(primes, exponents); - factorization = new FactorCacheEntry(primes, exponents, count); - - if (cache is not null && cacheResult) + finally { - cache[value] = factorization; + ReleaseFactorScratchDictionary(scratch); } - - return true; } private static bool TryFactorIntoCountsInternal(ulong value, Dictionary counts) @@ -608,6 +680,53 @@ public class MersenneDivisorCycles } } + // Thread-local dictionary pools keep the factoring helpers reusable so the GC stays out of the by-divisor CPU loop. + private static Dictionary AcquireFactorCountDictionary(int capacityHint) + { + Dictionary? dictionary = s_factorCountsPool; + if (dictionary is null) + { + return new Dictionary(capacityHint); + } + + s_factorCountsPool = null; + dictionary.Clear(); + dictionary.EnsureCapacity(capacityHint); + return dictionary; + } + + private static void ReleaseFactorCountDictionary(Dictionary dictionary) + { + dictionary.Clear(); + if (s_factorCountsPool is null) + { + s_factorCountsPool = dictionary; + } + } + + private static Dictionary AcquireFactorScratchDictionary(int capacityHint) + { + Dictionary? dictionary = s_factorScratchPool; + if (dictionary is null) + { + return new Dictionary(capacityHint); + } + + s_factorScratchPool = null; + dictionary.Clear(); + dictionary.EnsureCapacity(capacityHint); + return dictionary; + } + + private static void ReleaseFactorScratchDictionary(Dictionary dictionary) + { + dictionary.Clear(); + if (s_factorScratchPool is null) + { + s_factorScratchPool = dictionary; + } + } + private static ulong NextRandomUInt64() { Span buffer = stackalloc byte[8]; diff --git a/PerfectNumbers.Core/PrimeFactorPartialResult.cs b/PerfectNumbers.Core/PrimeFactorPartialResult.cs new file mode 100644 index 0000000..549f401 --- /dev/null +++ b/PerfectNumbers.Core/PrimeFactorPartialResult.cs @@ -0,0 +1,150 @@ +using System; +using System.Buffers; + +namespace PerfectNumbers.Core +{ + internal sealed class PrimeFactorPartialResult : IDisposable + { + [ThreadStatic] + private static PrimeFactorPartialResult? s_poolHead; + + private PrimeFactorPartialResult? _next; + private ulong[]? _primes; + private byte[]? _exponents; + private bool _arraysFromPool; + private int _capacity; + + private PrimeFactorPartialResult() + { + } + + public static PrimeFactorPartialResult Rent(int capacity) + { + PrimeFactorPartialResult? instance = s_poolHead; + if (instance is null) + { + instance = new PrimeFactorPartialResult(); + } + else + { + s_poolHead = instance._next; + } + + instance._next = null; + instance.EnsureCapacity(capacity); + instance.Count = 0; + return instance; + } + + public int Count { get; private set; } + + internal ReadOnlySpan Primes => _primes is null ? ReadOnlySpan.Empty : _primes.AsSpan(0, Count); + + internal ReadOnlySpan Exponents => _exponents is null ? ReadOnlySpan.Empty : _exponents.AsSpan(0, Count); + + internal Span PrimeWriteSpan => _primes is null ? Span.Empty : _primes.AsSpan(0, _capacity); + + internal Span ExponentWriteSpan => _exponents is null ? Span.Empty : _exponents.AsSpan(0, _capacity); + + internal void CommitCount(int count) + { + Count = count; + } + + internal void Sort() + { + if (Count == 0 || _primes is null || _exponents is null) + { + return; + } + + Array.Sort(_primes, _exponents, 0, Count); + } + + internal MersenneDivisorCycles.FactorCacheEntry ToCacheEntry() + { + if (Count == 0 || _primes is null || _exponents is null) + { + return default; + } + + ulong[] primes = new ulong[Count]; + byte[] exponents = new byte[Count]; + Array.Copy(_primes, 0, primes, 0, Count); + Array.Copy(_exponents, 0, exponents, 0, Count); + return new MersenneDivisorCycles.FactorCacheEntry(primes, exponents, Count); + } + + public void Dispose() + { + if (_arraysFromPool) + { + if (_primes is not null) + { + ArrayPool.Shared.Return(_primes, clearArray: false); + } + + if (_exponents is not null) + { + ArrayPool.Shared.Return(_exponents, clearArray: false); + } + + _arraysFromPool = false; + _primes = null; + _exponents = null; + } + + _capacity = 0; + Count = 0; + + _next = s_poolHead; + s_poolHead = this; + } + + private void EnsureCapacity(int capacity) + { + if (capacity <= 0) + { + _capacity = 0; + return; + } + + if (_primes is not null && _primes.Length >= capacity) + { + _capacity = capacity; + return; + } + + if (_arraysFromPool) + { + if (_primes is not null) + { + ArrayPool.Shared.Return(_primes, clearArray: false); + } + + if (_exponents is not null) + { + ArrayPool.Shared.Return(_exponents, clearArray: false); + } + + _arraysFromPool = false; + _primes = null; + _exponents = null; + } + + if (capacity >= PerfectNumberConstants.PooledArrayThreshold) + { + _primes = ArrayPool.Shared.Rent(capacity); + _exponents = ArrayPool.Shared.Rent(capacity); + _arraysFromPool = true; + } + else + { + _primes = new ulong[capacity]; + _exponents = new byte[capacity]; + } + + _capacity = capacity; + } + } +}