Merge pull request #150 from mlemanczyk/codex/analyze-garbage-collector-cost-issue

Pool prime factor partial results for divisor factoring
This commit is contained in:
Marek Lemańczyk authored and GitHub committed 2025-10-11 16:39:24 +02:00
commit e1be80048f
2 files changed
+315 -46

No files matched your search

+165 -46
View File
@@ -1,3 +1,4 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
@@ -18,6 +19,12 @@ public class MersenneDivisorCycles
// Lightweight read-mostly cache for small divisors (<= 4,000,000). 0 => unknown
private ulong[]? _smallCycles;
[ThreadStatic]
private static Dictionary<ulong, int>? s_factorCountsPool;
[ThreadStatic]
private static Dictionary<ulong, int>? s_factorScratchPool;
public static MersenneDivisorCycles Shared { get; } = new MersenneDivisorCycles();
public MersenneDivisorCycles()
@@ -330,21 +337,28 @@ public class MersenneDivisorCycles
// return false;
// }
Dictionary<ulong, int> factorCounts = new(8);
// The divisor candidates are odd, so phi is even and this branch always records at least one factor of two.
// if (twoCount > 0)
// {
// factorCounts[2UL] = twoCount;
// }
factorCounts[2UL] = twoCount;
if (!AccumulateFactors(exponent, factorCache, factorCounts, cacheResult: true) || !AccumulateFactors(k, factorCache, factorCounts, cacheResult: false))
Dictionary<ulong, int> factorCounts = AcquireFactorCountDictionary(8);
try
{
return false;
}
// The divisor candidates are odd, so phi is even and this branch always records at least one factor of two.
// if (twoCount > 0)
// {
// factorCounts[2UL] = twoCount;
// }
factorCounts[2UL] = twoCount;
cycleLength = ReduceOrder(divisorData, phi, factorCounts);
return true;
if (!AccumulateFactors(exponent, factorCache, factorCounts, cacheResult: true) || !AccumulateFactors(k, factorCache, factorCounts, cacheResult: false))
{
return false;
}
cycleLength = ReduceOrder(divisorData, phi, factorCounts);
return true;
}
finally
{
ReleaseFactorCountDictionary(factorCounts);
}
}
private static bool AccumulateFactors(
@@ -358,61 +372,119 @@ public class MersenneDivisorCycles
return true;
}
if (!TryGetFactorization(value, cache, cacheResult, out FactorCacheEntry factorization))
FactorCacheEntry cachedFactorization;
PrimeFactorPartialResult? lease = null;
bool fromCache;
try
{
return false;
if (!TryGetFactorization(value, cache, cacheResult, out cachedFactorization, out lease, out fromCache))
{
return false;
}
if (fromCache)
{
ulong[] primes = cachedFactorization.Primes;
byte[] exponents = cachedFactorization.Exponents;
int length = cachedFactorization.Count;
for (int i = 0; i < length; i++)
{
AddFactor(counts, primes[i], exponents[i]);
}
return true;
}
if (lease is null || lease.Count == 0)
{
return true;
}
ReadOnlySpan<ulong> primesSpan = lease.Primes;
ReadOnlySpan<byte> exponentsSpan = lease.Exponents;
int spanLength = lease.Count;
for (int i = 0; i < spanLength; i++)
{
AddFactor(counts, primesSpan[i], exponentsSpan[i]);
}
return true;
}
ulong[] primes = factorization.Primes;
byte[] exponents = factorization.Exponents;
int length = factorization.Count;
for (int i = 0; i < length; i++)
finally
{
AddFactor(counts, primes[i], exponents[i]);
lease?.Dispose();
}
return true;
}
private static bool TryGetFactorization(
ulong value,
Dictionary<ulong, FactorCacheEntry>? cache,
bool cacheResult,
out FactorCacheEntry factorization)
out FactorCacheEntry factorization,
out PrimeFactorPartialResult? lease,
out bool fromCache)
{
if (cache is not null && cache.TryGetValue(value, out factorization))
factorization = default;
lease = null;
fromCache = false;
if (value <= 1UL)
{
return true;
}
Dictionary<ulong, int> scratch = new(8);
if (!TryFactorIntoCountsInternal(value, scratch))
if (cache is not null && cache.TryGetValue(value, out factorization))
{
factorization = default!;
return false;
fromCache = true;
return true;
}
int count = scratch.Count;
ulong[] primes = new ulong[count];
byte[] exponents = new byte[count];
int index = 0;
foreach (KeyValuePair<ulong, int> entry in scratch)
Dictionary<ulong, int> scratch = AcquireFactorScratchDictionary(8);
try
{
primes[index] = entry.Key;
exponents[index] = checked((byte)entry.Value);
index++;
if (!TryFactorIntoCountsInternal(value, scratch))
{
factorization = default;
return false;
}
int count = scratch.Count;
if (count == 0)
{
return true;
}
PrimeFactorPartialResult result = PrimeFactorPartialResult.Rent(count);
lease = result;
Span<ulong> primes = result.PrimeWriteSpan;
Span<byte> exponents = result.ExponentWriteSpan;
int index = 0;
foreach (KeyValuePair<ulong, int> entry in scratch)
{
primes[index] = entry.Key;
exponents[index] = checked((byte)entry.Value);
index++;
}
result.CommitCount(index);
result.Sort();
if (cache is not null && cacheResult)
{
FactorCacheEntry cacheEntry = result.ToCacheEntry();
cache[value] = cacheEntry;
}
return true;
}
Array.Sort(primes, exponents);
factorization = new FactorCacheEntry(primes, exponents, count);
if (cache is not null && cacheResult)
finally
{
cache[value] = factorization;
ReleaseFactorScratchDictionary(scratch);
}
return true;
}
private static bool TryFactorIntoCountsInternal(ulong value, Dictionary<ulong, int> counts)
@@ -608,6 +680,53 @@ public class MersenneDivisorCycles
}
}
// Thread-local dictionary pools keep the factoring helpers reusable so the GC stays out of the by-divisor CPU loop.
private static Dictionary<ulong, int> AcquireFactorCountDictionary(int capacityHint)
{
Dictionary<ulong, int>? dictionary = s_factorCountsPool;
if (dictionary is null)
{
return new Dictionary<ulong, int>(capacityHint);
}
s_factorCountsPool = null;
dictionary.Clear();
dictionary.EnsureCapacity(capacityHint);
return dictionary;
}
private static void ReleaseFactorCountDictionary(Dictionary<ulong, int> dictionary)
{
dictionary.Clear();
if (s_factorCountsPool is null)
{
s_factorCountsPool = dictionary;
}
}
private static Dictionary<ulong, int> AcquireFactorScratchDictionary(int capacityHint)
{
Dictionary<ulong, int>? dictionary = s_factorScratchPool;
if (dictionary is null)
{
return new Dictionary<ulong, int>(capacityHint);
}
s_factorScratchPool = null;
dictionary.Clear();
dictionary.EnsureCapacity(capacityHint);
return dictionary;
}
private static void ReleaseFactorScratchDictionary(Dictionary<ulong, int> dictionary)
{
dictionary.Clear();
if (s_factorScratchPool is null)
{
s_factorScratchPool = dictionary;
}
}
private static ulong NextRandomUInt64()
{
Span<byte> buffer = stackalloc byte[8];
@@ -0,0 +1,150 @@
using System;
using System.Buffers;
namespace PerfectNumbers.Core
{
internal sealed class PrimeFactorPartialResult : IDisposable
{
[ThreadStatic]
private static PrimeFactorPartialResult? s_poolHead;
private PrimeFactorPartialResult? _next;
private ulong[]? _primes;
private byte[]? _exponents;
private bool _arraysFromPool;
private int _capacity;
private PrimeFactorPartialResult()
{
}
public static PrimeFactorPartialResult Rent(int capacity)
{
PrimeFactorPartialResult? instance = s_poolHead;
if (instance is null)
{
instance = new PrimeFactorPartialResult();
}
else
{
s_poolHead = instance._next;
}
instance._next = null;
instance.EnsureCapacity(capacity);
instance.Count = 0;
return instance;
}
public int Count { get; private set; }
internal ReadOnlySpan<ulong> Primes => _primes is null ? ReadOnlySpan<ulong>.Empty : _primes.AsSpan(0, Count);
internal ReadOnlySpan<byte> Exponents => _exponents is null ? ReadOnlySpan<byte>.Empty : _exponents.AsSpan(0, Count);
internal Span<ulong> PrimeWriteSpan => _primes is null ? Span<ulong>.Empty : _primes.AsSpan(0, _capacity);
internal Span<byte> ExponentWriteSpan => _exponents is null ? Span<byte>.Empty : _exponents.AsSpan(0, _capacity);
internal void CommitCount(int count)
{
Count = count;
}
internal void Sort()
{
if (Count == 0 || _primes is null || _exponents is null)
{
return;
}
Array.Sort(_primes, _exponents, 0, Count);
}
internal MersenneDivisorCycles.FactorCacheEntry ToCacheEntry()
{
if (Count == 0 || _primes is null || _exponents is null)
{
return default;
}
ulong[] primes = new ulong[Count];
byte[] exponents = new byte[Count];
Array.Copy(_primes, 0, primes, 0, Count);
Array.Copy(_exponents, 0, exponents, 0, Count);
return new MersenneDivisorCycles.FactorCacheEntry(primes, exponents, Count);
}
public void Dispose()
{
if (_arraysFromPool)
{
if (_primes is not null)
{
ArrayPool<ulong>.Shared.Return(_primes, clearArray: false);
}
if (_exponents is not null)
{
ArrayPool<byte>.Shared.Return(_exponents, clearArray: false);
}
_arraysFromPool = false;
_primes = null;
_exponents = null;
}
_capacity = 0;
Count = 0;
_next = s_poolHead;
s_poolHead = this;
}
private void EnsureCapacity(int capacity)
{
if (capacity <= 0)
{
_capacity = 0;
return;
}
if (_primes is not null && _primes.Length >= capacity)
{
_capacity = capacity;
return;
}
if (_arraysFromPool)
{
if (_primes is not null)
{
ArrayPool<ulong>.Shared.Return(_primes, clearArray: false);
}
if (_exponents is not null)
{
ArrayPool<byte>.Shared.Return(_exponents, clearArray: false);
}
_arraysFromPool = false;
_primes = null;
_exponents = null;
}
if (capacity >= PerfectNumberConstants.PooledArrayThreshold)
{
_primes = ArrayPool<ulong>.Shared.Rent(capacity);
_exponents = ArrayPool<byte>.Shared.Rent(capacity);
_arraysFromPool = true;
}
else
{
_primes = new ulong[capacity];
_exponents = new byte[capacity];
}
_capacity = capacity;
}
}
}