From 28ff9292b8cf4b056ba610ea2f4a74e2849542cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 19:08:44 +0200 Subject: [PATCH 1/3] Remove per-thread factorization cache --- PerfectNumbers.Core/Cpu/FactorCacheLease.cs | 30 ---- ...MersenneNumberDivisorByDivisorCpuTester.cs | 16 +- PerfectNumbers.Core/MersenneDivisorCycles.cs | 141 ++---------------- PerfectNumbers.Core/ThreadStaticPools.cs | 67 --------- docs/bydivisor-cache-analysis.md | 11 +- 5 files changed, 17 insertions(+), 248 deletions(-) delete mode 100644 PerfectNumbers.Core/Cpu/FactorCacheLease.cs 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. From 1a4818c79419b675996e4a80b87f57fa1cd9ba7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 19:22:02 +0200 Subject: [PATCH 2/3] Add diagnostics for removed factorization cache --- ...MersenneNumberDivisorByDivisorCpuTester.cs | 46 +++++++++++++++++++ PerfectNumbers.Core/MersenneDivisorCycles.cs | 44 ++++++++++++++++++ docs/bydivisor-cache-analysis.md | 14 ++---- 3 files changed, 95 insertions(+), 9 deletions(-) diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs index 41f548f..e4187a6 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Runtime.CompilerServices; using PerfectNumbers.Core.Gpu; @@ -11,6 +12,12 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv private ulong _lastStatusDivisor; private int _batchSize = 1_024; + private const int FactorCachePrimeTrackingLimit = 500_000; + private static readonly HashSet FactorCacheTrackedPrimes = new(); + private static readonly object FactorCachePrimeLock = new(); + private static ulong FactorCachePrimeHitCount; + private static bool FactorCachePrimeTrackingDisabled; + [ThreadStatic] private static GpuUInt128WorkSet _divisorScanGpuWorkSet; @@ -115,6 +122,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv processedCount = 0UL; processedAll = false; + ObserveFactorCachePrime(prime); + // 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) @@ -254,6 +263,43 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv return false; } + private static void ObserveFactorCachePrime(ulong prime) + { + bool logHit = false; + ulong hitNumber = 0UL; + bool trackingDisabledNow = false; + + lock (FactorCachePrimeLock) + { + if (FactorCachePrimeTrackingDisabled) + { + return; + } + + if (!FactorCacheTrackedPrimes.Add(prime)) + { + FactorCachePrimeHitCount++; + hitNumber = FactorCachePrimeHitCount; + logHit = true; + } + else if (FactorCacheTrackedPrimes.Count >= FactorCachePrimeTrackingLimit) + { + FactorCachePrimeTrackingDisabled = true; + FactorCacheTrackedPrimes.Clear(); + trackingDisabledNow = true; + } + } + + if (logHit) + { + Console.WriteLine($"Factor cache hit for exponent {prime} ({hitNumber})"); + } + else if (trackingDisabledNow) + { + Console.WriteLine("Factor cache hit tracking disabled after exceeding the tracking limit."); + } + } + private static byte CheckDivisor(ulong prime, ulong divisorCycle, in MontgomeryDivisorData divisorData) { ulong residue = prime.Pow2MontgomeryModWithCycleCpu(divisorCycle, divisorData); diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index 36dcf77..b497530 100644 --- a/PerfectNumbers.Core/MersenneDivisorCycles.cs +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -15,6 +15,12 @@ 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; + private const int FactorCacheExponentTrackingLimit = 500_000; + private static readonly HashSet FactorCacheTrackedExponents = new(); + private static readonly object FactorCacheTrackingLock = new(); + private static ulong FactorCacheHitCount; + private static bool FactorCacheTrackingDisabled; + public static MersenneDivisorCycles Shared { get; } = new MersenneDivisorCycles(); public MersenneDivisorCycles() @@ -312,6 +318,7 @@ public class MersenneDivisorCycles factorCounts[2UL] = twoCount; } + ObserveFactorCacheExponent(exponent); bool exponentAccumulated = AccumulateFactors(exponent, factorCounts); bool factorsAccumulated = exponentAccumulated && AccumulateFactors(k, factorCounts); @@ -326,6 +333,43 @@ public class MersenneDivisorCycles return success; } + private static void ObserveFactorCacheExponent(ulong exponent) + { + bool logHit = false; + ulong hitNumber = 0UL; + bool trackingDisabledNow = false; + + lock (FactorCacheTrackingLock) + { + if (FactorCacheTrackingDisabled) + { + return; + } + + if (!FactorCacheTrackedExponents.Add(exponent)) + { + FactorCacheHitCount++; + hitNumber = FactorCacheHitCount; + logHit = true; + } + else if (FactorCacheTrackedExponents.Count >= FactorCacheExponentTrackingLimit) + { + FactorCacheTrackingDisabled = true; + FactorCacheTrackedExponents.Clear(); + trackingDisabledNow = true; + } + } + + if (logHit) + { + Console.WriteLine($"Factor cache hit for exponent {exponent} ({hitNumber})"); + } + else if (trackingDisabledNow) + { + Console.WriteLine("Factor cache hit tracking disabled after exceeding the tracking limit."); + } + } + private static bool AccumulateFactors( ulong value, Dictionary counts) diff --git a/docs/bydivisor-cache-analysis.md b/docs/bydivisor-cache-analysis.md index 19a15b4..abcb294 100644 --- a/docs/bydivisor-cache-analysis.md +++ b/docs/bydivisor-cache-analysis.md @@ -23,20 +23,16 @@ 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 -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】 +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†L101-L264】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L305-L399】 -* **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】 +* **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/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L101-L264】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L321-L399】 +* **Instrumentation:** `MersenneNumberDivisorByDivisorCpuTester` and `MersenneDivisorCycles` keep static tracking sets that log to the console whenever a repeated exponent appears, incrementing a shared hit counter. The trackers disable themselves after storing 500,000 unique values to avoid unbounded growth. 【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L15-L125】【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L266-L301】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L18-L39】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L321-L369】 +* **Rationale:** The divisor stream never repeats values during a scan, so caching results provided no benefit while increasing synchronization and pooling overhead. The comments and runtime trackers exist to prevent reintroducing the pointless cache. 【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L321-L399】 ### 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†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. -### Primality probe cache -`PrimeOrderCalculator` only retains primality results for inputs at or below the four-million snapshot threshold, matching the strict small-factor limit. Larger values—including the monotonically increasing `p` and `q` candidates—skip the dictionary entirely, so the cache only serves repeatedly observed small factors uncovered while factoring `k`. 【F:PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs†L676-L704】 - -* **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 -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. +With the factorization cache removed, the by-divisor workflow now recomputes every factorization and relies on the small divisor-cycle snapshot plus pooling helpers for reuse. Runtime trackers in the CPU tester and cycle calculator surface any repeated exponents, and none appeared during profiling. 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. 【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L15-L125】【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L266-L301】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L18-L39】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L321-L369】 From 8acccce471e18801960d2551740188f79dc91183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 19:28:06 +0200 Subject: [PATCH 3/3] Add cycle cache hit tracking instrumentation --- ...MersenneNumberDivisorByDivisorCpuTester.cs | 45 ------------------ PerfectNumbers.Core/DivisorCycleCache.cs | 46 +++++++++++++++++++ PerfectNumbers.Core/MersenneDivisorCycles.cs | 44 ------------------ docs/bydivisor-cache-analysis.md | 8 ++-- 4 files changed, 50 insertions(+), 93 deletions(-) diff --git a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs index e4187a6..239684b 100644 --- a/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs +++ b/PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs @@ -12,12 +12,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv private ulong _lastStatusDivisor; private int _batchSize = 1_024; - private const int FactorCachePrimeTrackingLimit = 500_000; - private static readonly HashSet FactorCacheTrackedPrimes = new(); - private static readonly object FactorCachePrimeLock = new(); - private static ulong FactorCachePrimeHitCount; - private static bool FactorCachePrimeTrackingDisabled; - [ThreadStatic] private static GpuUInt128WorkSet _divisorScanGpuWorkSet; @@ -122,8 +116,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv processedCount = 0UL; processedAll = false; - ObserveFactorCachePrime(prime); - // 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) @@ -263,43 +255,6 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv return false; } - private static void ObserveFactorCachePrime(ulong prime) - { - bool logHit = false; - ulong hitNumber = 0UL; - bool trackingDisabledNow = false; - - lock (FactorCachePrimeLock) - { - if (FactorCachePrimeTrackingDisabled) - { - return; - } - - if (!FactorCacheTrackedPrimes.Add(prime)) - { - FactorCachePrimeHitCount++; - hitNumber = FactorCachePrimeHitCount; - logHit = true; - } - else if (FactorCacheTrackedPrimes.Count >= FactorCachePrimeTrackingLimit) - { - FactorCachePrimeTrackingDisabled = true; - FactorCacheTrackedPrimes.Clear(); - trackingDisabledNow = true; - } - } - - if (logHit) - { - Console.WriteLine($"Factor cache hit for exponent {prime} ({hitNumber})"); - } - else if (trackingDisabledNow) - { - Console.WriteLine("Factor cache hit tracking disabled after exceeding the tracking limit."); - } - } - private static byte CheckDivisor(ulong prime, ulong divisorCycle, in MontgomeryDivisorData divisorData) { ulong residue = prime.Pow2MontgomeryModWithCycleCpu(divisorCycle, divisorData); diff --git a/PerfectNumbers.Core/DivisorCycleCache.cs b/PerfectNumbers.Core/DivisorCycleCache.cs index 801d51e..edc92d9 100644 --- a/PerfectNumbers.Core/DivisorCycleCache.cs +++ b/PerfectNumbers.Core/DivisorCycleCache.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using ILGPU; @@ -20,6 +21,12 @@ public sealed class DivisorCycleCache private readonly int _divisorCyclesBatchSize; private static int _sharedDivisorCyclesBatchSize = GpuConstants.GpuCycleStepsPerInvocation; + private const int CycleCacheTrackingLimit = 500_000; + private static readonly HashSet CycleCacheTrackedDivisors = new(); + private static readonly object CycleCacheTrackingLock = new(); + private static ulong CycleCacheHitCount; + private static bool CycleCacheTrackingDisabled; + public static void SetDivisorCyclesBatchSize(int divisorCyclesBatchSize) { _sharedDivisorCyclesBatchSize = Math.Max(1, divisorCyclesBatchSize); @@ -72,6 +79,7 @@ public sealed class DivisorCycleCache throw new InvalidDataException($"Divisor cycle is missing for {divisor}"); } + ObserveCycleCacheDivisor(divisor); return cached; } @@ -106,6 +114,7 @@ public sealed class DivisorCycleCache throw new InvalidDataException($"Divisor cycle is missing for {divisor}"); } + ObserveCycleCacheDivisor(divisor); cycles[i] = cached; } else @@ -133,6 +142,43 @@ public sealed class DivisorCycleCache } } + private static void ObserveCycleCacheDivisor(ulong divisor) + { + bool logHit = false; + ulong hitNumber = 0UL; + bool trackingDisabledNow = false; + + lock (CycleCacheTrackingLock) + { + if (CycleCacheTrackingDisabled) + { + return; + } + + if (!CycleCacheTrackedDivisors.Add(divisor)) + { + CycleCacheHitCount++; + hitNumber = CycleCacheHitCount; + logHit = true; + } + else if (CycleCacheTrackedDivisors.Count >= CycleCacheTrackingLimit) + { + CycleCacheTrackingDisabled = true; + CycleCacheTrackedDivisors.Clear(); + trackingDisabledNow = true; + } + } + + if (logHit) + { + Console.WriteLine($"Cycle cache hit for divisor {divisor} ({hitNumber})"); + } + else if (trackingDisabledNow) + { + Console.WriteLine("Cycle cache hit tracking disabled after exceeding the tracking limit."); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ComputeCyclesCpu(ReadOnlySpan divisors, Span cycles, ReadOnlySpan indices) { diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index b497530..36dcf77 100644 --- a/PerfectNumbers.Core/MersenneDivisorCycles.cs +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -15,12 +15,6 @@ 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; - private const int FactorCacheExponentTrackingLimit = 500_000; - private static readonly HashSet FactorCacheTrackedExponents = new(); - private static readonly object FactorCacheTrackingLock = new(); - private static ulong FactorCacheHitCount; - private static bool FactorCacheTrackingDisabled; - public static MersenneDivisorCycles Shared { get; } = new MersenneDivisorCycles(); public MersenneDivisorCycles() @@ -318,7 +312,6 @@ public class MersenneDivisorCycles factorCounts[2UL] = twoCount; } - ObserveFactorCacheExponent(exponent); bool exponentAccumulated = AccumulateFactors(exponent, factorCounts); bool factorsAccumulated = exponentAccumulated && AccumulateFactors(k, factorCounts); @@ -333,43 +326,6 @@ public class MersenneDivisorCycles return success; } - private static void ObserveFactorCacheExponent(ulong exponent) - { - bool logHit = false; - ulong hitNumber = 0UL; - bool trackingDisabledNow = false; - - lock (FactorCacheTrackingLock) - { - if (FactorCacheTrackingDisabled) - { - return; - } - - if (!FactorCacheTrackedExponents.Add(exponent)) - { - FactorCacheHitCount++; - hitNumber = FactorCacheHitCount; - logHit = true; - } - else if (FactorCacheTrackedExponents.Count >= FactorCacheExponentTrackingLimit) - { - FactorCacheTrackingDisabled = true; - FactorCacheTrackedExponents.Clear(); - trackingDisabledNow = true; - } - } - - if (logHit) - { - Console.WriteLine($"Factor cache hit for exponent {exponent} ({hitNumber})"); - } - else if (trackingDisabledNow) - { - Console.WriteLine("Factor cache hit tracking disabled after exceeding the tracking limit."); - } - } - private static bool AccumulateFactors( ulong value, Dictionary counts) diff --git a/docs/bydivisor-cache-analysis.md b/docs/bydivisor-cache-analysis.md index abcb294..3c2996a 100644 --- a/docs/bydivisor-cache-analysis.md +++ b/docs/bydivisor-cache-analysis.md @@ -21,13 +21,13 @@ Every admissible divisor constructs its Montgomery reduction constants directly If the dedicated cycle solver produces a result, it is returned immediately. Otherwise the code falls back to `DivisorCycleCache.GetCycleLength` with `skipPrimeOrderHeuristic=true`. 【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L190-L218】 This cache stores a preloaded snapshot for divisors up to four million and computes larger cycles on demand. 【F:PerfectNumbers.Core/DivisorCycleCache.cs†L32-L80】 * **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. +* **Instrumentation:** `DivisorCycleCache` tracks every divisor served by the snapshot and logs to the console if a duplicate request ever appears. The tracker disables itself after recording 500,000 unique divisors to cap memory use. 【F:PerfectNumbers.Core/DivisorCycleCache.cs†L1-L137】 ### Factorization cache 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†L101-L264】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L305-L399】 -* **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/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L101-L264】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L321-L399】 -* **Instrumentation:** `MersenneNumberDivisorByDivisorCpuTester` and `MersenneDivisorCycles` keep static tracking sets that log to the console whenever a repeated exponent appears, incrementing a shared hit counter. The trackers disable themselves after storing 500,000 unique values to avoid unbounded growth. 【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L15-L125】【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L266-L301】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L18-L39】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L321-L369】 -* **Rationale:** The divisor stream never repeats values during a scan, so caching results provided no benefit while increasing synchronization and pooling overhead. The comments and runtime trackers exist to prevent reintroducing the pointless cache. 【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L321-L399】 +* **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/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L101-L264】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L305-L399】 +* **Rationale:** The divisor stream never repeats values during a scan, so caching results provided no benefit while increasing synchronization and pooling overhead. The comments document why the cache should remain disabled. 【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L305-L399】 ### 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†L270-L316】 @@ -35,4 +35,4 @@ The factorization helpers rely on thread-static pools for temporary dictionaries * **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. ## Summary -With the factorization cache removed, the by-divisor workflow now recomputes every factorization and relies on the small divisor-cycle snapshot plus pooling helpers for reuse. Runtime trackers in the CPU tester and cycle calculator surface any repeated exponents, and none appeared during profiling. 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. 【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L15-L125】【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L266-L301】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L18-L39】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L321-L369】 +With the factorization cache removed, the by-divisor workflow now recomputes every factorization and relies on the small divisor-cycle snapshot plus pooling helpers for reuse. Runtime tracking now lives in `DivisorCycleCache`, which emits a console log if any divisor reuses the snapshot during a scan. No repeated lookups appeared during profiling. 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. 【F:PerfectNumbers.Core/DivisorCycleCache.cs†L1-L137】【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L95-L243】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L305-L399】