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】