Merge branch 'codex/analyze-gc-instances-in-checkdivisors-method' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into codex/analyze-gc-instances-in-checkdivisors-method

This commit is contained in:
mlemanczyk committed 2025-10-28 20:12:45 +01:00
commit 2f461ee001
7 files changed
+390 -173

No files matched your search

@@ -121,7 +121,7 @@ public class GpuKernelCompilationTests
private static readonly Action<Index1D, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>> MersenneDivisorCyclesKernel = private static readonly Action<Index1D, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>> MersenneDivisorCyclesKernel =
DivisorCycleKernels.GpuDivisorCycleKernel; DivisorCycleKernels.GpuDivisorCycleKernel;
private static readonly Action<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<byte>> PrimeTesterSmallPrimeKernel = private static readonly Action<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<byte>> PrimeTesterSmallPrimeKernel =
PrimeTesterKernels.SmallPrimeSieveKernel; PrimeTesterKernels.SmallPrimeSieveKernel;
private static readonly Action<Index1D, ArrayView<ulong>, ArrayView<byte>> PrimeTesterSharesFactorKernel = private static readonly Action<Index1D, ArrayView<ulong>, ArrayView<byte>> PrimeTesterSharesFactorKernel =
@@ -336,7 +336,7 @@ public class GpuKernelCompilationTests
private static void CompilePrimeTesterSmallPrimeKernel(Accelerator accelerator) private static void CompilePrimeTesterSmallPrimeKernel(Accelerator accelerator)
{ {
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<byte>>(PrimeTesterSmallPrimeKernel); var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<byte>>(PrimeTesterSmallPrimeKernel);
_ = kernel; _ = kernel;
} }
@@ -17,6 +17,10 @@ public class PrimeTesterIsPrimeTests
[InlineData(97UL, true)] [InlineData(97UL, true)]
[InlineData(133UL, false)] [InlineData(133UL, false)]
[InlineData(137UL, true)] [InlineData(137UL, true)]
[InlineData(143UL, false)]
[InlineData(199UL, true)]
[InlineData(209UL, false)]
[InlineData(403UL, false)]
[InlineData(341UL, false)] [InlineData(341UL, false)]
public void IsPrime_returns_expected_results(ulong n, bool expected) public void IsPrime_returns_expected_results(ulong n, bool expected)
{ {
@@ -25,6 +29,20 @@ public class PrimeTesterIsPrimeTests
tester.IsPrime(n, CancellationToken.None).Should().Be(expected); tester.IsPrime(n, CancellationToken.None).Should().Be(expected);
} }
[Theory]
[Trait("Category", "Fast")]
[InlineData(139UL, true)]
[InlineData(189UL, false)]
[InlineData(193UL, true)]
[InlineData(279UL, false)]
public void IsPrimeGpu_uses_residue_specific_tables(ulong n, bool expected)
{
GpuContextPool.ForceCpu = false;
var tester = new PrimeTester();
tester.IsPrimeGpu(n, CancellationToken.None).Should().Be(expected);
}
[Fact] [Fact]
[Trait("Category", "Fast")] [Trait("Category", "Fast")]
public void IsPrimeGpu_falls_back_to_cpu_when_forced() public void IsPrimeGpu_falls_back_to_cpu_when_forced()
@@ -32,14 +32,24 @@ public class PrimesGeneratorTests
var lastOnePow2 = PrimesGenerator.SmallPrimesPow2LastOne; var lastOnePow2 = PrimesGenerator.SmallPrimesPow2LastOne;
var lastSeven = PrimesGenerator.SmallPrimesLastSeven; var lastSeven = PrimesGenerator.SmallPrimesLastSeven;
var lastSevenPow2 = PrimesGenerator.SmallPrimesPow2LastSeven; var lastSevenPow2 = PrimesGenerator.SmallPrimesPow2LastSeven;
var lastThree = DivisorGenerator.SmallPrimesLastThree;
var lastThreePow2 = DivisorGenerator.SmallPrimesPow2LastThree;
var lastNine = DivisorGenerator.SmallPrimesLastNine;
var lastNinePow2 = DivisorGenerator.SmallPrimesPow2LastNine;
lastOne.Length.Should().Be((int)PerfectNumberConstants.PrimesLimit); lastOne.Length.Should().Be((int)PerfectNumberConstants.PrimesLimit);
lastSeven.Length.Should().Be((int)PerfectNumberConstants.PrimesLimit); lastSeven.Length.Should().Be((int)PerfectNumberConstants.PrimesLimit);
lastThree.Length.Should().Be((int)PerfectNumberConstants.PrimesLimit);
lastNine.Length.Should().Be((int)PerfectNumberConstants.PrimesLimit);
lastOnePow2.Length.Should().Be(lastOne.Length); lastOnePow2.Length.Should().Be(lastOne.Length);
lastSevenPow2.Length.Should().Be(lastSeven.Length); lastSevenPow2.Length.Should().Be(lastSeven.Length);
lastThreePow2.Length.Should().Be(lastThree.Length);
lastNinePow2.Length.Should().Be(lastNine.Length);
lastOne[..5].Should().Equal([3U, 7U, 11U, 13U, 19U]); lastOne[..5].Should().Equal([3U, 7U, 11U, 13U, 19U]);
lastSeven[..5].Should().Equal([3U, 7U, 11U, 13U, 17U]); lastSeven[..5].Should().Equal([3U, 7U, 11U, 13U, 17U]);
lastThree[..6].Should().Equal([3U, 7U, 11U, 13U, 17U, 19U]);
lastNine[..6].Should().Equal([3U, 7U, 11U, 13U, 17U, 19U]);
for (int i = 0; i < lastOne.Length; i++) for (int i = 0; i < lastOne.Length; i++)
{ {
@@ -58,6 +68,23 @@ public class PrimesGeneratorTests
.Should().BeTrue(); .Should().BeTrue();
lastSevenPow2[i].Should().Be(prime * (ulong)prime); lastSevenPow2[i].Should().Be(prime * (ulong)prime);
} }
for (int i = 0; i < lastThree.Length; i++)
{
uint prime = lastThree[i];
uint mod10 = prime % 10U;
(mod10 == 3U || mod10 == 7U || prime == 11U || prime == 19U)
.Should().BeTrue();
lastThreePow2[i].Should().Be(prime * (ulong)prime);
}
for (int i = 0; i < lastNine.Length; i++)
{
uint prime = lastNine[i];
uint mod10 = prime % 10U;
(mod10 == 3U || mod10 == 7U || mod10 == 9U || prime == 11U)
.Should().BeTrue();
lastNinePow2[i].Should().Be(prime * (ulong)prime);
}
} }
} }
+213 -1
View File
@@ -1,16 +1,228 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core; namespace PerfectNumbers.Core;
internal static class DivisorGenerator internal static class DivisorGenerator
{ {
public static readonly uint[] SmallPrimes;
public static readonly ulong[] SmallPrimesPow2;
public static readonly uint[] SmallPrimesLastOne;
public static readonly ulong[] SmallPrimesPow2LastOne;
public static readonly uint[] SmallPrimesLastSeven;
public static readonly ulong[] SmallPrimesPow2LastSeven;
public static readonly uint[] SmallPrimesLastThree;
public static readonly ulong[] SmallPrimesPow2LastThree;
public static readonly uint[] SmallPrimesLastNine;
public static readonly ulong[] SmallPrimesPow2LastNine;
private const ushort DecimalMaskWhenLastIsSeven = (1 << 3) | (1 << 7) | (1 << 9); private const ushort DecimalMaskWhenLastIsSeven = (1 << 3) | (1 << 7) | (1 << 9);
private const ushort DecimalMaskWhenLastIsThree = (1 << 1) | (1 << 3) | (1 << 7) | (1 << 9);
private const ushort DecimalMaskWhenLastIsNine = (1 << 1) | (1 << 3) | (1 << 7) | (1 << 9);
private const ushort DecimalMaskOtherwise = (1 << 1) | (1 << 3) | (1 << 9); private const ushort DecimalMaskOtherwise = (1 << 1) | (1 << 3) | (1 << 9);
static DivisorGenerator()
{
BuildSmallPrimes(
out SmallPrimes,
out SmallPrimesPow2,
out SmallPrimesLastOne,
out SmallPrimesPow2LastOne,
out SmallPrimesLastSeven,
out SmallPrimesPow2LastSeven,
out SmallPrimesLastThree,
out SmallPrimesPow2LastThree,
out SmallPrimesLastNine,
out SmallPrimesPow2LastNine);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort GetDecimalMask(LastDigit lastDigit) public static ushort GetDecimalMask(LastDigit lastDigit)
{ {
return lastDigit == LastDigit.Seven ? DecimalMaskWhenLastIsSeven : DecimalMaskOtherwise; return lastDigit switch
{
LastDigit.Seven => DecimalMaskWhenLastIsSeven,
LastDigit.Three => DecimalMaskWhenLastIsThree,
LastDigit.Nine => DecimalMaskWhenLastIsNine,
_ => DecimalMaskOtherwise,
};
} }
private static void BuildSmallPrimes(
out uint[] all,
out ulong[] allPow2,
out uint[] lastOne,
out ulong[] lastOnePow2,
out uint[] lastSeven,
out ulong[] lastSevenPow2,
out uint[] lastThree,
out ulong[] lastThreePow2,
out uint[] lastNine,
out ulong[] lastNinePow2)
{
uint target = PerfectNumberConstants.PrimesLimit;
int targetInt = checked((int)target);
all = new uint[targetInt];
allPow2 = new ulong[targetInt];
lastOne = new uint[targetInt];
lastOnePow2 = new ulong[targetInt];
lastSeven = new uint[targetInt];
lastSevenPow2 = new ulong[targetInt];
lastThree = new uint[targetInt];
lastThreePow2 = new ulong[targetInt];
lastNine = new uint[targetInt];
lastNinePow2 = new ulong[targetInt];
List<uint> primes = new(targetInt * 4 / 3 + 16);
uint candidate = 2U;
int allCount = 0;
int lastOneCount = 0;
int lastSevenCount = 0;
int lastThreeCount = 0;
int lastNineCount = 0;
while (allCount < targetInt || lastOneCount < targetInt || lastSevenCount < targetInt || lastThreeCount < targetInt || lastNineCount < targetInt)
{
bool isPrime = true;
int primesCount = primes.Count;
for (int i = 0; i < primesCount; i++)
{
uint p = primes[i];
// TODO: Reuse a single squared local for `p` within this loop so we avoid recalculating
// `p * p` repeatedly without storing every squared prime globally (the cache must remain
// limited to the small-divisor tables to honor the memory constraints).
if (p * p > candidate)
{
break;
}
// TODO: Replace this trial-division `%` with the sieve-based generator that avoids
// per-candidate modulo work so building the small-prime tables stops dominating
// startup time for large scans.
if (candidate % p == 0U)
{
isPrime = false;
break;
}
}
if (isPrime)
{
primes.Add(candidate);
if (allCount < targetInt)
{
// TODO: Reuse a single cached `candidateSquared` per iteration so we avoid issuing
// three separate 64-bit multiplications when populating the pow2 arrays here and
// in the last-one/last-seven branches below.
all[allCount] = candidate;
allPow2[allCount] = checked((ulong)candidate * candidate);
allCount++;
}
if (candidate != 2U)
{
LastDigit lastDigit = GetLastDigit(candidate);
if (lastOneCount < targetInt && IsAllowedForLastOne(candidate, lastDigit))
{
lastOne[lastOneCount] = candidate;
lastOnePow2[lastOneCount] = checked((ulong)candidate * candidate);
lastOneCount++;
}
if (lastSevenCount < targetInt && IsAllowedForLastSeven(candidate, lastDigit))
{
lastSeven[lastSevenCount] = candidate;
lastSevenPow2[lastSevenCount] = checked((ulong)candidate * candidate);
lastSevenCount++;
}
if (lastThreeCount < targetInt && IsAllowedForLastThree(candidate, lastDigit))
{
lastThree[lastThreeCount] = candidate;
lastThreePow2[lastThreeCount] = checked((ulong)candidate * candidate);
lastThreeCount++;
}
if (lastNineCount < targetInt && IsAllowedForLastNine(candidate, lastDigit))
{
lastNine[lastNineCount] = candidate;
lastNinePow2[lastNineCount] = checked((ulong)candidate * candidate);
lastNineCount++;
}
}
}
// TODO: Switch this increment to the Mod6 stepping table identified in the Mod6ComparisonBenchmarks
// so we skip the composite residues without relying on per-iteration `%` checks.
candidate = candidate == 2U ? 3U : candidate + 2U;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static LastDigit GetLastDigit(uint prime)
{
return (prime % 10U) switch
{
1U => LastDigit.One,
3U => LastDigit.Three,
5U => LastDigit.Five,
7U => LastDigit.Seven,
9U => LastDigit.Nine,
_ => throw new ArgumentOutOfRangeException(nameof(prime)),
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAllowedForLastOne(uint prime, LastDigit lastDigit)
{
// TODO: Swap the `% 10` usage for ULongExtensions.Mod10 so the hot classification path
// reuses the benchmarked residue helper instead of repeated divisions.
return lastDigit switch
{
LastDigit.One or LastDigit.Three or LastDigit.Nine => true,
LastDigit.Seven => prime == 7U,
_ => prime == 11U,
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAllowedForLastSeven(uint prime, LastDigit lastDigit)
{
// TODO: Route this `% 10` classification through ULongExtensions.Mod10 to match the faster
// residue helper used elsewhere in the scanner.
return lastDigit switch
{
LastDigit.Three or LastDigit.Seven or LastDigit.Nine => true,
_ => prime == 11U,
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAllowedForLastThree(uint prime, LastDigit lastDigit)
{
// TODO: Replace the `% 10` residue classification with ULongExtensions.Mod10 once the helper
// migrates to UInt32 so we can reuse the optimized decimal residue path everywhere.
return lastDigit switch
{
LastDigit.Three or LastDigit.Seven => true,
LastDigit.One => prime == 11U,
LastDigit.Nine => prime == 19U,
_ => false,
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAllowedForLastNine(uint prime, LastDigit lastDigit)
{
// TODO: Replace the `% 10` residue classification with ULongExtensions.Mod10 when the helper
// supports UInt32 inputs so the residue tables share the optimized decimal path.
return lastDigit switch
{
LastDigit.Three or LastDigit.Seven or LastDigit.Nine => true,
LastDigit.One => prime == 11U,
_ => false,
};
}
} }
@@ -7,22 +7,42 @@ namespace PerfectNumbers.Core.Gpu;
internal static class PrimeTesterKernels internal static class PrimeTesterKernels
{ {
// GPU kernel: small-prime sieve only. Returns 1 if passes sieve (probable prime), 0 otherwise. // GPU kernel: small-prime sieve only. Returns 1 if passes sieve (probable prime), 0 otherwise.
public static void SmallPrimeSieveKernel(Index1D index, ArrayView<ulong> numbers, ArrayView<uint> smallPrimes, ArrayView<byte> results) public static void SmallPrimeSieveKernel(
Index1D index,
ArrayView<ulong> numbers,
ArrayView<uint> smallPrimes,
ArrayView<uint> smallPrimesLastOne,
ArrayView<uint> smallPrimesLastSeven,
ArrayView<uint> smallPrimesLastThree,
ArrayView<uint> smallPrimesLastNine,
ArrayView<byte> results)
{ {
ulong n = numbers[index]; ulong n = numbers[index];
byte result = 1; byte result = 1;
// EvenPerfectBitScanner filters candidates so production GPU launches only see: ArrayView<uint> primes = smallPrimes;
// - n >= 31 (the scanner starts at 136,279,841 and test mode seeds 31) ulong lastDigit = n % 10UL;
// - n odd (candidate generators and ModResidueTracker eliminate even exponents) switch (lastDigit)
// - n not divisible by 5 (residue prefiltering and AddPrimes transform cover these composites) {
// Tests should honor the same constraints so the host wrapper can remain branchless. case 1UL:
primes = smallPrimesLastOne;
break;
case 3UL:
primes = smallPrimesLastThree;
break;
case 7UL:
primes = smallPrimesLastSeven;
break;
case 9UL:
primes = smallPrimesLastNine;
break;
}
long length = smallPrimes.Length; int length = (int)primes.Length;
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
{ {
ulong prime = smallPrimes[i]; ulong prime = primes[i];
ulong primeSquare = (ulong)prime * prime; ulong primeSquare = prime * prime;
if (primeSquare > n) if (primeSquare > n)
{ {
break; break;
+94 -14
View File
@@ -48,9 +48,31 @@ public sealed class PrimeTester
if (result) if (result)
{ {
var smallPrimeDivisorsLength = PrimesGenerator.SmallPrimes.Length;
uint[] smallPrimeDivisors = PrimesGenerator.SmallPrimes; uint[] smallPrimeDivisors = PrimesGenerator.SmallPrimes;
ulong[] smallPrimeDivisorsMul = PrimesGenerator.SmallPrimesPow2; ulong[] smallPrimeDivisorsMul = PrimesGenerator.SmallPrimesPow2;
ulong nMod10 = n.Mod10();
switch (nMod10)
{
case 1UL:
smallPrimeDivisors = PrimesGenerator.SmallPrimesLastOne;
smallPrimeDivisorsMul = PrimesGenerator.SmallPrimesPow2LastOne;
break;
case 3UL:
smallPrimeDivisors = DivisorGenerator.SmallPrimesLastThree;
smallPrimeDivisorsMul = DivisorGenerator.SmallPrimesPow2LastThree;
break;
case 7UL:
smallPrimeDivisors = PrimesGenerator.SmallPrimesLastSeven;
smallPrimeDivisorsMul = PrimesGenerator.SmallPrimesPow2LastSeven;
break;
case 9UL:
smallPrimeDivisors = DivisorGenerator.SmallPrimesLastNine;
smallPrimeDivisorsMul = DivisorGenerator.SmallPrimesPow2LastNine;
break;
}
int smallPrimeDivisorsLength = smallPrimeDivisors.Length;
for (int i = 0; i < smallPrimeDivisorsLength; i++) for (int i = 0; i < smallPrimeDivisorsLength; i++)
{ {
if (smallPrimeDivisorsMul[i] > n) if (smallPrimeDivisorsMul[i] > n)
@@ -110,8 +132,8 @@ public sealed class PrimeTester
{ {
var limiter = GpuPrimeWorkLimiter.Acquire(); var limiter = GpuPrimeWorkLimiter.Acquire();
var gpu = PrimeTesterGpuContextPool.Rent(); var gpu = PrimeTesterGpuContextPool.Rent();
var state = gpu.State;
var accelerator = gpu.Accelerator; var accelerator = gpu.Accelerator;
var state = GpuKernelState.GetOrCreate(accelerator);
int totalLength = values.Length; int totalLength = values.Length;
int batchSize = Math.Max(1, GpuBatchSize); int batchSize = Math.Max(1, GpuBatchSize);
@@ -132,7 +154,15 @@ public sealed class PrimeTester
values.Slice(pos, count).CopyTo(temp); values.Slice(pos, count).CopyTo(temp);
input.View.CopyFromCPU(ref temp[0], count); input.View.CopyFromCPU(ref temp[0], count);
state.Kernel(count, input.View, state.DevicePrimes.View, output.View); state.Kernel(
count,
input.View,
state.DevicePrimesDefault.View,
state.DevicePrimesLastOne.View,
state.DevicePrimesLastSeven.View,
state.DevicePrimesLastThree.View,
state.DevicePrimesLastNine.View,
output.View);
accelerator.Synchronize(); accelerator.Synchronize();
output.View.CopyToCPU(ref results[pos], count); output.View.CopyToCPU(ref results[pos], count);
@@ -151,22 +181,40 @@ public sealed class PrimeTester
{ {
internal sealed class PooledContext internal sealed class PooledContext
{ {
public Context Context { get; } public Context Context { get; }
public Accelerator Accelerator { get; } public Accelerator Accelerator { get; }
public object ExecutionLock { get; } = new(); public object ExecutionLock { get; } = new();
private KernelState? _kernelState;
public PooledContext() public PooledContext()
{ {
Context = Context.CreateDefault(); Context = Context.CreateDefault();
Accelerator = Context.GetPreferredDevice(false).CreateAccelerator(Context); Accelerator = Context.GetPreferredDevice(false).CreateAccelerator(Context);
_kernelState = GpuKernelState.GetOrCreate(Accelerator);
}
public KernelState KernelState
{
get
{
var state = _kernelState;
if (state is null || state.IsDisposed)
{
state = GpuKernelState.GetOrCreate(Accelerator);
_kernelState = state;
}
return state;
}
} }
public void Dispose() public void Dispose()
{ {
PrimeTester.ClearGpuCaches(Accelerator); ClearGpuCaches(Accelerator);
_kernelState = null;
Accelerator.Dispose(); Accelerator.Dispose();
Context.Dispose(); Context.Dispose();
} }
@@ -215,6 +263,8 @@ public sealed class PrimeTester
public object ExecutionLock => _ctx!.ExecutionLock; public object ExecutionLock => _ctx!.ExecutionLock;
public KernelState State => _ctx!.KernelState;
public void Dispose() public void Dispose()
{ {
Return(_ctx ?? throw new InvalidOperationException("GPU context lease is not initialized.")); Return(_ctx ?? throw new InvalidOperationException("GPU context lease is not initialized."));
@@ -225,10 +275,14 @@ public sealed class PrimeTester
// Per-accelerator GPU state for prime sieve (kernel + uploaded primes). // Per-accelerator GPU state for prime sieve (kernel + uploaded primes).
internal sealed class KernelState internal sealed class KernelState
{ {
public Action<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<byte>> Kernel { get; } public Action<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<byte>> Kernel { get; }
public Action<Index1D, ArrayView<ulong>, ulong, ArrayView<byte>> HeuristicTrialDivisionKernel { get; } public Action<Index1D, ArrayView<ulong>, ulong, ArrayView<byte>> HeuristicTrialDivisionKernel { get; }
public MemoryBuffer1D<uint, Stride1D.Dense> DevicePrimes { get; } public MemoryBuffer1D<uint, Stride1D.Dense> DevicePrimesDefault { get; }
private readonly Accelerator _accel; public MemoryBuffer1D<uint, Stride1D.Dense> DevicePrimesLastOne { get; }
public MemoryBuffer1D<uint, Stride1D.Dense> DevicePrimesLastSeven { get; }
public MemoryBuffer1D<uint, Stride1D.Dense> DevicePrimesLastThree { get; }
public MemoryBuffer1D<uint, Stride1D.Dense> DevicePrimesLastNine { get; }
public bool IsDisposed { get; private set; }
private readonly System.Collections.Concurrent.ConcurrentBag<ScratchBuffers> _scratchPool = []; private readonly System.Collections.Concurrent.ConcurrentBag<ScratchBuffers> _scratchPool = [];
// TODO: Replace this ConcurrentBag with the lock-free ring buffer variant validated in // TODO: Replace this ConcurrentBag with the lock-free ring buffer variant validated in
// GpuModularArithmeticBenchmarks so renting scratch buffers stops contending on the bag's internal locks when // GpuModularArithmeticBenchmarks so renting scratch buffers stops contending on the bag's internal locks when
@@ -236,14 +290,30 @@ public sealed class PrimeTester
public KernelState(Accelerator accelerator) public KernelState(Accelerator accelerator)
{ {
_accel = accelerator; IsDisposed = false;
// Compile once per accelerator and upload primes once. // Compile once per accelerator and upload primes once.
Kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<byte>>(PrimeTesterKernels.SmallPrimeSieveKernel); Kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<uint>, ArrayView<byte>>(PrimeTesterKernels.SmallPrimeSieveKernel);
HeuristicTrialDivisionKernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ulong, ArrayView<byte>>(PrimeTesterKernels.HeuristicTrialDivisionKernel); HeuristicTrialDivisionKernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ulong, ArrayView<byte>>(PrimeTesterKernels.HeuristicTrialDivisionKernel);
var primes = PrimesGenerator.SmallPrimes; var primesDefault = DivisorGenerator.SmallPrimes;
DevicePrimes = accelerator.Allocate1D<uint>(primes.Length); DevicePrimesDefault = accelerator.Allocate1D<uint>(primesDefault.Length);
DevicePrimes.View.CopyFromCPU(primes); DevicePrimesDefault.View.CopyFromCPU(primesDefault);
var primesLastOne = DivisorGenerator.SmallPrimesLastOne;
DevicePrimesLastOne = accelerator.Allocate1D<uint>(primesLastOne.Length);
DevicePrimesLastOne.View.CopyFromCPU(primesLastOne);
var primesLastSeven = DivisorGenerator.SmallPrimesLastSeven;
DevicePrimesLastSeven = accelerator.Allocate1D<uint>(primesLastSeven.Length);
DevicePrimesLastSeven.View.CopyFromCPU(primesLastSeven);
var primesLastThree = DivisorGenerator.SmallPrimesLastThree;
DevicePrimesLastThree = accelerator.Allocate1D<uint>(primesLastThree.Length);
DevicePrimesLastThree.View.CopyFromCPU(primesLastThree);
var primesLastNine = DivisorGenerator.SmallPrimesLastNine;
DevicePrimesLastNine = accelerator.Allocate1D<uint>(primesLastNine.Length);
DevicePrimesLastNine.View.CopyFromCPU(primesLastNine);
} }
internal sealed class ScratchBuffers internal sealed class ScratchBuffers
@@ -291,12 +361,22 @@ public sealed class PrimeTester
public void Clear() public void Clear()
{ {
if (IsDisposed)
{
return;
}
while (_scratchPool.TryTake(out var sb)) while (_scratchPool.TryTake(out var sb))
{ {
sb.Dispose(); sb.Dispose();
} }
DevicePrimes.Dispose(); DevicePrimesDefault.Dispose();
DevicePrimesLastOne.Dispose();
DevicePrimesLastSeven.Dispose();
DevicePrimesLastThree.Dispose();
DevicePrimesLastNine.Dispose();
IsDisposed = true;
} }
} }
+6 -146
View File
@@ -1,151 +1,11 @@
using System;
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core; namespace PerfectNumbers.Core;
public static class PrimesGenerator public static class PrimesGenerator
{ {
public static readonly uint[] SmallPrimes; public static readonly uint[] SmallPrimes = DivisorGenerator.SmallPrimes;
public static readonly ulong[] SmallPrimesPow2; public static readonly ulong[] SmallPrimesPow2 = DivisorGenerator.SmallPrimesPow2;
public static readonly uint[] SmallPrimesLastOne; public static readonly uint[] SmallPrimesLastOne = DivisorGenerator.SmallPrimesLastOne;
public static readonly ulong[] SmallPrimesPow2LastOne; public static readonly ulong[] SmallPrimesPow2LastOne = DivisorGenerator.SmallPrimesPow2LastOne;
public static readonly uint[] SmallPrimesLastSeven; public static readonly uint[] SmallPrimesLastSeven = DivisorGenerator.SmallPrimesLastSeven;
public static readonly ulong[] SmallPrimesPow2LastSeven; public static readonly ulong[] SmallPrimesPow2LastSeven = DivisorGenerator.SmallPrimesPow2LastSeven;
static PrimesGenerator()
{
BuildSmallPrimes(
out SmallPrimes,
out SmallPrimesPow2,
out SmallPrimesLastOne,
out SmallPrimesPow2LastOne,
out SmallPrimesLastSeven,
out SmallPrimesPow2LastSeven);
}
private static void BuildSmallPrimes(
out uint[] all,
out ulong[] allPow2,
out uint[] lastOne,
out ulong[] lastOnePow2,
out uint[] lastSeven,
out ulong[] lastSevenPow2)
{
uint target = PerfectNumberConstants.PrimesLimit;
int targetInt = checked((int)target);
all = new uint[targetInt];
allPow2 = new ulong[targetInt];
lastOne = new uint[targetInt];
lastOnePow2 = new ulong[targetInt];
lastSeven = new uint[targetInt];
lastSevenPow2 = new ulong[targetInt];
List<uint> primes = new(targetInt * 4 / 3 + 16);
uint candidate = 2U;
int allCount = 0;
int lastOneCount = 0;
int lastSevenCount = 0;
while (allCount < targetInt || lastOneCount < targetInt || lastSevenCount < targetInt)
{
bool isPrime = true;
int primesCount = primes.Count;
for (int i = 0; i < primesCount; i++)
{
uint p = primes[i];
// TODO: Reuse a single squared local for `p` within this loop so we avoid recalculating
// `p * p` repeatedly without storing every squared prime globally (the cache must remain
// limited to the small-divisor tables to honor the memory constraints).
if (p * p > candidate)
{
break;
}
// TODO: Replace this trial-division `%` with the sieve-based generator that avoids
// per-candidate modulo work so building the small-prime tables stops dominating
// startup time for large scans.
if (candidate % p == 0U)
{
isPrime = false;
break;
}
}
if (isPrime)
{
primes.Add(candidate);
if (allCount < targetInt)
{
// TODO: Reuse a single cached `candidateSquared` per iteration so we avoid issuing
// three separate 64-bit multiplications when populating the pow2 arrays here and
// in the last-one/last-seven branches below.
all[allCount] = candidate;
allPow2[allCount] = checked((ulong)candidate * candidate);
allCount++;
}
if (candidate != 2U)
{
LastDigit lastDigit = GetLastDigit(candidate);
if (lastOneCount < targetInt && IsAllowedForLastOne(candidate, lastDigit))
{
lastOne[lastOneCount] = candidate;
lastOnePow2[lastOneCount] = checked((ulong)candidate * candidate);
lastOneCount++;
}
if (lastSevenCount < targetInt && IsAllowedForLastSeven(candidate, lastDigit))
{
lastSeven[lastSevenCount] = candidate;
lastSevenPow2[lastSevenCount] = checked((ulong)candidate * candidate);
lastSevenCount++;
}
}
}
// TODO: Switch this increment to the Mod6 stepping table identified in the Mod6ComparisonBenchmarks
// so we skip the composite residues without relying on per-iteration `%` checks.
candidate = candidate == 2U ? 3U : candidate + 2U;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static LastDigit GetLastDigit(uint prime)
{
return (prime % 10U) switch
{
1U => LastDigit.One,
3U => LastDigit.Three,
5U => LastDigit.Five,
7U => LastDigit.Seven,
9U => LastDigit.Nine,
_ => throw new ArgumentOutOfRangeException(nameof(prime)),
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAllowedForLastOne(uint prime, LastDigit lastDigit)
{
// TODO: Swap the `% 10` usage for ULongExtensions.Mod10 so the hot classification path
// reuses the benchmarked residue helper instead of repeated divisions.
return lastDigit switch
{
LastDigit.One or LastDigit.Three or LastDigit.Nine => true,
LastDigit.Seven => prime == 7U,
_ => prime == 11U,
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAllowedForLastSeven(uint prime, LastDigit lastDigit)
{
// TODO: Route this `% 10` classification through ULongExtensions.Mod10 to match the faster
// residue helper used elsewhere in the scanner.
return lastDigit switch
{
LastDigit.Three or LastDigit.Seven or LastDigit.Nine => true,
_ => prime == 11U,
};
}
} }