Pool factor cache entries to keep CPU factoring allocation-free
This commit is contained in:
1 parent
34fcd62c23
commit
b0ce7152f7
4 files changed
+150
-79
No files matched your search
@@ -106,6 +106,7 @@ There is only one agents file - this one. Don't search for another one.
|
||||
|
||||
Always use indentation with four space characters in source code files, even if a file currently uses tabs or appears to rely on eight-space indentation. Increase indentation level by exactly one indentation unit (four spaces) per nested block. When parameters or long expressions wrap, indent the continuation with two indentation units for clarity.
|
||||
Explicitly prefer reusing existing variables once their previous values are no longer required instead of declaring new locals of the same type. Call out the variable reuse in comments when its meaning changes so the intent stays clear and register pressure remains minimal.
|
||||
Do not introduce try/finally blocks solely to release pooled resources. Restructure the code so that every return path releases the resources without relying on finally unless the control flow would become unclear or unmaintainable.
|
||||
|
||||
Keep the divisor cycle cache limited to a single block in memory (only the on-disk snapshot). When a lookup misses, compute the required cycle on the configured device and discard the transient result instead of scheduling background generation or caching additional blocks.
|
||||
|
||||
|
||||
@@ -149,6 +149,13 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
||||
// }
|
||||
|
||||
UInt128 step = (UInt128)prime << 1;
|
||||
// The EvenPerfectBitScanner advances primes far below the overflow boundary, so a zero step here would indicate
|
||||
// a corrupted input. Keeping the guard commented out documents the intent without putting a branch on the hot path.
|
||||
// if (step == UInt128.Zero)
|
||||
// {
|
||||
// processedAll = true;
|
||||
// return false;
|
||||
// }
|
||||
|
||||
UInt128 limit = allowedMax;
|
||||
UInt128 divisor = step + UInt128.One;
|
||||
@@ -161,6 +168,11 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
||||
FactorCacheLease factorCacheLease = new FactorCacheLease();
|
||||
Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? factorCache = null;
|
||||
bool foundDivisor = false;
|
||||
bool requireLargeCycleCache = allowedMax > PerfectNumberConstants.MaxQForDivisorCycles;
|
||||
if (requireLargeCycleCache)
|
||||
{
|
||||
factorCacheLease.EnsureInitialized(ref factorCache);
|
||||
}
|
||||
|
||||
byte step10 = (byte)(step % 10UL);
|
||||
byte step8 = (byte)(step % 8UL);
|
||||
@@ -176,6 +188,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
||||
byte remainder7 = (byte)(divisor % 7UL);
|
||||
byte remainder11 = (byte)(divisor % 11UL);
|
||||
|
||||
// DivisorCycleCache cycleCache = DivisorCycleCache.Shared; // Preserve the reference for easy restoration if the fallback cache is re-enabled.
|
||||
|
||||
// Keep the divisibility filters aligned with the divisor-cycle generator so the
|
||||
// CPU path never requests cycles that were skipped during cache creation.
|
||||
bool lastIsSeven = (prime & 3UL) == 3UL;
|
||||
@@ -193,9 +207,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
||||
{
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(candidate);
|
||||
Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? cacheForCycle = null;
|
||||
if (candidate > PerfectNumberConstants.MaxQForDivisorCycles)
|
||||
if (requireLargeCycleCache && candidate > PerfectNumberConstants.MaxQForDivisorCycles)
|
||||
{
|
||||
factorCacheLease.EnsureInitialized(ref factorCache);
|
||||
cacheForCycle = factorCache;
|
||||
}
|
||||
|
||||
@@ -206,6 +219,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
||||
|
||||
if (divisorCycle == prime)
|
||||
{
|
||||
// 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.
|
||||
foundDivisor = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -314,6 +314,12 @@ public class MersenneDivisorCycles
|
||||
}
|
||||
|
||||
ulong phi = divisor - 1UL;
|
||||
// A zero totient would imply divisor == 0, which never occurs on the tested path. Keep the guard documented without
|
||||
// branching in the hot loop.
|
||||
// if (phi == 0UL)
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
int twoCount = BitOperations.TrailingZeroCount(phi);
|
||||
ulong reducedPhi = phi >> twoCount;
|
||||
@@ -323,30 +329,33 @@ public class MersenneDivisorCycles
|
||||
}
|
||||
|
||||
ulong k = reducedPhi / exponent;
|
||||
// k == 0 would indicate reducedPhi < exponent, which the EvenPerfectBitScanner call flow prevents. Document the
|
||||
// assumption instead of branching.
|
||||
// if (k == 0UL)
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
Dictionary<ulong, int> factorCounts = ThreadStaticPools.RentFactorCountDictionary();
|
||||
bool success = false;
|
||||
|
||||
try
|
||||
if (twoCount > 0)
|
||||
{
|
||||
if (twoCount > 0)
|
||||
{
|
||||
factorCounts[2UL] = twoCount;
|
||||
}
|
||||
factorCounts[2UL] = twoCount;
|
||||
}
|
||||
|
||||
bool poolExponentFactors = usePooledCache || factorCache is null;
|
||||
|
||||
if (!AccumulateFactors(exponent, factorCache, factorCounts, cacheResult: true, poolExponentFactors) || !AccumulateFactors(k, factorCache, factorCounts, cacheResult: false, allowPooledStorage: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool poolExponentFactors = usePooledCache || factorCache is null;
|
||||
bool exponentAccumulated = AccumulateFactors(exponent, factorCache, factorCounts, cacheResult: true, allowPooledStorage: poolExponentFactors);
|
||||
bool factorsAccumulated = exponentAccumulated && AccumulateFactors(k, factorCache, factorCounts, cacheResult: false, allowPooledStorage: true);
|
||||
|
||||
if (factorsAccumulated)
|
||||
{
|
||||
cycleLength = ReduceOrder(divisorData, phi, factorCounts);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ThreadStaticPools.ReturnFactorCountDictionary(factorCounts);
|
||||
success = true;
|
||||
}
|
||||
|
||||
ThreadStaticPools.ReturnFactorCountDictionary(factorCounts);
|
||||
return success;
|
||||
}
|
||||
|
||||
private static bool AccumulateFactors(
|
||||
@@ -363,15 +372,16 @@ public class MersenneDivisorCycles
|
||||
|
||||
bool poolStorage = allowPooledStorage || cache is null || !cacheResult;
|
||||
|
||||
if (!TryGetFactorization(value, cache, cacheResult, poolStorage, out FactorCacheEntry factorization))
|
||||
if (!TryGetFactorization(value, cache, cacheResult, poolStorage, out FactorCacheEntry? factorization, out bool releaseAfterUse) || factorization is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FactorEntry[]? entries = factorization.Entries;
|
||||
FactorCacheEntry actualFactorization = factorization;
|
||||
FactorEntry[]? entries = actualFactorization.Entries;
|
||||
if (entries is not null)
|
||||
{
|
||||
int length = factorization.Count;
|
||||
int length = actualFactorization.Count;
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
@@ -380,9 +390,9 @@ public class MersenneDivisorCycles
|
||||
}
|
||||
}
|
||||
|
||||
if (cache is null || !cacheResult)
|
||||
if (releaseAfterUse)
|
||||
{
|
||||
factorization.ReturnToPool();
|
||||
actualFactorization.ReturnToPool();
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -393,60 +403,82 @@ public class MersenneDivisorCycles
|
||||
Dictionary<ulong, FactorCacheEntry>? cache,
|
||||
bool cacheResult,
|
||||
bool allowPooledStorage,
|
||||
out FactorCacheEntry factorization)
|
||||
out FactorCacheEntry? factorization,
|
||||
out bool releaseAfterUse)
|
||||
{
|
||||
if (cache is not null && cache.TryGetValue(value, out factorization))
|
||||
releaseAfterUse = false;
|
||||
|
||||
if (cache is not null && cache.TryGetValue(value, out FactorCacheEntry? cachedEntry))
|
||||
{
|
||||
factorization = cachedEntry;
|
||||
return true;
|
||||
}
|
||||
|
||||
Dictionary<ulong, int> scratch = ThreadStaticPools.RentFactorScratchDictionary();
|
||||
|
||||
try
|
||||
{
|
||||
if (!TryFactorIntoCountsInternal(value, scratch))
|
||||
{
|
||||
factorization = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
int count = scratch.Count;
|
||||
if (count == 0)
|
||||
{
|
||||
factorization = default!;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool fromPool = allowPooledStorage;
|
||||
FactorEntry[] entries = fromPool
|
||||
? ThreadStaticPools.FactorEntryPool.Rent(count)
|
||||
: new FactorEntry[count];
|
||||
|
||||
int index = 0;
|
||||
foreach (KeyValuePair<ulong, int> entry in scratch)
|
||||
{
|
||||
entries[index] = new FactorEntry(entry.Key, entry.Value);
|
||||
index++;
|
||||
}
|
||||
|
||||
Array.Sort(entries, 0, count, FactorEntryValueComparer.Instance);
|
||||
factorization = new FactorCacheEntry(entries, count, fromPool);
|
||||
|
||||
if (cache is not null && cacheResult)
|
||||
{
|
||||
cache[value] = factorization;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
bool factored = TryFactorIntoCountsInternal(value, scratch);
|
||||
if (!factored)
|
||||
{
|
||||
ThreadStaticPools.ReturnFactorScratchDictionary(scratch);
|
||||
factorization = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
int count = scratch.Count;
|
||||
FactorCacheEntry entry = ThreadStaticPools.RentFactorCacheEntry();
|
||||
if (count == 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
bool fromPool = allowPooledStorage;
|
||||
FactorEntry[] entries = fromPool
|
||||
? ThreadStaticPools.FactorEntryPool.Rent(count)
|
||||
: new FactorEntry[count];
|
||||
|
||||
Dictionary<ulong, int>.Enumerator enumerator = scratch.GetEnumerator();
|
||||
int index = 0;
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
KeyValuePair<ulong, int> current = enumerator.Current;
|
||||
entries[index] = new FactorEntry(current.Key, current.Value);
|
||||
index++;
|
||||
}
|
||||
|
||||
ThreadStaticPools.ReturnFactorScratchDictionary(scratch);
|
||||
Array.Sort(entries, 0, count, FactorEntryValueComparer.Instance);
|
||||
|
||||
entry.Initialize(entries, count, fromPool);
|
||||
factorization = entry;
|
||||
|
||||
if (cache is not null && cacheResult)
|
||||
{
|
||||
cache[value] = entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
releaseAfterUse = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryFactorIntoCountsInternal(ulong value, Dictionary<ulong, int> counts)
|
||||
{
|
||||
// The EvenPerfectBitScanner only requests factors for values >= 2 here. Leave the guard documented without executing it.
|
||||
// if (value <= 1UL)
|
||||
// {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
ulong remaining = value;
|
||||
uint[] smallPrimes = PrimesGenerator.SmallPrimes;
|
||||
ulong[] smallPrimesSquared = PrimesGenerator.SmallPrimesPow2;
|
||||
@@ -675,36 +707,35 @@ public class MersenneDivisorCycles
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct FactorCacheEntry
|
||||
public sealed class FactorCacheEntry
|
||||
{
|
||||
private readonly bool _fromPool;
|
||||
internal FactorCacheEntry? Next;
|
||||
internal FactorEntry[]? Entries;
|
||||
internal int Count;
|
||||
private bool _fromPool;
|
||||
|
||||
internal FactorCacheEntry(FactorEntry[] entries, int count, bool fromPool)
|
||||
internal void Initialize(FactorEntry[]? entries, int count, bool fromPool)
|
||||
{
|
||||
Entries = entries;
|
||||
Count = count;
|
||||
_fromPool = fromPool;
|
||||
Next = null;
|
||||
}
|
||||
|
||||
internal FactorEntry[]? Entries { get; }
|
||||
|
||||
public int Count { get; }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void ReturnToPool()
|
||||
{
|
||||
if (!_fromPool)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FactorEntry[]? entries = Entries;
|
||||
if (entries is null)
|
||||
if (_fromPool && entries is not null)
|
||||
{
|
||||
return;
|
||||
ThreadStaticPools.FactorEntryPool.Return(entries, clearArray: false);
|
||||
}
|
||||
|
||||
ThreadStaticPools.FactorEntryPool.Return(entries, clearArray: false);
|
||||
Entries = null;
|
||||
Count = 0;
|
||||
_fromPool = false;
|
||||
Next = null;
|
||||
ThreadStaticPools.ReturnFactorCacheEntry(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,30 @@ namespace PerfectNumbers.Core
|
||||
_mersenneFactorCacheDictionary = cache;
|
||||
}
|
||||
|
||||
[ThreadStatic]
|
||||
private static MersenneDivisorCycles.FactorCacheEntry? _factorCacheEntryPoolHead;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static MersenneDivisorCycles.FactorCacheEntry RentFactorCacheEntry()
|
||||
{
|
||||
MersenneDivisorCycles.FactorCacheEntry? entry = _factorCacheEntryPoolHead;
|
||||
if (entry is null)
|
||||
{
|
||||
return new MersenneDivisorCycles.FactorCacheEntry();
|
||||
}
|
||||
|
||||
_factorCacheEntryPoolHead = entry.Next;
|
||||
entry.Next = null;
|
||||
return entry;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReturnFactorCacheEntry(MersenneDivisorCycles.FactorCacheEntry entry)
|
||||
{
|
||||
entry.Next = _factorCacheEntryPoolHead;
|
||||
_factorCacheEntryPoolHead = entry;
|
||||
}
|
||||
|
||||
[ThreadStatic]
|
||||
private static Dictionary<ulong, int>? _factorCountDictionary;
|
||||
|
||||
|
||||
Reference in new issue
Block a user