diff --git a/PerfectNumbers.Core/Cpu/FactorCacheLease.cs b/PerfectNumbers.Core/Cpu/FactorCacheLease.cs deleted file mode 100644 index 2ba1ceb..0000000 --- a/PerfectNumbers.Core/Cpu/FactorCacheLease.cs +++ /dev/null @@ -1,30 +0,0 @@ -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/MersenneNumberDivisorByDivisorCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs index 5a98378..41f548f 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs @@ -149,9 +149,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv return false; } - FactorCacheLease factorCacheLease = new FactorCacheLease(); - Dictionary? factorCache = null; - + // Intentionally recomputes factorizations without a per-thread cache. + // The previous factor cache recorded virtually no hits and only slowed down the scan. DivisorCycleCache cycleCache = DivisorCycleCache.Shared; ulong stepHigh = step.High; @@ -196,16 +195,10 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv // } // else { - if (factorCache is null) - { - factorCacheLease.EnsureInitialized(ref factorCache); - } - if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponent( candidate, prime, divisorData, - factorCache, out ulong computedCycle, out bool primeOrderFailed) || computedCycle == 0UL) { @@ -226,8 +219,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv { // 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. - factorCacheLease.Dispose(); - factorCache = null; divisor.High = 0UL; divisor.Low = 0UL; step.High = 0UL; @@ -253,9 +244,6 @@ 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. - processedAll = divisor.CompareTo(limit) > 0; divisor.High = 0UL; divisor.Low = 0UL; diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index c07f9ff..36dcf77 100644 --- a/PerfectNumbers.Core/MersenneDivisorCycles.cs +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -52,12 +52,6 @@ public class MersenneDivisorCycles } public static bool CycleEqualsExponent(ulong divisor, in MontgomeryDivisorData divisorData, ulong exponent) - { - Dictionary? cache = null; - return CycleEqualsExponent(divisor, exponent, divisorData, ref cache); - } - - public static bool CycleEqualsExponent(ulong divisor, ulong exponent, in MontgomeryDivisorData divisorData, ref Dictionary? factorCache) { if (CycleEqualsExponentForMersenneCandidate(divisor, divisorData, exponent)) { @@ -87,14 +81,7 @@ public class MersenneDivisorCycles return heuristicCycle == exponent; } - Dictionary? cache = factorCache; - if (cache is null) - { - cache = new Dictionary(8); - factorCache = cache; - } - - if (TryCalculateCycleLengthForExponent(divisor, exponent, divisorData, cache, out ulong cycleLength, out bool _) && cycleLength != 0UL) + if (TryCalculateCycleLengthForExponent(divisor, exponent, divisorData, out ulong cycleLength, out bool _) && cycleLength != 0UL) { return cycleLength == exponent; } @@ -262,7 +249,6 @@ public class MersenneDivisorCycles ulong divisor, ulong exponent, in MontgomeryDivisorData divisorData, - Dictionary? factorCache, out ulong cycleLength, out bool primeOrderFailed) { @@ -326,8 +312,8 @@ public class MersenneDivisorCycles factorCounts[2UL] = twoCount; } - bool exponentAccumulated = AccumulateFactors(exponent, factorCache, factorCounts, cacheResult: true); - bool factorsAccumulated = exponentAccumulated && AccumulateFactors(k, factorCache, factorCounts, cacheResult: false); + bool exponentAccumulated = AccumulateFactors(exponent, factorCounts); + bool factorsAccumulated = exponentAccumulated && AccumulateFactors(k, factorCounts); if (factorsAccumulated) { @@ -342,98 +328,29 @@ public class MersenneDivisorCycles private static bool AccumulateFactors( ulong value, - Dictionary? cache, - Dictionary counts, - bool cacheResult) + Dictionary counts) { if (value <= 1UL) { return true; } - if (!TryGetFactorization(value, cache, cacheResult, out FactorCacheEntry? factorization, out bool releaseAfterUse) || factorization is null) - { - return false; - } - - FactorCacheEntry actualFactorization = factorization; - KeyValuePair[]? pairs = actualFactorization.Pairs; - if (pairs is not null) - { - int length = actualFactorization.Count; - - for (int i = 0; i < length; i++) - { - KeyValuePair pair = pairs[i]; - AddFactor(counts, pair.Key, pair.Value); - } - } - - if (releaseAfterUse) - { - actualFactorization.ReturnToPool(); - } - - return true; - } - - private static bool TryGetFactorization( - ulong value, - Dictionary? cache, - bool cacheResult, - out FactorCacheEntry? factorization, - out bool releaseAfterUse) - { - releaseAfterUse = false; - - if (cache is not null && cache.TryGetValue(value, out FactorCacheEntry? cachedEntry)) - { - factorization = cachedEntry; - return true; - } - Dictionary scratch = ThreadStaticPools.RentFactorScratchDictionary(); - bool factored = TryFactorIntoCountsInternal(value, scratch); - if (!factored) + bool success = TryFactorIntoCountsInternal(value, scratch); + if (!success) { + // The scan recalculates factors every time; the old cache never produced hits. ThreadStaticPools.ReturnFactorScratchDictionary(scratch); - factorization = null; return false; } - int count = scratch.Count; - FactorCacheEntry entry = ThreadStaticPools.RentFactorCacheEntry(); - if (count == 0) + for (Dictionary.Enumerator enumerator = scratch.GetEnumerator(); enumerator.MoveNext();) { - entry.Initialize(null, 0, fromPool: false); - factorization = entry; - releaseAfterUse = cache is null || !cacheResult; - if (cache is not null && cacheResult) - { - cache[value] = entry; - } - - ThreadStaticPools.ReturnFactorScratchDictionary(scratch); - return true; + KeyValuePair pair = enumerator.Current; + AddFactor(counts, pair.Key, pair.Value); } - KeyValuePair[] pairs = ThreadStaticPools.FactorKeyValuePairPool.Rent(count); - ((ICollection>)scratch).CopyTo(pairs, 0); ThreadStaticPools.ReturnFactorScratchDictionary(scratch); - Array.Sort(pairs, 0, count, FactorPairValueComparer.Instance); - - entry.Initialize(pairs, count, fromPool: true); - factorization = entry; - - if (cache is not null && cacheResult) - { - cache[value] = entry; - } - else - { - releaseAfterUse = true; - } - return true; } @@ -686,44 +603,6 @@ public class MersenneDivisorCycles } } - public sealed class FactorCacheEntry - { - internal FactorCacheEntry? Next; - internal KeyValuePair[]? Pairs; - internal int Count; - private bool _fromPool; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void Reset() - { - Pairs = null; - Count = 0; - _fromPool = false; - Next = null; - } - - internal void Initialize(KeyValuePair[]? pairs, int count, bool fromPool) - { - Pairs = pairs; - Count = count; - _fromPool = fromPool; - Next = null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void ReturnToPool() - { - KeyValuePair[]? pairs = Pairs; - if (_fromPool && pairs is not null) - { - ThreadStaticPools.FactorKeyValuePairPool.Return(pairs, clearArray: false); - } - - Reset(); - ThreadStaticPools.ReturnFactorCacheEntry(this); - } - } - public static (long nextPosition, long completeCount) FindLast(string path) { using Stream outputStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, BufferSize10M, useAsync: false); diff --git a/PerfectNumbers.Core/ThreadStaticPools.cs b/PerfectNumbers.Core/ThreadStaticPools.cs index 33a9de2..4a2bb97 100644 --- a/PerfectNumbers.Core/ThreadStaticPools.cs +++ b/PerfectNumbers.Core/ThreadStaticPools.cs @@ -31,17 +31,6 @@ 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; @@ -278,62 +267,6 @@ namespace PerfectNumbers.Core pool.Add(dictionary); } - [ThreadStatic] - private static Dictionary? _mersenneFactorCacheDictionary; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Dictionary RentMersenneFactorCacheDictionary() - { - 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) - { - for (Dictionary.Enumerator enumerator = cache.GetEnumerator(); enumerator.MoveNext();) - { - enumerator.Current.Value.ReturnToPool(); - } - - cache.Clear(); - _mersenneFactorCacheDictionary = cache; - } - - [ThreadStatic] - private static MersenneDivisorCycles.FactorCacheEntry? _factorCacheEntryPoolHead; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MersenneDivisorCycles.FactorCacheEntry RentFactorCacheEntry() - { - MersenneDivisorCycles.FactorCacheEntry? entry = _factorCacheEntryPoolHead; - if (entry is null) - { - entry = new MersenneDivisorCycles.FactorCacheEntry(); - } - else - { - _factorCacheEntryPoolHead = entry.Next; - } - - entry.Reset(); - return entry; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ReturnFactorCacheEntry(MersenneDivisorCycles.FactorCacheEntry entry) - { - entry.Next = _factorCacheEntryPoolHead; - _factorCacheEntryPoolHead = entry; - } - [ThreadStatic] private static Dictionary? _factorCountDictionary; diff --git a/docs/bydivisor-cache-analysis.md b/docs/bydivisor-cache-analysis.md index 8b0a56b..19a15b4 100644 --- a/docs/bydivisor-cache-analysis.md +++ b/docs/bydivisor-cache-analysis.md @@ -23,14 +23,13 @@ If the dedicated cycle solver produces a result, it is returned immediately. Oth * **Observed reuse:** The tester only queries the snapshot for divisors at or below four million; larger values invoke `MersenneDivisorCycles.CalculateCycleLength` directly. The by-divisor workload never emits small divisors, so the cache is effectively idle and every large divisor triggers a fresh computation without persisting the result. ### Factorization cache -`MersenneNumberDivisorByDivisorCpuTester` attaches a per-prime `FactorCacheLease` before scanning its divisors. 【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L152-L225】 The cycle calculator then factors `φ(q) = q − 1` by splitting out the power of two, caching the factorization of the current prime exponent, and factoring the residual `k` without caching. 【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L306-L437】 +Previous revisions attached a per-prime factorization cache, but profiling showed virtually no reuse and the added bookkeeping slowed the scan. The implementation now recomputes every factorization on demand and documents why the cache should not return. 【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L152-L221】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L308-L351】 -* **Exponent entries:** The first divisor processed for a prime forces a full factorization of `p`, including a primality check. The resulting `(p → {p})` entry stays in the dictionary and is reused for every later divisor tied to the same prime, eliminating repeated primality work on subsequent iterations. -* **`k` values:** Each divisor introduces a fresh `k`. Those factorisations set `cacheResult:false`, so the results are discarded immediately after use. Even when the Pollard–Rho recursion discovers small shared factors (for example, `k` divisible by three), the cache never sees those values again and therefore captures no reuse for the smaller composites. 【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L329-L437】 -* **Lifetime:** Once the tester leaves the loop, `FactorCacheLease.Dispose` returns the dictionary to the thread-static pool after releasing every stored factorization entry. 【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L251-L259】【F:PerfectNumbers.Core/Cpu/FactorCacheLease.cs†L3-L29】 The next prime obtains a cleared dictionary, so cached data never crosses prime boundaries. +* **Current behavior:** Each divisor recomputes the factorization for the exponent and `k` values using thread-static scratch dictionaries only. Inline comments note that the retired cache produced zero hits in production runs. 【F:PerfectNumbers.Core/MersenneDivisorByDivisorCpuTester.cs†L150-L204】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L332-L347】 +* **Rationale:** The divisor stream never repeats values during a scan, so caching results provided no benefit while increasing synchronization and pooling overhead. The comments exist to prevent reintroducing the pointless cache. 【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L336-L343】 ### Factorization helper pools -The factorization helpers rely on thread-static pools for temporary dictionaries, reusable factor entries, and key-value buffers. These pools only recycle storage; they deliberately clear their contents before handing objects to the next caller. 【F:PerfectNumbers.Core/ThreadStaticPools.cs†L281-L335】 +The factorization helpers rely on thread-static pools for temporary dictionaries, reusable factor entries, and key-value buffers. These pools only recycle storage; they deliberately clear their contents before handing objects to the next caller. 【F:PerfectNumbers.Core/ThreadStaticPools.cs†L270-L316】 * **Observed reuse:** The pools successfully avoid new allocations, but they do not provide value-based caching. All number-theoretic results are recomputed for every divisor except the cached exponent entry described above. @@ -40,4 +39,4 @@ The factorization helpers rely on thread-static pools for temporary dictionaries * **Observed reuse:** The bounded cache keeps quick answers for recurring small primes such as `3` or `5`, while unique large composites are evaluated once and discarded. This prevents the cache from growing with one-off exponent or divisor candidates and aligns its scope with the actual reuse opportunities. ## Summary -Only the cached prime factorization of the current exponent and the bounded ≤4,000,000 primality dictionary deliver meaningful reuse on the CPU by-divisor path. All other caches either hold a single most-recent value, serve small divisors that never appear in this workload, or are limited to memory pooling without retaining computed results. The fallback that bypasses the prime-order solver still funnels every large divisor into a fresh cycle computation, so no additional reuse occurs there. Every branch of the factorization workflow—including the direct prime-order success case, the fallback that invokes `DivisorCycleCache`, and the recursive Pollard–Rho reducer—was reviewed in code; no remaining cache-related areas of this path require inspection. +With the factorization cache removed, the bounded ≤4,000,000 primality dictionary remains the only meaningful reuse on the CPU by-divisor path. All other caches either hold a single most-recent value, serve small divisors that never appear in this workload, or are limited to memory pooling without retaining computed results. The fallback that bypasses the prime-order solver still funnels every large divisor into a fresh cycle computation, so no additional reuse occurs there. Every branch of the factorization workflow—including the direct prime-order success case, the fallback that invokes `DivisorCycleCache`, and the recursive Pollard–Rho reducer—was reviewed in code; no remaining cache-related areas of this path require inspection.