From 64c3721db4a4b382a8b8a4893888b96ec83c4e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Wed, 15 Oct 2025 21:50:11 +0200 Subject: [PATCH] Streamline divisor factor pooling for CPU hot path --- ...MersenneNumberDivisorByDivisorCpuTester.cs | 1 - PerfectNumbers.Core/MersenneDivisorCycles.cs | 76 ++++++++----------- PerfectNumbers.Core/ThreadStaticPools.cs | 16 +++- 3 files changed, 46 insertions(+), 47 deletions(-) diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs index 4bc941b..776f536 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs @@ -184,7 +184,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv prime, divisorData, cacheForCycle, - cacheForCycle is not null, out ulong computedCycle) || computedCycle == 0UL) { divisorCycle = cycleCache.GetCycleLength(candidate); diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index 0258347..c92bd5f 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, usePooledCache: false, out ulong cycleLength) && cycleLength != 0UL) + if (TryCalculateCycleLengthForExponent(divisor, exponent, divisorData, cache, out ulong cycleLength) && cycleLength != 0UL) { return cycleLength == exponent; } @@ -268,7 +268,6 @@ public class MersenneDivisorCycles ulong exponent, in MontgomeryDivisorData divisorData, Dictionary? factorCache, - bool usePooledCache, out ulong cycleLength) { cycleLength = 0UL; @@ -328,9 +327,8 @@ public class MersenneDivisorCycles factorCounts[2UL] = twoCount; } - bool poolExponentFactors = usePooledCache || factorCache is null; - bool exponentAccumulated = AccumulateFactors(exponent, factorCache, factorCounts, cacheResult: true, allowPooledStorage: poolExponentFactors); - bool factorsAccumulated = exponentAccumulated && AccumulateFactors(k, factorCache, factorCounts, cacheResult: false, allowPooledStorage: true); + bool exponentAccumulated = AccumulateFactors(exponent, factorCache, factorCounts, cacheResult: true); + bool factorsAccumulated = exponentAccumulated && AccumulateFactors(k, factorCache, factorCounts, cacheResult: false); if (factorsAccumulated) { @@ -346,31 +344,28 @@ public class MersenneDivisorCycles ulong value, Dictionary? cache, Dictionary counts, - bool cacheResult, - bool allowPooledStorage) + bool cacheResult) { if (value <= 1UL) { return true; } - bool poolStorage = allowPooledStorage || cache is null || !cacheResult; - - if (!TryGetFactorization(value, cache, cacheResult, poolStorage, out FactorCacheEntry? factorization, out bool releaseAfterUse) || factorization is null) + if (!TryGetFactorization(value, cache, cacheResult, out FactorCacheEntry? factorization, out bool releaseAfterUse) || factorization is null) { return false; } FactorCacheEntry actualFactorization = factorization; - FactorEntry[]? entries = actualFactorization.Entries; - if (entries is not null) + KeyValuePair[]? pairs = actualFactorization.Pairs; + if (pairs is not null) { int length = actualFactorization.Count; for (int i = 0; i < length; i++) { - FactorEntry entry = entries[i]; - AddFactor(counts, entry.Value, entry.Exponent); + KeyValuePair pair = pairs[i]; + AddFactor(counts, pair.Key, pair.Value); } } @@ -386,7 +381,6 @@ public class MersenneDivisorCycles ulong value, Dictionary? cache, bool cacheResult, - bool allowPooledStorage, out FactorCacheEntry? factorization, out bool releaseAfterUse) { @@ -423,24 +417,12 @@ public class MersenneDivisorCycles return true; } - bool fromPool = allowPooledStorage; - FactorEntry[] entries = fromPool - ? ThreadStaticPools.FactorEntryPool.Rent(count) - : new FactorEntry[count]; - - Dictionary.Enumerator enumerator = scratch.GetEnumerator(); - int index = 0; - while (enumerator.MoveNext()) - { - KeyValuePair current = enumerator.Current; - entries[index] = new FactorEntry(current.Key, current.Value); - index++; - } - + KeyValuePair[] pairs = ThreadStaticPools.FactorKeyValuePairPool.Rent(count); + ((ICollection>)scratch).CopyTo(pairs, 0); ThreadStaticPools.ReturnFactorScratchDictionary(scratch); - Array.Sort(entries, 0, count, FactorEntryValueComparer.Instance); + Array.Sort(pairs, 0, count, FactorPairValueComparer.Instance); - entry.Initialize(entries, count, fromPool); + entry.Initialize(pairs, count, fromPool: true); factorization = entry; if (cache is not null && cacheResult) @@ -476,6 +458,7 @@ public class MersenneDivisorCycles break; } + // TODO: Reuse the remainder-tracking update from the cycle heuristic so repeated divisions disappear from this loop. while ((remaining % prime) == 0UL) { AddFactor(counts, prime); @@ -488,18 +471,23 @@ public class MersenneDivisorCycles return true; } + // TODO: Benchmark PrimeTester.IsPrimeInternal against Open.Numeric.Primes on the EvenPerfectBitScanner workloads to confirm the faster option for wide composites. if (PrimeTester.IsPrimeInternal(remaining, CancellationToken.None)) { AddFactor(counts, remaining); return true; } + // TODO: Evaluate folding the remainder-tracking mechanism into PollardRho64 so the walk leverages previously computed residues. ulong factor = PollardRho64(remaining); if (factor == 0UL || factor == remaining) { + // Pollard-Rho can still stall on rare stubborn composites during test and benchmark runs, so keep the failure guard in place + // and let the caller fall back to the cached cycle calculator. return false; } + // TODO: Investigate computing this quotient via remainder tracking or reciprocal multiplication to avoid the division. ulong quotient = remaining / factor; // Pollard-Rho never returns factors <= 1 on the EvenPerfectBitScanner path; keep the old guard documented // for completeness without branching in the hot path. @@ -674,22 +662,22 @@ public class MersenneDivisorCycles return BinaryPrimitives.ReadUInt64LittleEndian(buffer); } - private sealed class FactorEntryValueComparer : IComparer + private sealed class FactorPairValueComparer : IComparer> { - public static readonly FactorEntryValueComparer Instance = new FactorEntryValueComparer(); + public static readonly FactorPairValueComparer Instance = new FactorPairValueComparer(); - private FactorEntryValueComparer() + private FactorPairValueComparer() { } - public int Compare(FactorEntry x, FactorEntry y) + public int Compare(KeyValuePair x, KeyValuePair y) { - if (x.Value < y.Value) + if (x.Key < y.Key) { return -1; } - if (x.Value > y.Value) + if (x.Key > y.Key) { return 1; } @@ -701,22 +689,22 @@ public class MersenneDivisorCycles public sealed class FactorCacheEntry { internal FactorCacheEntry? Next; - internal FactorEntry[]? Entries; + internal KeyValuePair[]? Pairs; internal int Count; private bool _fromPool; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void Reset() { - Entries = null; + Pairs = null; Count = 0; _fromPool = false; Next = null; } - internal void Initialize(FactorEntry[]? entries, int count, bool fromPool) + internal void Initialize(KeyValuePair[]? pairs, int count, bool fromPool) { - Entries = entries; + Pairs = pairs; Count = count; _fromPool = fromPool; Next = null; @@ -725,10 +713,10 @@ public class MersenneDivisorCycles [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void ReturnToPool() { - FactorEntry[]? entries = Entries; - if (_fromPool && entries is not null) + KeyValuePair[]? pairs = Pairs; + if (_fromPool && pairs is not null) { - ThreadStaticPools.FactorEntryPool.Return(entries, clearArray: false); + ThreadStaticPools.FactorKeyValuePairPool.Return(pairs, clearArray: false); } Reset(); diff --git a/PerfectNumbers.Core/ThreadStaticPools.cs b/PerfectNumbers.Core/ThreadStaticPools.cs index 07a9081..313e30b 100644 --- a/PerfectNumbers.Core/ThreadStaticPools.cs +++ b/PerfectNumbers.Core/ThreadStaticPools.cs @@ -32,6 +32,18 @@ namespace PerfectNumbers.Core } } + + [ThreadStatic] + private static ArrayPool>? _factorKeyValuePairPool; + + public static ArrayPool> FactorKeyValuePairPool + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return _factorKeyValuePairPool ??= ArrayPool>.Create(); + } + } [ThreadStatic] private static MersenneCpuDivisorScanSession? _mersenneCpuDivisorSession; @@ -87,9 +99,9 @@ namespace PerfectNumbers.Core [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ReturnMersenneFactorCacheDictionary(Dictionary cache) { - foreach (KeyValuePair entry in cache) + for (Dictionary.Enumerator enumerator = cache.GetEnumerator(); enumerator.MoveNext();) { - entry.Value.ReturnToPool(); + enumerator.Current.Value.ReturnToPool(); } cache.Clear();