Merge pull request #216 from mlemanczyk/codex/remove-factorcache-usage-in-evenperfectbitscanner

Remove per-thread factorization cache from Mersenne divisor workflows
This commit is contained in:
Marek Lemańczyk authored and GitHub committed 2025-10-20 19:28:17 +02:00
commit 1b3a89ca53
6 files changed
+65 -253

No files matched your search

@@ -1,30 +0,0 @@
namespace PerfectNumbers.Core.Cpu;
internal struct FactorCacheLease
{
private Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? _cache;
public void EnsureInitialized(ref Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? cache)
{
Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? current = cache;
if (current is null)
{
current = ThreadStaticPools.RentMersenneFactorCacheDictionary();
cache = current;
}
_cache = current;
}
public void Dispose()
{
Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? cache = _cache;
if (cache is null)
{
return;
}
ThreadStaticPools.ReturnMersenneFactorCacheDictionary(cache);
_cache = null;
}
}
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using PerfectNumbers.Core.Gpu;
@@ -149,9 +150,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return false;
}
FactorCacheLease factorCacheLease = new FactorCacheLease();
Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? 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 +196,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 +220,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 +245,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;
+46
View File
@@ -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<ulong> 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<ulong> divisors, Span<ulong> cycles, ReadOnlySpan<int> indices)
{
+10 -131
View File
@@ -52,12 +52,6 @@ public class MersenneDivisorCycles
}
public static bool CycleEqualsExponent(ulong divisor, in MontgomeryDivisorData divisorData, ulong exponent)
{
Dictionary<ulong, FactorCacheEntry>? cache = null;
return CycleEqualsExponent(divisor, exponent, divisorData, ref cache);
}
public static bool CycleEqualsExponent(ulong divisor, ulong exponent, in MontgomeryDivisorData divisorData, ref Dictionary<ulong, FactorCacheEntry>? factorCache)
{
if (CycleEqualsExponentForMersenneCandidate(divisor, divisorData, exponent))
{
@@ -87,14 +81,7 @@ public class MersenneDivisorCycles
return heuristicCycle == exponent;
}
Dictionary<ulong, FactorCacheEntry>? cache = factorCache;
if (cache is null)
{
cache = new Dictionary<ulong, FactorCacheEntry>(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<ulong, FactorCacheEntry>? 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<ulong, FactorCacheEntry>? cache,
Dictionary<ulong, int> counts,
bool cacheResult)
Dictionary<ulong, int> 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<ulong, int>[]? pairs = actualFactorization.Pairs;
if (pairs is not null)
{
int length = actualFactorization.Count;
for (int i = 0; i < length; i++)
{
KeyValuePair<ulong, int> pair = pairs[i];
AddFactor(counts, pair.Key, pair.Value);
}
}
if (releaseAfterUse)
{
actualFactorization.ReturnToPool();
}
return true;
}
private static bool TryGetFactorization(
ulong value,
Dictionary<ulong, FactorCacheEntry>? 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<ulong, int> 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<ulong, int>.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<ulong, int> pair = enumerator.Current;
AddFactor(counts, pair.Key, pair.Value);
}
KeyValuePair<ulong, int>[] pairs = ThreadStaticPools.FactorKeyValuePairPool.Rent(count);
((ICollection<KeyValuePair<ulong, int>>)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<ulong, int>[]? 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<ulong, int>[]? pairs, int count, bool fromPool)
{
Pairs = pairs;
Count = count;
_fromPool = fromPool;
Next = null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void ReturnToPool()
{
KeyValuePair<ulong, int>[]? 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);
-67
View File
@@ -31,17 +31,6 @@ namespace PerfectNumbers.Core
}
[ThreadStatic]
private static ArrayPool<KeyValuePair<ulong, int>>? _factorKeyValuePairPool;
public static ArrayPool<KeyValuePair<ulong, int>> FactorKeyValuePairPool
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return _factorKeyValuePairPool ??= ArrayPool<KeyValuePair<ulong, int>>.Create();
}
}
[ThreadStatic]
private static MersenneCpuDivisorScanSession? _mersenneCpuDivisorSession;
@@ -278,62 +267,6 @@ namespace PerfectNumbers.Core
pool.Add(dictionary);
}
[ThreadStatic]
private static Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? _mersenneFactorCacheDictionary;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry> RentMersenneFactorCacheDictionary()
{
Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? cache = _mersenneFactorCacheDictionary;
if (cache is null)
{
return new Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>(8);
}
_mersenneFactorCacheDictionary = null;
cache.Clear();
return cache;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReturnMersenneFactorCacheDictionary(Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry> cache)
{
for (Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>.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<ulong, int>? _factorCountDictionary;
+6 -11
View File
@@ -21,23 +21,18 @@ 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
`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†L101-L264】【F:PerfectNumbers.Core/MersenneDivisorCycles.cs†L305-L399】
* **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/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†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.
### 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.
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】