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
@@ -112,6 +112,10 @@ Keep the divisor cycle cache limited to a single block in memory (only the on-di
|
|||||||
|
|
||||||
All code, comments, commit messages, branch names and PR descriptions must be written in American English.
|
All code, comments, commit messages, branch names and PR descriptions must be written in American English.
|
||||||
|
|
||||||
|
- When modifying existing files, preserve all relevant TODO notes and explanatory comments unless they are no longer valid.
|
||||||
|
- Prefer defining data holders as structs or classes with public fields over auto-properties when performance on the hot path is a concern.
|
||||||
|
- Do not add or keep methods that simply wrap another method call without adding any logic; inline those calls or extend the wrapper with meaningful work.
|
||||||
|
|
||||||
## Performance implementation checklist
|
## Performance implementation checklist
|
||||||
|
|
||||||
- Prefer dedicated kernels over flag-controlled or parameterized kernels. Avoid conditional branches in kernels and tight loops by creating separate kernels when necessary.
|
- Prefer dedicated kernels over flag-controlled or parameterized kernels. Avoid conditional branches in kernels and tight loops by creating separate kernels when necessary.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace PerfectNumbers.Core.Cpu;
|
namespace PerfectNumbers.Core.Cpu;
|
||||||
@@ -6,8 +7,6 @@ namespace PerfectNumbers.Core.Cpu;
|
|||||||
public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDivisorByDivisorTester
|
public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDivisorByDivisorTester
|
||||||
{
|
{
|
||||||
private readonly object _sync = new();
|
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 _divisorLimit;
|
||||||
private ulong _lastStatusDivisor;
|
private ulong _lastStatusDivisor;
|
||||||
@@ -44,6 +43,8 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
|||||||
|
|
||||||
if (allowedMax < 3UL)
|
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;
|
divisorsExhausted = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -87,49 +88,13 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
|||||||
|
|
||||||
public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession()
|
public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession()
|
||||||
{
|
{
|
||||||
DivisorScanSession? session = _threadLocalSession;
|
MersenneCpuDivisorScanSession? session = ThreadStaticPools.RentMersenneCpuDivisorSession();
|
||||||
if (session is not null)
|
if (session is not null)
|
||||||
{
|
{
|
||||||
_threadLocalSession = null;
|
|
||||||
session.Reset();
|
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new DivisorScanSession(this);
|
return new MersenneCpuDivisorScanSession();
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CheckDivisors(
|
private bool CheckDivisors(
|
||||||
@@ -188,7 +153,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
|
|||||||
byte remainder7 = (byte)(divisor % 7UL);
|
byte remainder7 = (byte)(divisor % 7UL);
|
||||||
byte remainder11 = (byte)(divisor % 11UL);
|
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
|
// Keep the divisibility filters aligned with the divisor-cycle generator so the
|
||||||
// CPU path never requests cycles that were skipped during cache creation.
|
// 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);
|
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;
|
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;
|
return cycleLength == exponent;
|
||||||
}
|
}
|
||||||
@@ -263,22 +263,6 @@ public class MersenneDivisorCycles
|
|||||||
return order;
|
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(
|
public static bool TryCalculateCycleLengthForExponent(
|
||||||
ulong divisor,
|
ulong divisor,
|
||||||
ulong exponent,
|
ulong exponent,
|
||||||
@@ -517,7 +501,14 @@ public class MersenneDivisorCycles
|
|||||||
}
|
}
|
||||||
|
|
||||||
ulong quotient = remaining / factor;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -714,6 +705,15 @@ public class MersenneDivisorCycles
|
|||||||
internal int Count;
|
internal int Count;
|
||||||
private bool _fromPool;
|
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)
|
internal void Initialize(FactorEntry[]? entries, int count, bool fromPool)
|
||||||
{
|
{
|
||||||
Entries = entries;
|
Entries = entries;
|
||||||
@@ -731,10 +731,7 @@ public class MersenneDivisorCycles
|
|||||||
ThreadStaticPools.FactorEntryPool.Return(entries, clearArray: false);
|
ThreadStaticPools.FactorEntryPool.Return(entries, clearArray: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Entries = null;
|
Reset();
|
||||||
Count = 0;
|
|
||||||
_fromPool = false;
|
|
||||||
Next = null;
|
|
||||||
ThreadStaticPools.ReturnFactorCacheEntry(this);
|
ThreadStaticPools.ReturnFactorCacheEntry(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ using System;
|
|||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using PerfectNumbers.Core.Cpu;
|
||||||
|
|
||||||
namespace PerfectNumbers.Core
|
namespace PerfectNumbers.Core
|
||||||
{
|
{
|
||||||
public static class ThreadStaticPools
|
public readonly struct ThreadStaticPools
|
||||||
{
|
{
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static ArrayPool<FactorEntry>? _factorEntryPool;
|
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]
|
[ThreadStatic]
|
||||||
private static ArrayPool<ulong>? _ulongPool;
|
private static ArrayPool<ulong>? _ulongPool;
|
||||||
|
|
||||||
@@ -81,11 +105,14 @@ namespace PerfectNumbers.Core
|
|||||||
MersenneDivisorCycles.FactorCacheEntry? entry = _factorCacheEntryPoolHead;
|
MersenneDivisorCycles.FactorCacheEntry? entry = _factorCacheEntryPoolHead;
|
||||||
if (entry is null)
|
if (entry is null)
|
||||||
{
|
{
|
||||||
return new MersenneDivisorCycles.FactorCacheEntry();
|
entry = new MersenneDivisorCycles.FactorCacheEntry();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_factorCacheEntryPoolHead = entry.Next;
|
||||||
}
|
}
|
||||||
|
|
||||||
_factorCacheEntryPoolHead = entry.Next;
|
entry.Reset();
|
||||||
entry.Next = null;
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user