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
@@ -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