Eliminate GC allocations from CPU divisor scan hot path
This commit is contained in:
1 parent
b0ce7152f7
commit
763714307f
6 files changed
+174
-147
No files matched your search
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
|
||||
namespace PerfectNumbers.Core.Cpu;
|
||||
|
||||
internal sealed class MersenneCpuDivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
|
||||
{
|
||||
private bool _disposed;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_disposed = false;
|
||||
}
|
||||
|
||||
public void CheckDivisor(
|
||||
ulong divisor,
|
||||
in MontgomeryDivisorData divisorData,
|
||||
ulong divisorCycle,
|
||||
in ReadOnlySpan<ulong> primes,
|
||||
Span<byte> hits)
|
||||
{
|
||||
int length = primes.Length;
|
||||
if (length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MontgomeryDivisorData cachedData = divisorData;
|
||||
if (cachedData.Modulus != divisor)
|
||||
{
|
||||
cachedData = MontgomeryDivisorDataCache.Get(divisor);
|
||||
}
|
||||
|
||||
if (divisorCycle == 0UL)
|
||||
{
|
||||
divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
|
||||
if (divisorCycle == 0UL)
|
||||
{
|
||||
hits.Clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep these remainder steppers in place so future updates continue reusing the previously computed residues.
|
||||
// They are critical for avoiding repeated full Montgomery exponentiation work when scanning divisors.
|
||||
var exponentStepper = new ExponentRemainderStepper(cachedData);
|
||||
if (!exponentStepper.IsValidModulus)
|
||||
{
|
||||
hits.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var cycleStepper = new CycleRemainderStepper(divisorCycle);
|
||||
|
||||
ulong remainder = cycleStepper.Initialize(primes[0]);
|
||||
hits[0] = remainder == 0UL
|
||||
? (exponentStepper.ComputeNextIsUnity(primes[0]) ? (byte)1 : (byte)0)
|
||||
: (byte)0;
|
||||
|
||||
for (int i = 1; i < length; i++)
|
||||
{
|
||||
remainder = cycleStepper.ComputeNext(primes[i]);
|
||||
if (remainder != 0UL)
|
||||
{
|
||||
hits[i] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
hits[i] = exponentStepper.ComputeNextIsUnity(primes[i]) ? (byte)1 : (byte)0;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
ThreadStaticPools.ReturnMersenneCpuDivisorSession(this);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace PerfectNumbers.Core.Cpu;
|
||||
@@ -6,8 +7,6 @@ namespace PerfectNumbers.Core.Cpu;
|
||||
public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDivisorByDivisorTester
|
||||
{
|
||||
private readonly object _sync = new();
|
||||
[ThreadStatic]
|
||||
private static DivisorScanSession? _threadLocalSession; // The nested session type stays private, so pool it here.
|
||||
|
||||
private ulong _divisorLimit;
|
||||
private ulong _lastStatusDivisor;
|
||||
@@ -44,6 +43,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
||||
|
||||
if (allowedMax < 3UL)
|
||||
{
|
||||
// EvenPerfectBitScanner routes primes below the small-divisor cutoff to the GPU path, so the CPU path still sees
|
||||
// trivial candidates during targeted tests. Short-circuit here to keep those runs aligned with the production flow.
|
||||
divisorsExhausted = true;
|
||||
return true;
|
||||
}
|
||||
@@ -87,49 +88,13 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
||||
|
||||
public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession()
|
||||
{
|
||||
DivisorScanSession? session = _threadLocalSession;
|
||||
MersenneCpuDivisorScanSession? session = ThreadStaticPools.RentMersenneCpuDivisorSession();
|
||||
if (session is not null)
|
||||
{
|
||||
_threadLocalSession = null;
|
||||
session.Reset();
|
||||
return session;
|
||||
}
|
||||
|
||||
return new DivisorScanSession(this);
|
||||
}
|
||||
|
||||
private void ReturnSession(DivisorScanSession session)
|
||||
{
|
||||
_threadLocalSession = session;
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
return new MersenneCpuDivisorScanSession();
|
||||
}
|
||||
|
||||
private bool CheckDivisors(
|
||||
@@ -188,7 +153,7 @@ 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.
|
||||
// DivisorCycleCache cycleCache = DivisorCycleCache.Shared; // Do not restore the fallback cache; divisors do not repeat on this path.
|
||||
|
||||
// Keep the divisibility filters aligned with the divisor-cycle generator so the
|
||||
// CPU path never requests cycles that were skipped during cache creation.
|
||||
@@ -320,85 +285,5 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
||||
return Math.Min((1UL << (int)(prime - 1UL)) - 1UL, divisorLimit);
|
||||
}
|
||||
|
||||
private sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
|
||||
{
|
||||
private readonly MersenneNumberDivisorByDivisorCpuTester _owner;
|
||||
private bool _disposed;
|
||||
|
||||
internal DivisorScanSession(MersenneNumberDivisorByDivisorCpuTester owner)
|
||||
{
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
internal void Reset()
|
||||
{
|
||||
_disposed = false;
|
||||
}
|
||||
|
||||
public void CheckDivisor(ulong divisor, in MontgomeryDivisorData divisorData, ulong divisorCycle, in ReadOnlySpan<ulong> primes, Span<byte> hits)
|
||||
{
|
||||
int length = primes.Length;
|
||||
if (length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MontgomeryDivisorData cachedData = divisorData;
|
||||
if (cachedData.Modulus != divisor)
|
||||
{
|
||||
cachedData = MontgomeryDivisorDataCache.Get(divisor);
|
||||
}
|
||||
|
||||
if (divisorCycle == 0UL)
|
||||
{
|
||||
divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
|
||||
if (divisorCycle == 0UL)
|
||||
{
|
||||
hits.Clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep these remainder steppers in place so future updates continue reusing the previously computed residues.
|
||||
// They are critical for avoiding repeated full Montgomery exponentiation work when scanning divisors.
|
||||
var exponentStepper = new ExponentRemainderStepper(cachedData);
|
||||
if (!exponentStepper.IsValidModulus)
|
||||
{
|
||||
hits.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var cycleStepper = new CycleRemainderStepper(divisorCycle);
|
||||
|
||||
ulong remainder = cycleStepper.Initialize(primes[0]);
|
||||
hits[0] = remainder == 0UL
|
||||
? (exponentStepper.ComputeNextIsUnity(primes[0]) ? (byte)1 : (byte)0)
|
||||
: (byte)0;
|
||||
|
||||
for (int i = 1; i < length; i++)
|
||||
{
|
||||
remainder = cycleStepper.ComputeNext(primes[i]);
|
||||
if (remainder != 0UL)
|
||||
{
|
||||
hits[i] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
hits[i] = exponentStepper.ComputeNextIsUnity(primes[i]) ? (byte)1 : (byte)0;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_owner.ReturnSession(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ public class MersenneDivisorCycles
|
||||
factorCache = cache;
|
||||
}
|
||||
|
||||
if (TryCalculateCycleLengthForExponent(divisor, exponent, divisorData, cache, out ulong cycleLength) && cycleLength != 0UL)
|
||||
if (TryCalculateCycleLengthForExponent(divisor, exponent, divisorData, cache, usePooledCache: false, out ulong cycleLength) && cycleLength != 0UL)
|
||||
{
|
||||
return cycleLength == exponent;
|
||||
}
|
||||
@@ -263,22 +263,6 @@ public class MersenneDivisorCycles
|
||||
return order;
|
||||
}
|
||||
|
||||
public static bool TryCalculateCycleLengthForExponent(
|
||||
ulong divisor,
|
||||
ulong exponent,
|
||||
in MontgomeryDivisorData divisorData,
|
||||
Dictionary<ulong, FactorCacheEntry>? factorCache,
|
||||
out ulong cycleLength)
|
||||
{
|
||||
return TryCalculateCycleLengthForExponent(
|
||||
divisor,
|
||||
exponent,
|
||||
divisorData,
|
||||
factorCache,
|
||||
usePooledCache: false,
|
||||
out cycleLength);
|
||||
}
|
||||
|
||||
public static bool TryCalculateCycleLengthForExponent(
|
||||
ulong divisor,
|
||||
ulong exponent,
|
||||
@@ -517,7 +501,14 @@ public class MersenneDivisorCycles
|
||||
}
|
||||
|
||||
ulong quotient = remaining / factor;
|
||||
if (factor > 1UL && !TryFactorIntoCountsInternal(factor, counts))
|
||||
// Pollard-Rho never returns factors <= 1 on the EvenPerfectBitScanner path; keep the old guard documented
|
||||
// for completeness without branching in the hot path.
|
||||
// if (factor > 1UL && !TryFactorIntoCountsInternal(factor, counts))
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
if (!TryFactorIntoCountsInternal(factor, counts))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -714,6 +705,15 @@ public class MersenneDivisorCycles
|
||||
internal int Count;
|
||||
private bool _fromPool;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void Reset()
|
||||
{
|
||||
Entries = null;
|
||||
Count = 0;
|
||||
_fromPool = false;
|
||||
Next = null;
|
||||
}
|
||||
|
||||
internal void Initialize(FactorEntry[]? entries, int count, bool fromPool)
|
||||
{
|
||||
Entries = entries;
|
||||
@@ -731,10 +731,7 @@ public class MersenneDivisorCycles
|
||||
ThreadStaticPools.FactorEntryPool.Return(entries, clearArray: false);
|
||||
}
|
||||
|
||||
Entries = null;
|
||||
Count = 0;
|
||||
_fromPool = false;
|
||||
Next = null;
|
||||
Reset();
|
||||
ThreadStaticPools.ReturnFactorCacheEntry(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using PerfectNumbers.Core.Cpu;
|
||||
|
||||
namespace PerfectNumbers.Core
|
||||
{
|
||||
public static class ThreadStaticPools
|
||||
public readonly struct ThreadStaticPools
|
||||
{
|
||||
[ThreadStatic]
|
||||
private static ArrayPool<FactorEntry>? _factorEntryPool;
|
||||
@@ -31,6 +32,29 @@ namespace PerfectNumbers.Core
|
||||
}
|
||||
}
|
||||
|
||||
[ThreadStatic]
|
||||
private static MersenneCpuDivisorScanSession? _mersenneCpuDivisorSession;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static MersenneCpuDivisorScanSession? RentMersenneCpuDivisorSession()
|
||||
{
|
||||
MersenneCpuDivisorScanSession? session = _mersenneCpuDivisorSession;
|
||||
if (session is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_mersenneCpuDivisorSession = null;
|
||||
session.Reset();
|
||||
return session;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static void ReturnMersenneCpuDivisorSession(MersenneCpuDivisorScanSession session)
|
||||
{
|
||||
_mersenneCpuDivisorSession = session;
|
||||
}
|
||||
|
||||
[ThreadStatic]
|
||||
private static ArrayPool<ulong>? _ulongPool;
|
||||
|
||||
@@ -81,11 +105,14 @@ namespace PerfectNumbers.Core
|
||||
MersenneDivisorCycles.FactorCacheEntry? entry = _factorCacheEntryPoolHead;
|
||||
if (entry is null)
|
||||
{
|
||||
return new MersenneDivisorCycles.FactorCacheEntry();
|
||||
entry = new MersenneDivisorCycles.FactorCacheEntry();
|
||||
}
|
||||
else
|
||||
{
|
||||
_factorCacheEntryPoolHead = entry.Next;
|
||||
}
|
||||
|
||||
_factorCacheEntryPoolHead = entry.Next;
|
||||
entry.Next = null;
|
||||
entry.Reset();
|
||||
return entry;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user