Merge pull request #208 from mlemanczyk/codex/analyze-cache-usage-in-evenperfectbitscanner
Remove unused by-divisor cache fallback
This commit is contained in:
5 files changed
+64
-31
No files matched your search
@@ -27,7 +27,7 @@ internal sealed class MersenneCpuDivisorScanSession : IMersenneNumberDivisorByDi
|
||||
MontgomeryDivisorData cachedData = divisorData;
|
||||
if (cachedData.Modulus != divisor)
|
||||
{
|
||||
cachedData = MontgomeryDivisorDataCache.Get(divisor);
|
||||
cachedData = MontgomeryDivisorData.FromModulus(divisor);
|
||||
}
|
||||
|
||||
if (divisorCycle == 0UL)
|
||||
|
||||
@@ -187,7 +187,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
||||
|
||||
if (admissible && (remainder8 == 1 || remainder8 == 7) && remainder3 != 0 && remainder5 != 0 && remainder7 != 0 && remainder11 != 0)
|
||||
{
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(candidate);
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(candidate);
|
||||
ulong divisorCycle;
|
||||
// Divisors generated from 2 * k * p + 1 exceed the small-cycle snapshot when p >= 138,000,000, so the short path below never runs.
|
||||
// if (candidate <= PerfectNumberConstants.MaxQForDivisorCycles)
|
||||
@@ -209,7 +209,12 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
||||
out ulong computedCycle,
|
||||
out bool primeOrderFailed) || computedCycle == 0UL)
|
||||
{
|
||||
divisorCycle = cycleCache.GetCycleLength(candidate, skipPrimeOrderHeuristic: primeOrderFailed);
|
||||
// Divisors produced by 2 * k * p + 1 always exceed PerfectNumberConstants.MaxQForDivisorCycles
|
||||
// for the exponents scanned here, so skip the unused cache fallback and compute directly.
|
||||
divisorCycle = MersenneDivisorCycles.CalculateCycleLength(
|
||||
candidate,
|
||||
divisorData,
|
||||
skipPrimeOrderHeuristic: primeOrderFailed);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -45,30 +45,3 @@ public readonly struct MontgomeryDivisorData(ulong modulus, ulong nPrime, ulong
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal static class MontgomeryDivisorDataCache
|
||||
{
|
||||
[ThreadStatic]
|
||||
private static bool s_hasCachedValue;
|
||||
|
||||
[ThreadStatic]
|
||||
private static ulong s_cachedModulus;
|
||||
|
||||
[ThreadStatic]
|
||||
private static MontgomeryDivisorData s_cachedData;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static MontgomeryDivisorData Get(ulong modulus)
|
||||
{
|
||||
if (s_hasCachedValue && s_cachedModulus == modulus)
|
||||
{
|
||||
return s_cachedData;
|
||||
}
|
||||
|
||||
MontgomeryDivisorData computed = MontgomeryDivisorData.FromModulus(modulus);
|
||||
s_cachedModulus = modulus;
|
||||
s_cachedData = computed;
|
||||
s_hasCachedValue = true;
|
||||
return computed;
|
||||
}
|
||||
}
|
||||
@@ -673,8 +673,9 @@ internal static partial class PrimeOrderCalculator
|
||||
return exponent.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL;
|
||||
}
|
||||
|
||||
|
||||
// [ThreadStatic]
|
||||
private static readonly Dictionary<ulong, bool> _primalityCache = new(16_777_216);
|
||||
private static readonly Dictionary<ulong, bool> _primalityCache = new(4_096);
|
||||
private static readonly ReaderWriterLockSlim _primalityCacheLock = new(LockRecursionPolicy.NoRecursion);
|
||||
|
||||
// [ThreadStatic]
|
||||
@@ -683,6 +684,12 @@ internal static partial class PrimeOrderCalculator
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool TryGetCachedPrimality(ulong value, out bool isPrime)
|
||||
{
|
||||
if (value > (ulong)PerfectNumberConstants.MaxQForDivisorCycles)
|
||||
{
|
||||
isPrime = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
_primalityCacheLock.EnterReadLock();
|
||||
bool result = _primalityCache.TryGetValue(value, out isPrime);
|
||||
_primalityCacheLock.ExitReadLock();
|
||||
@@ -692,6 +699,11 @@ internal static partial class PrimeOrderCalculator
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CachePrimalityResult(ulong value, bool isPrime)
|
||||
{
|
||||
if (value > (ulong)PerfectNumberConstants.MaxQForDivisorCycles)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_primalityCacheLock.EnterWriteLock();
|
||||
_primalityCache[value] = isPrime;
|
||||
_primalityCacheLock.ExitWriteLock();
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# CPU by-divisor cache reuse analysis
|
||||
|
||||
## Scope
|
||||
The EvenPerfectBitScanner switches to the by-divisor implementation when the CLI selects `--mersenne=bydivisor`. In CPU-only runs it instantiates `MersenneNumberDivisorByDivisorCpuTester` and feeds it the primes loaded from the filter file together with any previously recorded outcomes. 【F:EvenPerfectBitScanner/Program.cs†L216-L305】
|
||||
|
||||
Each candidate exponent `p` is a prime at least `138,000,000`, the divisors tested for that exponent are generated as `q = 2·k·p + 1` with strictly increasing integer `k ≥ 1`, and both the prime and divisor streams are monotonically increasing with no repetitions. The small-cycle snapshot bundled with the binaries only covers divisors up to four million. 【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L179-L214】【F:PerfectNumbers.Core/PerfectNumberConstants.cs†L3-L16】
|
||||
|
||||
## Caches touched in this path
|
||||
|
||||
### Previous results skip map
|
||||
When the results file already exists, the loader records every stored `p` in a dictionary so the new run can skip those primes entirely. 【F:EvenPerfectBitScanner/Program.cs†L286-L305】
|
||||
|
||||
* **Observed reuse:** Entries are only revisited if the user reruns the scan on the same results file. Within a single execution the filter contains each prime at most once, so the dictionary does not experience repeated lookups for identical keys beyond that replay scenario.
|
||||
|
||||
### Montgomery divisor data creation
|
||||
Every admissible divisor constructs its Montgomery reduction constants directly before evaluating the cycle length, both in the per-divisor loop and when the reusable scan session validates cached state. 【F:PerfectNumbers.Core/Cpu/MersenneNumberDivisorByDivisorCpuTester.cs†L188-L218】【F:PerfectNumbers.Core/Cpu/MersenneCpuDivisorScanSession.cs†L27-L69】【F:PerfectNumbers.Core/MontgomeryDivisorData.cs†L32-L45】
|
||||
|
||||
* **Observed reuse:** The helper still runs for every divisor, but because `q = 2·k·p + 1` never repeats, no value-based caching is retained. The direct construction simply reflects that reusing previously computed constants provided no benefit on this path.
|
||||
|
||||
### Divisor cycle cache
|
||||
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.
|
||||
|
||||
### 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】
|
||||
|
||||
* **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.
|
||||
|
||||
### 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】
|
||||
|
||||
* **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
|
||||
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.
|
||||
Reference in new issue
Block a user