Eliminate GC allocations from CPU divisor scan hot path

This commit is contained in:
Marek Lemańczyk committed 2025-10-15 20:48:42 +02:00
1 parent b0ce7152f7
commit 763714307f
6 files changed
+174 -147

No files matched your search

+4
View File
@@ -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);
}
}
} }
+19 -22
View File
@@ -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);
} }
} }
+31 -4
View File
@@ -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;
} }