Compare commits

..
73 changed files with 2091 additions and 5715 deletions

No files matched your search

@@ -15,7 +15,4 @@
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
</ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project>
@@ -50,7 +50,7 @@ public class GpuPow2ModBenchmarks
return Pow2ModSingleBit(Input.Exponent, Input.Modulus);
}
private static GpuUInt128 Pow2ModSingleBit(ulong exponent, GpuUInt128 modulus)
private static GpuUInt128 Pow2ModSingleBit(ulong exponent, in GpuUInt128 modulus)
{
if (modulus.IsZero || modulus == GpuUInt128.One)
{
@@ -158,16 +158,36 @@ public class MulMod64Benchmarks
}
/// <summary>
/// Uses the production <see cref="ULongExtensions.MulMod64(ulong, ulong, ulong)"/> helper shared by CPU and GPU callers.
/// Uses the GPU-friendly <see cref="ULongExtensions.MulMod64GpuCompatible(ulong, ulong, ulong)"/> helper; dense inputs now
/// cost 24.4–61.9 ns, while NearFullRange and SparseOperands finish near 7.27 ns and ZeroOperands stay baseline-fast at
/// 3.59 ns.
/// </summary>
/// <remarks>
/// Observed means prior to the helper consolidation: CrossWordBlend 24.422 ns, MixedBitPattern 61.850 ns, NearFullRange 7.267 ns,
/// Observed means: CrossWordBlend 24.422 ns (6.86×), MixedBitPattern 61.850 ns, NearFullRange 7.267 ns,
/// PrimeSizedModulus 55.114 ns, SparseOperands 7.261 ns, ZeroOperands 3.587 ns.
/// </remarks>
[Benchmark]
public ulong UnifiedMulMod()
public ulong GpuCompatibleBaseline()
{
return Input.Left.MulMod64(Input.Right, Input.Modulus);
// TODO: Callers in production should migrate to ULongExtensions.MulMod64 where GPU parity
// is not required; that baseline stayed 6–17× faster for dense 64-bit operands.
return Input.Left.MulMod64GpuCompatible(Input.Right, Input.Modulus);
}
/// <summary>
/// Uses the deferred native-modulo helper <see cref="ULongExtensions.MulMod64GpuCompatibleDeferred(ulong, ulong, ulong)"/>;
/// excels when operands are tiny (2.01 ns on ZeroOperands) yet remains the slowest choice on dense data at 146–295 ns.
/// </summary>
/// <remarks>
/// Observed means: CrossWordBlend 146.559 ns (40.63×), MixedBitPattern 255.555 ns, NearFullRange 21.390 ns,
/// PrimeSizedModulus 294.486 ns, SparseOperands 18.182 ns, ZeroOperands 2.009 ns.
/// </remarks>
[Benchmark]
public ulong GpuCompatibleDeferred()
{
// TODO: Retire the deferred GPU shim from runtime paths and keep it only for benchmarks;
// ULongExtensions.MulMod64 avoids the 6×–82× slowdown on real-world operand mixes.
return Input.Left.MulMod64GpuCompatibleDeferred(Input.Right, Input.Modulus);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -1,75 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[ShortRunJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MutableUInt128AddBenchmarks
{
private static readonly BenchmarkInput[] Inputs =
[
new(Create(0UL, 0x0000_0000_0000_0007UL), Create(0, 0x3UL), "TinyOperands"),
new(Create(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL), Create(0xFFFF_FFFF, 0x0000_0001UL), "MixedOperands"),
new(Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), Create(0xFFFF_FFFF, 0xFFFF_FFFBUL), "NearMaxOperands"),
];
[ParamsSource(nameof(GetInputs))]
public BenchmarkInput Input { get; set; }
public static IEnumerable<BenchmarkInput> GetInputs() => Inputs;
private static MutableUInt128 Create(ulong high, ulong low) => new(high, low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void StandardInternal(ref MutableUInt128 source, MutableUInt128 value)
{
ulong temp = source.Low + value.Low;
source.Low = temp;
temp = temp < source.Low ? 1UL : 0UL;
temp += source.High;
temp += value.High;
source.High = temp;
}
/// <summary>
/// Baseline MultiplyAdd implementation that chains Multiply and Add; measured 3.370 ns with mixed operands,
/// 3.361 ns when both limbs and inputs are near their maxima, and 3.368 ns with tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.370 ns, NearMaxOperands 3.361 ns, TinyOperands 3.368 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public MutableUInt128 Standard()
{
MutableUInt128 value = Input.InitialValue;
StandardInternal(ref value, Input.Addend);
return value;
}
/// <summary>
/// Fully inlined MultiplyAdd implementation that kept all arithmetic inside the method; landed at 3.175 ns on mixed operands,
/// 3.177 ns on near-max inputs, and 3.178 ns on tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.175 ns, NearMaxOperands 3.177 ns, TinyOperands 3.178 ns.
/// Status: Implemented
/// </remarks>
[Benchmark]
public MutableUInt128 Optimized()
{
MutableUInt128 value = Input.InitialValue;
value.Add(Input.Addend);
return value;
}
public readonly record struct BenchmarkInput(MutableUInt128 InitialValue, MutableUInt128 Addend, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,100 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[ShortRunJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MutableUInt128Mul64Benchmarks
{
private static readonly BenchmarkInput[] Inputs =
[
new(Create(0UL, 0x0000_0000_0000_0007UL), 0x3UL, "TinyOperands"),
new(Create(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL), 0xFFFF_FFFF_0000_0001UL, "MixedOperands"),
new(Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), 0xFFFF_FFFF_FFFF_FFFBUL, "NearMaxOperands"),
];
[ParamsSource(nameof(GetInputs))]
public BenchmarkInput Input { get; set; }
public static IEnumerable<BenchmarkInput> GetInputs() => Inputs;
private static MutableUInt128 Create(ulong high, ulong low) => new(high, low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mul64(ulong left, ulong right, out ulong low)
{
const ulong Mask32 = 0xFFFF_FFFFUL;
ulong leftLow = left & Mask32;
ulong leftHigh = left >> 32;
ulong rightLow = right & Mask32;
ulong rightHigh = right >> 32;
ulong lowLow = leftLow * rightLow;
ulong lowHigh = leftLow * rightHigh;
ulong highLow = leftHigh * rightLow;
ulong highHigh = leftHigh * rightHigh;
ulong carry = (lowLow >> 32) + (lowHigh & Mask32) + (highLow & Mask32);
ulong high = highHigh + (lowHigh >> 32) + (highLow >> 32) + (carry >> 32);
low = ((carry & Mask32) << 32) | (lowLow & Mask32);
return high;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void StandardInternal(ref MutableUInt128 source, ulong value)
{
ulong currentHigh = source.High;
ulong currentLow = source.Low;
ulong carry = Mul64(currentLow, value, out ulong newLow);
Mul64(currentHigh, value, out ulong shiftedHigh);
ulong newHigh = shiftedHigh + carry;
source.Low = newLow;
source.High = newHigh;
}
/// <summary>
/// Baseline MultiplyAdd implementation that chains Multiply and Add; measured 3.113 ns with mixed operands,
/// 3.128 ns when both limbs and inputs are near their maxima, and 3.131 ns with tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.113 ns, NearMaxOperands 3.128 ns, TinyOperands 3.131 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public MutableUInt128 Standard()
{
MutableUInt128 value = Input.InitialValue;
StandardInternal(ref value, Input.Addend);
return value;
}
/// <summary>
/// Fully inlined MultiplyAdd implementation that kept all arithmetic inside the method; landed at 2.969 ns on mixed operands,
/// 2.912 ns on near-max inputs, and 2.935 ns on tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 2.969 ns, NearMaxOperands 2.912 ns, TinyOperands 2.935 ns.
/// Status: Implemented
/// </remarks>
[Benchmark]
public MutableUInt128 Optimized()
{
MutableUInt128 value = Input.InitialValue;
value.Mul(Input.Addend);
return value;
}
public readonly record struct BenchmarkInput(MutableUInt128 InitialValue, ulong Addend, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,76 +0,0 @@
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[ShortRunJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MutableUInt128MultiplyAddBenchmarks
{
private static readonly MultiplyAddInput[] Inputs =
[
new(Create(0UL, 0x0000_0000_0000_0007UL), 0x3UL, 0x5UL, "TinyOperands"),
new(Create(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL), 0xFFFF_FFFF_0000_0001UL, 0x1357_9BDF_2468_ACEEUL, "MixedOperands"),
new(Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), 0xFFFF_FFFF_FFFF_FFFBUL, 0xFFFF_FFFF_FFFF_FFF7UL, "NearMaxOperands"),
];
[ParamsSource(nameof(GetInputs))]
public MultiplyAddInput Input { get; set; }
public static IEnumerable<MultiplyAddInput> GetInputs() => Inputs;
private static UInt128 Create(ulong high, ulong low)
{
return ((UInt128)high << 64) | low;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void MultiplyAdd(ref MutableUInt128 source, ulong multiplier, ulong addend)
{
source.Mul(multiplier);
source.Add(addend);
}
/// <summary>
/// Baseline MultiplyAdd implementation that chains Multiply and Add; measured 3.370 ns with mixed operands,
/// 3.361 ns when both limbs and inputs are near their maxima, and 3.368 ns with tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.370 ns, NearMaxOperands 3.361 ns, TinyOperands 3.368 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public MutableUInt128 MultiplyAddChained()
{
MutableUInt128 value = new(Input.InitialValue);
MultiplyAdd(ref value, Input.Multiplier, Input.Addend);
return value;
}
/// <summary>
/// Fully inlined MultiplyAdd implementation that kept all arithmetic inside the method; landed at 3.175 ns on mixed operands,
/// 3.177 ns on near-max inputs, and 3.178 ns on tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.175 ns, NearMaxOperands 3.177 ns, TinyOperands 3.178 ns.
/// Status: Implemented
/// </remarks>
[Benchmark]
public MutableUInt128 MultiplyAddFullyInlined()
{
MutableUInt128 value = new(Input.InitialValue);
value.MulAdd(Input.Multiplier, Input.Addend);
return value;
}
public readonly record struct MultiplyAddInput(UInt128 InitialValue, ulong Multiplier, ulong Addend, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,75 +0,0 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[ShortRunJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MutableUInt128SubMutableBenchmarks
{
private static readonly BenchmarkInput[] Inputs =
[
new(Create(0UL, 0x0000_0000_0000_0007UL), Create(0, 0x3UL), "TinyOperands"),
new(Create(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL), Create(0xFFFF_FFFF, 0x0000_0001UL), "MixedOperands"),
new(Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), Create(0xFFFF_FFFF, 0xFFFF_FFFBUL), "NearMaxOperands"),
];
[ParamsSource(nameof(GetInputs))]
public BenchmarkInput Input { get; set; }
public static IEnumerable<BenchmarkInput> GetInputs() => Inputs;
private static MutableUInt128 Create(ulong high, ulong low) => new(high, low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void StandardInternal(ref MutableUInt128 source, MutableUInt128 value)
{
ulong low = source.Low;
ulong result = low - value.Low;
ulong borrow = low < value.Low ? 1UL : 0UL;
source.Low = result;
source.High = source.High - value.High - borrow;
}
/// <summary>
/// Baseline MultiplyAdd implementation that chains Multiply and Add; measured 3.370 ns with mixed operands,
/// 3.361 ns when both limbs and inputs are near their maxima, and 3.368 ns with tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.370 ns, NearMaxOperands 3.361 ns, TinyOperands 3.368 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public MutableUInt128 Standard()
{
MutableUInt128 value = Input.InitialValue;
StandardInternal(ref value, Input.Addend);
return value;
}
/// <summary>
/// Fully inlined MultiplyAdd implementation that kept all arithmetic inside the method; landed at 3.175 ns on mixed operands,
/// 3.177 ns on near-max inputs, and 3.178 ns on tiny operands.
/// </summary>
/// <remarks>
/// Observed means: MixedOperands 3.175 ns, NearMaxOperands 3.177 ns, TinyOperands 3.178 ns.
/// Status: Implemented
/// </remarks>
[Benchmark]
public MutableUInt128 Optimized()
{
MutableUInt128 value = Input.InitialValue;
value.Sub(Input.Addend);
return value;
}
public readonly record struct BenchmarkInput(MutableUInt128 InitialValue, MutableUInt128 Addend, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -52,31 +52,14 @@ public class Pow2MontgomeryModBenchmarks
/// Baseline right-to-left Montgomery ladder; measured 36.36 μs on the small sample set and 117.32 μs on the large one.
/// </summary>
[Benchmark(Baseline = true)]
public ulong BaselineCpu()
public ulong Baseline()
{
GetData(out var exponents, out var divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWindowedCpu(divisors[i], keepMontgomery: false);
}
return checksum;
}
/// <summary>
/// Baseline right-to-left Montgomery ladder; measured ??.?? μs on the small sample set and ??.?? μs on the large one.
/// </summary>
[Benchmark]
public ulong BaselineGpu()
{
GetData(out var exponents, out var divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWindowedGpu(divisors[i], keepMontgomery: false);
checksum ^= exponents[i].Pow2MontgomeryModWindowed(divisors[i], keepMontgomery: false);
}
return checksum;
@@ -86,31 +69,14 @@ public class Pow2MontgomeryModBenchmarks
/// Uses a known divisor cycle to fold the exponent first, dropping runtimes to 26.04 μs on small inputs (0.72× baseline) and 9.53 μs on large inputs (0.08× baseline).
/// </summary>
[Benchmark]
public ulong BaselineWithKnownCycleCpu()
public ulong BaselineWithKnownCycle()
{
GetData(out var exponents, out var divisors, out var cycles);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycles[i], divisors[i]);
}
return checksum;
}
/// <summary>
/// Uses a known divisor cycle to fold the exponent first, dropping runtimes to 26.04 μs on small inputs (0.72× baseline) and 9.53 μs on large inputs (0.08× baseline).
/// </summary>
[Benchmark]
public ulong BaselineWithKnownCycleGpu()
{
GetData(out var exponents, out var divisors, out var cycles);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWithCycleGpu(cycles[i], divisors[i]);
checksum ^= exponents[i].Pow2MontgomeryModWithCycle(cycles[i], divisors[i]);
}
return checksum;
@@ -22,391 +22,320 @@ namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
public class Pow2MontgomeryModCycleComputationBenchmarks
{
private const int SampleCount = 256;
private const ulong OneShiftedLeft63 = 1UL << 63;
private const long OneShiftedLeft60 = 1L << 60;
private readonly MontgomeryDivisorData[] _smallDivisors = new MontgomeryDivisorData[SampleCount];
private readonly MontgomeryDivisorData[] _largeDivisors = new MontgomeryDivisorData[SampleCount];
private readonly MontgomeryDivisorData[] _veryLargeDivisors = new MontgomeryDivisorData[SampleCount];
private readonly UInt128[] _wideModuli = new UInt128[SampleCount];
private readonly ulong[] _smallExponents = new ulong[SampleCount];
private readonly ulong[] _largeExponents = new ulong[SampleCount];
private readonly ulong[] _veryLargeExponents = new ulong[SampleCount];
private readonly ulong[] _smallCycles = new ulong[SampleCount];
private readonly ulong[] _largeCycles = new ulong[SampleCount];
private readonly ulong[] _veryLargeCycles = new ulong[SampleCount];
private readonly UInt128[] _wideCycles = new UInt128[SampleCount];
private const int SampleCount = 256;
private const ulong OneShiftedLeft63 = 1UL << 63;
private const long OneShiftedLeft60 = 1L << 60;
private readonly MontgomeryDivisorData[] _smallDivisors = new MontgomeryDivisorData[SampleCount];
private readonly MontgomeryDivisorData[] _largeDivisors = new MontgomeryDivisorData[SampleCount];
private readonly MontgomeryDivisorData[] _veryLargeDivisors = new MontgomeryDivisorData[SampleCount];
private readonly UInt128[] _wideModuli = new UInt128[SampleCount];
private readonly ulong[] _smallExponents = new ulong[SampleCount];
private readonly ulong[] _largeExponents = new ulong[SampleCount];
private readonly ulong[] _veryLargeExponents = new ulong[SampleCount];
private readonly ulong[] _smallCycles = new ulong[SampleCount];
private readonly ulong[] _largeCycles = new ulong[SampleCount];
private readonly ulong[] _veryLargeCycles = new ulong[SampleCount];
private readonly UInt128[] _wideCycles = new UInt128[SampleCount];
private readonly Random _random = new(113);
private ulong? _previousPrimeOrder;
private UInt128? _previousWidePrimeOrder;
private readonly Random _random = new(113);
private ulong? _previousPrimeOrder;
private UInt128? _previousWidePrimeOrder;
public enum InputScale
{
Small,
Large,
VeryLarge
}
public enum InputScale
{
Small,
Large,
VeryLarge
}
[ParamsAllValues]
public InputScale Scale { get; set; }
[ParamsAllValues]
public InputScale Scale { get; set; }
[GlobalSetup]
public void Setup()
{
for (int i = 0; i < SampleCount; i++)
{
[GlobalSetup]
public void Setup()
{
for (int i = 0; i < SampleCount; i++)
{
#if DEBUG
Console.WriteLine($"Generating sample {i}");
Console.WriteLine($"Generating sample {i}");
#endif
ulong smallModulus = NextSmallOddModulus();
_smallDivisors[i] = CreateMontgomeryDivisorData(smallModulus);
_smallExponents[i] = NextSmallExponent();
_smallCycles[i] = CalculateCycleLengthWithHeuristics(smallModulus);
ulong smallModulus = NextSmallOddModulus();
_smallDivisors[i] = CreateMontgomeryDivisorData(smallModulus);
_smallExponents[i] = NextSmallExponent();
_smallCycles[i] = CalculateCycleLengthWithHeuristics(smallModulus);
(ulong largeModulus, ulong largeCycle) = NextLargeModulusAndCycle();
_largeDivisors[i] = CreateMontgomeryDivisorData(largeModulus);
_largeExponents[i] = NextLargeExponent();
_largeCycles[i] = CalculateCycleLengthWithHeuristics(largeModulus, largeCycle);
(ulong largeModulus, ulong largeCycle) = NextLargeModulusAndCycle();
_largeDivisors[i] = CreateMontgomeryDivisorData(largeModulus);
_largeExponents[i] = NextLargeExponent();
_largeCycles[i] = CalculateCycleLengthWithHeuristics(largeModulus, largeCycle);
ulong veryLargeModulus = NextVeryLargeOddModulus();
_veryLargeDivisors[i] = CreateMontgomeryDivisorData(veryLargeModulus);
_veryLargeExponents[i] = NextVeryLargeExponent();
ulong veryLargeModulus = NextVeryLargeOddModulus();
_veryLargeDivisors[i] = CreateMontgomeryDivisorData(veryLargeModulus);
_veryLargeExponents[i] = NextVeryLargeExponent();
#if DEBUG
Console.WriteLine($"Calculating cycle length {veryLargeModulus}");
Console.WriteLine($"Calculating cycle length {veryLargeModulus}");
#endif
_veryLargeCycles[i] = CalculateCycleLengthWithHeuristics(veryLargeModulus);
_veryLargeCycles[i] = CalculateCycleLengthWithHeuristics(veryLargeModulus);
Console.WriteLine($"Generating wide sample {i}");
UInt128 wideModulus = NextWideOddModulus();
_wideModuli[i] = wideModulus;
_wideCycles[i] = CalculateCycleLengthWithHeuristics(wideModulus);
}
}
Console.WriteLine($"Generating wide sample {i}");
UInt128 wideModulus = NextWideOddModulus();
_wideModuli[i] = wideModulus;
_wideCycles[i] = CalculateCycleLengthWithHeuristics(wideModulus);
}
}
/// <summary>
/// Baseline Montgomery reduction without precomputed cycle data; measured 35.41 μs on the small sample and 117.33 μs on the
/// large set.
/// </summary>
// [Benchmark(Baseline = true)]
public ulong MontgomeryWithoutCycleCpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
/// <summary>
/// Baseline Montgomery reduction without precomputed cycle data; measured 35.41 μs on the small sample and 117.33 μs on the
/// large set.
/// </summary>
// [Benchmark(Baseline = true)]
public ulong MontgomeryWithoutCycle()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWindowedCpu(divisors[i], keepMontgomery: false);
}
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWindowed(divisors[i], keepMontgomery: false);
}
return checksum;
}
return checksum;
}
/// <summary>
/// Baseline Montgomery reduction without precomputed cycle data; measured 35.41 μs on the small sample and 117.33 μs on the
/// large set.
/// </summary>
// [Benchmark(Baseline = true)]
public ulong MontgomeryWithoutCycleGpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
/// <summary>
/// Montgomery reduction with a known cycle length (cycle lookup only); 25.81 μs on the small set (0.73× baseline) and 9.12 μs
/// on the large one (0.08×).
/// </summary>
[Benchmark]
public ulong MontgomeryWithPrecomputedCycle()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out ulong[] cycles);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWindowedGpu(divisors[i], keepMontgomery: false);
}
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWithCycle(cycles[i], divisors[i]);
}
return checksum;
}
return checksum;
}
/// <summary>
/// Montgomery reduction with a known cycle length (cycle lookup only); 25.81 μs on the small set (0.73× baseline) and 9.12 μs
/// on the large one (0.08×).
/// </summary>
[Benchmark]
public ulong MontgomeryWithPrecomputedCycleCpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out ulong[] cycles);
ulong checksum = 0UL;
/// <summary>
/// Montgomery reduction with the cycle computed on the fly using the GPU helper; costs 54.09 ms on the small benchmark due to
/// cycle discovery, but drops to 12.91 μs on the large set (0.11× baseline).
/// </summary>
// [Benchmark]
public ulong MontgomeryWithGpuCycleComputation()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycles[i], divisors[i]);
}
for (int i = 0; i < SampleCount; i++)
{
ulong cycle = MersenneDivisorCycles.CalculateCycleLengthGpu(divisors[i].Modulus);
checksum ^= exponents[i].Pow2MontgomeryModWithCycle(cycle, divisors[i]);
}
return checksum;
}
/// <summary>
/// Montgomery reduction with a known cycle length (cycle lookup only); 25.81 μs on the small set (0.73× baseline) and 9.12 μs
/// on the large one (0.08×).
/// </summary>
[Benchmark]
public ulong MontgomeryWithPrecomputedCycleGpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out ulong[] cycles);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWithCycleGpu(cycles[i], divisors[i]);
}
return checksum;
}
/// <summary>
/// Montgomery reduction with the cycle computed on the fly using the GPU helper; costs 54.09 ms on the small benchmark due to
/// cycle discovery, but drops to 12.91 μs on the large set (0.11× baseline).
/// </summary>
// [Benchmark]
public ulong MontgomeryWithGpuCycleComputationCpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
ulong cycle = MersenneDivisorCycles.CalculateCycleLengthGpu(divisors[i].Modulus);
checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycle, divisors[i]);
}
return checksum;
}
/// <summary>
/// Montgomery reduction with the cycle computed on the fly using the GPU helper; costs 54.09 ms on the small benchmark due to
/// cycle discovery, but drops to 12.91 μs on the large set (0.11× baseline).
/// </summary>
// [Benchmark]
public ulong MontgomeryWithGpuCycleComputationGpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
ulong cycle = MersenneDivisorCycles.CalculateCycleLengthGpu(divisors[i].Modulus);
checksum ^= exponents[i].Pow2MontgomeryModWithCycleGpu(cycle, divisors[i]);
}
return checksum;
}
[Benchmark]
public ulong MontgomeryWithHeuristicCycleComputationCpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
ulong cycle = CalculateCycleLengthWithHeuristics(divisors[i].Modulus);
checksum ^= exponents[i].Pow2MontgomeryModWithCycleCpu(cycle, divisors[i]);
}
return checksum;
}
[Benchmark]
public ulong MontgomeryWithHeuristicCycleComputationGpu()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
ulong cycle = CalculateCycleLengthWithHeuristics(divisors[i].Modulus);
checksum ^= exponents[i].Pow2MontgomeryModWithCycleGpu(cycle, divisors[i]);
}
return checksum;
}
[Benchmark]
public UInt128 HeuristicWideCycleComputation()
{
UInt128 checksum = UInt128.Zero;
for (int i = 0; i < SampleCount; i++)
{
UInt128 cycle = CalculateCycleLengthWithHeuristics(_wideModuli[i]);
_wideCycles[i] = cycle;
checksum ^= cycle;
}
return checksum;
}
private void GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out ulong[] cycles)
{
switch (Scale)
{
case InputScale.Small:
exponents = _smallExponents;
divisors = _smallDivisors;
cycles = _smallCycles;
break;
case InputScale.Large:
exponents = _largeExponents;
divisors = _largeDivisors;
cycles = _largeCycles;
break;
case InputScale.VeryLarge:
exponents = _veryLargeExponents;
divisors = _veryLargeDivisors;
cycles = _veryLargeCycles;
break;
default:
throw new ArgumentOutOfRangeException(nameof(Scale), Scale, null);
}
}
private ulong NextSmallExponent() => (ulong)_random.NextInt64(1L, 1_000_000L);
private ulong NextLargeExponent() => (ulong)_random.NextInt64(OneShiftedLeft60, long.MaxValue);
private ulong NextVeryLargeExponent()
{
Span<byte> buffer = stackalloc byte[8];
_random.NextBytes(buffer);
ulong value = checked(BinaryPrimitives.ReadUInt64LittleEndian(buffer) | OneShiftedLeft63);
return value;
}
private ulong NextSmallOddModulus()
{
ulong value = (ulong)_random.NextInt64(3L, 1_000_000L);
return value | 1UL;
}
private (ulong modulus, ulong cycleLength) NextLargeModulusAndCycle()
{
int bitLength = _random.Next(48, 64);
ulong modulus = (1UL << bitLength) - 1UL;
return (modulus, (ulong)bitLength);
}
private ulong NextVeryLargeOddModulus()
{
Span<byte> buffer = stackalloc byte[8];
_random.NextBytes(buffer);
ulong value = BinaryPrimitives.ReadUInt64LittleEndian(buffer) | OneShiftedLeft63 | 1UL;
return value;
}
private UInt128 NextWideOddModulus()
{
Span<byte> buffer = stackalloc byte[16];
_random.NextBytes(buffer);
ulong low = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
Span<byte> highSlice = buffer.Slice(8, 8);
ulong high = BinaryPrimitives.ReadUInt64LittleEndian(highSlice) | (1UL << 63);
UInt128 value = ((UInt128)high << 64) | low;
return value | UInt128.One;
}
private static MontgomeryDivisorData CreateMontgomeryDivisorData(ulong modulus)
{
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return new MontgomeryDivisorData(modulus, 0UL, 0UL, 0UL, 0UL);
}
ulong nPrime = ComputeMontgomeryNPrime(modulus);
ulong montgomeryOne = ComputeMontgomeryResidue(1UL, modulus);
ulong montgomeryTwo = ComputeMontgomeryResidue(2UL, modulus);
ulong montgomeryTwoSquared = ULongExtensions.MontgomeryMultiply(montgomeryTwo, montgomeryTwo, modulus, nPrime);
return new MontgomeryDivisorData(
modulus,
nPrime,
montgomeryOne,
montgomeryTwo,
montgomeryTwoSquared);
}
return checksum;
}
private ulong CalculateCycleLengthWithHeuristics(ulong modulus, ulong fallbackOrder = 0UL)
{
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(modulus);
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLength(modulus, divisorData);
}
[Benchmark]
public ulong MontgomeryWithHeuristicCycleComputation()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
// When the below is uncommented, the benchmark never completes due to the primality test cost on very large random set.
for (int i = 0; i < SampleCount; i++)
{
ulong cycle = CalculateCycleLengthWithHeuristics(divisors[i].Modulus);
checksum ^= exponents[i].Pow2MontgomeryModWithCycle(cycle, divisors[i]);
}
// bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(modulus);
// // bool isPrime = PrimeTester.IsPrimeInternal(modulus, CancellationToken.None);
// if (!isPrime)
// {
// return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLength(modulus);
// }
return checksum;
}
[Benchmark]
public UInt128 HeuristicWideCycleComputation()
{
UInt128 checksum = UInt128.Zero;
for (int i = 0; i < SampleCount; i++)
{
UInt128 cycle = CalculateCycleLengthWithHeuristics(_wideModuli[i]);
_wideCycles[i] = cycle;
checksum ^= cycle;
}
return checksum;
}
private void GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out ulong[] cycles)
{
switch (Scale)
{
case InputScale.Small:
exponents = _smallExponents;
divisors = _smallDivisors;
cycles = _smallCycles;
break;
case InputScale.Large:
exponents = _largeExponents;
divisors = _largeDivisors;
cycles = _largeCycles;
break;
case InputScale.VeryLarge:
exponents = _veryLargeExponents;
divisors = _veryLargeDivisors;
cycles = _veryLargeCycles;
break;
default:
throw new ArgumentOutOfRangeException(nameof(Scale), Scale, null);
}
}
private ulong NextSmallExponent() => (ulong)_random.NextInt64(1L, 1_000_000L);
private ulong NextLargeExponent() => (ulong)_random.NextInt64(OneShiftedLeft60, long.MaxValue);
private ulong NextVeryLargeExponent()
{
Span<byte> buffer = stackalloc byte[8];
_random.NextBytes(buffer);
ulong value = checked(BinaryPrimitives.ReadUInt64LittleEndian(buffer) | OneShiftedLeft63);
return value;
}
private ulong NextSmallOddModulus()
{
ulong value = (ulong)_random.NextInt64(3L, 1_000_000L);
return value | 1UL;
}
private (ulong modulus, ulong cycleLength) NextLargeModulusAndCycle()
{
int bitLength = _random.Next(48, 64);
ulong modulus = (1UL << bitLength) - 1UL;
return (modulus, (ulong)bitLength);
}
private ulong NextVeryLargeOddModulus()
{
Span<byte> buffer = stackalloc byte[8];
_random.NextBytes(buffer);
ulong value = BinaryPrimitives.ReadUInt64LittleEndian(buffer) | OneShiftedLeft63 | 1UL;
return value;
}
private UInt128 NextWideOddModulus()
{
Span<byte> buffer = stackalloc byte[16];
_random.NextBytes(buffer);
ulong low = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
Span<byte> highSlice = buffer.Slice(8, 8);
ulong high = BinaryPrimitives.ReadUInt64LittleEndian(highSlice) | (1UL << 63);
UInt128 value = ((UInt128)high << 64) | low;
return value | UInt128.One;
}
private static MontgomeryDivisorData CreateMontgomeryDivisorData(ulong modulus)
{
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return new MontgomeryDivisorData(modulus, 0UL, 0UL, 0UL, 0UL);
}
ulong nPrime = ComputeMontgomeryNPrime(modulus);
ulong montgomeryOne = ComputeMontgomeryResidue(1UL, modulus);
ulong montgomeryTwo = ComputeMontgomeryResidue(2UL, modulus);
ulong montgomeryTwoSquared = ULongExtensions.MontgomeryMultiply(montgomeryTwo, montgomeryTwo, modulus, nPrime);
return new MontgomeryDivisorData(
modulus,
nPrime,
montgomeryOne,
montgomeryTwo,
montgomeryTwoSquared);
}
private ulong CalculateCycleLengthWithHeuristics(ulong modulus, ulong fallbackOrder = 0UL)
{
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(modulus);
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLength(modulus, divisorData);
}
// When the below is uncommented, the benchmark never completes due to the primality test cost on very large random set.
// bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(modulus);
// // bool isPrime = PrimeTester.IsPrimeInternal(modulus, CancellationToken.None);
// if (!isPrime)
// {
// return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLength(modulus);
// }
#if DEBUG
Console.WriteLine("Trying heuristic. Prime order calculation");
Console.WriteLine("Trying heuristic. Prime order calculation");
#endif
ulong order = PrimeOrderCalculator.Calculate(
modulus,
_previousPrimeOrder,
divisorData,
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault,
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu);
PrimeOrderCalculator.PrimeOrderResult orderResult = PrimeOrderCalculator.Calculate(
modulus,
_previousPrimeOrder,
divisorData,
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault);
if (order != 0UL)
{
_previousPrimeOrder = order;
return order;
}
if (orderResult.Order != 0UL)
{
_previousPrimeOrder = orderResult.Order;
return orderResult.Order;
}
_previousPrimeOrder = null;
_previousPrimeOrder = null;
Console.WriteLine($"Heuristic failed for {modulus}, falling back to full cycle calculation");
return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLength(modulus, divisorData);
}
Console.WriteLine($"Heuristic failed for {modulus}, falling back to full cycle calculation");
return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLength(modulus, divisorData);
}
private UInt128 CalculateCycleLengthWithHeuristics(UInt128 modulus)
{
if (modulus <= UInt128.One || (modulus & UInt128.One) == UInt128.Zero)
{
return MersenneDivisorCycles.GetCycle(modulus);
}
private UInt128 CalculateCycleLengthWithHeuristics(UInt128 modulus)
{
if (modulus <= UInt128.One || (modulus & UInt128.One) == UInt128.Zero)
{
return MersenneDivisorCycles.GetCycle(modulus);
}
#if DEBUG
Console.WriteLine("Trying heuristic. Wide prime order calculation");
Console.WriteLine("Trying heuristic. Wide prime order calculation");
#endif
UInt128 order = PrimeOrderCalculator.Calculate(
modulus,
_previousWidePrimeOrder,
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault,
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu);
PrimeOrderCalculator.PrimeOrderResultWide orderResult = PrimeOrderCalculator.Calculate(
modulus,
_previousWidePrimeOrder,
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault);
if (order != UInt128.Zero)
{
_previousWidePrimeOrder = order;
return order;
}
if (orderResult.Order != UInt128.Zero)
{
_previousWidePrimeOrder = orderResult.Order;
return orderResult.Order;
}
_previousWidePrimeOrder = null;
_previousWidePrimeOrder = null;
Console.WriteLine($"Wide heuristic failed for {modulus}, falling back to full cycle calculation");
return MersenneDivisorCycles.GetCycle(modulus);
}
Console.WriteLine($"Wide heuristic failed for {modulus}, falling back to full cycle calculation");
return MersenneDivisorCycles.GetCycle(modulus);
}
private static ulong ComputeMontgomeryResidue(ulong value, ulong modulus) => (ulong)((UInt128)value * (UInt128.One << 64) % modulus);
private static ulong ComputeMontgomeryResidue(ulong value, ulong modulus) => (ulong)((UInt128)value * (UInt128.One << 64) % modulus);
private static ulong ComputeMontgomeryNPrime(ulong modulus)
{
ulong inv = modulus;
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
return unchecked(0UL - inv);
}
private static ulong ComputeMontgomeryNPrime(ulong modulus)
{
ulong inv = modulus;
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
return unchecked(0UL - inv);
}
}
@@ -176,7 +176,7 @@ public class Pow2MontgomeryModUInt128Benchmarks
return (UInt128)result;
}
private static bool ShouldUseSingleBit(GpuUInt128 exponent)
private static bool ShouldUseSingleBit(in GpuUInt128 exponent)
{
if (exponent.High == 0UL && exponent.Low <= Pow2WindowFallbackThreshold)
{
@@ -257,7 +257,7 @@ public class Pow2MontgomeryModUInt128Benchmarks
return result;
}
private static bool IsBitSet(GpuUInt128 value, int bitIndex)
private static bool IsBitSet(in GpuUInt128 value, int bitIndex)
{
if (bitIndex >= 64)
{
@@ -267,7 +267,7 @@ public class Pow2MontgomeryModUInt128Benchmarks
return ((value.Low >> bitIndex) & 1UL) != 0UL;
}
private static ulong ExtractWindowValue(GpuUInt128 exponent, int windowStart, int windowBitCount)
private static ulong ExtractWindowValue(in GpuUInt128 exponent, int windowStart, int windowBitCount)
{
if (windowStart != 0)
{
+8 -14
View File
@@ -8,22 +8,16 @@ public static class Program
{
public static void Main(string[] args)
{
if (args.Length > 0)
{
IConfig config = DefaultConfig.Instance.WithOptions(ConfigOptions.JoinSummary | ConfigOptions.DisableOptimizationsValidator);
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
return;
}
// var config = DefaultConfig.Instance.WithOptions(ConfigOptions.JoinSummary | ConfigOptions.DisableOptimizationsValidator);
// BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run();
Pow2MontgomeryModCycleComputationBenchmarks benchmarks = new()
{
Scale = Pow2MontgomeryModCycleComputationBenchmarks.InputScale.Large
};
// Pow2MontgomeryModCycleComputationBenchmarks benchmarks = new()
// {
// Scale = Pow2MontgomeryModCycleComputationBenchmarks.InputScale.Large
// };
// GpuPrimeWorkLimiter.SetLimit(1);
// benchmarks.Setup();
GpuPrimeWorkLimiter.SetLimit(1);
benchmarks.Setup();
// Console.WriteLine("Press ENTER to run");
// Console.ReadLine();
@@ -11,7 +11,4 @@
<PackageReference Include="Open.Numeric.Primes" Version="4.0.4" />
</ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project>
@@ -22,7 +22,4 @@
<ItemGroup>
<ProjectReference Include="../EvenPerfectBitScanner/EvenPerfectBitScanner.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project>
+1 -2
View File
@@ -540,7 +540,7 @@ public class ProgramTests
public void Main_displays_help_when_requested()
{
var main = typeof(Program).GetMethod("Main", BindingFlags.NonPublic | BindingFlags.Static)!;
var writer = new StringWriter();
using var writer = new StringWriter();
TextWriter original = Console.Out;
Console.SetOut(writer);
@@ -554,7 +554,6 @@ public class ProgramTests
}
string output = writer.ToString();
writer.Dispose();
output.Should().Contain("Usage:");
output.Should().Contain("--mersenne-device=cpu|gpu");
output.Should().Contain("--primes-device=cpu|gpu");
@@ -16,7 +16,4 @@
<PackageReference Include="Open.Numeric" Version="3.0.1" />
<PackageReference Include="Open.Numeric.Primes" Version="4.0.4" />
</ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project>
+37 -132
View File
@@ -44,7 +44,6 @@ internal static class Program
private static string? _resultsPrefix;
private static readonly Optimized PrimeIterator = new();
private static ulong _byDivisorStartPrime;
private static TimeSpan? _primeTestLimit;
[ThreadStatic]
private static bool _lastCompositeP;
@@ -75,8 +74,6 @@ internal static class Program
bool useResidue = false; // M_p test via residue divisors (replaces LL/incremental)
bool useDivisor = false; // M_p divisibility by specific divisor
bool useByDivisor = false; // Iterative divisor scan across primes UInt128 divisor = UInt128.Zero;
ByDivisorDeltasDevice byDivisorDeltasDevice = ByDivisorDeltasDevice.Cpu;
ByDivisorMontgomeryDevice byDivisorMontgomeryDevice = ByDivisorMontgomeryDevice.Cpu;
// Device routing
bool useGpuCycles = true;
bool mersenneOnGpu = true; // controls Lucas/incremental/pow2mod device
@@ -184,51 +181,6 @@ internal static class Program
{
mersenneOnGpu = !arg.AsSpan(arg.IndexOf('=') + 1).Equals("cpu", StringComparison.OrdinalIgnoreCase);
}
else if (arg.StartsWith("--bydivisor-deltas-device=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> deviceValue = arg.AsSpan(arg.IndexOf('=') + 1);
if (deviceValue.Equals("gpu", StringComparison.OrdinalIgnoreCase))
{
byDivisorDeltasDevice = ByDivisorDeltasDevice.Gpu;
}
else if (deviceValue.Equals("cpu", StringComparison.OrdinalIgnoreCase))
{
byDivisorDeltasDevice = ByDivisorDeltasDevice.Cpu;
}
else
{
Console.WriteLine("Invalid --bydivisor-deltas-device value. Expected cpu or gpu.");
return;
}
}
else if (arg.StartsWith("--bydivisor-montgomery-device=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> deviceValue = arg.AsSpan(arg.IndexOf('=') + 1);
if (deviceValue.Equals("gpu", StringComparison.OrdinalIgnoreCase))
{
byDivisorMontgomeryDevice = ByDivisorMontgomeryDevice.Gpu;
}
else if (deviceValue.Equals("cpu", StringComparison.OrdinalIgnoreCase))
{
byDivisorMontgomeryDevice = ByDivisorMontgomeryDevice.Cpu;
}
else
{
Console.WriteLine("Invalid --bydivisor-montgomery-device value. Expected cpu or gpu.");
return;
}
}
else if (arg.StartsWith("--prime-test-limit=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = arg.AsSpan(arg.IndexOf('=') + 1);
if (!TryParsePrimeTestLimit(value, out TimeSpan parsedLimitValue))
{
Console.WriteLine("Failed to parse --prime-test-limit value.");
return;
}
_primeTestLimit = parsedLimitValue;
}
else if (arg.Equals("--workaround-mod", StringComparison.OrdinalIgnoreCase))
{
useModuloWorkaround = true;
@@ -425,8 +377,6 @@ internal static class Program
}
}
GpuKernelPool.CpuCycleUsesGpu = orderOnGpu;
if (useByDivisor)
{
_byDivisorStartPrime = startPrimeProvided ? currentP : 0UL;
@@ -565,13 +515,10 @@ internal static class Program
{
_byDivisorTester = mersenneOnGpu
? new MersenneNumberDivisorByDivisorGpuTester()
: new MersenneNumberDivisorByDivisorCpuTester();
: new MersenneNumberDivisorByDivisorCpuTester(
useGpuPow2Mod: !GpuContextPool.ForceCpu,
useGpuCycleComputation: orderOnGpu);
_byDivisorTester.BatchSize = scanBatchSize;
if (_byDivisorTester is MersenneNumberDivisorByDivisorCpuTester cpuByDivisorTester)
{
cpuByDivisorTester.DeltasDevice = byDivisorDeltasDevice;
cpuByDivisorTester.MontgomeryDevice = byDivisorMontgomeryDevice;
}
}
// Load RLE blacklist (optional)
@@ -607,18 +554,18 @@ internal static class Program
useDivisor,
useByDivisor,
mersenneOnGpu,
useOrder,
useModuloWorkaround,
_useGcdFilter,
NttGpuMath.GpuTransformBackend,
gpuPrimeThreads,
sliceSize,
scanBatchSize,
_orderWarmupLimitOverride ?? 5_000_000UL,
NttGpuMath.ReductionMode,
mersenneOnGpu ? "gpu" : "cpu",
(GpuContextPool.ForceCpu ? "cpu" : "gpu"),
orderOnGpu ? "gpu" : "cpu");
useOrder,
useModuloWorkaround,
_useGcdFilter,
NttGpuMath.GpuTransformBackend,
gpuPrimeThreads,
sliceSize,
scanBatchSize,
_orderWarmupLimitOverride ?? 5_000_000UL,
NttGpuMath.ReductionMode,
mersenneOnGpu ? "gpu" : "cpu",
(GpuContextPool.ForceCpu ? "cpu" : "gpu"),
orderOnGpu ? "gpu" : "cpu");
if (!string.IsNullOrEmpty(_resultsPrefix))
{
@@ -720,21 +667,7 @@ internal static class Program
if (_useByDivisorMode)
{
if (_byDivisorTester is null)
{
throw new InvalidOperationException("By-divisor tester has not been initialized.");
}
MersenneNumberDivisorByDivisorTester.Run(
byDivisorCandidates,
_byDivisorTester,
_byDivisorPreviousResults,
_byDivisorStartPrime,
static () => _lastCompositeP = true,
static () => _lastCompositeP = false,
PrintResult,
threadCount,
_primeTestLimit);
RunByDivisorMode(byDivisorCandidates, threadCount);
FlushBuffer();
StringBuilderPool.Return(_outputBuilder!);
return;
@@ -875,6 +808,26 @@ internal static class Program
return candidates;
}
private static void RunByDivisorMode(List<ulong> candidates, int threadCount)
{
if (_byDivisorTester is null)
{
throw new InvalidOperationException("By-divisor tester has not been initialized.");
}
// TODO: Inline this helper by invoking MersenneNumberDivisorByDivisorTester.Run directly at call sites so
// the CLI avoids this extra frame during the hot composite-reporting path.
MersenneNumberDivisorByDivisorTester.Run(
candidates,
_byDivisorTester,
_byDivisorPreviousResults,
_byDivisorStartPrime,
static () => _lastCompositeP = true,
static () => _lastCompositeP = false,
PrintResult,
threadCount);
}
private static unsafe void LoadResultsFile(string resultsFileName, Action<ulong, bool, bool> lineProcessorAction)
{
using FileStream readStream = new(resultsFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
@@ -939,51 +892,6 @@ internal static class Program
}
}
private static bool TryParsePrimeTestLimit(ReadOnlySpan<char> value, out TimeSpan result)
{
result = TimeSpan.Zero;
if (value.IsEmpty)
{
return false;
}
ReadOnlySpan<char> magnitudeSpan = value;
double magnitude;
if (value.Length >= 2 && value.EndsWith("ms", StringComparison.OrdinalIgnoreCase))
{
magnitudeSpan = value[..^2];
if (!double.TryParse(magnitudeSpan, NumberStyles.Float, CultureInfo.InvariantCulture, out magnitude))
{
return false;
}
result = TimeSpan.FromMilliseconds(magnitude);
return true;
}
char suffix = char.ToLowerInvariant(value[^1]);
if (suffix == 's' || suffix == 'm')
{
magnitudeSpan = value[..^1];
if (!double.TryParse(magnitudeSpan, NumberStyles.Float, CultureInfo.InvariantCulture, out magnitude))
{
return false;
}
result = suffix == 's' ? TimeSpan.FromSeconds(magnitude) : TimeSpan.FromMinutes(magnitude);
return true;
}
if (!double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out magnitude))
{
return false;
}
result = TimeSpan.FromMilliseconds(magnitude);
return true;
}
private static void PrintHelp()
{
Console.WriteLine("Usage: EvenPerfectBitScanner [options]");
@@ -1017,13 +925,10 @@ internal static class Program
Console.WriteLine(" --divisor-cycles-batch=<value> batch size for cycles generation (default 512)");
Console.WriteLine(" --divisor-cycles-continue continue divisor cycles generation");
Console.WriteLine(" --divisor-cycles-limit=<value> cycle search iterations when --mersenne=divisor");
Console.WriteLine(" --prime-test-limit=<value> Time limit for --mersenne=bydivisor prime checks (e.g. 5s, 500ms)");
Console.WriteLine(" --bydivisor-deltas-device=cpu|gpu Device for --mersenne=bydivisor residue batches (default: cpu)");
Console.WriteLine(" --bydivisor-montgomery-device=cpu|gpu Device for Montgomery data when using --mersenne=bydivisor (default: cpu)");
Console.WriteLine(" --use-order test primality via q order");
Console.WriteLine(" --workaround-mod avoid '%' operator on the GPU");
// mod-automaton removed
Console.WriteLine(" --filter-p=<path> process only p from previous run results (required for --mersenne=bydivisor; primes must be strictly increasing)");
Console.WriteLine(" --filter-p=<path> process only p from previous run results (required for --mersenne=bydivisor)");
Console.WriteLine(" --write-batch-size=<value> overwrite frequency of disk writes (default 100 lines)");
Console.WriteLine(" --gcd-filter enable early sieve based on GCD");
Console.WriteLine(" --help, -help, --?, -?, /? show this help message");
@@ -15,15 +15,9 @@ public class GpuContextPoolTests
{
GpuContextPool.DisposeAll();
GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
try
{
lease.Accelerator.AcceleratorType.Should().Be(AcceleratorType.CPU);
}
finally
{
lease.Dispose();
}
using var lease = GpuContextPool.RentPreferred(preferCpu: true);
lease.Accelerator.AcceleratorType.Should().Be(AcceleratorType.CPU);
GpuContextPool.DisposeAll();
}
@@ -36,27 +30,17 @@ public class GpuContextPoolTests
Context firstContext;
Accelerator firstAccelerator;
GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
try
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
{
firstContext = lease.Context;
firstAccelerator = lease.Accelerator;
}
finally
{
lease.Dispose();
}
lease = GpuContextPool.RentPreferred(preferCpu: true);
try
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
{
lease.Context.Should().BeSameAs(firstContext);
lease.Accelerator.Should().BeSameAs(firstAccelerator);
}
finally
{
lease.Dispose();
}
GpuContextPool.DisposeAll();
}
@@ -68,27 +52,17 @@ public class GpuContextPoolTests
GpuContextPool.DisposeAll();
Context firstContext;
GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
try
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
{
firstContext = lease.Context;
}
finally
{
lease.Dispose();
}
GpuContextPool.DisposeAll();
lease = GpuContextPool.RentPreferred(preferCpu: true);
try
using (var lease = GpuContextPool.RentPreferred(preferCpu: true))
{
lease.Context.Should().NotBeSameAs(firstContext);
}
finally
{
lease.Dispose();
}
GpuContextPool.DisposeAll();
}
@@ -199,17 +199,10 @@ public class GpuKernelCompilationTests
[MemberData(nameof(KernelLoaders))]
public void Kernel_compiles_without_internal_compiler_errors(string name, Action<Accelerator> compile)
{
GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
try
{
var accelerator = lease.Accelerator;
Action action = () => compile(accelerator);
action.Should().NotThrow($"kernel {name} should compile");
}
finally
{
lease.Dispose();
}
using var lease = GpuContextPool.RentPreferred(preferCpu: true);
var accelerator = lease.Accelerator;
Action action = () => compile(accelerator);
action.Should().NotThrow($"kernel {name} should compile");
}
private static void CompileDivisorCycleCacheKernel(Accelerator accelerator)
@@ -18,14 +18,13 @@ public class GpuPrimeLimiterTests
var startedSecond = new TaskCompletionSource<bool>();
var sw = new Stopwatch();
var first = GpuPrimeWorkLimiter.Acquire();
using var first = GpuPrimeWorkLimiter.Acquire();
sw.Start();
var secondTask = Task.Run(() =>
{
var second = GpuPrimeWorkLimiter.Acquire();
using var second = GpuPrimeWorkLimiter.Acquire();
startedSecond.TrySetResult(true);
second.Dispose();
});
// Give secondTask a moment to attempt acquire (it should block).
@@ -150,36 +150,29 @@ public class NttGpuMathTests
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
GpuContextPool.GpuContextLease gpu = GpuContextPool.Rent();
try
using var gpu = GpuContextPool.Rent();
var accelerator = gpu.Accelerator;
var method = typeof(NttGpuMath).GetMethod("GetSquareCache", BindingFlags.NonPublic | BindingFlags.Static);
method.Should().NotBeNull();
var cache = method!.Invoke(null, new object[] { accelerator, 8, modulus, primitiveRoot })!;
var cacheType = cache.GetType();
var twiddlesMont = (MemoryBuffer1D<GpuUInt128, Stride1D.Dense>)cacheType.GetProperty("TwiddlesMont")!.GetValue(cache)!;
var twiddlesInvMont = (MemoryBuffer1D<GpuUInt128, Stride1D.Dense>)cacheType.GetProperty("TwiddlesInvMont")!.GetValue(cache)!;
var r2 = (ulong)cacheType.GetProperty("MontR2Mod64")!.GetValue(cache)!;
var mod = (ulong)cacheType.GetProperty("ModulusLow")!.GetValue(cache)!;
int count = 7; // length 8 -> 7 twiddles
var forward = new GpuUInt128[count];
var inverse = new GpuUInt128[count];
twiddlesMont.CopyToCPU(forward);
twiddlesInvMont.CopyToCPU(inverse);
for (int i = 0; i < count; i++)
{
var accelerator = gpu.Accelerator;
var method = typeof(NttGpuMath).GetMethod("GetSquareCache", BindingFlags.NonPublic | BindingFlags.Static);
method.Should().NotBeNull();
var cache = method!.Invoke(null, new object[] { accelerator, 8, modulus, primitiveRoot })!;
var cacheType = cache.GetType();
var twiddlesMont = (MemoryBuffer1D<GpuUInt128, Stride1D.Dense>)cacheType.GetProperty("TwiddlesMont")!.GetValue(cache)!;
var twiddlesInvMont = (MemoryBuffer1D<GpuUInt128, Stride1D.Dense>)cacheType.GetProperty("TwiddlesInvMont")!.GetValue(cache)!;
var r2 = (ulong)cacheType.GetProperty("MontR2Mod64")!.GetValue(cache)!;
var mod = (ulong)cacheType.GetProperty("ModulusLow")!.GetValue(cache)!;
int count = 7; // length 8 -> 7 twiddles
var forward = new GpuUInt128[count];
var inverse = new GpuUInt128[count];
twiddlesMont.CopyToCPU(forward);
twiddlesInvMont.CopyToCPU(inverse);
for (int i = 0; i < count; i++)
{
ulong prod = (ulong)((((BigInteger)forward[i].Low) * inverse[i].Low) % mod);
prod.Should().Be(r2);
}
}
finally
{
gpu.Dispose();
ulong prod = (ulong)((((BigInteger)forward[i].Low) * inverse[i].Low) % mod);
prod.Should().Be(r2);
}
}
@@ -345,23 +338,12 @@ public class NttGpuMathTests
values[1] = new GpuUInt128(0UL, 2UL);
var modulus = new GpuUInt128(0UL, 17UL);
var primitiveRoot = new GpuUInt128(0UL, 3UL);
GpuContextPool.GpuContextLease gpu = GpuContextPool.Rent();
try
using (var gpu = GpuContextPool.Rent())
{
var buffer = gpu.Accelerator.Allocate1D<GpuUInt128>(values.Length);
try
{
buffer.View.CopyFromCPU(ref values[0], values.Length);
NttGpuMath.SquareDevice(gpu.Accelerator, buffer.View, modulus, primitiveRoot);
}
finally
{
buffer.Dispose();
}
}
finally
{
gpu.Dispose();
buffer.View.CopyFromCPU(ref values[0], values.Length);
NttGpuMath.SquareDevice(gpu.Accelerator, buffer.View, modulus, primitiveRoot);
buffer.Dispose();
}
// Verify that the square cache now holds entries for the accelerator.
@@ -33,9 +33,9 @@ public class NttMontgomeryTests
NttGpuMath.Square(cpu, modulus, primitiveRoot);
// Device execution
GpuContextPool.GpuContextLease context = GpuContextPool.Rent();
using var context = GpuContextPool.Rent();
var accelerator = context.Accelerator;
var buffer = accelerator.Allocate1D<GpuUInt128>(gpu.Length);
using var buffer = accelerator.Allocate1D<GpuUInt128>(gpu.Length);
buffer.View.CopyFromCPU(ref gpu[0], gpu.Length);
NttGpuMath.SquareDevice(accelerator, buffer.View, modulus, primitiveRoot);
buffer.View.CopyToCPU(ref gpu[0], gpu.Length);
@@ -44,9 +44,6 @@ public class NttMontgomeryTests
{
gpu[i].Should().Be(cpu[i]);
}
buffer.Dispose();
context.Dispose();
}
[Fact]
@@ -359,9 +359,10 @@ public class PrimeOrderGpuHeuristicsTests
PrimeOrderGpuHeuristics.OverflowRegistry[prime] = 0;
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
ulong result = PrimeOrderCalculator.Calculate(prime, null, divisorData, PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault, PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu);
PrimeOrderCalculator.PrimeOrderResult result = PrimeOrderCalculator.Calculate(prime, null, divisorData, PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault);
result.Should().Be(3UL);
result.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found);
result.Order.Should().Be(3UL);
}
finally
{
@@ -11,21 +11,14 @@ public class GpuKernelPoolTests
[Trait("Category", "Fast")]
public void EnsureSmallCyclesOnDevice_returns_same_view_on_subsequent_calls()
{
GpuContextPool.GpuContextLease lease = GpuContextPool.RentPreferred(preferCpu: true);
try
{
var accelerator = lease.Accelerator;
using var lease = GpuContextPool.RentPreferred(preferCpu: true);
var accelerator = lease.Accelerator;
var first = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
var second = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
var first = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
var second = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
second.Equals(first).Should().BeTrue();
second.Length.Should().Be(MersenneDivisorCycles.SmallDivisorsMax + 1);
}
finally
{
lease.Dispose();
}
second.Equals(first).Should().BeTrue();
second.Length.Should().Be(MersenneDivisorCycles.SmallDivisorsMax + 1);
}
[Fact]
@@ -17,14 +17,13 @@ public class GpuWorkLimiterTests
var startedSecond = new TaskCompletionSource<bool>();
var sw = new Stopwatch();
var first = GpuWorkLimiter.Acquire();
using var first = GpuWorkLimiter.Acquire();
sw.Start();
var secondTask = Task.Run(() =>
{
var second = GpuWorkLimiter.Acquire();
using var second = GpuWorkLimiter.Acquire();
startedSecond.TrySetResult(true);
second.Dispose();
});
await Task.Delay(50);
@@ -14,8 +14,8 @@ public class MersenneDivisorCyclesGenerateGpuTests
{
MersenneDivisorCycles.GenerateGpu(path, maxDivisor: 50UL, batchSize: 16, skipCount: 0, nextPosition: 0);
var pairs = new List<(ulong divisor, ulong cycle)>();
var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
var reader = new BinaryReader(stream);
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
using var reader = new BinaryReader(stream);
while (stream.Position < stream.Length)
{
ulong d = reader.ReadUInt64();
@@ -41,9 +41,6 @@ public class MersenneDivisorCyclesGenerateGpuTests
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(pair.divisor);
pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLength(pair.divisor, divisorData));
}
reader.Dispose();
stream.Dispose();
}
finally
{
@@ -64,8 +61,8 @@ public class MersenneDivisorCyclesGenerateGpuTests
MersenneDivisorCycles.Generate(path, maxDivisor: 50UL, threads: 4);
var pairs = new List<(ulong divisor, ulong cycle)>();
var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
var reader = new BinaryReader(stream);
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
using var reader = new BinaryReader(stream);
while (stream.Position < stream.Length)
{
ulong divisor = reader.ReadUInt64();
@@ -98,9 +95,6 @@ public class MersenneDivisorCyclesGenerateGpuTests
var (_, completeCount) = MersenneDivisorCycles.FindLast(path);
completeCount.Should().Be(expected.Count);
reader.Dispose();
stream.Dispose();
}
finally
{
@@ -26,8 +26,7 @@ public class MersenneNumberDivisorByDivisorTesterTests
markComposite: () => compositeMarks++,
clearComposite: () => clearedMarks++,
printResult: (prime, searched, detailed, passed) => results.Add((prime, searched, detailed, passed)),
threadCount: 1,
primeTestLimit: null);
threadCount: 1);
tester.ConfiguredMaxPrime.Should().Be(13UL);
compositeMarks.Should().Be(2);
@@ -60,7 +59,7 @@ public class MersenneNumberDivisorByDivisorTesterTests
return DivisorLimit;
}
public bool IsPrime(ulong prime, out bool divisorsExhausted, TimeSpan? timeLimit = null)
public bool IsPrime(ulong prime, out bool divisorsExhausted)
{
divisorsExhausted = true;
return prime % 7UL != 0UL && prime % 11UL != 0UL;
@@ -1,6 +1,5 @@
using System;
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Cpu;
using Xunit;
@@ -8,125 +7,78 @@ namespace PerfectNumbers.Core.Tests;
public class MersenneNumberDivisorCpuTesterTests
{
private const ulong LargeDivisor1 = 4_000_169UL;
private const ulong LargeDivisor2 = 4_000_561UL;
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_tester_tracks_divisors_across_primes()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(83UL);
tester.IsPrime(59UL, out bool divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
tester.IsPrime(61UL, out divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
tester.IsPrime(67UL, out divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_prime_test_limit_disables_divisor_exhaustion()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(83UL);
tester.IsPrime(59UL, out bool divisorsExhausted, TimeSpan.Zero).Should().BeTrue();
divisorsExhausted.Should().BeFalse();
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_checks_divisors_across_exponents()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(197UL);
var session = tester.CreateDivisorSession();
ulong[] exponents = { 59UL, 61UL, 67UL, 71UL, 73UL, 79UL, 117_652UL, 333_380UL };
byte[] hits = new byte[exponents.Length];
MontgomeryDivisorData divisor1 = MontgomeryDivisorData.FromModulus(LargeDivisor1);
ulong cycle1 = MersenneDivisorCycles.CalculateCycleLength(LargeDivisor1, divisor1);
session.CheckDivisor(LargeDivisor1, divisor1, cycle1, exponents, hits);
byte[] expected1 = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_tester_tracks_divisors_across_primes()
{
expected1[i] = exponents[i].Pow2ModWindowedCpu(divisor1.Modulus) == 1UL ? (byte)1 : (byte)0;
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(11UL);
tester.IsPrime(5UL, out bool divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
tester.IsPrime(7UL, out divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
tester.IsPrime(11UL, out divisorsExhausted).Should().BeFalse();
divisorsExhausted.Should().BeTrue();
}
hits.Should().Equal(expected1);
Array.Fill(hits, (byte)0);
MontgomeryDivisorData divisor2 = MontgomeryDivisorData.FromModulus(LargeDivisor2);
ulong cycle2 = MersenneDivisorCycles.CalculateCycleLength(LargeDivisor2, divisor2);
session.CheckDivisor(LargeDivisor2, divisor2, cycle2, exponents, hits);
byte[] expected2 = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_checks_divisors_across_primes()
{
expected2[i] = exponents[i].Pow2ModWindowedCpu(divisor2.Modulus) == 1UL ? (byte)1 : (byte)0;
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(13UL);
using var session = tester.CreateDivisorSession();
ulong[] primes = { 5UL, 7UL, 11UL, 13UL };
byte[] hits = new byte[primes.Length];
ulong cycle23 = MersenneDivisorCycles.CalculateCycleLength(23UL, MontgomeryDivisorData.FromModulus(23UL));
session.CheckDivisor(23UL, MontgomeryDivisorData.FromModulus(23UL), cycle23, primes, hits);
hits.Should().ContainInOrder(new byte[] { 0, 0, 1, 0 });
Array.Fill(hits, (byte)0);
ulong cycle31 = MersenneDivisorCycles.CalculateCycleLength(31UL, MontgomeryDivisorData.FromModulus(31UL));
session.CheckDivisor(31UL, MontgomeryDivisorData.FromModulus(31UL), cycle31, primes, hits);
hits.Should().ContainInOrder(new byte[] { 1, 0, 0, 0 });
}
hits.Should().Equal(expected2);
session.Dispose();
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_large_divisor1_as_composite()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(197UL);
var session = tester.CreateDivisorSession();
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(LargeDivisor1);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(LargeDivisor1, divisorData);
ulong[] exponents = { 59UL, 67UL, cycle, cycle * 2UL, cycle + 1UL };
byte[] hits = new byte[exponents.Length];
session.CheckDivisor(LargeDivisor1, divisorData, cycle, exponents, hits);
byte[] expected = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_seven_as_composite()
{
expected[i] = exponents[i].Pow2ModWindowedCpu(divisorData.Modulus) == 1UL ? (byte)1 : (byte)0;
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(43UL);
using var session = tester.CreateDivisorSession();
ulong[] exponents = { 6UL, 7UL, 9UL, 10UL, 12UL };
byte[] hits = new byte[exponents.Length];
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(7UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(7UL, divisorData);
session.CheckDivisor(7UL, divisorData, cycle, exponents, hits);
hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 });
}
hits.Should().Equal(expected);
session.Dispose();
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_large_divisor2_as_composite()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(197UL);
var session = tester.CreateDivisorSession();
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(LargeDivisor2);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(LargeDivisor2, divisorData);
ulong[] exponents = { 61UL, 73UL, cycle, cycle * 2UL, cycle - 1UL };
byte[] hits = new byte[exponents.Length];
session.CheckDivisor(LargeDivisor2, divisorData, cycle, exponents, hits);
byte[] expected = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_eleven_as_composite()
{
expected[i] = exponents[i].Pow2ModWindowedCpu(divisorData.Modulus) == 1UL ? (byte)1 : (byte)0;
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(61UL);
using var session = tester.CreateDivisorSession();
ulong[] exponents = { 10UL, 11UL, 20UL, 21UL, 30UL };
byte[] hits = new byte[exponents.Length];
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(11UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(11UL, divisorData);
session.CheckDivisor(11UL, divisorData, cycle, exponents, hits);
hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 });
}
hits.Should().Equal(expected);
session.Dispose();
}
}
@@ -12,12 +12,12 @@ namespace PerfectNumbers.Core.Tests;
public class MersenneNumberDivisorGpuTesterTests
{
[Theory]
[InlineData(117_652UL, 4_000_169UL, true)]
[InlineData(333_380UL, 4_000_561UL, true)]
[InlineData(59UL, 4_000_169UL, false)]
[InlineData(61UL, 4_000_561UL, false)]
[InlineData(333_379UL, 4_000_561UL, false)]
[InlineData(117_653UL, 4_000_169UL, false)]
[InlineData(33UL, 7UL, true)]
[InlineData(35UL, 31UL, true)]
[InlineData(37UL, 223UL, true)]
[InlineData(33UL, 13UL, false)]
[InlineData(35UL, 73UL, false)]
[InlineData(37UL, 227UL, false)]
[Trait("Category", "Fast")]
public void IsDivisible_returns_expected(ulong exponent, ulong divisor, bool expected)
{
@@ -86,17 +86,6 @@ public class MersenneNumberDivisorGpuTesterTests
exhausted.Should().BeTrue();
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_gpu_prime_test_limit_stops_scan_immediately()
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
tester.ConfigureFromMaxPrime(43UL);
tester.IsPrime(31UL, out bool divisorsExhausted, TimeSpan.Zero).Should().BeTrue();
divisorsExhausted.Should().BeFalse();
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_tester_requires_configuration()
@@ -152,41 +141,23 @@ public class MersenneNumberDivisorGpuTesterTests
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_checks_divisors_across_exponents()
public void ByDivisor_session_checks_divisors_across_primes()
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
tester.ConfigureFromMaxPrime(197UL);
tester.ConfigureFromMaxPrime(43UL);
var session = tester.CreateDivisorSession();
ulong[] exponents = { 59UL, 61UL, 67UL, 71UL, 73UL, 79UL, 117_652UL, 333_380UL };
byte[] hits = new byte[exponents.Length];
using var session = tester.CreateDivisorSession();
ulong[] primes = { 31UL, 37UL, 41UL, 43UL };
byte[] hits = new byte[primes.Length];
MontgomeryDivisorData divisor1 = MontgomeryDivisorData.FromModulus(4_000_169UL);
ulong cycle1 = MersenneDivisorCycles.CalculateCycleLength(4_000_169UL, divisor1);
session.CheckDivisor(4_000_169UL, divisor1, cycle1, exponents, hits);
byte[] expected1 = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{
expected1[i] = exponents[i].Pow2ModWindowedCpu(divisor1.Modulus) == 1UL ? (byte)1 : (byte)0;
}
hits.Should().Equal(expected1);
ulong cycle223 = MersenneDivisorCycles.CalculateCycleLength(223UL, MontgomeryDivisorData.FromModulus(223UL));
session.CheckDivisor(223UL, MontgomeryDivisorData.FromModulus(223UL), cycle223, primes, hits);
hits.Should().ContainInOrder(new byte[] { 0, 1, 0, 0 });
Array.Fill(hits, (byte)0);
MontgomeryDivisorData divisor2 = MontgomeryDivisorData.FromModulus(4_000_561UL);
ulong cycle2 = MersenneDivisorCycles.CalculateCycleLength(4_000_561UL, divisor2);
session.CheckDivisor(4_000_561UL, divisor2, cycle2, exponents, hits);
byte[] expected2 = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{
expected2[i] = exponents[i].Pow2ModWindowedCpu(divisor2.Modulus) == 1UL ? (byte)1 : (byte)0;
}
hits.Should().Equal(expected2);
session.Dispose();
ulong cycle13367 = MersenneDivisorCycles.CalculateCycleLength(13367UL, MontgomeryDivisorData.FromModulus(13367UL));
session.CheckDivisor(13367UL, MontgomeryDivisorData.FromModulus(13367UL), cycle13367, primes, hits);
hits.Should().ContainInOrder(new byte[] { 0, 0, 1, 0 });
}
[Fact]
@@ -198,134 +169,122 @@ public class MersenneNumberDivisorGpuTesterTests
GpuBatchSize = 2,
};
tester.ConfigureFromMaxPrime(197UL);
tester.ConfigureFromMaxPrime(47UL);
var session = tester.CreateDivisorSession();
ulong[] exponents = { 59UL, 61UL, 67UL, 71UL, 73UL, 79UL, 117_652UL };
byte[] hits = new byte[exponents.Length];
using var session = tester.CreateDivisorSession();
ulong[] primes = { 31UL, 37UL, 41UL, 43UL, 47UL };
byte[] hits = new byte[primes.Length];
MontgomeryDivisorData divisor = MontgomeryDivisorData.FromModulus(4_000_169UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(4_000_169UL, divisor);
session.CheckDivisor(4_000_169UL, divisor, cycle, exponents, hits);
ulong cycle223 = MersenneDivisorCycles.CalculateCycleLength(223UL, MontgomeryDivisorData.FromModulus(223UL));
session.CheckDivisor(223UL, MontgomeryDivisorData.FromModulus(223UL), cycle223, primes, hits);
byte[] expected = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{
expected[i] = exponents[i].Pow2ModWindowedCpu(divisor.Modulus) == 1UL ? (byte)1 : (byte)0;
}
hits.Should().Equal(expected);
session.Dispose();
hits.Should().ContainInOrder(new byte[] { 0, 1, 0, 0, 0 });
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_large_divisor1_as_composite()
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_seven_as_composite()
{
var tester = new MersenneNumberDivisorByDivisorGpuTester
{
GpuBatchSize = 5,
};
tester.ConfigureFromMaxPrime(197UL);
tester.ConfigureFromMaxPrime(43UL);
var session = tester.CreateDivisorSession();
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(4_000_169UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(4_000_169UL, divisorData);
ulong[] exponents = { 59UL, 67UL, cycle, cycle * 2UL, cycle + 1UL };
using var session = tester.CreateDivisorSession();
ulong[] exponents = { 6UL, 7UL, 9UL, 10UL, 12UL };
byte[] hits = new byte[exponents.Length];
session.CheckDivisor(4_000_169UL, divisorData, cycle, exponents, hits);
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(7UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(7UL, divisorData);
session.CheckDivisor(7UL, divisorData, cycle, exponents, hits);
var hostBufferField = typeof(MersenneNumberDivisorByDivisorGpuTester.DivisorScanSession)
.GetField("_hostBuffer", BindingFlags.NonPublic | BindingFlags.Instance)!;
ulong[] hostBuffer = (ulong[])hostBufferField.GetValue(session)!;
Span<ulong> residues = hostBuffer.AsSpan(0, exponents.Length);
byte[] expected = new byte[exponents.Length];
byte[] computedHits = new byte[exponents.Length];
for (int i = 0; i < residues.Length; i++)
{
expected[i] = residues[i] == 1UL ? (byte)1 : (byte)0;
computedHits[i] = residues[i] == divisorData.MontgomeryOne ? (byte)1 : (byte)0;
}
byte[] direct = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{
direct[i] = exponents[i].Pow2ModWindowedCpu(divisorData.Modulus) == 1UL ? (byte)1 : (byte)0;
}
expected.Should().Equal(direct);
hits.Should().Equal(direct);
session.Dispose();
computedHits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 });
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_large_divisor2_as_composite()
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_eleven_as_composite()
{
var tester = new MersenneNumberDivisorByDivisorGpuTester
{
GpuBatchSize = 5,
};
tester.ConfigureFromMaxPrime(197UL);
tester.ConfigureFromMaxPrime(61UL);
var session = tester.CreateDivisorSession();
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(4_000_561UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(4_000_561UL, divisorData);
ulong[] exponents = { 61UL, 73UL, cycle, cycle * 2UL, cycle - 1UL };
using var session = tester.CreateDivisorSession();
ulong[] exponents = { 10UL, 11UL, 20UL, 21UL, 30UL };
byte[] hits = new byte[exponents.Length];
session.CheckDivisor(4_000_561UL, divisorData, cycle, exponents, hits);
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(11UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(11UL, divisorData);
session.CheckDivisor(11UL, divisorData, cycle, exponents, hits);
var hostBufferField = typeof(MersenneNumberDivisorByDivisorGpuTester.DivisorScanSession)
.GetField("_hostBuffer", BindingFlags.NonPublic | BindingFlags.Instance)!;
ulong[] hostBuffer = (ulong[])hostBufferField.GetValue(session)!;
Span<ulong> residues = hostBuffer.AsSpan(0, exponents.Length);
byte[] expected = new byte[exponents.Length];
byte[] computedHits = new byte[exponents.Length];
for (int i = 0; i < residues.Length; i++)
{
expected[i] = residues[i] == 1UL ? (byte)1 : (byte)0;
computedHits[i] = residues[i] == divisorData.MontgomeryOne ? (byte)1 : (byte)0;
}
byte[] direct = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{
direct[i] = exponents[i].Pow2ModWindowedCpu(divisorData.Modulus) == 1UL ? (byte)1 : (byte)0;
}
expected.Should().Equal(direct);
hits.Should().Equal(direct);
session.Dispose();
computedHits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 });
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_gpu_tester_generates_cycles_for_large_divisors()
public void ByDivisor_gpu_tester_skips_divisors_excluded_by_small_cycle_generation()
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
tester.ConfigureFromMaxPrime(197UL);
var cycles = MersenneDivisorCycles.Shared;
var tableField = typeof(MersenneDivisorCycles).GetField("_table", BindingFlags.NonPublic | BindingFlags.Instance)!;
var smallCyclesField = typeof(MersenneDivisorCycles).GetField("_smallCycles", BindingFlags.NonPublic | BindingFlags.Instance)!;
var session = tester.CreateDivisorSession();
ulong[] exponents = { 59UL, 67UL, 117_652UL };
byte[] hits = new byte[exponents.Length];
var originalTable = (List<(ulong Divisor, ulong Cycle)>)tableField.GetValue(cycles)!;
var originalSmall = (ulong[]?)smallCyclesField.GetValue(cycles);
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(4_000_169UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(4_000_169UL, divisorData);
session.CheckDivisor(4_000_169UL, divisorData, cycle, exponents, hits);
byte[] expected = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
try
{
expected[i] = exponents[i].Pow2ModWindowedCpu(divisorData.Modulus) == 1UL ? (byte)1 : (byte)0;
var patchedTable = new List<(ulong Divisor, ulong Cycle)>();
ulong[] patchedSmall = new ulong[PerfectNumberConstants.MaxQForDivisorCycles + 1];
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(191UL);
patchedSmall[191] = MersenneDivisorCycles.CalculateCycleLength(191UL, divisorData);
tableField.SetValue(cycles, patchedTable);
smallCyclesField.SetValue(cycles, patchedSmall);
DivisorCycleCache.Shared.RefreshSnapshot();
var tester = new MersenneNumberDivisorByDivisorGpuTester();
tester.ConfigureFromMaxPrime(19UL);
typeof(MersenneNumberDivisorByDivisorGpuTester)
.GetField("_divisorLimit", BindingFlags.NonPublic | BindingFlags.Instance)!
.SetValue(tester, 200UL);
tester.IsPrime(19UL, out bool divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
}
finally
{
tableField.SetValue(cycles, originalTable);
smallCyclesField.SetValue(cycles, originalSmall);
DivisorCycleCache.Shared.RefreshSnapshot();
GpuContextPool.DisposeAll();
}
hits.Should().Equal(expected);
session.Dispose();
}
[Fact]
@@ -337,25 +296,16 @@ public class MersenneNumberDivisorGpuTesterTests
BatchSize = 2,
};
tester.ConfigureFromMaxPrime(197UL);
tester.ConfigureFromMaxPrime(47UL);
var session = tester.CreateDivisorSession();
ulong[] exponents = { 59UL, 61UL, 67UL, 71UL, 73UL, 79UL, 117_652UL };
byte[] hits = new byte[exponents.Length];
using var session = tester.CreateDivisorSession();
ulong[] primes = { 31UL, 37UL, 41UL, 43UL, 47UL };
byte[] hits = new byte[primes.Length];
MontgomeryDivisorData divisor = MontgomeryDivisorData.FromModulus(4_000_169UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(4_000_169UL, divisor);
session.CheckDivisor(4_000_169UL, divisor, cycle, exponents, hits);
ulong cycle223 = MersenneDivisorCycles.CalculateCycleLength(223UL, MontgomeryDivisorData.FromModulus(223UL));
session.CheckDivisor(223UL, MontgomeryDivisorData.FromModulus(223UL), cycle223, primes, hits);
byte[] expected = new byte[exponents.Length];
for (int i = 0; i < exponents.Length; i++)
{
expected[i] = exponents[i].Pow2ModWindowedCpu(divisor.Modulus) == 1UL ? (byte)1 : (byte)0;
}
hits.Should().Equal(expected);
session.Dispose();
hits.Should().ContainInOrder(new byte[] { 0, 1, 0, 0, 0 });
}
}
@@ -22,7 +22,4 @@
<ItemGroup>
<ProjectReference Include="../PerfectNumbers.Core/PerfectNumbers.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project>
@@ -7,9 +7,6 @@ namespace PerfectNumbers.Core.Tests;
public class PrimeOrderCalculatorTests
{
// EvenPerfectBitScanner never exercises the GPU heuristic device or the small-prime guards that the legacy tests cover,
// so those scenarios are commented out because they are unreachable in the production pipeline.
/*
public static IEnumerable<object[]> SmallPrimes()
{
yield return new object[] { 3UL };
@@ -48,14 +45,14 @@ public class PrimeOrderCalculatorTests
{
ulong expected = ComputeOrderByDoubling(prime);
ulong result = PrimeOrderCalculator.Calculate(
PrimeOrderCalculator.PrimeOrderResult result = PrimeOrderCalculator.Calculate(
prime,
previousOrder: null,
MontgomeryDivisorData.FromModulus(prime),
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault,
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu);
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault);
result.Should().Be(expected);
result.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found);
result.Order.Should().Be(expected);
}
[Theory]
@@ -65,21 +62,21 @@ public class PrimeOrderCalculatorTests
{
ulong previousOrder = ComputeOrderByDoubling(previousPrime);
ulong heuristic = PrimeOrderCalculator.Calculate(
PrimeOrderCalculator.PrimeOrderResult heuristic = PrimeOrderCalculator.Calculate(
prime,
previousOrder,
MontgomeryDivisorData.FromModulus(prime),
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault,
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu);
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault);
ulong strict = PrimeOrderCalculator.Calculate(
PrimeOrderCalculator.PrimeOrderResult strict = PrimeOrderCalculator.Calculate(
prime,
previousOrder: null,
MontgomeryDivisorData.FromModulus(prime),
PrimeOrderCalculator.PrimeOrderSearchConfig.StrictDefault,
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu);
PrimeOrderCalculator.PrimeOrderSearchConfig.StrictDefault);
heuristic.Should().Be(strict);
heuristic.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found);
strict.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found);
heuristic.Order.Should().Be(strict.Order);
}
[Theory]
@@ -88,14 +85,14 @@ public class PrimeOrderCalculatorTests
[InlineData(3UL, 2UL)]
public void Calculate_handles_trivial_primes(ulong prime, ulong expectedOrder)
{
ulong result = PrimeOrderCalculator.Calculate(
PrimeOrderCalculator.PrimeOrderResult result = PrimeOrderCalculator.Calculate(
prime,
previousOrder: null,
MontgomeryDivisorData.FromModulus(prime),
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault,
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu);
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault);
result.Should().Be(expectedOrder);
result.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found);
result.Order.Should().Be(expectedOrder);
}
[Fact]
@@ -108,14 +105,14 @@ public class PrimeOrderCalculatorTests
maxPowChecks: 8,
mode: PrimeOrderCalculator.PrimeOrderMode.Heuristic);
ulong result = PrimeOrderCalculator.Calculate(
PrimeOrderCalculator.PrimeOrderResult result = PrimeOrderCalculator.Calculate(
prime: 13UL,
previousOrder: null,
MontgomeryDivisorData.FromModulus(13UL),
config,
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu);
config);
result.Should().Be(ComputeOrderByDoubling(13UL));
result.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.HeuristicUnresolved);
result.Order.Should().Be(ComputeOrderByDoubling(13UL));
}
[Fact]
@@ -129,14 +126,14 @@ public class PrimeOrderCalculatorTests
mode: PrimeOrderCalculator.PrimeOrderMode.Heuristic);
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(239UL);
ulong heuristic = PrimeOrderCalculator.Calculate(
PrimeOrderCalculator.PrimeOrderResult heuristic = PrimeOrderCalculator.Calculate(
prime: 239UL,
previousOrder: null,
divisorData,
heuristicConfig,
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu);
heuristicConfig);
heuristic.Should().Be(ComputeOrderByDoubling(239UL));
heuristic.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.HeuristicUnresolved);
heuristic.Order.Should().Be(ComputeOrderByDoubling(239UL));
var strictConfig = new PrimeOrderCalculator.PrimeOrderSearchConfig(
smallFactorLimit: 2,
@@ -144,14 +141,14 @@ public class PrimeOrderCalculatorTests
maxPowChecks: 1,
mode: PrimeOrderCalculator.PrimeOrderMode.Strict);
ulong strict = PrimeOrderCalculator.Calculate(
PrimeOrderCalculator.PrimeOrderResult strict = PrimeOrderCalculator.Calculate(
prime: 239UL,
previousOrder: null,
divisorData,
strictConfig,
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu);
strictConfig);
strict.Should().Be(heuristic);
strict.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found);
strict.Order.Should().Be(heuristic.Order);
}
[Fact]
@@ -160,13 +157,13 @@ public class PrimeOrderCalculatorTests
{
UInt128 prime = UInt128.Parse("18446744073709641691");
UInt128 result = PrimeOrderCalculator.Calculate(
prime,
previousOrder: null,
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault,
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Gpu);
PrimeOrderCalculator.PrimeOrderResultWide result = PrimeOrderCalculator.Calculate(
prime,
previousOrder: null,
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault);
result.Should().Be((UInt128)1229782938247309446UL);
result.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found);
result.Order.Should().Be((UInt128)1229782938247309446UL);
}
[Fact]
@@ -200,5 +197,4 @@ public class PrimeOrderCalculatorTests
return order;
}
*/
}
@@ -1,7 +0,0 @@
namespace PerfectNumbers.Core;
public enum ByDivisorDeltasDevice
{
Cpu,
Gpu
}
@@ -1,7 +0,0 @@
namespace PerfectNumbers.Core;
public enum ByDivisorMontgomeryDevice
{
Cpu = 0,
Gpu = 1,
}
File diff suppressed because it is too large. Load diff
@@ -1,49 +0,0 @@
using System;
using System.Threading;
namespace PerfectNumbers.Core;
internal static class DeterministicRandom
{
[ThreadStatic]
private static ulong s_threadState;
private static long s_seedCounter;
public static ulong NextUInt64()
{
ulong state = s_threadState;
if (state == 0UL)
{
state = InitializeThreadState();
}
state ^= state >> 12;
state ^= state << 25;
state ^= state >> 27;
s_threadState = state;
return state * 2685821657736338717UL;
}
public static UInt128 NextUInt128()
{
UInt128 high = (UInt128)NextUInt64();
UInt128 low = (UInt128)NextUInt64();
return (high << 64) | low;
}
private static ulong InitializeThreadState()
{
ulong seed = (ulong)Interlocked.Increment(ref s_seedCounter);
seed += 0x9E3779B97F4A7C15UL;
seed = (seed ^ (seed >> 30)) * 0xBF58476D1CE4E5B9UL;
seed = (seed ^ (seed >> 27)) * 0x94D049BB133111EBUL;
seed ^= seed >> 31;
if (seed == 0UL)
{
seed = 0x9E3779B97F4A7C15UL;
}
return seed;
}
}
+18 -48
View File
@@ -63,9 +63,7 @@ public sealed class DivisorCycleCache
int[]? rentedMissing = null;
Span<int> missingBuffer = length <= StackBufferThreshold
? stackalloc int[length]
: length < PerfectNumberConstants.PooledArrayThreshold
? new Span<int>(new int[length])
: new Span<int>(rentedMissing = ThreadLocalArrayPool<int>.Shared.Rent(length), 0, length);
: new Span<int>(rentedMissing = ArrayPool<int>.Shared.Rent(length), 0, length);
int missingCount = 0;
@@ -86,10 +84,11 @@ public sealed class DivisorCycleCache
}
cycles[i] = cached;
continue;
}
missingBuffer[missingCount++] = i;
else
{
missingBuffer[missingCount++] = i;
}
}
if (missingCount != 0)
@@ -103,12 +102,11 @@ public sealed class DivisorCycleCache
{
ComputeCyclesCpu(divisors, cycles, missingIndices);
}
}
if (rentedMissing is not null)
{
ThreadLocalArrayPool<int>.Shared.Return(rentedMissing, clearArray: false);
ArrayPool<int>.Shared.Return(rentedMissing, clearArray: false);
}
}
@@ -130,16 +128,12 @@ public sealed class DivisorCycleCache
ulong[]? rentedDivisors = null;
Span<ulong> workDivisors = count <= StackBufferThreshold
? stackalloc ulong[count]
: count < PerfectNumberConstants.PooledArrayThreshold
? new Span<ulong>(new ulong[count])
: new Span<ulong>(rentedDivisors = ThreadLocalArrayPool<ulong>.Shared.Rent(count), 0, count);
: new Span<ulong>(rentedDivisors = ArrayPool<ulong>.Shared.Rent(count), 0, count);
ulong[]? rentedResults = null;
Span<ulong> workResults = count <= StackBufferThreshold
? stackalloc ulong[count]
: count < PerfectNumberConstants.PooledArrayThreshold
? new Span<ulong>(new ulong[count])
: new Span<ulong>(rentedResults = ThreadLocalArrayPool<ulong>.Shared.Rent(count), 0, count);
: new Span<ulong>(rentedResults = ArrayPool<ulong>.Shared.Rent(count), 0, count);
for (int i = 0; i < count; i++)
{
@@ -155,12 +149,8 @@ public sealed class DivisorCycleCache
if (rentedDivisors is not null)
{
ThreadLocalArrayPool<ulong>.Shared.Return(rentedDivisors, clearArray: false);
}
if (rentedResults is not null)
{
ThreadLocalArrayPool<ulong>.Shared.Return(rentedResults, clearArray: false);
ArrayPool<ulong>.Shared.Return(rentedDivisors, clearArray: false);
ArrayPool<ulong>.Shared.Return(rentedResults!, clearArray: false);
}
}
@@ -182,30 +172,22 @@ public sealed class DivisorCycleCache
ulong[]? rentedPow = null;
Span<ulong> powSpan = length <= StackBufferThreshold
? stackalloc ulong[length]
: length < PerfectNumberConstants.PooledArrayThreshold
? new Span<ulong>(new ulong[length])
: new Span<ulong>(rentedPow = ThreadLocalArrayPool<ulong>.Shared.Rent(length), 0, length);
: new Span<ulong>(rentedPow = ArrayPool<ulong>.Shared.Rent(length), 0, length);
ulong[]? rentedOrder = null;
Span<ulong> orderSpan = length <= StackBufferThreshold
? stackalloc ulong[length]
: length < PerfectNumberConstants.PooledArrayThreshold
? new Span<ulong>(new ulong[length])
: new Span<ulong>(rentedOrder = ThreadLocalArrayPool<ulong>.Shared.Rent(length), 0, length);
: new Span<ulong>(rentedOrder = ArrayPool<ulong>.Shared.Rent(length), 0, length);
ulong[]? rentedResult = null;
Span<ulong> resultSpan = length <= StackBufferThreshold
? stackalloc ulong[length]
: length < PerfectNumberConstants.PooledArrayThreshold
? new Span<ulong>(new ulong[length])
: new Span<ulong>(rentedResult = ThreadLocalArrayPool<ulong>.Shared.Rent(length), 0, length);
: new Span<ulong>(rentedResult = ArrayPool<ulong>.Shared.Rent(length), 0, length);
byte[]? rentedStatus = null;
Span<byte> statusSpan = length <= StackBufferThreshold
? stackalloc byte[length]
: length < PerfectNumberConstants.PooledArrayThreshold
? new Span<byte>(new byte[length])
: new Span<byte>(rentedStatus = ThreadLocalArrayPool<byte>.Shared.Rent(length), 0, length);
: new Span<byte>(rentedStatus = ArrayPool<byte>.Shared.Rent(length), 0, length);
ref ulong divisorRef = ref MemoryMarshal.GetReference(divisors);
divisorBuffer.View.CopyFromCPU(ref divisorRef, length);
@@ -252,22 +234,10 @@ public sealed class DivisorCycleCache
resultSpan.CopyTo(destination);
if (rentedPow is not null)
{
ThreadLocalArrayPool<ulong>.Shared.Return(rentedPow, clearArray: false);
}
if (rentedOrder is not null)
{
ThreadLocalArrayPool<ulong>.Shared.Return(rentedOrder, clearArray: false);
}
if (rentedResult is not null)
{
ThreadLocalArrayPool<ulong>.Shared.Return(rentedResult, clearArray: false);
}
if (rentedStatus is not null)
{
ThreadLocalArrayPool<byte>.Shared.Return(rentedStatus, clearArray: false);
ArrayPool<ulong>.Shared.Return(rentedPow, clearArray: false);
ArrayPool<ulong>.Shared.Return(rentedOrder!, clearArray: false);
ArrayPool<ulong>.Shared.Return(rentedResult!, clearArray: false);
ArrayPool<byte>.Shared.Return(rentedStatus!, clearArray: false);
}
execution.Dispose();
@@ -47,7 +47,7 @@ internal struct ExponentRemainderStepper
// TODO: Route these state resets through the ProcessEightBitWindows helper once the scalar
// Pow2MontgomeryMod implementation adopts it so delta stepping inherits the benchmarked
// 2× gains for large exponents.
_currentMontgomery = exponent.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true);
_currentMontgomery = exponent.Pow2MontgomeryModWindowed(_divisor, keepMontgomery: true);
_previousExponent = exponent;
_hasState = true;
return ReduceCurrent();
@@ -59,7 +59,7 @@ internal struct ExponentRemainderStepper
// cached Montgomery residue ladder highlighted in MersenneDivisorCycleLengthGpuBenchmarks.
// TODO: Replace this per-delta powmod with the upcoming windowed ladder so incremental
// updates stop paying the single-bit cost highlighted in GpuPow2ModBenchmarks.
ulong multiplier = delta.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true);
ulong multiplier = delta.Pow2MontgomeryModWindowed(_divisor, keepMontgomery: true);
_currentMontgomery = _currentMontgomery.MontgomeryMultiply(multiplier, _modulus, _nPrime);
_previousExponent = exponent;
return ReduceCurrent();
@@ -77,7 +77,7 @@ internal struct ExponentRemainderStepper
{
// TODO: Switch this reload to the shared windowed pow2 helper once available so CPU
// residue checks align with the optimized ProcessEightBitWindows timings.
_currentMontgomery = exponent.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true);
_currentMontgomery = exponent.Pow2MontgomeryModWindowed(_divisor, keepMontgomery: true);
_previousExponent = exponent;
_hasState = true;
return _currentMontgomery == _montgomeryOne;
@@ -88,7 +88,7 @@ internal struct ExponentRemainderStepper
// lookups so this branch also avoids recomputing powmods when the snapshot lacks the divisor.
// TODO: Use the windowed delta pow2 helper here as well to avoid the single-bit ladder that
// currently lags behind the benchmarked implementation for huge divisor cycles.
ulong multiplier = delta.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true);
ulong multiplier = delta.Pow2MontgomeryModWindowed(_divisor, keepMontgomery: true);
_currentMontgomery = _currentMontgomery.MontgomeryMultiply(multiplier, _modulus, _nPrime);
_previousExponent = exponent;
return _currentMontgomery == _montgomeryOne;
@@ -123,7 +123,7 @@ internal struct ExponentRemainderStepper
{
// TODO: Replace this fallback path with the upcoming ProcessEightBitWindows helper so
// fresh Montgomery states also benefit from the faster pow2 ladder measured on CPUs.
_currentMontgomery = exponent.Pow2MontgomeryModWindowedCpu(_divisor, keepMontgomery: true);
_currentMontgomery = exponent.Pow2MontgomeryModWindowed(_divisor, keepMontgomery: true);
_previousExponent = exponent;
_hasState = true;
isUnity = _currentMontgomery == _montgomeryOne;
-19
View File
@@ -1,19 +0,0 @@
namespace PerfectNumbers.Core
{
public readonly struct FactorEntry
{
public readonly ulong Value;
public readonly int Exponent = 1;
public FactorEntry(ulong value, int exponent)
{
Value = value;
Exponent = exponent;
}
public FactorEntry(ulong value)
{
Value = value;
}
}
}
+132 -135
View File
@@ -6,159 +6,156 @@ namespace PerfectNumbers.Core.Gpu;
public static class GpuContextPool
{
private static readonly bool PoolingEnabled = true;
// TODO: Introduce accelerator-specific warmup so pooled contexts precompile the ProcessEightBitWindows kernels and load
// divisor-cycle data before the first scan begins.
// Default device preference for generic GPU kernels (prime scans, NTT, etc.)
public static bool ForceCpu { get; set; } = false;
private static readonly bool PoolingEnabled = true;
// TODO: Introduce accelerator-specific warmup so pooled contexts precompile the ProcessEightBitWindows kernels and load
// divisor-cycle data before the first scan begins.
// Default device preference for generic GPU kernels (prime scans, NTT, etc.)
public static bool ForceCpu { get; set; } = false;
internal sealed class PooledContext
{
private bool disposed;
public Context Context { get; }
public Accelerator Accelerator { get; }
public bool IsCpu { get; }
public object ExecutionLock { get; } = new();
public PooledContext(bool preferCpu)
internal sealed class PooledContext
{
Context = Context.CreateDefault();
Accelerator = Context.GetPreferredDevice(preferCpu).CreateAccelerator(Context);
IsCpu = Accelerator.AcceleratorType == AcceleratorType.CPU;
// NOTE: Avoid loading/compiling any kernel here to prevent implicit
// CL stream/queue creation during accelerator construction.
// Some OpenCL drivers are fragile when a queue is created immediately
// after device init. Real kernels will be JIT-loaded at first use.
private bool disposed;
public Context Context { get; }
public Accelerator Accelerator { get; }
public bool IsCpu { get; }
public object ExecutionLock { get; } = new();
public PooledContext(bool preferCpu)
{
Context = Context.CreateDefault();
Accelerator = Context.GetPreferredDevice(preferCpu).CreateAccelerator(Context);
IsCpu = Accelerator.AcceleratorType == AcceleratorType.CPU;
// NOTE: Avoid loading/compiling any kernel here to prevent implicit
// CL stream/queue creation during accelerator construction.
// Some OpenCL drivers are fragile when a queue is created immediately
// after device init. Real kernels will be JIT-loaded at first use.
}
public void Dispose()
{
if (disposed)
{
return;
}
// Ensure all cached GPU buffers for this accelerator are released.
NttGpuMath.ClearCaches(Accelerator);
// Release PrimeTester GPU state for this accelerator (kernel/device primes)
// Clear any per-accelerator cached resources to avoid releasing
// after the accelerator is destroyed.
PrimeTester.ClearGpuCaches(Accelerator);
Accelerator.Dispose();
Context.Dispose();
disposed = true;
}
}
private static readonly ConcurrentQueue<PooledContext> CpuPool = new();
private static readonly ConcurrentQueue<PooledContext> GpuPool = new();
private static readonly object CreationLock = new();
public static GpuContextLease Rent()
{
return RentPreferred(ForceCpu);
}
// Allows callers to choose CPU/GPU per use-case, decoupled from ForceCpu.
public static GpuContextLease RentPreferred(bool preferCpu)
{
if (PoolingEnabled)
{
if (preferCpu && CpuPool.TryDequeue(out var cpu))
{
return new GpuContextLease(cpu, CreationLock);
}
if (!preferCpu && GpuPool.TryDequeue(out var gpu))
{
return new GpuContextLease(gpu, CreationLock);
}
}
// Serialize accelerator creation to avoid concurrent CL device/stream setup across threads.
lock (CreationLock)
{
return new GpuContextLease(new PooledContext(preferCpu), CreationLock);
}
}
public static void DisposeAll()
{
if (!PoolingEnabled)
{
return;
}
while (CpuPool.TryDequeue(out var cpu))
{
cpu.Dispose();
}
while (GpuPool.TryDequeue(out var gpu))
{
gpu.Dispose();
}
}
private static void Return(PooledContext ctx)
{
if (PoolingEnabled)
{
ctx.Accelerator.Synchronize();
if (ctx.IsCpu)
{
CpuPool.Enqueue(ctx);
}
else
{
GpuPool.Enqueue(ctx);
}
}
else
{
lock (CreationLock)
{
ctx.Dispose();
}
}
}
public void Dispose()
{
if (disposed)
{
return;
}
// Ensure all cached GPU buffers for this accelerator are released.
NttGpuMath.ClearCaches(Accelerator);
// Release PrimeTester GPU state for this accelerator (kernel/device primes)
// Clear any per-accelerator cached resources to avoid releasing
// after the accelerator is destroyed.
PrimeTester.ClearGpuCaches(Accelerator);
Accelerator.Dispose();
Context.Dispose();
disposed = true;
}
}
private static readonly ConcurrentQueue<PooledContext> CpuPool = new();
private static readonly ConcurrentQueue<PooledContext> GpuPool = new();
private static readonly object CreationLock = new();
public static GpuContextLease Rent()
{
return RentPreferred(ForceCpu);
}
// Allows callers to choose CPU/GPU per use-case, decoupled from ForceCpu.
public static GpuContextLease RentPreferred(bool preferCpu)
{
if (PoolingEnabled)
{
if (preferCpu && CpuPool.TryDequeue(out var cpu))
{
return new GpuContextLease(cpu, CreationLock);
}
if (!preferCpu && GpuPool.TryDequeue(out var gpu))
{
return new GpuContextLease(gpu, CreationLock);
}
}
// Serialize accelerator creation to avoid concurrent CL device/stream setup across threads.
lock (CreationLock)
{
return new GpuContextLease(new PooledContext(preferCpu), CreationLock);
}
}
public static void DisposeAll()
{
if (!PoolingEnabled)
{
return;
}
while (CpuPool.TryDequeue(out var cpu))
{
cpu.Dispose();
}
while (GpuPool.TryDequeue(out var gpu))
{
gpu.Dispose();
}
}
private static void Return(PooledContext ctx)
{
if (PoolingEnabled)
{
ctx.Accelerator.Synchronize();
if (ctx.IsCpu)
{
CpuPool.Enqueue(ctx);
}
else
{
GpuPool.Enqueue(ctx);
}
}
else
{
lock (CreationLock)
{
ctx.Dispose();
}
}
}
public struct GpuContextLease
public struct GpuContextLease : IDisposable
{
private readonly PooledContext _ctx;
private bool disposed;
private bool disposed;
internal GpuContextLease(PooledContext ctx, object kernelInitLock)
internal GpuContextLease(PooledContext ctx, object kernelInitLock)
{
_ctx = ctx;
KernelInitLock = kernelInitLock;
}
KernelInitLock = kernelInitLock;
}
public Context Context => _ctx.Context;
public Accelerator Accelerator => _ctx.Accelerator;
public object KernelInitLock { get; }
public object KernelInitLock { get; }
public object ExecutionLock => _ctx.ExecutionLock;
public object ExecutionLock => _ctx.ExecutionLock;
internal void ReturnToPool()
{
if (disposed)
{
return;
}
Return(_ctx);
disposed = true;
}
public void Dispose()
{
if (disposed)
{
return;
}
public void Dispose()
{
ReturnToPool();
Return(_ctx);
disposed = true;
}
}
}
+33 -90
View File
@@ -37,7 +37,6 @@ public sealed class KernelContainer
ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>? IncrementalOrder;
public Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong,
ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>? Pow2ModOrder;
public Action<AcceleratorStream, Index1D, ArrayView1D<MontgomeryDivisorData, Stride1D.Dense>, ArrayView1D<int, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, int>? MontgomeryOddPowers;
// Optional device buffer with small divisor cycles (<= 4M). Index = divisor, value = cycle length.
public MemoryBuffer1D<ulong, Stride1D.Dense>? SmallCycles;
@@ -76,10 +75,10 @@ public readonly struct ResiduePrimeViews(
public readonly ArrayView1D<ulong, Stride1D.Dense> LastSevenPow2 = lastSevenPow2;
}
public ref struct GpuKernelLease(GpuPrimeWorkLimiter.Lease limiter, GpuContextLease gpu, KernelContainer kernels)
public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, KernelContainer kernels)
{
private bool disposedValue;
private GpuPrimeWorkLimiter.Lease? _limiter = limiter;
private IDisposable? _limiter = limiter;
private GpuContextLease? _gpu = gpu;
private KernelContainer _kernels = kernels;
private AcceleratorStream? _stream;
@@ -110,15 +109,15 @@ public ref struct GpuKernelLease(GpuPrimeWorkLimiter.Lease limiter, GpuContextLe
get
{
var accel = Accelerator; // avoid capturing 'this' in lambda
// lock (_gpu!.Value.KernelInitLock)
// {
lock (_gpu!.Value.KernelInitLock)
{
return KernelContainer.InitOnce(ref _kernels.Order, () =>
{
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, ulong, ArrayView<GpuUInt128>, ArrayView<ulong>>(OrderKernelScan);
var kernel = KernelUtil.GetKernel(loaded);
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, ulong, ArrayView<GpuUInt128>, ArrayView<ulong>>>();
});
// }
}
}
}
@@ -127,15 +126,15 @@ public ref struct GpuKernelLease(GpuPrimeWorkLimiter.Lease limiter, GpuContextLe
get
{
var accel = Accelerator;
// lock (_gpu!.Value.KernelInitLock)
// {
lock (_gpu!.Value.KernelInitLock)
{
return KernelContainer.InitOnce(ref _kernels.Pow2Mod, () =>
{
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>(Pow2ModKernelScan);
var kernel = KernelUtil.GetKernel(loaded);
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>>();
});
// }
}
}
}
@@ -144,15 +143,15 @@ public ref struct GpuKernelLease(GpuPrimeWorkLimiter.Lease limiter, GpuContextLe
get
{
var accel = Accelerator;
// lock (_gpu!.Value.KernelInitLock)
// {
lock (_gpu!.Value.KernelInitLock)
{
return KernelContainer.InitOnce(ref _kernels.Incremental, () =>
{
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ulong, ulong, ulong, ulong, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>>(IncrementalKernelScan);
var kernel = KernelUtil.GetKernel(loaded);
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ulong, ulong, ulong, ulong, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>>>();
});
// }
}
}
}
@@ -161,15 +160,15 @@ public ref struct GpuKernelLease(GpuPrimeWorkLimiter.Lease limiter, GpuContextLe
get
{
var accel = Accelerator;
// lock (_gpu!.Value.KernelInitLock)
// {
lock (_gpu!.Value.KernelInitLock)
{
return KernelContainer.InitOnce(ref _kernels.IncrementalOrder, () =>
{
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>(IncrementalOrderKernelScan);
var kernel = KernelUtil.GetKernel(loaded);
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>>();
});
// }
}
}
}
@@ -178,66 +177,15 @@ public ref struct GpuKernelLease(GpuPrimeWorkLimiter.Lease limiter, GpuContextLe
get
{
var accel = Accelerator;
// lock (_gpu!.Value.KernelInitLock)
// {
lock (_gpu!.Value.KernelInitLock)
{
return KernelContainer.InitOnce(ref _kernels.Pow2ModOrder, () =>
{
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>(Pow2ModOrderKernelScan);
var kernel = KernelUtil.GetKernel(loaded);
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>>();
});
// }
}
}
public readonly Action<AcceleratorStream, Index1D, ArrayView1D<MontgomeryDivisorData, Stride1D.Dense>, ArrayView1D<int, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, int> MontgomeryOddPowerKernel
{
get
{
var accel = Accelerator;
// lock (_gpu!.Value.KernelInitLock)
// {
return KernelContainer.InitOnce(ref _kernels.MontgomeryOddPowers, () =>
{
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<MontgomeryDivisorData, Stride1D.Dense>, ArrayView1D<int, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, int>(MontgomeryOddPowerKernelScan);
var kernel = KernelUtil.GetKernel(loaded);
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ArrayView1D<MontgomeryDivisorData, Stride1D.Dense>, ArrayView1D<int, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, int>>();
});
// }
}
}
private static void MontgomeryOddPowerKernelScan(
Index1D index,
ArrayView1D<MontgomeryDivisorData, Stride1D.Dense> divisors,
ArrayView1D<int, Stride1D.Dense> counts,
ArrayView1D<ulong, Stride1D.Dense> results,
int stride)
{
int tableIndex = index.X;
int baseOffset = tableIndex * stride;
MontgomeryDivisorData divisor = divisors[tableIndex];
int oddPowerCount = counts[tableIndex];
if (oddPowerCount <= 0)
{
return;
}
ulong montgomeryTwo = divisor.MontgomeryTwo;
results[baseOffset] = montgomeryTwo;
if (oddPowerCount == 1)
{
return;
}
ulong modulus = divisor.Modulus;
ulong nPrime = divisor.NPrime;
ulong square = divisor.MontgomeryTwoSquared;
ulong current = montgomeryTwo;
for (int i = 1; i < oddPowerCount; i++)
{
current = ULongExtensions.MontgomeryMultiply(current, square, modulus, nPrime);
results[baseOffset + i] = current;
}
}
}
@@ -396,10 +344,7 @@ public ref struct GpuKernelLease(GpuPrimeWorkLimiter.Lease limiter, GpuContextLe
_stream?.Dispose();
_stream = null;
if (_gpu.HasValue)
{
_gpu.Value.ReturnToPool();
}
_gpu?.Dispose();
_gpu = null;
_executionLock = null;
@@ -732,25 +677,25 @@ public ref struct GpuKernelLease(GpuPrimeWorkLimiter.Lease limiter, GpuContextLe
// GC.SuppressFinalize(this);
}
public readonly struct ExecutionScope
public readonly struct ExecutionScope : IDisposable
{
private readonly object? _lock;
public ExecutionScope(object? sync)
{
_lock = sync;
// if (_lock is not null)
// {
// Monitor.Enter(_lock);
// }
if (_lock is not null)
{
Monitor.Enter(_lock);
}
}
public void Dispose()
{
// if (_lock is not null)
// {
// Monitor.Exit(_lock);
// }
if (_lock is not null)
{
Monitor.Exit(_lock);
}
}
}
}
@@ -759,8 +704,6 @@ public class GpuKernelPool
{
private static readonly ConcurrentDictionary<Accelerator, KernelContainer> KernelCache = new(); // TODO: Replace this concurrent map with a simple accelerator-indexed lookup once kernel launchers are prewarmed during startup so we can drop the thread-safe wrapper entirely.
public static bool CpuCycleUsesGpu { get; set; }
private static KernelContainer GetKernels(Accelerator accelerator)
{
return KernelCache.GetOrAdd(accelerator, _ => new KernelContainer());
@@ -777,8 +720,8 @@ public class GpuKernelPool
}
// Ensure single upload per accelerator even if multiple threads race here.
// lock (kernels) // TODO: Remove this lock by pre-uploading the immutable small-cycle snapshot during initialization; once no mutation happens at runtime, the pool must expose a simple reference without synchronization.
// {
lock (kernels) // TODO: Remove this lock by pre-uploading the immutable small-cycle snapshot during initialization; once no mutation happens at runtime, the pool must expose a simple reference without synchronization.
{
if (kernels.SmallCycles is { } existing)
{
return existing.View;
@@ -789,7 +732,7 @@ public class GpuKernelPool
device.View.CopyFromCPU(host);
kernels.SmallCycles = device;
return device.View;
// }
}
}
public static ResiduePrimeViews EnsureSmallPrimesOnDevice(Accelerator accelerator)
@@ -803,8 +746,8 @@ public class GpuKernelPool
return new ResiduePrimeViews(lastOne.View, lastSeven.View, lastOnePow2.View, lastSevenPow2.View);
}
// lock (kernels) // TODO: Inline these small-prime uploads into startup initialization alongside the small-cycle snapshot so we can drop runtime locking and keep the GPU pool free of synchronization.
// {
lock (kernels) // TODO: Inline these small-prime uploads into startup initialization alongside the small-cycle snapshot so we can drop runtime locking and keep the GPU pool free of synchronization.
{
if (kernels.SmallPrimesLastOne is { } existingLastOne &&
kernels.SmallPrimesLastSeven is { } existingLastSeven &&
kernels.SmallPrimesPow2LastOne is { } existingLastOnePow2 &&
@@ -833,7 +776,7 @@ public class GpuKernelPool
kernels.SmallPrimesPow2LastSeven = deviceLastSevenPow2;
return new ResiduePrimeViews(deviceLastOne.View, deviceLastSeven.View, deviceLastOnePow2.View, deviceLastSevenPow2.View);
// }
}
}
public static GpuKernelLease GetKernel(bool useGpuOrder)
@@ -6,13 +6,13 @@ public static class GpuPrimeWorkLimiter
private static readonly object _lock = new();
private static int _currentLimit = 1;
public static Lease Acquire()
public static IDisposable Acquire()
{
// TODO: Inline the pooled limiter guard from the GpuLimiterThroughputBenchmarks so prime-sieve
// GPU jobs stop allocating Releaser instances on every acquisition.
var sem = _semaphore;
sem.Wait();
return new Lease(sem);
return new Releaser(sem);
}
public static void SetLimit(int value)
@@ -36,12 +36,12 @@ public static class GpuPrimeWorkLimiter
}
}
public sealed class Lease
private sealed class Releaser : IDisposable
{
private readonly SemaphoreSlim _sem;
private bool _disposed;
public Lease(SemaphoreSlim sem)
public Releaser(SemaphoreSlim sem)
{
_sem = sem;
}
+12 -12
View File
@@ -267,7 +267,7 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
public void Add(in ReadOnlyGpuUInt128 other) => AddInternal(other.High, other.Low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(GpuUInt128 other) => AddInternal(other.High, other.Low);
public void Add(in GpuUInt128 other) => AddInternal(other.High, other.Low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AddInternal(ulong otherHigh, ulong otherLow)
@@ -294,17 +294,17 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
public void AddMod(in ReadOnlyGpuUInt128 value, in ReadOnlyGpuUInt128 modulus) => AddModInternal(value.High, value.Low, in modulus);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddMod(GpuUInt128 value, in ReadOnlyGpuUInt128 modulus) => AddModInternal(value.High, value.Low, in modulus);
public void AddMod(in GpuUInt128 value, in ReadOnlyGpuUInt128 modulus) => AddModInternal(value.High, value.Low, in modulus);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddMod(in ReadOnlyGpuUInt128 value, GpuUInt128 modulus)
public void AddMod(in ReadOnlyGpuUInt128 value, in GpuUInt128 modulus)
{
ReadOnlyGpuUInt128 readOnlyMod = modulus.AsReadOnly();
AddModInternal(value.High, value.Low, in readOnlyMod);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddMod(GpuUInt128 value, GpuUInt128 modulus)
public void AddMod(in GpuUInt128 value, in GpuUInt128 modulus)
{
ReadOnlyGpuUInt128 readOnlyMod = modulus.AsReadOnly();
AddModInternal(value.High, value.Low, in readOnlyMod);
@@ -390,7 +390,7 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddMod(GpuUInt128 value, ulong modulus) => AddMod(value.Low, modulus);
public void AddMod(in GpuUInt128 value, ulong modulus) => AddMod(value.Low, modulus);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddMod(ulong value, ulong modulus)
@@ -414,7 +414,7 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
public void Sub(in ReadOnlyGpuUInt128 other) => SubInternal(other.High, other.Low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Sub(GpuUInt128 other) => SubInternal(other.High, other.Low);
public void Sub(in GpuUInt128 other) => SubInternal(other.High, other.Low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SubInternal(ulong otherHigh, ulong otherLow)
@@ -461,7 +461,7 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
public void Mul64(in ReadOnlyGpuUInt128 other) => Mul64Internal(other.High, other.Low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Mul64(GpuUInt128 other) => Mul64Internal(other.High, other.Low);
public void Mul64(in GpuUInt128 other) => Mul64Internal(other.High, other.Low);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Mul64Internal(ulong otherHigh, ulong otherLow)
@@ -588,7 +588,7 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int TrailingZeroCount(GpuUInt128 value)
public static int TrailingZeroCount(in GpuUInt128 value)
{
if (value.Low != 0UL)
{
@@ -646,17 +646,17 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
public void SubMod(in ReadOnlyGpuUInt128 value, in ReadOnlyGpuUInt128 modulus) => SubModInternal(value.High, value.Low, in modulus);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SubMod(GpuUInt128 value, in ReadOnlyGpuUInt128 modulus) => SubModInternal(value.High, value.Low, in modulus);
public void SubMod(in GpuUInt128 value, in ReadOnlyGpuUInt128 modulus) => SubModInternal(value.High, value.Low, in modulus);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SubMod(in ReadOnlyGpuUInt128 value, GpuUInt128 modulus)
public void SubMod(in ReadOnlyGpuUInt128 value, in GpuUInt128 modulus)
{
ReadOnlyGpuUInt128 readOnlyMod = modulus.AsReadOnly();
SubModInternal(value.High, value.Low, in readOnlyMod);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SubMod(GpuUInt128 value, GpuUInt128 modulus)
public void SubMod(in GpuUInt128 value, in GpuUInt128 modulus)
{
ReadOnlyGpuUInt128 readOnlyMod = modulus.AsReadOnly();
SubModInternal(value.High, value.Low, in readOnlyMod);
@@ -727,7 +727,7 @@ public struct GpuUInt128 : IComparable<GpuUInt128>, IEquatable<GpuUInt128>
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Mul(GpuUInt128 other)
public void Mul(in GpuUInt128 other)
{
ReadOnlyGpuUInt128 readOnlyThis = AsReadOnly();
ReadOnlyGpuUInt128 readOnlyOther = other.AsReadOnly();
+4 -4
View File
@@ -6,14 +6,14 @@ public static class GpuWorkLimiter
private static readonly object _lock = new();
private static int _currentLimit = 1;
public static Lease Acquire()
public static IDisposable Acquire()
{
// TODO: Replace the per-call Releaser allocation with the pooled struct-based guard from the
// GpuLimiterThroughputBenchmarks so limiter acquisition does not allocate when we enter the
// GPU scanning hot path.
var sem = _semaphore;
sem.Wait();
return new Lease(sem);
return new Releaser(sem);
}
public static void SetLimit(int value)
@@ -40,12 +40,12 @@ public static class GpuWorkLimiter
}
}
public sealed class Lease
private sealed class Releaser : IDisposable
{
private readonly SemaphoreSlim _sem;
private bool _disposed;
public Lease(SemaphoreSlim sem)
public Releaser(SemaphoreSlim sem)
{
_sem = sem;
}
@@ -1,13 +1,13 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Numerics;
using ILGPU;
using ILGPU.Runtime;
using MontgomeryDivisorData = PerfectNumbers.Core.MontgomeryDivisorData;
using MontgomeryDivisorDataCache = PerfectNumbers.Core.MontgomeryDivisorDataCache;
using CycleRemainderStepper = PerfectNumbers.Core.CycleRemainderStepper;
namespace PerfectNumbers.Core.Gpu;
@@ -20,6 +20,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
private const int RemainderSlotCount = 4;
private int _gpuBatchSize = GpuConstants.ScanBatchSize;
private readonly object _sync = new();
private ulong _divisorLimit;
private bool _isConfigured;
@@ -35,7 +36,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
_kernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>>(CheckKernel));
private Action<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>> GetKernelByPrimeExponent(Accelerator accelerator) =>
_kernelExponentCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>>(ComputeExponentKernel));
_kernelExponentCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>>(ComputeMontgomeryExponentKernel));
private Action<Index1D, ArrayView<ulong>, ArrayView<byte>, byte> GetRemainderDeltaKernel(Accelerator accelerator) =>
_remainderDeltaKernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<byte>, byte>(ComputeRemainderDeltasKernel));
@@ -64,30 +65,34 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
public void ConfigureFromMaxPrime(ulong maxPrime)
{
// Configuration happens once during startup, so no synchronization is required to cache the divisor limit.
_divisorLimit = ComputeDivisorLimitFromMaxPrime(maxPrime);
_isConfigured = true;
lock (_sync)
{
_divisorLimit = ComputeDivisorLimitFromMaxPrime(maxPrime);
_isConfigured = true;
}
}
public bool IsPrime(ulong prime, out bool divisorsExhausted, TimeSpan? timeLimit = null)
public bool IsPrime(ulong prime, out bool divisorsExhausted)
{
// The CLI always configures the tester during startup, so the historical guard is commented out to avoid branching.
// if (!_isConfigured)
// {
// throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
// }
ulong allowedMax;
int batchCapacity;
// Configuration is immutable, so the cached values can be read without locking.
ulong allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit);
int batchCapacity = _gpuBatchSize;
lock (_sync)
{
if (!_isConfigured)
{
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
// The GPU path always receives an allowed maximum well above the minimal divisor, so the legacy
// guard stays commented out to document the invariant without branching.
// if (allowedMax < 3UL)
// {
// divisorsExhausted = true;
// return true;
// }
allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit);
batchCapacity = _gpuBatchSize;
}
if (allowedMax < 3UL)
{
divisorsExhausted = true;
return true;
}
bool composite;
bool coveredRange;
@@ -104,7 +109,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
composite = CheckDivisors(
prime,
allowedMax,
timeLimit,
accelerator,
kernel,
resources.DivisorsBuffer,
@@ -123,7 +127,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
resources.CycleCandidates,
resources.CycleLengths,
resources.CycleCapacity,
resources,
out lastProcessed,
out coveredRange,
out processedCount
@@ -152,14 +155,18 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
throw new ArgumentException("allowedMaxValues span must be at least as long as primes span.", nameof(allowedMaxValues));
}
// The GPU pipeline calls ConfigureFromMaxPrime before preparing candidates, so the legacy guard
// remains commented out to document the invariant without branching.
// if (!_isConfigured)
// {
// throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
// }
ulong divisorLimit;
lock (_sync)
{
if (!_isConfigured)
{
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
divisorLimit = _divisorLimit;
}
ulong divisorLimit = _divisorLimit;
for (int index = 0; index < primes.Length; index++)
{
allowedMaxValues[index] = ComputeAllowedMaxDivisor(primes[index], divisorLimit);
@@ -169,7 +176,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
private static bool CheckDivisors(
ulong prime,
ulong allowedMax,
TimeSpan? timeLimit,
Accelerator accelerator,
Action<Index1D, ArrayView<MontgomeryDivisorData>, ArrayView<ulong>, ArrayView<byte>> kernel,
MemoryBuffer1D<MontgomeryDivisorData, Stride1D.Dense> divisorsBuffer,
@@ -188,7 +194,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ulong[] cycleCandidates,
ulong[] cycleLengths,
int cycleCapacity,
BatchResources resources,
out ulong lastProcessed,
out bool coveredRange,
out ulong processedCount)
@@ -198,15 +203,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
bool processedAll = false;
processedCount = 0UL;
lastProcessed = 0UL;
coveredRange = false;
PrimeTestTimeLimit limitGuard;
if (!PrimeTestTimeLimit.TryCreate(timeLimit, out limitGuard))
{
return false;
}
bool enforceLimit = limitGuard.IsActive;
Span<MontgomeryDivisorData> divisorDataSpan;
Span<ulong> divisorSpan;
@@ -216,8 +212,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ArrayView1D<ulong, Stride1D.Dense> exponentView;
ArrayView1D<byte, Stride1D.Dense> hitsView;
// Batch resources expose a positive cycle capacity, so skip the redundant clamp here.
// cycleCapacity = Math.Max(1, cycleCapacity);
cycleCapacity = Math.Max(1, cycleCapacity);
int chunkCapacity = Math.Min(batchCapacity, cycleCapacity);
int maxThreadsPerGroup = (int)accelerator.MaxNumThreadsPerGroup;
@@ -226,22 +221,20 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
chunkCapacity = Math.Min(chunkCapacity, maxThreadsPerGroup);
}
// cycleCapacity already bounds chunkCapacity above zero, so the fallback would never execute.
// if (chunkCapacity <= 0)
// {
// chunkCapacity = cycleCapacity;
// }
if (chunkCapacity <= 0)
{
chunkCapacity = cycleCapacity;
}
ulong[] filteredDivisors = ArrayPool<ulong>.Shared.Rent(chunkCapacity);
ulong[] divisorGaps = ArrayPool<ulong>.Shared.Rent(chunkCapacity);
UInt128 twoP128 = (UInt128)prime << 1;
// Primes above one yield a non-zero 2p value, so the legacy zero guard remains commented out.
// if (twoP128 == UInt128.Zero)
// {
// coveredRange = true;
// return false;
// }
if (twoP128 == UInt128.Zero)
{
coveredRange = true;
return false;
}
UInt128 allowedMax128 = allowedMax;
UInt128 firstDivisor128 = twoP128 + UInt128.One;
@@ -252,13 +245,11 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
}
UInt128 maxK128 = (allowedMax128 - UInt128.One) / twoP128;
// Once the first divisor falls within the allowed range, at least one multiplier k is admissible,
// so keep the defensive zero check commented out.
// if (maxK128 == UInt128.Zero)
// {
// coveredRange = true;
// return false;
// }
if (maxK128 == UInt128.Zero)
{
coveredRange = true;
return false;
}
ulong maxK = maxK128 > ulong.MaxValue ? ulong.MaxValue : (ulong)maxK128;
ulong currentK = 1UL;
@@ -287,12 +278,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
{
while (currentK <= maxK)
{
if (enforceLimit && limitGuard.HasExpired())
{
processedAll = false;
return false;
}
int chunkCount = Math.Min(chunkCapacity, batchCapacity);
ulong remainingK = maxK - currentK + 1UL;
if ((ulong)chunkCount > remainingK)
@@ -300,12 +285,10 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
chunkCount = (int)remainingK;
}
// chunkCapacity and the remaining multiplier range are strictly positive here, so the zero
// chunk fallback never triggers on production scans.
// if (chunkCount <= 0)
// {
// break;
// }
if (chunkCount <= 0)
{
break;
}
Span<ulong> candidateSpan = cycleCandidates.AsSpan(0, chunkCount);
Span<ulong> gapSpan = divisorGaps.AsSpan(0, chunkCount);
@@ -343,10 +326,10 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
gapView.CopyFromCPU(ref MemoryMarshal.GetReference(gapSpan), chunkCount);
ComputeRemaindersOnGpu(chunkCount, remainder10, 10, gapView, deltaView, remainder10View, remainderDeltaKernel, remainderScanKernel, resources.GetRemainderScanConfig);
ComputeRemaindersOnGpu(chunkCount, remainder8, 8, gapView, deltaView, remainder8View, remainderDeltaKernel, remainderScanKernel, resources.GetRemainderScanConfig);
ComputeRemaindersOnGpu(chunkCount, remainder5, 5, gapView, deltaView, remainder5View, remainderDeltaKernel, remainderScanKernel, resources.GetRemainderScanConfig);
ComputeRemaindersOnGpu(chunkCount, remainder3, 3, gapView, deltaView, remainder3View, remainderDeltaKernel, remainderScanKernel, resources.GetRemainderScanConfig);
ComputeRemaindersOnGpu(chunkCount, remainder10, 10, gapView, deltaView, remainder10View, remainderDeltaKernel, remainderScanKernel);
ComputeRemaindersOnGpu(chunkCount, remainder8, 8, gapView, deltaView, remainder8View, remainderDeltaKernel, remainderScanKernel);
ComputeRemaindersOnGpu(chunkCount, remainder5, 5, gapView, deltaView, remainder5View, remainderDeltaKernel, remainderScanKernel);
ComputeRemaindersOnGpu(chunkCount, remainder3, 3, gapView, deltaView, remainder3View, remainderDeltaKernel, remainderScanKernel);
BuildCandidateMaskOnGpu(chunkCount, remainder10View, remainder8View, remainder5View, remainder3View, lastIsSevenFlag, maskView, maskSpan, candidateMaskKernel);
@@ -413,8 +396,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ulong divisorValue = filteredDivisorsSpan[i];
divisorSpan[i] = divisorValue;
exponentSpan[i] = prime;
// Divisors never repeat while scanning the monotonically increasing sequence, so regenerate
// the Montgomery data for each entry rather than storing cache lines that no future batch would hit.
divisorDataSpan[i] = MontgomeryDivisorData.FromModulus(divisorValue);
}
@@ -464,18 +445,18 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ArrayView1D<byte, Stride1D.Dense> deltaView,
ArrayView1D<byte, Stride1D.Dense> remainderView,
Action<Index1D, ArrayView<ulong>, ArrayView<byte>, byte> deltaKernel,
Action<KernelConfig, Index1D, ArrayView<byte>, ArrayView<byte>, byte, byte> scanKernel,
Func<int, KernelConfig> configProvider)
Action<KernelConfig, Index1D, ArrayView<byte>, ArrayView<byte>, byte, byte> scanKernel)
{
// GPU batches always contain at least one divisor gap, so the empty-count guard remains commented
// to eliminate redundant branching.
// if (count <= 0)
// {
// return;
// }
if (count <= 0)
{
return;
}
deltaKernel(count, gapView, deltaView, modulus);
KernelConfig kernelConfig = configProvider(count);
SharedMemoryConfig sharedMemoryConfig = SharedMemoryConfig.RequestDynamic<int>(count);
Index1D gridDim = new Index1D(1);
Index1D groupDim = new Index1D(count);
KernelConfig kernelConfig = new KernelConfig(gridDim, groupDim, sharedMemoryConfig);
scanKernel(kernelConfig, new Index1D(count), deltaView, remainderView, baseRemainder, modulus);
}
@@ -492,13 +473,11 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
Span<byte> destination,
Action<Index1D, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>, byte, ArrayView<byte>> maskKernel)
{
// Candidate batches always provide at least one entry, so the zero-count guard stays commented out
// to avoid clearing spans unnecessarily.
// if (count <= 0)
// {
// destination.Clear();
// return;
// }
if (count <= 0)
{
destination.Clear();
return;
}
maskKernel(count, remainder10View, remainder8View, remainder5View, remainder3View, lastIsSevenFlag, maskView);
maskView.CopyToCPU(ref MemoryMarshal.GetReference(destination), count);
@@ -506,12 +485,10 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
private static byte FetchLastRemainder(ArrayView1D<byte, Stride1D.Dense> remainderView, int count)
{
// Kernels always process at least one remainder in this path, so the empty-span guard remains
// commented to document the invariant.
// if (count <= 0)
// {
// return 0;
// }
if (count <= 0)
{
return 0;
}
byte value = 0;
remainderView.SubView(count - 1, 1).CopyToCPU(ref value, 1);
@@ -524,11 +501,10 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
if (result >= modulus)
{
result -= modulus;
// The step deltas remain below the modulus, so a single subtraction suffices on this path.
// if (result >= modulus)
// {
// result %= modulus;
// }
if (result >= modulus)
{
result %= modulus;
}
}
return (byte)result;
@@ -536,60 +512,46 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
private static ulong ComputeDivisorLimitFromMaxPrime(ulong maxPrime)
{
// The production configuration seeds the scan with exponents above the first odd prime, so the defensive guard remains
// commented to document the invariant without branching.
// if (maxPrime <= 1UL)
// {
// return 0UL;
// }
if (maxPrime <= 1UL)
{
return 0UL;
}
// Real workloads push maxPrime well past the 64-bit shift boundary, so keep the historical implementation commented out
// and return ulong.MaxValue directly.
// if (maxPrime - 1UL >= 64UL)
// {
// return ulong.MaxValue;
// }
//
// return (1UL << (int)(maxPrime - 1UL)) - 1UL;
if (maxPrime - 1UL >= 64UL)
{
return ulong.MaxValue;
}
return ulong.MaxValue;
return (1UL << (int)(maxPrime - 1UL)) - 1UL;
}
private static ulong ComputeAllowedMaxDivisor(ulong prime, ulong divisorLimit)
{
// The by-divisor search emits candidate exponents starting at the first odd prime, so the defensive guard remains
// commented to document that invariant.
// if (prime <= 1UL)
// {
// return 0UL;
// }
if (prime <= 1UL)
{
return 0UL;
}
// Production exponents are always large enough that the shift-based clamp would overflow, so skip the legacy branch and
// return the configured divisor limit directly.
// if (prime - 1UL >= 64UL)
// {
// return divisorLimit;
// }
//
// return Math.Min((1UL << (int)(prime - 1UL)) - 1UL, divisorLimit);
if (prime - 1UL >= 64UL)
{
return divisorLimit;
}
return divisorLimit;
return Math.Min((1UL << (int)(prime - 1UL)) - 1UL, divisorLimit);
}
private static void CheckKernel(Index1D index, ArrayView<MontgomeryDivisorData> divisors, ArrayView<ulong> exponents, ArrayView<byte> hits)
{
MontgomeryDivisorData divisor = divisors[index];
ulong modulus = divisor.Modulus;
// The divisor staging path filters out non-prime and even moduli before they reach the device, so this guard
// would never fire for EvenPerfectBitScanner workloads.
// if (modulus <= 1UL || (modulus & 1UL) == 0UL)
// {
// hits[index] = 0;
// return;
// }
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
hits[index] = 0;
return;
}
ulong exponent = exponents[index];
hits[index] = exponent.Pow2ModWindowedGpu(modulus) == 1UL ? (byte)1 : (byte)0;
hits[index] = exponent.Pow2MontgomeryModWindowed(divisor, keepMontgomery: false) == 1UL ? (byte)1 : (byte)0;
}
public sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
@@ -629,20 +591,16 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
}
int length = primes.Length;
// Production batches always contain at least one exponent, so the empty-span guard remains commented out to remove
// the redundant branch.
// if (length == 0)
// {
// return;
// }
if (length == 0)
{
return;
}
// The divisor staging path filters non-prime and even moduli before kernels launch, so keep the defensive
// modulus guard commented out to avoid branching in the hot loop.
// if (divisorData.Modulus <= 1UL || (divisorData.Modulus & 1UL) == 0UL)
// {
// hits.Clear();
// return;
// }
if (divisorData.Modulus <= 1UL || (divisorData.Modulus & 1UL) == 0UL)
{
hits.Clear();
return;
}
ArrayView1D<ulong, Stride1D.Dense> exponentsView = _exponentsBuffer.View;
ArrayView1D<ulong, Stride1D.Dense> resultsView = _resultsBuffer.View;
@@ -801,34 +759,33 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
mask[globalIndex] = 1;
}
private static void ComputeExponentKernel(Index1D index, MontgomeryDivisorData divisor, ArrayView<ulong> exponents, ArrayView<ulong> results)
private static void ComputeMontgomeryExponentKernel(Index1D index, MontgomeryDivisorData divisor, ArrayView<ulong> exponents, ArrayView<ulong> results)
{
ulong modulus = divisor.Modulus;
// Kernel inputs are constrained to odd prime moduli, so this defensive branch would never execute on the configured
// scanning path.
// if (modulus <= 1UL || (modulus & 1UL) == 0UL)
// {
// results[index] = 0UL;
// return;
// }
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
results[index] = 0UL;
return;
}
ulong exponent = exponents[index];
results[index] = exponent.Pow2ModWindowedGpu(modulus);
results[index] = exponent.Pow2MontgomeryModWindowed(divisor, keepMontgomery: true);
}
public ulong DivisorLimit
{
get
{
// The by-divisor scanner always configures the tester before exposing the limit, so the guard remains
// commented out to document the invariant without forcing a runtime branch.
// if (!_isConfigured)
// {
// throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
// }
lock (_sync)
{
if (!_isConfigured)
{
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
return _divisorLimit;
return _divisorLimit;
}
}
}
@@ -845,17 +802,15 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
private void ReturnBatchResources(Accelerator accelerator, BatchResources resources) => _resourcePools.GetOrAdd(accelerator, static _ => []).Add(resources);
private sealed class BatchResources
private sealed class BatchResources : IDisposable
{
internal BatchResources(MersenneNumberDivisorByDivisorGpuTester owner, Accelerator accelerator, int capacity)
{
int cycleCapacity = DivisorCycleCache.Shared.PreferredBatchSize;
// The shared cache always reports a positive preferred size on production workloads, so the fallback remains
// commented out to avoid branching during resource initialization.
// if (cycleCapacity <= 0)
// {
// cycleCapacity = 1;
// }
if (cycleCapacity <= 0)
{
cycleCapacity = 1;
}
int actualCapacity = Math.Max(capacity, cycleCapacity);
@@ -907,24 +862,10 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
internal readonly Action<KernelConfig, Index1D, ArrayView<byte>, ArrayView<byte>, byte, byte> RemainderScanKernel;
internal readonly Action<Index1D, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>, byte, ArrayView<byte>> CandidateMaskKernel;
private readonly Dictionary<int, KernelConfig> _remainderScanConfigs = new();
internal readonly int Capacity;
internal readonly int CycleCapacity;
internal KernelConfig GetRemainderScanConfig(int count)
{
if (!_remainderScanConfigs.TryGetValue(count, out KernelConfig config))
{
SharedMemoryConfig sharedMemoryConfig = SharedMemoryConfig.RequestDynamic<int>(count);
config = new KernelConfig(new Index1D(1), new Index1D(count), sharedMemoryConfig);
_remainderScanConfigs[count] = config;
}
return config;
}
public void Dispose()
{
DivisorsBuffer.Dispose();
@@ -953,33 +894,37 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
public ulong GetAllowedMaxDivisor(ulong prime)
{
// The CLI configures the GPU tester once at startup, so callers never request divisor limits before
// initialization completes.
// if (!_isConfigured)
// {
// throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
// }
return ComputeAllowedMaxDivisor(prime, _divisorLimit);
}
public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession()
{
// Divisor sessions are handed out only after configuration, so this guard would never trigger on the
// production path.
// if (!_isConfigured)
// {
// throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
// }
if (_sessionPool.TryTake(out DivisorScanSession? session))
lock (_sync)
{
session.Reset();
return session;
}
if (!_isConfigured)
{
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
return new DivisorScanSession(this);
return ComputeAllowedMaxDivisor(prime, _divisorLimit);
}
}
public DivisorScanSession CreateDivisorSession()
{
lock (_sync)
{
if (!_isConfigured)
{
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
if (_sessionPool.TryTake(out DivisorScanSession? session))
{
session.Reset();
return session;
}
return new DivisorScanSession(this);
}
}
IMersenneNumberDivisorByDivisorTester.IDivisorScanSession IMersenneNumberDivisorByDivisorTester.CreateDivisorSession() => CreateDivisorSession();
}
@@ -1,273 +0,0 @@
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using ILGPU;
using ILGPU.Runtime;
namespace PerfectNumbers.Core.Gpu;
internal sealed class MersenneNumberDivisorCandidateGpuEvaluator
{
private const int RemainderCount = 6;
private readonly GpuContextPool.GpuContextLease _lease;
private readonly Accelerator _accelerator;
private readonly Action<Index1D, CandidateFilterArgs, ArrayView<ulong>, ArrayView<byte>> _kernel;
private MemoryBuffer1D<ulong, Stride1D.Dense> _candidateBuffer;
private MemoryBuffer1D<byte, Stride1D.Dense> _maskBuffer;
private ulong[] _hostCandidates;
private byte[] _hostMask;
private int _capacity;
private bool _disposed;
internal MersenneNumberDivisorCandidateGpuEvaluator(int batchSize)
{
_lease = GpuContextPool.RentPreferred(preferCpu: false);
_accelerator = _lease.Accelerator;
_kernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, CandidateFilterArgs, ArrayView<ulong>, ArrayView<byte>>(EvaluateCandidatesKernel);
_capacity = Math.Max(1, batchSize);
_candidateBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_maskBuffer = _accelerator.Allocate1D<byte>(_capacity);
_hostCandidates = ArrayPool<ulong>.Shared.Rent(_capacity);
_hostMask = ArrayPool<byte>.Shared.Rent(_capacity);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_candidateBuffer.Dispose();
_maskBuffer.Dispose();
ArrayPool<ulong>.Shared.Return(_hostCandidates, clearArray: false);
ArrayPool<byte>.Shared.Return(_hostMask, clearArray: false);
_lease.Dispose();
}
internal void EvaluateCandidates(
ulong startDivisor,
ulong step,
ulong limit,
ReadOnlySpan<byte> remainders,
ReadOnlySpan<byte> steps,
bool lastIsSeven,
Span<ulong> candidates,
Span<byte> mask)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(MersenneNumberDivisorCandidateGpuEvaluator));
}
if (remainders.Length != RemainderCount || steps.Length != RemainderCount)
{
throw new ArgumentException("Remainder and step tables must contain exactly six entries.");
}
int count = candidates.Length;
// Candidate batches always carry at least one divisor on the by-divisor path, so the empty-count
// guard remains commented out to eliminate the branch.
// if (count == 0)
// {
// return;
// }
if (count > _capacity)
{
EnsureCapacity(count);
}
Span<ulong> hostCandidateSpan = _hostCandidates.AsSpan(0, count);
Span<byte> hostMaskSpan = _hostMask.AsSpan(0, count);
ArrayView1D<ulong, Stride1D.Dense> candidateView = _candidateBuffer.View.SubView(0, count);
ArrayView1D<byte, Stride1D.Dense> maskView = _maskBuffer.View.SubView(0, count);
var args = new CandidateFilterArgs(
startDivisor,
step,
limit,
remainders[0],
remainders[1],
remainders[2],
remainders[3],
remainders[4],
remainders[5],
steps[0],
steps[1],
steps[2],
steps[3],
steps[4],
steps[5],
lastIsSeven ? (byte)1 : (byte)0);
_kernel(count, args, candidateView, maskView);
candidateView.CopyToCPU(ref MemoryMarshal.GetReference(hostCandidateSpan), count);
maskView.CopyToCPU(ref MemoryMarshal.GetReference(hostMaskSpan), count);
hostCandidateSpan.CopyTo(candidates);
hostMaskSpan.CopyTo(mask);
}
private void EnsureCapacity(int required)
{
if (required <= _capacity)
{
return;
}
int newCapacity = _capacity;
// The evaluator always starts with a positive capacity, so the zero-capacity fallback remains commented.
// if (newCapacity == 0)
// {
// newCapacity = 1;
// }
while (newCapacity < required)
{
newCapacity <<= 1;
}
_candidateBuffer.Dispose();
_maskBuffer.Dispose();
ArrayPool<ulong>.Shared.Return(_hostCandidates, clearArray: false);
ArrayPool<byte>.Shared.Return(_hostMask, clearArray: false);
_capacity = newCapacity;
_candidateBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_maskBuffer = _accelerator.Allocate1D<byte>(_capacity);
_hostCandidates = ArrayPool<ulong>.Shared.Rent(_capacity);
_hostMask = ArrayPool<byte>.Shared.Rent(_capacity);
}
private static void EvaluateCandidatesKernel(
Index1D index,
CandidateFilterArgs args,
ArrayView<ulong> candidates,
ArrayView<byte> mask)
{
int idx = index;
if (idx >= mask.Length)
{
return;
}
ulong offset = (ulong)idx;
ulong candidate = args.StartDivisor + args.Step * offset;
candidates[idx] = candidate;
if (candidate > args.Limit || (args.Step != 0UL && candidate < args.StartDivisor))
{
mask[idx] = 0;
return;
}
int remainderValue10 = ComputeRemainder(args.Remainder10, args.Step10, idx, 10);
int remainderValue8 = ComputeRemainder(args.Remainder8, args.Step8, idx, 8);
int remainderValue5 = ComputeRemainder(args.Remainder5, args.Step5, idx, 5);
int remainderValue3 = ComputeRemainder(args.Remainder3, args.Step3, idx, 3);
int remainderValue7 = ComputeRemainder(args.Remainder7, args.Step7, idx, 7);
int remainderValue11 = ComputeRemainder(args.Remainder11, args.Step11, idx, 11);
bool lastIsSeven = args.LastIsSevenFlag != 0;
bool admissible = lastIsSeven
? (remainderValue10 == 3 || remainderValue10 == 7 || remainderValue10 == 9)
: (remainderValue10 == 1 || remainderValue10 == 3 || remainderValue10 == 9);
if (!admissible)
{
mask[idx] = 0;
return;
}
if ((remainderValue8 != 1 && remainderValue8 != 7)
|| remainderValue3 == 0
|| remainderValue5 == 0
|| remainderValue7 == 0
|| remainderValue11 == 0)
{
mask[idx] = 0;
return;
}
mask[idx] = 1;
}
private static int ComputeRemainder(byte remainder, byte step, int index, int modulus)
{
// Modulus lookups only cover {10, 8, 5, 3, 7, 11}, so the zero-modulus guard stays commented out.
// if (modulus == 0)
// {
// return 0;
// }
long value = remainder;
value += (long)step * index;
int result = (int)(value % modulus);
if (result < 0)
{
result += modulus;
}
return result;
}
private readonly struct CandidateFilterArgs
{
public CandidateFilterArgs(
ulong startDivisor,
ulong step,
ulong limit,
byte remainder10,
byte remainder8,
byte remainder5,
byte remainder3,
byte remainder7,
byte remainder11,
byte step10,
byte step8,
byte step5,
byte step3,
byte step7,
byte step11,
byte lastIsSevenFlag)
{
StartDivisor = startDivisor;
Step = step;
Limit = limit;
Remainder10 = remainder10;
Remainder8 = remainder8;
Remainder5 = remainder5;
Remainder3 = remainder3;
Remainder7 = remainder7;
Remainder11 = remainder11;
Step10 = step10;
Step8 = step8;
Step5 = step5;
Step3 = step3;
Step7 = step7;
Step11 = step11;
LastIsSevenFlag = lastIsSevenFlag;
}
public ulong StartDivisor { get; }
public ulong Step { get; }
public ulong Limit { get; }
public byte Remainder10 { get; }
public byte Remainder8 { get; }
public byte Remainder5 { get; }
public byte Remainder3 { get; }
public byte Remainder7 { get; }
public byte Remainder11 { get; }
public byte Step10 { get; }
public byte Step8 { get; }
public byte Step5 { get; }
public byte Step3 { get; }
public byte Step7 { get; }
public byte Step11 { get; }
public byte LastIsSevenFlag { get; }
}
}
@@ -1,190 +0,0 @@
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using PerfectNumbers.Core;
using ILGPU;
using ILGPU.Runtime;
namespace PerfectNumbers.Core.Gpu;
internal sealed class MersenneNumberDivisorMontgomeryGpuBuilder
{
private readonly GpuContextPool.GpuContextLease _lease;
private readonly Accelerator _accelerator;
private readonly Action<Index1D, ArrayView<ulong>, ArrayView<MontgomeryDivisorData>> _kernel;
private MemoryBuffer1D<ulong, Stride1D.Dense> _modulusBuffer;
private MemoryBuffer1D<MontgomeryDivisorData, Stride1D.Dense> _resultBuffer;
private ulong[] _hostModulusBuffer;
private MontgomeryDivisorData[] _hostResultBuffer;
private int _capacity;
private bool _disposed;
internal MersenneNumberDivisorMontgomeryGpuBuilder(int batchSize)
{
_lease = GpuContextPool.RentPreferred(preferCpu: false);
_accelerator = _lease.Accelerator;
_kernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<MontgomeryDivisorData>>(BuildKernel);
_capacity = Math.Max(1, batchSize);
_modulusBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_resultBuffer = _accelerator.Allocate1D<MontgomeryDivisorData>(_capacity);
_hostModulusBuffer = ArrayPool<ulong>.Shared.Rent(_capacity);
_hostResultBuffer = ArrayPool<MontgomeryDivisorData>.Shared.Rent(_capacity);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_modulusBuffer.Dispose();
_resultBuffer.Dispose();
ArrayPool<ulong>.Shared.Return(_hostModulusBuffer, clearArray: false);
ArrayPool<MontgomeryDivisorData>.Shared.Return(_hostResultBuffer, clearArray: false);
_lease.Dispose();
}
internal void Build(ReadOnlySpan<ulong> moduli, Span<MontgomeryDivisorData> destination)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(MersenneNumberDivisorMontgomeryGpuBuilder));
}
int length = moduli.Length;
// Montgomery batches always include at least one modulus on the by-divisor path, so the empty-length
// guard stays commented out to avoid branching.
// if (length == 0)
// {
// return;
// }
EnsureCapacity(length);
Span<ulong> hostModulusSpan = _hostModulusBuffer.AsSpan(0, length);
moduli.CopyTo(hostModulusSpan);
ArrayView1D<ulong, Stride1D.Dense> modulusView = _modulusBuffer.View.SubView(0, length);
modulusView.CopyFromCPU(ref MemoryMarshal.GetReference(hostModulusSpan), length);
ArrayView1D<MontgomeryDivisorData, Stride1D.Dense> resultView = _resultBuffer.View.SubView(0, length);
_kernel(length, modulusView, resultView);
Span<MontgomeryDivisorData> hostResultSpan = _hostResultBuffer.AsSpan(0, length);
resultView.CopyToCPU(ref MemoryMarshal.GetReference(hostResultSpan), length);
hostResultSpan.CopyTo(destination);
}
private void EnsureCapacity(int required)
{
if (required <= _capacity)
{
return;
}
int newCapacity = _capacity;
// The builder always starts with a positive capacity, so the zero-capacity fallback remains commented.
// if (newCapacity == 0)
// {
// newCapacity = 1;
// }
while (newCapacity < required)
{
newCapacity <<= 1;
}
_modulusBuffer.Dispose();
_resultBuffer.Dispose();
ArrayPool<ulong>.Shared.Return(_hostModulusBuffer, clearArray: false);
ArrayPool<MontgomeryDivisorData>.Shared.Return(_hostResultBuffer, clearArray: false);
_capacity = newCapacity;
_modulusBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_resultBuffer = _accelerator.Allocate1D<MontgomeryDivisorData>(_capacity);
_hostModulusBuffer = ArrayPool<ulong>.Shared.Rent(_capacity);
_hostResultBuffer = ArrayPool<MontgomeryDivisorData>.Shared.Rent(_capacity);
}
private static void BuildKernel(Index1D index, ArrayView<ulong> moduli, ArrayView<MontgomeryDivisorData> destination)
{
int idx = index;
// The builder launches kernels with an extent equal to the modulus count, so the bounds guard remains commented
// out to avoid redundant branching on every thread.
// if (idx >= moduli.Length)
// {
// return;
// }
ulong modulus = moduli[idx];
// All staged divisors are odd primes on the scanner path, so keep the defensive modulus guard commented out to
// remove the branch from the kernel.
// if (modulus <= 1UL || (modulus & 1UL) == 0UL)
// {
// destination[idx] = default;
// return;
// }
ulong nPrime = ComputeMontgomeryNPrime(modulus);
ulong montgomeryOne = ComputeMontgomeryResidue64(modulus);
ulong montgomeryTwo = montgomeryOne.MulMod64(2UL, modulus);
ulong montgomeryTwoSquared = MontgomeryMultiplyGpu(montgomeryTwo, montgomeryTwo, modulus, nPrime);
destination[idx] = new MontgomeryDivisorData(
modulus,
nPrime,
montgomeryOne,
montgomeryTwo,
montgomeryTwoSquared);
}
private static ulong ComputeMontgomeryResidue64(ulong modulus)
{
ulong remainder = ulong.MaxValue % modulus;
remainder++;
if (remainder >= modulus)
{
remainder -= modulus;
}
return remainder;
}
private static ulong MontgomeryMultiplyGpu(ulong a, ulong b, ulong modulus, ulong nPrime)
{
GpuUInt128 product = new(a);
product.Mul64(new GpuUInt128(b));
ulong tLow = product.Low;
ulong tHigh = product.High;
ulong m = unchecked(tLow * nPrime);
GpuUInt128 mProduct = new(m);
mProduct.Mul64(new GpuUInt128(modulus));
ulong mLow = mProduct.Low;
ulong mHigh = mProduct.High;
ulong carry = unchecked((tLow + mLow) < tLow ? 1UL : 0UL);
ulong result = tHigh + mHigh + carry;
if (result >= modulus)
{
result -= modulus;
}
return result;
}
private static ulong ComputeMontgomeryNPrime(ulong modulus)
{
ulong inv = modulus;
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
return unchecked(0UL - inv);
}
}
@@ -1,146 +0,0 @@
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using ILGPU;
using ILGPU.Runtime;
namespace PerfectNumbers.Core.Gpu;
internal sealed class MersenneNumberDivisorRemainderGpuStepper
{
private const int RemainderCount = 6;
private readonly GpuContextPool.GpuContextLease _lease;
private readonly Accelerator _accelerator;
private readonly Action<Index1D, int, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>> _kernel;
private MemoryBuffer1D<byte, Stride1D.Dense> _remainderBuffer;
private MemoryBuffer1D<byte, Stride1D.Dense> _stepBuffer;
private MemoryBuffer1D<byte, Stride1D.Dense> _resultBuffer;
private byte[] _hostRemainders;
private byte[] _hostSteps;
private readonly int _length;
private int _capacity;
private bool _disposed;
internal MersenneNumberDivisorRemainderGpuStepper(ReadOnlySpan<byte> steps)
{
if (steps.Length != RemainderCount)
{
throw new ArgumentException("Step table must contain exactly six entries.");
}
_lease = GpuContextPool.RentPreferred(preferCpu: false);
_accelerator = _lease.Accelerator;
_kernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, int, ArrayView<byte>, ArrayView<byte>, ArrayView<byte>>(AdvanceRemaindersKernel);
_length = steps.Length;
_capacity = Math.Max(1, _length);
_remainderBuffer = _accelerator.Allocate1D<byte>(_capacity);
_stepBuffer = _accelerator.Allocate1D<byte>(_capacity);
_resultBuffer = _accelerator.Allocate1D<byte>(_capacity);
_hostRemainders = ArrayPool<byte>.Shared.Rent(_capacity);
_hostSteps = ArrayPool<byte>.Shared.Rent(_capacity);
steps.CopyTo(_hostSteps.AsSpan(0, _length));
ArrayView1D<byte, Stride1D.Dense> stepView = _stepBuffer.View.SubView(0, _length);
stepView.CopyFromCPU(ref MemoryMarshal.GetReference(_hostSteps.AsSpan(0, _length)), _length);
}
public void Dispose()
{
if (_disposed)
{
throw new InvalidOperationException("MersenneNumberDivisorRemainderGpuStepper was disposed more than once.");
}
_disposed = true;
_remainderBuffer.Dispose();
_stepBuffer.Dispose();
_resultBuffer.Dispose();
ArrayPool<byte>.Shared.Return(_hostRemainders, clearArray: false);
ArrayPool<byte>.Shared.Return(_hostSteps, clearArray: false);
_lease.Dispose();
}
internal void Advance(int advanceCount, Span<byte> remainders)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(MersenneNumberDivisorRemainderGpuStepper));
}
// GPU remainder batches always advance by a positive chunk count and operate on fixed-size tables, so
// the defensive early-return remains commented out.
// if (advanceCount <= 0 || remainders.Length == 0)
// {
// return;
// }
// Production code always calls the stepper with a fixed-length table, so the historical length guard stays commented.
// if (remainders.Length != _length)
// {
// throw new ArgumentException("Remainder buffer length must match the configured length.");
// }
Span<byte> hostRemainderSpan = _hostRemainders.AsSpan(0, _length);
remainders.CopyTo(hostRemainderSpan);
ArrayView1D<byte, Stride1D.Dense> remainderView = _remainderBuffer.View.SubView(0, _length);
remainderView.CopyFromCPU(ref MemoryMarshal.GetReference(hostRemainderSpan), _length);
ArrayView1D<byte, Stride1D.Dense> stepView = _stepBuffer.View.SubView(0, _length);
ArrayView1D<byte, Stride1D.Dense> resultView = _resultBuffer.View.SubView(0, _length);
_kernel(_length, advanceCount, remainderView, stepView, resultView);
resultView.CopyToCPU(ref MemoryMarshal.GetReference(hostRemainderSpan), _length);
hostRemainderSpan.CopyTo(remainders);
}
private static void AdvanceRemaindersKernel(Index1D index, int advanceCount, ArrayView<byte> remainders, ArrayView<byte> steps, ArrayView<byte> results)
{
int idx = index;
// Kernel launches match the remainder table length, so the bounds guard stays commented to avoid redundant branching.
// if (idx >= remainders.Length)
// {
// return;
// }
int modulus = GetModulus(idx);
// The modulus selector only emits positive bases (10, 8, 5, 3, 7, 11) for the configured indices, so the zero-modulus
// fallback remains commented out.
// if (modulus == 0)
// {
// results[idx] = remainders[idx];
// return;
// }
int remainder = remainders[idx] % modulus;
int step = steps[idx] % modulus;
long delta = (long)step * advanceCount;
int adjusted = (int)((remainder + delta) % modulus);
if (adjusted < 0)
{
adjusted += modulus;
}
results[idx] = (byte)adjusted;
}
private static int GetModulus(int index)
{
return index switch
{
0 => 10,
1 => 8,
2 => 5,
3 => 3,
4 => 7,
5 => 11,
_ => 0,
};
}
}
@@ -1,128 +0,0 @@
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using PerfectNumbers.Core;
using ILGPU;
using ILGPU.Runtime;
namespace PerfectNumbers.Core.Gpu;
internal sealed class MersenneNumberDivisorResidueGpuEvaluator
{
private readonly GpuContextPool.GpuContextLease _lease;
private readonly Accelerator _accelerator;
private readonly Action<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>> _kernel;
private MemoryBuffer1D<ulong, Stride1D.Dense> _exponentBuffer;
private MemoryBuffer1D<ulong, Stride1D.Dense> _resultBuffer;
private ulong[] _hostBuffer;
private int _capacity;
private bool _disposed;
internal MersenneNumberDivisorResidueGpuEvaluator(int batchSize)
{
_lease = GpuContextPool.RentPreferred(preferCpu: false);
_accelerator = _lease.Accelerator;
_kernel = _accelerator.LoadAutoGroupedStreamKernel<Index1D, MontgomeryDivisorData, ArrayView<ulong>, ArrayView<ulong>>(ComputeResiduesKernel);
_capacity = Math.Max(1, batchSize);
_exponentBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_resultBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_hostBuffer = ArrayPool<ulong>.Shared.Rent(_capacity);
}
public void Dispose()
{
if (_disposed)
{
throw new InvalidOperationException("MersenneNumberDivisorResidueGpuEvaluator was disposed more than once.");
}
_disposed = true;
_exponentBuffer.Dispose();
_resultBuffer.Dispose();
ArrayPool<ulong>.Shared.Return(_hostBuffer, clearArray: false);
_lease.Dispose();
}
internal void ComputeResidues(ReadOnlySpan<ulong> exponents, in MontgomeryDivisorData divisorData, Span<ulong> destination)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(MersenneNumberDivisorResidueGpuEvaluator));
}
int length = exponents.Length;
// Residue batches always include at least one exponent when scheduled from the by-divisor scanner, so
// the empty-length guard remains commented out to avoid branching.
// if (length == 0)
// {
// return;
// }
EnsureCapacity(length);
Span<ulong> hostSpan = _hostBuffer.AsSpan(0, length);
exponents.CopyTo(hostSpan);
ArrayView1D<ulong, Stride1D.Dense> exponentView = _exponentBuffer.View.SubView(0, length);
exponentView.CopyFromCPU(ref MemoryMarshal.GetReference(hostSpan), length);
ArrayView1D<ulong, Stride1D.Dense> resultView = _resultBuffer.View.SubView(0, length);
_kernel(length, divisorData, exponentView, resultView);
resultView.CopyToCPU(ref MemoryMarshal.GetReference(hostSpan), length);
hostSpan.CopyTo(destination);
}
private void EnsureCapacity(int required)
{
if (required <= _capacity)
{
return;
}
int newCapacity = _capacity;
// The evaluator always starts with a positive capacity, so the zero-capacity fallback stays commented.
// if (newCapacity == 0)
// {
// newCapacity = 1;
// }
while (newCapacity < required)
{
newCapacity <<= 1;
}
_exponentBuffer.Dispose();
_resultBuffer.Dispose();
ArrayPool<ulong>.Shared.Return(_hostBuffer, clearArray: false);
_capacity = newCapacity;
_exponentBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_resultBuffer = _accelerator.Allocate1D<ulong>(_capacity);
_hostBuffer = ArrayPool<ulong>.Shared.Rent(_capacity);
}
private static void ComputeResiduesKernel(Index1D index, MontgomeryDivisorData divisorData, ArrayView<ulong> exponents, ArrayView<ulong> results)
{
int idx = index;
int length = (int)exponents.Length;
// Batched launches match the buffer length on production workloads, so the bounds guard stays commented out to remove
// the redundant comparison.
// if (idx >= length)
// {
// return;
// }
ulong exponent = exponents[idx];
ulong modulus = divisorData.Modulus;
// Residue batches only include valid odd moduli, so the defensive check stays commented to avoid re-validating
// the invariant on every iteration.
// if (modulus <= 1UL || (modulus & 1UL) == 0UL)
// {
// results[idx] = 0UL;
// return;
// }
results[idx] = exponent.Pow2ModWindowedGpu(modulus);
}
}
@@ -327,9 +327,8 @@ public class MersenneNumberLucasLehmerGpuTester
// TODO: Replace StreamWriter with the pooled TextFileWriter pipeline so persisting NTT
// parameters reuses the zero-allocation buffered writes highlighted in the scanner I/O
// benchmarks instead of allocating a new encoder per append.
var writer = new StreamWriter(ParameterFilePath, append: true);
using var writer = new StreamWriter(ParameterFilePath, append: true);
writer.WriteLine($"{length} {modulus.High} {modulus.Low} {primitiveRoot.High} {primitiveRoot.Low}");
writer.Dispose();
}
}
@@ -339,7 +338,7 @@ public class MersenneNumberLucasLehmerGpuTester
ulong foundRoot = 0UL;
bool success = false;
var processorCount = Environment.ProcessorCount;
var cts = new System.Threading.CancellationTokenSource();
using var cts = new System.Threading.CancellationTokenSource();
object sync = new();
// TODO: Move this Parallel.For to the shared low-overhead work scheduler once the NTT parameter
@@ -382,13 +381,11 @@ public class MersenneNumberLucasLehmerGpuTester
{
modulus = new GpuUInt128(0UL, foundCandidate);
primitiveRoot = new GpuUInt128(0UL, foundRoot);
cts.Dispose();
return true;
}
modulus = new GpuUInt128(0UL, 18446744069414584321UL);
primitiveRoot = new GpuUInt128(0UL, 7UL);
cts.Dispose();
return false;
}
@@ -1,88 +0,0 @@
using System;
using System.Runtime.InteropServices;
using ILGPU.Runtime;
namespace PerfectNumbers.Core.Gpu;
internal static class MontgomeryOddPowerGpu
{
public static bool TryCompute(in MontgomeryDivisorData divisor, int oddPowerCount, Span<ulong> destination)
{
if (oddPowerCount <= 0)
{
return true;
}
Span<MontgomeryDivisorData> divisorSpan = stackalloc MontgomeryDivisorData[1];
divisorSpan[0] = divisor;
Span<int> countSpan = stackalloc int[1];
countSpan[0] = oddPowerCount;
Span<ulong> resultSpan = stackalloc ulong[PerfectNumberConstants.MaxOddPowersCount];
if (!TryComputeBatch(divisorSpan, countSpan, resultSpan, PerfectNumberConstants.MaxOddPowersCount))
{
return false;
}
resultSpan[..oddPowerCount].CopyTo(destination);
return true;
}
public static bool TryComputeBatch(
ReadOnlySpan<MontgomeryDivisorData> divisors,
ReadOnlySpan<int> oddPowerCounts,
Span<ulong> results,
int stride)
{
int divisorCount = divisors.Length;
if (divisorCount == 0)
{
return true;
}
if (oddPowerCounts.Length < divisorCount)
{
throw new ArgumentException(
"oddPowerCounts span must contain at least as many elements as divisors span.",
nameof(oddPowerCounts));
}
if (results.Length < divisorCount * stride)
{
throw new ArgumentException(
"The result span is shorter than the required batched odd power storage.",
nameof(results));
}
if (GpuContextPool.ForceCpu)
{
return false;
}
var lease = GpuKernelPool.GetKernel(useGpuOrder: true);
var execution = lease.EnterExecutionScope();
var accelerator = lease.Accelerator;
var stream = lease.Stream;
var divisorBuffer = accelerator.Allocate1D<MontgomeryDivisorData>(divisorCount);
var countBuffer = accelerator.Allocate1D<int>(divisorCount);
var resultBuffer = accelerator.Allocate1D<ulong>(divisorCount * stride);
divisorBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(divisors), divisorCount);
countBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(oddPowerCounts), divisorCount);
resultBuffer.MemSetToZero();
var kernel = lease.MontgomeryOddPowerKernel;
kernel(stream, divisorCount, divisorBuffer.View, countBuffer.View, resultBuffer.View, stride);
stream.Synchronize();
resultBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(results), divisorCount * stride);
resultBuffer.Dispose();
countBuffer.Dispose();
divisorBuffer.Dispose();
execution.Dispose();
lease.Dispose();
return true;
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ public static class NttGpuMath
private static readonly ConcurrentDictionary<Accelerator, Action<Index1D, ArrayView<GpuUInt128>, int>> BitReverseKernelCache = new(); // TODO: Precompute bit-reversal kernels during boot so we can remove the concurrent lookup overhead flagged in the benchmarks.
private sealed class SquareCacheEntry
private sealed class SquareCacheEntry : IDisposable
{
public MemoryBuffer1D<GpuUInt128, Stride1D.Dense> Buffer { get; }
public GpuUInt128 Root { get; }
+112 -105
View File
@@ -4,10 +4,12 @@ using System.Collections.Concurrent;
using ILGPU;
using ILGPU.Runtime;
using System.Numerics;
using System.Globalization;
using PerfectNumbers.Core;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using ILGPU.Runtime.OpenCL;
namespace PerfectNumbers.Core.Gpu;
internal enum GpuPow2ModStatus
@@ -27,6 +29,25 @@ internal static partial class PrimeOrderGpuHeuristics
private static readonly ConcurrentDictionary<Accelerator, OrderKernelLauncher> OrderKernelCache = new();
private static readonly ConcurrentDictionary<Accelerator, SmallPrimeDeviceCache> SmallPrimeDeviceCaches = new();
private static void ReportPow2FailureCore(string context, string modulusDescription, GpuPow2ModStatus status)
{
string message = $"GPU pow2mod failed with status {status} for divisor {modulusDescription} during {context}.";
Console.Error.WriteLine(message);
throw new InvalidOperationException(message);
}
internal static void ReportPow2Failure(string context, ulong modulus, GpuPow2ModStatus status)
{
string modulusDescription = modulus.ToString(CultureInfo.InvariantCulture);
ReportPow2FailureCore(context, modulusDescription, status);
}
internal static void ReportPow2Failure(string context, UInt128 modulus, GpuPow2ModStatus status)
{
string modulusDescription = modulus.ToString(CultureInfo.InvariantCulture);
ReportPow2FailureCore(context, modulusDescription, status);
}
public readonly struct OrderKernelConfig(ulong previousOrder, byte hasPreviousOrder, uint smallFactorLimit, int maxPowChecks, int mode)
{
public readonly ulong PreviousOrder = previousOrder;
@@ -415,9 +436,9 @@ internal static partial class PrimeOrderGpuHeuristics
ulong? previousOrder,
PrimeOrderCalculator.PrimeOrderSearchConfig config,
in MontgomeryDivisorData divisorData,
out ulong order)
out PrimeOrderCalculator.PrimeOrderResult result)
{
order = 0UL;
result = default;
var lease = GpuKernelPool.GetKernel(useGpuOrder: true);
var execution = lease.EnterExecutionScope();
@@ -497,17 +518,18 @@ internal static partial class PrimeOrderGpuHeuristics
throw new InvalidOperationException("GPU Pollard Rho stack overflow; increase HeuristicStackCapacity.");
}
ulong order = 0UL;
resultBuffer.View.CopyToCPU(ref order, 1);
if (kernelStatus == PrimeOrderKernelStatus.FactoringFailure)
{
order = 0UL;
}
PrimeOrderCalculator.PrimeOrderStatus finalStatus = kernelStatus == PrimeOrderKernelStatus.Found
? PrimeOrderCalculator.PrimeOrderStatus.Found
: PrimeOrderCalculator.PrimeOrderStatus.HeuristicUnresolved;
result = new PrimeOrderCalculator.PrimeOrderResult(finalStatus, order);
DisposeResources();
lease.Dispose();
return order != 0UL;
return true;
void DisposeResources()
{
@@ -805,15 +827,13 @@ internal static partial class PrimeOrderGpuHeuristics
for (int i = 0; i < factorCount; i++)
{
ulong factor = factors[i];
// The factorization routine emits strictly greater-than-one factors for phi on this path, so the
// defensive continue would never execute.
// if (factor <= 1UL)
// {
// continue;
// }
if (factor <= 1UL)
{
continue;
}
ulong reduced = phi / factor;
if (reduced.Pow2ModWindowedGpu(divisor.Modulus) == 1UL)
if (reduced.Pow2MontgomeryModWindowed(divisor, keepMontgomery: false) == 1UL)
{
return false;
}
@@ -829,7 +849,7 @@ internal static partial class PrimeOrderGpuHeuristics
if (residue == 1UL || residue == 7UL)
{
ulong half = phi >> 1;
if ((half).Pow2ModWindowedGpu(divisor.Modulus) == 1UL)
if (Pow2EqualsOneKernel(half, divisor))
{
order = half;
}
@@ -850,11 +870,10 @@ internal static partial class PrimeOrderGpuHeuristics
{
ulong primeFactor = factors[i];
int exponent = exponents[i];
// Factorization never yields unity on the scanning path, so skip the redundant guard.
// if (primeFactor <= 1UL)
// {
// continue;
// }
if (primeFactor <= 1UL)
{
continue;
}
for (int iteration = 0; iteration < exponent; iteration++)
{
@@ -864,7 +883,7 @@ internal static partial class PrimeOrderGpuHeuristics
}
ulong reduced = order / primeFactor;
if ((reduced).Pow2ModWindowedGpu(divisor.Modulus) == 1UL)
if (Pow2EqualsOneKernel(reduced, divisor))
{
order = reduced;
continue;
@@ -890,14 +909,12 @@ internal static partial class PrimeOrderGpuHeuristics
ArrayView1D<ulong, Stride1D.Dense> compositeStack,
ArrayView1D<byte, Stride1D.Dense> statusOut)
{
// Order values handed to the kernel are strictly positive on the production path, so the zero guard stays commented
// out to remove the redundant branch.
// if (order == 0UL)
// {
// return false;
// }
if (order == 0UL)
{
return false;
}
if ((order).Pow2ModWindowedGpu(divisor.Modulus) != 1UL)
if (!Pow2EqualsOneKernel(order, divisor))
{
return false;
}
@@ -927,11 +944,10 @@ internal static partial class PrimeOrderGpuHeuristics
{
ulong primeFactor = factors[i];
int exponent = exponents[i];
// Factorization never yields one on the by-divisor path, so the defensive continue stays commented out.
// if (primeFactor <= 1UL)
// {
// continue;
// }
if (primeFactor <= 1UL)
{
continue;
}
ulong reduced = order;
for (int iteration = 0; iteration < exponent; iteration++)
@@ -942,7 +958,7 @@ internal static partial class PrimeOrderGpuHeuristics
}
reduced /= primeFactor;
if ((reduced).Pow2ModWindowedGpu(divisor.Modulus) == 1UL)
if (Pow2EqualsOneKernel(reduced, divisor))
{
return false;
}
@@ -1004,32 +1020,28 @@ internal static partial class PrimeOrderGpuHeuristics
long candidateCapacity = candidates.Length;
int candidateLimit = candidateCapacity < HeuristicCandidateLimit ? (int)candidateCapacity : HeuristicCandidateLimit;
int candidateCount = BuildCandidatesKernel(order, workFactors, workExponents, factorCount, candidates, stackIndex, stackExponent, stackProduct, candidateLimit);
// Candidate enumeration always yields at least one order on the configured workloads, so the zero-count guard remains
// commented out to eliminate the redundant branch.
// if (candidateCount == 0)
// {
// return false;
// }
if (candidateCount == 0)
{
return false;
}
SortCandidatesKernel(prime, previousOrder, hasPreviousOrder != 0, candidates, candidateCount);
int powBudget = maxPowChecks <= 0 ? candidateCount : maxPowChecks;
// The heuristics always reserve at least one check, so the zero-budget fallback remains commented out.
// if (powBudget <= 0)
// {
// powBudget = candidateCount;
// }
if (powBudget <= 0)
{
powBudget = candidateCount;
}
int powUsed = 0;
for (int i = 0; i < candidateCount && powUsed < powBudget; i++)
{
ulong candidate = candidates[i];
// Candidate generation skips 1 by construction, so the defensive continue stays commented out.
// if (candidate <= 1UL)
// {
// continue;
// }
if (candidate <= 1UL)
{
continue;
}
if (powUsed >= powBudget)
{
@@ -1037,7 +1049,7 @@ internal static partial class PrimeOrderGpuHeuristics
}
powUsed++;
if ((candidate).Pow2ModWindowedGpu(divisor.Modulus) != 1UL)
if (!Pow2EqualsOneKernel(candidate, divisor))
{
continue;
}
@@ -1317,11 +1329,10 @@ internal static partial class PrimeOrderGpuHeuristics
{
ulong primeFactor = factors[i];
int exponent = exponents[i];
// Factorization never yields one for candidate orders, so keep the guard commented out to avoid re-checking inputs.
// if (primeFactor <= 1UL)
// {
// continue;
// }
if (primeFactor <= 1UL)
{
continue;
}
ulong reduced = candidate;
for (int iteration = 0; iteration < exponent; iteration++)
@@ -1338,7 +1349,7 @@ internal static partial class PrimeOrderGpuHeuristics
}
powUsed++;
if ((reduced).Pow2ModWindowedGpu(divisor.Modulus) == 1UL)
if (Pow2EqualsOneKernel(reduced, divisor))
{
return false;
}
@@ -1360,21 +1371,17 @@ internal static partial class PrimeOrderGpuHeuristics
ArrayView1D<ulong, Stride1D.Dense> compositeStack,
ArrayView1D<byte, Stride1D.Dense> statusOut)
{
// Composite candidates sent here are always greater than one during production scans, so the trivial-return branch stays
// commented out.
// if (initial <= 1UL)
// {
// return true;
// }
if (initial <= 1UL)
{
return true;
}
int stackCapacity = (int)compositeStack.Length;
// Pollard-Rho always reserves a positive stack on production workloads, so the overflow guard remains commented out
// to avoid branching here.
// if (stackCapacity <= 0)
// {
// statusOut[0] = (byte)PrimeOrderKernelStatus.PollardOverflow;
// return false;
// }
if (stackCapacity <= 0)
{
statusOut[0] = (byte)PrimeOrderKernelStatus.PollardOverflow;
return false;
}
int stackTop = 0;
compositeStack[stackTop] = initial;
@@ -1384,12 +1391,10 @@ internal static partial class PrimeOrderGpuHeuristics
{
stackTop--;
ulong composite = compositeStack[stackTop];
// Composite values enqueued for factoring are always greater than one on production workloads, so the guard stays
// commented out to avoid extra branching.
// if (composite <= 1UL)
// {
// continue;
// }
if (composite <= 1UL)
{
continue;
}
if (!PeelSmallPrimesKernel(
composite,
@@ -1597,7 +1602,9 @@ internal static partial class PrimeOrderGpuHeuristics
bLocal >>= BitOperations.TrailingZeroCount(bLocal);
if (aLocal > bLocal)
{
(aLocal, bLocal) = (bLocal, aLocal);
ulong temp = aLocal;
aLocal = bLocal;
bLocal = temp;
}
bLocal -= aLocal;
@@ -1608,10 +1615,15 @@ internal static partial class PrimeOrderGpuHeuristics
}
}
private static bool Pow2EqualsOneKernel(ulong exponent, in MontgomeryDivisorData divisor)
{
return exponent.Pow2MontgomeryModWindowed(divisor, keepMontgomery: false) == 1UL;
}
private static ulong CalculateByDoublingKernel(ulong prime)
{
ulong order = 1UL;
ulong value = 2UL; // Candidate primes exceed two, so the modulo would always return two.
ulong value = 2UL % prime;
while (value != 1UL)
{
@@ -1629,11 +1641,10 @@ internal static partial class PrimeOrderGpuHeuristics
private static GpuPow2ModStatus TryPow2ModBatchInternal(ReadOnlySpan<UInt128> exponents, UInt128 prime, Span<UInt128> remainders)
{
// Residue batches are never empty on production workloads, so skip the zero-length success shortcut.
// if (exponents.Length == 0)
// {
// return GpuPow2ModStatus.Success;
// }
if (exponents.Length == 0)
{
return GpuPow2ModStatus.Success;
}
if (remainders.Length < exponents.Length)
{
@@ -1709,30 +1720,27 @@ internal static partial class PrimeOrderGpuHeuristics
private static void ComputePow2ModCpu(ReadOnlySpan<ulong> exponents, ulong prime, in MontgomeryDivisorData divisorData, Span<ulong> results)
{
int length = exponents.Length;
ulong modulus = divisorData.Modulus;
_ = prime; // modulus and divisorData both describe the same q; retain the parameter to preserve the public signature.
for (int i = 0; i < length; i++)
{
results[i] = exponents[i].Pow2ModWindowedCpu(modulus);
results[i] = Pow2ModCpu(exponents[i], prime, divisorData);
}
}
private static bool TryComputeOnGpuWide(ReadOnlySpan<UInt128> exponents, UInt128 prime, Span<UInt128> results)
{
int length = exponents.Length;
// Production launches always carry work, so keep the empty-batch shortcut commented out.
// if (length == 0)
// {
// return true;
// }
if (length == 0)
{
return true;
}
GpuUInt128[]? rentedExponents = null;
GpuUInt128[]? rentedResults = null;
var lease = GpuKernelPool.GetKernel(useGpuOrder: true);
var execution = lease.EnterExecutionScope();
try
{
var execution = lease.EnterExecutionScope();
Accelerator accelerator = lease.Accelerator;
AcceleratorStream stream = lease.Stream;
var kernel = GetPow2ModWideKernel(accelerator);
@@ -1769,6 +1777,7 @@ internal static partial class PrimeOrderGpuHeuristics
exponentBuffer.Dispose();
remainderBuffer.Dispose();
execution.Dispose();
return true;
}
catch (Exception)
@@ -1778,8 +1787,6 @@ internal static partial class PrimeOrderGpuHeuristics
}
finally
{
// Small batches use the stack-allocated fast path, leaving the rented arrays null. Retain the guards so pooled
// buffers are only returned when they were actually leased.
if (rentedExponents is not null)
{
ArrayPool<GpuUInt128>.Shared.Return(rentedExponents, clearArray: false);
@@ -1790,7 +1797,6 @@ internal static partial class PrimeOrderGpuHeuristics
ArrayPool<GpuUInt128>.Shared.Return(rentedResults, clearArray: false);
}
execution.Dispose();
lease.Dispose();
}
}
@@ -1804,6 +1810,16 @@ internal static partial class PrimeOrderGpuHeuristics
}
}
private static ulong Pow2ModCpu(ulong exponent, ulong modulus, in MontgomeryDivisorData divisorData)
{
if (modulus <= 1UL)
{
return 0UL;
}
return exponent.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false);
}
private static Action<AcceleratorStream, Index1D, ArrayView1D<ulong, Stride1D.Dense>, MontgomeryDivisorData, ArrayView1D<ulong, Stride1D.Dense>> GetPow2ModKernel(Accelerator accelerator)
{
return Pow2ModKernelCache.GetOrAdd(accelerator, static accel =>
@@ -1816,17 +1832,8 @@ internal static partial class PrimeOrderGpuHeuristics
private static void Pow2ModKernel(Index1D index, ArrayView1D<ulong, Stride1D.Dense> exponents, MontgomeryDivisorData divisor, ArrayView1D<ulong, Stride1D.Dense> remainders)
{
ulong modulus = divisor.Modulus;
// The by-divisor heuristics call this kernel only with odd prime moduli, so the defensive branch stays commented to
// document the invariant without spending a branch in the kernel.
// if (modulus <= 1UL || (modulus & 1UL) == 0UL)
// {
// remainders[index] = 0UL;
// return;
// }
ulong exponent = exponents[index];
remainders[index] = exponent.Pow2ModWindowedGpu(modulus);
remainders[index] = exponent.Pow2MontgomeryModWindowed(divisor, keepMontgomery: false);
}
private static Action<AcceleratorStream, Index1D, ArrayView1D<GpuUInt128, Stride1D.Dense>, GpuUInt128, ArrayView1D<GpuUInt128, Stride1D.Dense>> GetPow2ModWideKernel(Accelerator accelerator)
@@ -12,16 +12,14 @@ public interface IMersenneNumberDivisorByDivisorTester
ulong GetAllowedMaxDivisor(ulong prime);
bool IsPrime(ulong prime, out bool divisorsExhausted, TimeSpan? timeLimit = null);
bool IsPrime(ulong prime, out bool divisorsExhausted);
IDivisorScanSession CreateDivisorSession();
void PrepareCandidates(ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues);
public interface IDivisorScanSession
public interface IDivisorScanSession : IDisposable
{
void CheckDivisor(ulong divisor, in MontgomeryDivisorData divisorData, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits);
void Dispose();
}
}
+160 -344
View File
@@ -1,4 +1,3 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
@@ -19,12 +18,6 @@ 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()
@@ -74,27 +67,23 @@ public class MersenneDivisorCycles
return true;
}
// Exponent and divisor validation happens during scheduling, so the defensive guard remains commented out to keep the
// hot path branch-free.
// if (exponent <= 1UL || divisor <= 1UL || (divisor & 1UL) == 0UL)
// {
// return false;
// }
if (exponent <= 1UL || divisor <= 1UL || (divisor & 1UL) == 0UL)
{
return false;
}
// Production scans never revisit divisors in the small-cycle snapshot, so skip the legacy lookup here. Factoring and
// diagnostics can query TryMatchSmallCycleForFactoring when they need those cached values.
// if (divisor <= PerfectNumberConstants.MaxQForDivisorCycles)
// {
// var small = Shared._smallCycles;
// if (small is not null)
// {
// ulong cached = small[(int)divisor];
// if (cached != 0UL)
// {
// return cached == exponent;
// }
// }
// }
if (divisor <= PerfectNumberConstants.MaxQForDivisorCycles)
{
var small = Shared._smallCycles;
if (small is not null)
{
ulong cached = small[(int)divisor];
if (cached != 0UL)
{
return cached == exponent;
}
}
}
if (TryCalculateCycleLengthHeuristic(divisor, divisorData, out ulong heuristicCycle) && heuristicCycle != 0UL)
{
@@ -102,7 +91,6 @@ public class MersenneDivisorCycles
}
Dictionary<ulong, FactorCacheEntry>? cache = factorCache;
// Each by-divisor scan session starts with a null cache and fills it lazily, so keep the allocation guard in place.
if (cache is null)
{
cache = new Dictionary<ulong, FactorCacheEntry>(8);
@@ -114,7 +102,7 @@ public class MersenneDivisorCycles
return cycleLength == exponent;
}
return exponent.Pow2ModWindowedCpu(divisorData.Modulus) == 1UL;
return exponent.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL;
}
public static bool CycleEqualsExponentForMersenneCandidate(ulong divisor, in MontgomeryDivisorData divisorData, ulong exponent)
@@ -124,70 +112,35 @@ public class MersenneDivisorCycles
return false;
}
// Candidate divisors on the production path sit well above the precomputed snapshot, so skip the small-cycle lookup
// here and leave factoring scenarios to call TryMatchSmallCycleForFactoring when necessary.
// if (divisor <= PerfectNumberConstants.MaxQForDivisorCycles)
// {
// MersenneDivisorCycles shared = Shared;
// ulong[]? small = shared._smallCycles;
// if (small is not null)
// {
// ulong cached = small[(int)divisor];
// if (cached != 0UL)
// {
// return cached == exponent;
// }
// }
// }
return exponent.Pow2ModWindowedCpu(divisorData.Modulus) == 1UL;
}
public static bool TryMatchSmallCycleForFactoring(ulong divisor, ulong exponent, out ulong cycleLength)
{
if (divisor > PerfectNumberConstants.MaxQForDivisorCycles)
if (divisor <= PerfectNumberConstants.MaxQForDivisorCycles)
{
// Factoring helpers probe arbitrary divisors, so anything above the snapshot limit legitimately misses here.
cycleLength = 0UL;
return false;
MersenneDivisorCycles shared = Shared;
ulong[]? small = shared._smallCycles;
if (small is not null)
{
ulong cached = small[(int)divisor];
if (cached != 0UL)
{
return cached == exponent;
}
}
}
ulong[]? small = Shared._smallCycles;
if (small is null)
{
// Snapshot recycling during diagnostics can temporarily clear the table; surface the miss so callers recompute.
cycleLength = 0UL;
return false;
}
ulong cached = small[(int)divisor];
if (cached == 0UL)
{
// Only a subset of small divisors carry cached cycles; signal the miss so factoring falls back to the GPU helper.
cycleLength = 0UL;
return false;
}
cycleLength = cached;
return cached == exponent;
return exponent.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL;
}
private static bool IsValidMersenneDivisorCandidate(ulong divisor, ulong exponent)
{
// Production workloads pre-filter invalid inputs, so skip the defensive guards here. Factoring and diagnostics should
// validate inputs before calling into the candidate path.
// if (exponent <= 1UL || divisor <= 1UL || (divisor & 1UL) == 0UL)
// {
// return false;
// }
if (exponent <= 1UL || divisor <= 1UL || (divisor & 1UL) == 0UL)
{
return false;
}
UInt128 step = (UInt128)exponent << 1;
// The overflow bail-out is handled by factoring-specific call sites; keep the historical branch commented to document the
// legacy behavior.
// if (step == UInt128.Zero)
// {
// return false;
// }
if (step == UInt128.Zero)
{
return false;
}
UInt128 adjusted = (UInt128)divisor - UInt128.One;
return adjusted % step == UInt128.Zero;
@@ -196,7 +149,7 @@ public class MersenneDivisorCycles
public static IEnumerable<(ulong divisor, ulong cycleLength)> EnumerateStream(Stream compressor)
{
// Binary pairs: (ulong divisor, ulong cycle)
var reader = new BinaryReader(compressor, Encoding.UTF8, leaveOpen: true);
using var reader = new BinaryReader(compressor, Encoding.UTF8, leaveOpen: true);
while (true)
{
ulong d, c;
@@ -207,7 +160,6 @@ public class MersenneDivisorCycles
}
catch (EndOfStreamException)
{
reader.Dispose();
yield break;
}
@@ -282,14 +234,14 @@ public class MersenneDivisorCycles
return (UInt128)cycle;
}
UInt128 wideOrder = PrimeOrderCalculator.Calculate(
PrimeOrderCalculator.PrimeOrderResultWide wideResult = PrimeOrderCalculator.Calculate(
divisor,
previousOrder: null,
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault,
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Cpu);
if (wideOrder != UInt128.Zero)
if (wideResult.Order != UInt128.Zero)
{
return wideOrder;
return wideResult.Order;
}
// Otherwise, find order of 2 mod divisor
@@ -313,93 +265,72 @@ public class MersenneDivisorCycles
}
public static bool TryCalculateCycleLengthForExponent(
ulong divisor,
ulong divisor,
ulong exponent,
in MontgomeryDivisorData divisorData,
Dictionary<ulong, FactorCacheEntry>? factorCache,
out ulong cycleLength)
out ulong cycleLength,
bool allowGpuPow2 = false)
{
cycleLength = 0UL;
// The by-divisor scanner only supplies odd candidates of the form 2*k*exponent + 1,
// so this power-of-two fast-path never triggers on that call path.
// The historical fast-path is left here for reference but commented out because it is unreachable
// on the EvenPerfectBitScanner pipeline.
// if ((divisor & (divisor - 1UL)) == 0UL)
// {
// cycleLength = 1UL;
// return true;
// }
if ((divisor & (divisor - 1UL)) == 0UL)
{
cycleLength = 1UL;
return true;
}
// EvenPerfectBitScanner filters exponents <= 3 before invoking this helper and each
// divisor candidate is at least 2 * exponent + 1, so this guard never matches.
// The fallback calculation is therefore unreachable on the active call path.
// if (divisor <= 3UL || exponent <= 1UL)
// {
// cycleLength = CalculateCycleLength(divisor, divisorData);
// return true;
// }
if (divisor <= 3UL || exponent <= 1UL)
{
cycleLength = CalculateCycleLength(divisor, divisorData);
return true;
}
ulong computedOrder = PrimeOrderCalculator.Calculate(
PrimeOrderCalculator.PrimeOrderResult orderResult = PrimeOrderCalculator.Calculate(
divisor,
previousOrder: null,
divisorData,
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault,
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Cpu);
if (computedOrder != 0UL)
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Cpu,
allowGpuPow2);
if (orderResult.Order != 0UL)
{
cycleLength = computedOrder;
cycleLength = orderResult.Order;
return true;
}
ulong phi = divisor - 1UL;
// Divisors reach this helper only when they exceed the cached table, which means
// divisor >= 2 * exponent + 1 and phi cannot underflow to zero here.
// The defensive underflow guard is kept for posterity but disabled for the EvenPerfectBitScanner path.
// if (phi == 0UL)
// {
// return false;
// }
if (phi == 0UL)
{
return false;
}
int twoCount = BitOperations.TrailingZeroCount(phi);
ulong reducedPhi = phi >> twoCount;
// Every divisor candidate generated by the by-divisor scan satisfies q ≡ 1 (mod 2 * exponent),
// ensuring reducedPhi keeps a factor of exponent and remains non-zero in this context.
// The divisibility check is therefore unreachable for the current caller and is commented out.
// if (reducedPhi == 0UL || (reducedPhi % exponent) != 0UL)
// {
// return false;
// }
if (reducedPhi == 0UL || (reducedPhi % exponent) != 0UL)
{
return false;
}
ulong k = reducedPhi / exponent;
// reducedPhi always contains at least one factor of exponent, so dividing by exponent leaves k >= 1.
// if (k == 0UL)
// {
// return false;
// }
if (k == 0UL)
{
return false;
}
Dictionary<ulong, int> factorCounts = AcquireFactorCountDictionary(8);
try
Dictionary<ulong, int> factorCounts = new(8);
if (twoCount > 0)
{
// 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))
{
return false;
}
cycleLength = ReduceOrder(divisorData, phi, factorCounts, exponent);
return true;
}
finally
if (!AccumulateFactors(exponent, factorCache, factorCounts, cacheResult: true) || !AccumulateFactors(k, factorCache, factorCounts, cacheResult: false))
{
ReleaseFactorCountDictionary(factorCounts);
return false;
}
cycleLength = ReduceOrder(divisorData, phi, factorCounts, allowGpuPow2);
return true;
}
private static bool AccumulateFactors(
@@ -413,119 +344,61 @@ public class MersenneDivisorCycles
return true;
}
FactorCacheEntry cachedFactorization;
PrimeFactorPartialResult? lease = null;
bool fromCache;
try
if (!TryGetFactorization(value, cache, cacheResult, out FactorCacheEntry factorization))
{
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;
return false;
}
finally
ulong[] primes = factorization.Primes;
byte[] exponents = factorization.Exponents;
int length = factorization.Count;
for (int i = 0; i < length; i++)
{
lease?.Dispose();
AddFactor(counts, primes[i], exponents[i]);
}
return true;
}
private static bool TryGetFactorization(
ulong value,
Dictionary<ulong, FactorCacheEntry>? cache,
bool cacheResult,
out FactorCacheEntry factorization,
out PrimeFactorPartialResult? lease,
out bool fromCache)
out FactorCacheEntry factorization)
{
factorization = default;
lease = null;
fromCache = false;
if (value <= 1UL)
{
return true;
}
if (cache is not null && cache.TryGetValue(value, out factorization))
{
fromCache = true;
return true;
}
Dictionary<ulong, int> scratch = AcquireFactorScratchDictionary(8);
try
Dictionary<ulong, int> scratch = new(8);
if (!TryFactorIntoCountsInternal(value, scratch))
{
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;
factorization = default!;
return false;
}
finally
int count = scratch.Count;
ulong[] primes = new ulong[count];
byte[] exponents = new byte[count];
int index = 0;
foreach (KeyValuePair<ulong, int> entry in scratch)
{
ReleaseFactorScratchDictionary(scratch);
primes[index] = entry.Key;
exponents[index] = checked((byte)entry.Value);
index++;
}
Array.Sort(primes, exponents);
factorization = new FactorCacheEntry(primes, exponents, count);
if (cache is not null && cacheResult)
{
cache[value] = factorization;
}
return true;
}
private static bool TryFactorIntoCountsInternal(ulong value, Dictionary<ulong, int> counts)
@@ -600,22 +473,15 @@ public class MersenneDivisorCycles
}
}
private static ulong ReduceOrder(
in MontgomeryDivisorData divisorData,
ulong initialOrder,
Dictionary<ulong, int> factorCounts,
ulong targetOrder)
private static ulong ReduceOrder(in MontgomeryDivisorData divisorData, ulong initialOrder, Dictionary<ulong, int> factorCounts, bool allowGpuPow2)
{
if (factorCounts.Count == 0 || initialOrder == targetOrder)
if (factorCounts.Count == 0)
{
return initialOrder;
}
int count = factorCounts.Count;
bool rentFromPool = count >= PerfectNumberConstants.PooledArrayThreshold;
ulong[] primes = rentFromPool
? ThreadLocalArrayPool<ulong>.Shared.Rent(count)
: new ulong[count];
ulong[] primes = ArrayPool<ulong>.Shared.Rent(count);
try
{
int index = 0;
@@ -641,16 +507,9 @@ public class MersenneDivisorCycles
}
ulong candidate = order / prime;
if (candidate.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL)
if (Pow2EqualsOne(candidate, divisorData, allowGpuPow2))
{
order = candidate;
if (order == targetOrder)
{
// Early-exit once the order matches the tested exponent;
// further factor processing would not change the outcome for the by-divisor scan.
return order;
}
continue;
}
@@ -662,13 +521,30 @@ public class MersenneDivisorCycles
}
finally
{
if (rentFromPool)
{
ThreadLocalArrayPool<ulong>.Shared.Return(primes, clearArray: false);
}
ArrayPool<ulong>.Shared.Return(primes, clearArray: false);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool Pow2EqualsOne(ulong exponent, in MontgomeryDivisorData divisorData, bool allowGpuPow2)
{
if (allowGpuPow2)
{
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(exponent, divisorData.Modulus, out ulong remainder, divisorData);
if (status == GpuPow2ModStatus.Success)
{
return remainder == 1UL;
}
PrimeOrderGpuHeuristics.ReportPow2Failure(
"divisor cycle reduction",
divisorData.Modulus,
status);
}
return exponent.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL;
}
private static ulong PollardRho64(ulong n)
{
if ((n & 1UL) == 0UL)
@@ -738,53 +614,6 @@ 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];
@@ -830,7 +659,7 @@ public class MersenneDivisorCycles
// Start fresh to avoid mixing formats
outputStream.SetLength(0L);
outputStream.Position = 0L;
var writer = new BinaryWriter(outputStream, Encoding.UTF8, leaveOpen: false);
using var writer = new BinaryWriter(outputStream, Encoding.UTF8, leaveOpen: false);
for (var taskIndex = 0; taskIndex < threads; taskIndex++)
{
ulong threadStart = start + (ulong)taskIndex * blockSize;
@@ -851,11 +680,7 @@ public class MersenneDivisorCycles
tasks[taskIndex] = Task.Run(() =>
{
ulong rangeLength = localEnd - localStart + 1UL;
int localLength = checked((int)rangeLength);
bool rentLocalCycles = localLength >= PerfectNumberConstants.PooledArrayThreshold;
(ulong divisor, ulong cycle)[] localCycles = rentLocalCycles
? ThreadLocalArrayPool<(ulong, ulong)>.Shared.Rent(localLength)
: new (ulong divisor, ulong cycle)[localLength];
(ulong divisor, ulong cycle)[] localCycles = ArrayPool<(ulong, ulong)>.Shared.Rent(checked((int)rangeLength));
int localCycleIndex = 0;
try
@@ -903,16 +728,12 @@ public class MersenneDivisorCycles
}
finally
{
if (rentLocalCycles)
{
ThreadLocalArrayPool<(ulong, ulong)>.Shared.Return(localCycles, clearArray: false);
}
ArrayPool<(ulong, ulong)>.Shared.Return(localCycles, clearArray: false);
}
});
}
Task.WaitAll(tasks);
writer.Dispose();
}
public static void GenerateGpu(string path, ulong maxDivisor, int batchSize = 1_000_000, long skipCount = 0L, long nextPosition = 0L)
@@ -922,7 +743,7 @@ public class MersenneDivisorCycles
ulong start = 3UL;
var locker = new object();
var pool = ThreadLocalArrayPool<ulong>.Shared;
var pool = ArrayPool<ulong>.Shared;
ulong batchSizeUL = (ulong)batchSize, d, end;
int count = (int)Math.Min(batchSizeUL, maxDivisor), i, idx;
ulong[] divisors, outCycles, validDivisors;
@@ -949,7 +770,7 @@ public class MersenneDivisorCycles
outputStream.Position = 0L;
}
var writer = new BinaryWriter(outputStream, Encoding.UTF8, leaveOpen: false);
using var writer = new BinaryWriter(outputStream, Encoding.UTF8, leaveOpen: false);
try
{
while (skipCount > 0L && start <= maxDivisor)
@@ -1024,7 +845,6 @@ public class MersenneDivisorCycles
pool.Return(outCycles, clearArray: false);
}
writer.Dispose();
stream.Dispose();
lease.Dispose();
@@ -1126,36 +946,32 @@ public class MersenneDivisorCycles
return CalculateCycleLengthFallback(divisor);
}
internal static bool TryCalculateCycleLengthHeuristic(ulong divisor, in MontgomeryDivisorData divisorData, out ulong cycleLength)
internal static bool TryCalculateCycleLengthHeuristic(ulong divisor, in MontgomeryDivisorData divisorData, out ulong cycleLength, bool allowGpuPow2 = false)
{
// The by-divisor scanner only supplies odd candidates of the form 2*k*exponent + 1,
// so this power-of-two fast-path never triggers on that call path.
// The early exit is retained for reference but commented out because it is unreachable on the scanner path.
// if ((divisor & (divisor - 1UL)) == 0UL)
// {
// cycleLength = 1UL;
// return true;
// }
if ((divisor & (divisor - 1UL)) == 0UL)
{
cycleLength = 1UL;
return true;
}
// By the divisor-by-divisor pipeline, candidates have the shape 2 * exponent * k + 1 with exponent >= 5,
// so the divisor never drops below 11 on this call path.
// if (divisor <= 3UL)
// {
// cycleLength = CalculateCycleLengthFallback(divisor);
// return true;
// }
if (divisor <= 3UL)
{
cycleLength = CalculateCycleLengthFallback(divisor);
return true;
}
if (PrimeTester.IsPrimeInternal(divisor, CancellationToken.None))
{
ulong computedOrder = PrimeOrderCalculator.Calculate(
PrimeOrderCalculator.PrimeOrderResult orderResult = PrimeOrderCalculator.Calculate(
divisor,
previousOrder: null,
divisorData,
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault,
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Cpu);
if (computedOrder != 0UL)
PrimeOrderCalculator.PrimeOrderHeuristicDevice.Cpu,
allowGpuPow2);
if (orderResult.Order != 0UL)
{
cycleLength = computedOrder;
cycleLength = orderResult.Order;
return true;
}
}
@@ -18,8 +18,7 @@ public static class MersenneNumberDivisorByDivisorTester
Action markComposite,
Action clearComposite,
Action<ulong, bool, bool, bool> printResult,
int threadCount,
TimeSpan? primeTestLimit)
int threadCount)
{
if (candidates.Count == 0)
{
@@ -113,12 +112,9 @@ public static class MersenneNumberDivisorByDivisorTester
ulong maxPrime = 0UL;
int primeWriteIndex = 0;
// This implementation is terribly slow, while this method expect prime p given as --filter-p input already.
// We don't need to additionally check it.
// using IEnumerator<ulong> primeEnumerator = Prime.Numbers.GetEnumerator();
// bool hasPrime = primeEnumerator.MoveNext();
// ulong currentPrime = hasPrime ? primeEnumerator.Current : 0UL;
using IEnumerator<ulong> primeEnumerator = Prime.Numbers.GetEnumerator();
bool hasPrime = primeEnumerator.MoveNext();
ulong currentPrime = hasPrime ? primeEnumerator.Current : 0UL;
for (int index = startIndex; index < candidateCount; index++)
{
@@ -131,29 +127,25 @@ public static class MersenneNumberDivisorByDivisorTester
continue;
}
// This implementation is terribly slow, while this method expect prime p given as --filter-p input already.
// We don't need to additionally check it.
// while (hasPrime && currentPrime < candidate)
// {
// hasPrime = primeEnumerator.MoveNext();
// if (!hasPrime)
// {
// break;
// }
while (hasPrime && currentPrime < candidate)
{
hasPrime = primeEnumerator.MoveNext();
if (!hasPrime)
{
break;
}
// currentPrime = primeEnumerator.Current;
// }
currentPrime = primeEnumerator.Current;
}
// if (!hasPrime || currentPrime != candidate)
// if (!Number.IsPrime(candidate))
// {
// markComposite();
// printResult(candidate, false, false, false);
// continue;
// }
if (!hasPrime || currentPrime != candidate)
{
markComposite();
printResult(candidate, false, false, false);
continue;
}
primesSpan[primeWriteIndex++] = candidate;
primesSpan[primeWriteIndex++] = candidate;
if (candidate > maxPrime)
{
@@ -180,13 +172,11 @@ public static class MersenneNumberDivisorByDivisorTester
return;
}
// EvenPerfectBitScanner removes candidates <= 1 before reaching this point,
// so this guard never triggers during production runs.
// if (maxPrime <= 1UL)
// {
// Console.WriteLine("The filter specified by --filter-p must contain at least one prime exponent greater than 1 for --mersenne=bydivisor.");
// return;
// }
if (maxPrime <= 1UL)
{
Console.WriteLine("The filter specified by --filter-p must contain at least one prime exponent greater than 1 for --mersenne=bydivisor.");
return;
}
tester.ConfigureFromMaxPrime(maxPrime);
@@ -246,7 +236,7 @@ public static class MersenneNumberDivisorByDivisorTester
void ProcessPrime(ulong prime)
{
bool isPrime = tester.IsPrime(prime, out bool divisorsExhausted, primeTestLimit);
bool isPrime = tester.IsPrime(prime, out bool divisorsExhausted);
if (!isPrime)
{
@@ -259,52 +249,21 @@ public static class MersenneNumberDivisorByDivisorTester
printResult(prime, true, divisorsExhausted, true);
}
if (workerCount > filteredPrimes.Count)
{
workerCount = filteredPrimes.Count;
}
if (workerCount <= 1)
if (workerCount == 1)
{
foreach (ulong prime in filteredPrimes)
{
ProcessPrime(prime);
}
return;
}
(int Start, int End)[] workerRanges = new (int, int)[workerCount];
int totalPrimeCount = filteredPrimes.Count;
int baseGroupSize = totalPrimeCount / workerCount;
int remaining = totalPrimeCount % workerCount;
int groupStartIndex = 0;
for (int workerIndex = 0; workerIndex < workerCount; workerIndex++)
else
{
int groupSize = baseGroupSize;
if (workerIndex < remaining)
ParallelOptions options = new()
{
groupSize++;
}
MaxDegreeOfParallelism = workerCount
};
startIndex = groupStartIndex;
int endIndex = startIndex + groupSize;
workerRanges[workerIndex] = (startIndex, endIndex);
groupStartIndex = endIndex;
Parallel.ForEach(filteredPrimes, options, ProcessPrime);
}
ParallelOptions parallelOptions = new()
{
MaxDegreeOfParallelism = workerCount
};
Parallel.ForEach(workerRanges, parallelOptions, range =>
{
for (int candidateIndex = range.Start; candidateIndex < range.End; candidateIndex++)
{
ProcessPrime(filteredPrimes[candidateIndex]);
}
});
}
}
@@ -371,6 +371,10 @@ public static class MersennePrimeFactorTester
return 2UL;
}
var rng = new Random(1234567); // TODO: Replace this RNG with the deterministic span-based
// sequence measured faster in PollardRhoBenchmarks so we avoid
// per-call Random allocations.
ulong c, d, x, y, diff;
while (true)
{
@@ -379,8 +383,8 @@ public static class MersennePrimeFactorTester
return 0UL;
}
c = (DeterministicRandom.NextUInt64() % (n - 1UL)) + 1UL;
x = (DeterministicRandom.NextUInt64() % (n - 2UL)) + 2UL;
c = (ulong)rng.Next(2, int.MaxValue);
x = (ulong)rng.Next(2, int.MaxValue);
y = x;
d = 1UL;
@@ -519,6 +523,9 @@ public static class MersennePrimeFactorTester
return 2;
}
var rng = new Random(1234567); // TODO: Replace with the deterministic UInt128 sequence from
// PollardRho128Benchmarks so we eliminate Random allocations.
UInt128 c, d, x, y, diff;
while (true)
{
@@ -527,8 +534,8 @@ public static class MersennePrimeFactorTester
return 0;
}
c = (DeterministicRandom.NextUInt128() % (n - 1)) + 1;
x = (DeterministicRandom.NextUInt128() % (n - 2)) + 2;
c = (UInt128)rng.NextInt64(2, long.MaxValue);
x = (UInt128)rng.NextInt64(2, long.MaxValue);
y = x;
d = 1;
@@ -45,4 +45,32 @@ public readonly struct MontgomeryDivisorData(ulong modulus, ulong nPrime, ulong
montgomeryTwo,
montgomeryTwoSquared);
}
}
internal static class MontgomeryDivisorDataCache
{
[ThreadStatic]
private static bool s_hasCachedValue;
[ThreadStatic]
private static ulong s_cachedModulus;
[ThreadStatic]
private static MontgomeryDivisorData s_cachedData;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static MontgomeryDivisorData Get(ulong modulus)
{
if (s_hasCachedValue && s_cachedModulus == modulus)
{
return s_cachedData;
}
MontgomeryDivisorData computed = MontgomeryDivisorData.FromModulus(modulus);
s_cachedModulus = modulus;
s_cachedData = computed;
s_hasCachedValue = true;
return computed;
}
}
-243
View File
@@ -1,243 +0,0 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core;
/// <summary>
/// Mutable helper for 128-bit arithmetic derived from the .NET runtime UInt128 implementation.
/// Portions adapted from https://github.com/dotnet/runtime/blob/v8.0.0/src/libraries/System.Private.CoreLib/src/System/UInt128.cs.
/// </summary>
public struct MutableUInt128
{
public ulong High;
public ulong Low;
public MutableUInt128(ulong value)
{
Low = value;
}
public MutableUInt128(ulong high, ulong low)
{
High = high;
Low = low;
}
public MutableUInt128(UInt128 value)
{
High = (ulong)(value >> 64);
Low = (ulong)value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Set(ulong value)
{
High = 0UL;
Low = value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Set(UInt128 value)
{
High = (ulong)(value >> 64);
Low = (ulong)value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Set(ulong high, ulong low)
{
High = high;
Low = low;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly UInt128 ToUInt128() => ((UInt128)High << 64) | Low;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(ulong value)
{
ulong low = Low + value;
if (low < value)
{
High++;
}
Low = low;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(MutableUInt128 value)
{
ulong temp = Low + value.Low;
Low = temp;
temp = temp < Low ? 1UL : 0UL;
temp += High;
temp += value.High;
High = temp;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Sub(ulong value)
{
ulong temp = Low;
ulong result = temp - value;
temp = result > temp ? 1UL : 0UL;
Low = result;
High -= temp;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Sub(MutableUInt128 value)
{
ulong temp = Low;
ulong result = temp - value.Low;
temp = temp < value.Low ? 1UL : 0UL;
Low = result;
High = High - value.High - temp;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Mul(ulong value)
{
ulong currentHigh = High;
ulong currentLow = Low;
ulong temp = Mul64(currentLow, value, out currentLow);
Mul64(currentHigh, value, out currentHigh);
temp += currentHigh;
Low = currentLow;
High = temp;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Mul64(ulong left, ulong right, out ulong low)
{
const ulong Mask32 = 0xFFFF_FFFFUL;
ulong leftHigh = left >> 32;
ulong rightLow = right & Mask32;
ulong leftLow = left & Mask32;
ulong lowLow = leftLow * rightLow;
ulong temp = right >> 32;
leftLow *= temp;
rightLow *= leftHigh;
leftHigh *= temp;
temp = (lowLow >> 32) + (leftLow & Mask32) + (rightLow & Mask32);
leftHigh += (leftLow >> 32) + (rightLow >> 32) + (temp >> 32);
low = ((temp & Mask32) << 32) | (lowLow & Mask32);
return leftHigh;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void MulAdd(ulong multiplier, ulong addend)
{
const ulong Mask32 = 0xFFFF_FFFFUL;
ulong multiplierLow = multiplier & Mask32;
ulong multiplierHigh = multiplier >> 32;
ulong product11 = Low;
ulong product01 = product11 & Mask32;
product11 >>= 32;
ulong product00 = product01 * multiplierLow;
product01 *= multiplierHigh;
ulong product10 = product11 * multiplierLow;
product11 *= multiplierHigh;
ulong high = (product00 >> 32) + (product01 & Mask32) + (product10 & Mask32);
product11 = product11 + (product01 >> 32) + (product10 >> 32) + (high >> 32);
product10 = ((high & Mask32) << 32) | (product00 & Mask32);
high = High;
product00 = high & Mask32;
high >>= 32;
product01 = product00 * multiplierLow;
product00 *= multiplierHigh;
high *= multiplierLow;
high = (product01 >> 32) + (product00 & Mask32) + (high & Mask32);
product01 = ((high & Mask32) << 32) | (product01 & Mask32);
// Calculate low. We're assigning addend to local variable for a tiny bit better performance.
product00 = addend;
high = product10 + product00;
Low = high;
// Calculate high including carry
high = high < product00 ? 1UL : 0UL;
high += product11 + product01;
// Discard overflow above 128 bits to match Multiply's truncation semantics.
High = high;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ulong Mod(ulong modulus)
{
// This case will never happen in production code.
// if (modulus == 0UL)
// {
// throw new DivideByZeroException();
// }
ulong high = High;
if (high == 0UL)
{
return Low % modulus;
}
MutableUInt128 remainder = default;
int highBitLength = 64 - BitOperations.LeadingZeroCount(high);
if (highBitLength > 0)
{
ProcessWord(ref remainder, high, highBitLength, modulus);
}
ProcessWord(ref remainder, Low, 64, modulus);
return remainder.Low;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ProcessWord(ref MutableUInt128 remainder, ulong word, int bitCount, ulong modulus)
{
for (int bit = bitCount - 1; bit >= 0; bit--)
{
remainder.ShiftLeft();
if (((word >> bit) & 1UL) != 0)
{
remainder.Add(1UL);
}
remainder.Reduce(modulus);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ShiftLeft()
{
High = (High << 1) | (Low >> 63);
Low <<= 1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Reduce(ulong modulus)
{
if (High != 0UL || Low >= modulus)
{
Sub(modulus);
if (High != 0UL || Low >= modulus)
{
Sub(modulus);
}
}
}
}
@@ -1,94 +0,0 @@
using System;
namespace PerfectNumbers.Core
{
public sealed class PartialFactorResult
{
[ThreadStatic]
private static PartialFactorResult? s_poolHead;
public static readonly PartialFactorResult Empty = new()
{
Cofactor = 1UL,
FullyFactored = true,
};
public FactorEntry[]? Factors;
public ulong Cofactor;
public bool FullyFactored;
public int Count;
private PartialFactorResult? _next;
private bool _factorsFromPool;
private PartialFactorResult()
{
}
public static PartialFactorResult Rent(in FactorEntry[]? factors, ulong cofactor, bool fullyFactored, int count)
{
PartialFactorResult? instance = s_poolHead;
if (instance is null)
{
instance = new PartialFactorResult();
}
else
{
s_poolHead = instance._next;
}
FactorEntry[]? factorArray = factors;
instance._next = null;
instance.Factors = factorArray;
instance.Cofactor = cofactor;
instance.FullyFactored = fullyFactored;
instance.Count = count;
instance._factorsFromPool = factorArray is not null && factorArray.Length >= PerfectNumberConstants.PooledArrayThreshold;
return instance;
}
public PartialFactorResult WithAdditionalPrime(ulong prime)
{
FactorEntry[]? source = Factors;
if (source is null || Count == 0)
{
FactorEntry[] single = new FactorEntry[1];
single[0] = new FactorEntry(prime);
return Rent(single, 1UL, true, 1);
}
int newCount = Count + 1;
FactorEntry[] extended = newCount >= PerfectNumberConstants.PooledArrayThreshold
? ThreadLocalArrayPool<FactorEntry>.Shared.Rent(newCount)
: new FactorEntry[newCount];
Array.Copy(source, 0, extended, 0, Count);
extended[Count] = new FactorEntry(prime);
Span<FactorEntry> span = extended.AsSpan(0, newCount);
span.Sort(static (a, b) => a.Value.CompareTo(b.Value));
return Rent(extended, 1UL, true, newCount);
}
public void Dispose()
{
if (this == Empty)
{
return;
}
FactorEntry[]? factors = Factors;
if (factors is not null)
{
Factors = null;
if (_factorsFromPool)
{
ThreadLocalArrayPool<FactorEntry>.Shared.Return(factors, clearArray: false);
}
}
_factorsFromPool = false;
_next = s_poolHead;
s_poolHead = this;
}
}
}
@@ -8,7 +8,6 @@ public static class PerfectNumberConstants
public const int MaxQForDivisorCycles = 4_000_000;
public static readonly uint PrimesLimit = 1_000_000; //(ulong)Array.MaxLength;// 1_000_000;
public const int MaxOddPowersCount = 128;
public const int PooledArrayThreshold = 64;
// TODO: Load these limits from the benchmark-driven configuration so CPU and GPU scans stay aligned with the optimal
// divisor-cycle datasets we generate offline.
// TODO: Promote these magic numbers into a runtime profile derived from EvenPerfectBitScanner.Benchmarks so we can retune
@@ -22,7 +22,4 @@
<InternalsVisibleTo Include="EvenPerfectBitScanner.Benchmarks" />
</ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project>
@@ -1,150 +0,0 @@
using System;
using System.Buffers;
namespace PerfectNumbers.Core
{
internal sealed class PrimeFactorPartialResult
{
[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)
{
ThreadLocalArrayPool<ulong>.Shared.Return(_primes, clearArray: false);
}
if (_exponents is not null)
{
ThreadLocalArrayPool<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)
{
ThreadLocalArrayPool<ulong>.Shared.Return(_primes, clearArray: false);
}
if (_exponents is not null)
{
ThreadLocalArrayPool<byte>.Shared.Return(_exponents, clearArray: false);
}
_arraysFromPool = false;
_primes = null;
_exponents = null;
}
if (capacity >= PerfectNumberConstants.PooledArrayThreshold)
{
_primes = ThreadLocalArrayPool<ulong>.Shared.Rent(capacity);
_exponents = ThreadLocalArrayPool<byte>.Shared.Rent(capacity);
_arraysFromPool = true;
}
else
{
_primes = new ulong[capacity];
_exponents = new byte[capacity];
}
_capacity = capacity;
}
}
}
@@ -9,15 +9,31 @@ namespace PerfectNumbers.Core;
internal static partial class PrimeOrderCalculator
{
public static UInt128 Calculate(
internal readonly struct PrimeOrderResultWide
{
public PrimeOrderResultWide(PrimeOrderStatus status, UInt128 order)
{
Status = status;
Order = order;
}
public PrimeOrderStatus Status { get; }
public UInt128 Order { get; }
}
public static PrimeOrderResultWide Calculate(UInt128 prime, UInt128? previousOrder, PrimeOrderSearchConfig config)
=> Calculate(prime, previousOrder, config, PrimeOrderHeuristicDevice.Gpu);
public static PrimeOrderResultWide Calculate(
UInt128 prime,
UInt128? previousOrder,
PrimeOrderSearchConfig config,
PrimeOrderHeuristicDevice device)
{
var scope = UsePow2Mode(device);
bool enableGpuPow2 = device == PrimeOrderHeuristicDevice.Gpu;
using var scope = UsePow2Mode(device, enableGpuPow2);
MontgomeryDivisorData divisorData;
UInt128 result;
if (prime <= ulong.MaxValue)
{
ulong? previous = null;
@@ -36,49 +52,41 @@ internal static partial class PrimeOrderCalculator
ulong prime64 = (ulong)prime;
divisorData = MontgomeryDivisorData.FromModulus(prime64);
ulong order64 = Calculate(prime64, previous, divisorData, config, device);
result = order64 == 0UL ? UInt128.Zero : (UInt128)order64;
PrimeOrderResult result = Calculate(prime64, previous, divisorData, config, device, enableGpuPow2);
return new PrimeOrderResultWide(result.Status, result.Order);
}
else
{
divisorData = default;
result = CalculateWideInternal(prime, previousOrder, divisorData, config);
}
scope.Dispose();
return result;
return CalculateWideInternal(prime, previousOrder, divisorData, config);
}
private static UInt128 CalculateWideInternal(UInt128 prime, UInt128? previousOrder, in MontgomeryDivisorData divisorData, PrimeOrderSearchConfig config)
private static PrimeOrderResultWide CalculateWideInternal(UInt128 prime, UInt128? previousOrder, in MontgomeryDivisorData divisorData, PrimeOrderSearchConfig config)
{
if (prime <= UInt128.One)
{
return UInt128.One;
return new PrimeOrderResultWide(PrimeOrderStatus.Found, UInt128.One);
}
if (prime == (UInt128)3UL)
{
return (UInt128)2UL;
return new PrimeOrderResultWide(PrimeOrderStatus.Found, (UInt128)2UL);
}
UInt128 phi = prime - UInt128.One;
var debugScope = UseDebugLogging(!IsGpuHeuristicDevice);
using var debugScope = UseDebugLogging(!IsGpuHeuristicDevice);
PartialFactorResult128 phiFactors = PartialFactorWide(phi, config);
UInt128 result;
if (phiFactors.Factors is null)
{
result = FinishStrictlyWide(prime, divisorData);
}
else
{
result = RunHeuristicPipelineWide(prime, previousOrder, divisorData, config, phi, phiFactors);
return FinishStrictlyWide(prime, divisorData, config.Mode);
}
debugScope.Dispose();
return result;
return RunHeuristicPipelineWide(prime, previousOrder, divisorData, config, phi, phiFactors);
}
private static UInt128 RunHeuristicPipelineWide(
private static PrimeOrderResultWide RunHeuristicPipelineWide(
UInt128 prime,
UInt128? previousOrder,
in MontgomeryDivisorData divisorData,
@@ -88,7 +96,7 @@ internal static partial class PrimeOrderCalculator
{
if (phiFactors.FullyFactored && TrySpecialMaxWide(phi, prime, divisorData, phiFactors))
{
return phi;
return new PrimeOrderResultWide(PrimeOrderStatus.Found, phi);
}
UInt128 candidateOrder = InitializeStartingOrderWide(prime, phi, divisorData);
@@ -96,25 +104,27 @@ internal static partial class PrimeOrderCalculator
if (TryConfirmOrderWide(prime, candidateOrder, divisorData, config))
{
return candidateOrder;
return new PrimeOrderResultWide(PrimeOrderStatus.Found, candidateOrder);
}
if (config.Mode == PrimeOrderMode.Strict)
{
return FinishStrictlyWide(prime, divisorData);
return FinishStrictlyWide(prime, divisorData, PrimeOrderMode.Strict);
}
if (TryHeuristicFinishWide(prime, candidateOrder, previousOrder, divisorData, config, out UInt128 order))
{
return order;
return new PrimeOrderResultWide(PrimeOrderStatus.Found, order);
}
return candidateOrder;
return FinishStrictlyWide(prime, divisorData, config.Mode);
}
private static UInt128 FinishStrictlyWide(UInt128 prime, in MontgomeryDivisorData divisorData)
private static PrimeOrderResultWide FinishStrictlyWide(UInt128 prime, in MontgomeryDivisorData divisorData, PrimeOrderMode mode)
{
return CalculateByFactorizationWide(prime, divisorData);
UInt128 strictOrder = CalculateByFactorizationWide(prime, divisorData);
PrimeOrderStatus status = mode == PrimeOrderMode.Strict ? PrimeOrderStatus.Found : PrimeOrderStatus.HeuristicUnresolved;
return new PrimeOrderResultWide(status, strictOrder);
}
private static bool TrySpecialMaxWide(UInt128 phi, UInt128 prime, in MontgomeryDivisorData divisorData, PartialFactorResult128 factors)
@@ -162,7 +172,7 @@ internal static partial class PrimeOrderCalculator
return order;
}
tempArray = ThreadLocalArrayPool<FactorEntry128>.Shared.Rent(length + 1);
tempArray = ArrayPool<FactorEntry128>.Shared.Rent(length + 1);
Span<FactorEntry128> buffer = tempArray.AsSpan(0, length);
factorSpan.CopyTo(buffer);
@@ -200,7 +210,7 @@ internal static partial class PrimeOrderCalculator
{
if (tempArray is not null)
{
ThreadLocalArrayPool<FactorEntry128>.Shared.Return(tempArray, clearArray: false);
ArrayPool<FactorEntry128>.Shared.Return(tempArray, clearArray: false);
}
}
}
@@ -664,7 +674,7 @@ internal static partial class PrimeOrderCalculator
return new PartialFactorResult128(null, value, false, 0);
}
FactorEntry128[]? rented = counts.Count > 0 ? ThreadLocalArrayPool<FactorEntry128>.Shared.Rent(counts.Count) : null;
FactorEntry128[]? rented = counts.Count > 0 ? ArrayPool<FactorEntry128>.Shared.Rent(counts.Count) : null;
int index = 0;
if (rented is not null)
{
@@ -682,7 +692,7 @@ internal static partial class PrimeOrderCalculator
span.Sort(static (a, b) => a.Value.CompareTo(b.Value));
resultArray = new FactorEntry128[index];
span.CopyTo(resultArray);
ThreadLocalArrayPool<FactorEntry128>.Shared.Return(rented, clearArray: false);
ArrayPool<FactorEntry128>.Shared.Return(rented, clearArray: false);
}
bool fullyFactored = cofactor == UInt128.One;
@@ -1021,6 +1031,11 @@ internal static partial class PrimeOrderCalculator
{
return wideRemainder;
}
PrimeOrderGpuHeuristics.ReportPow2Failure(
"wide prime order pow2 check",
modulus,
wideStatus);
}
return exponent.Pow2MontgomeryModWindowed(modulus);
@@ -1062,20 +1077,15 @@ internal static partial class PrimeOrderCalculator
private readonly struct FactorEntry128
{
public FactorEntry128(UInt128 value)
{
Value = value;
}
public FactorEntry128(UInt128 value, int exponent)
{
Value = value;
Exponent = exponent;
}
public FactorEntry128(UInt128 value, int exponent)
{
Value = value;
Exponent = exponent;
}
public UInt128 Value { get; }
public readonly UInt128 Value;
public readonly int Exponent = 1;
public int Exponent { get; }
}
private readonly struct PartialFactorResult128
@@ -1088,13 +1098,13 @@ internal static partial class PrimeOrderCalculator
Count = count;
}
public readonly FactorEntry128[]? Factors;
public FactorEntry128[]? Factors { get; }
public readonly UInt128 Cofactor;
public UInt128 Cofactor { get; }
public readonly bool FullyFactored;
public bool FullyFactored { get; }
public readonly int Count;
public int Count { get; }
public static PartialFactorResult128 Empty => new(null, UInt128.One, true, 0);
@@ -1102,13 +1112,14 @@ internal static partial class PrimeOrderCalculator
{
if (Factors is null)
{
FactorEntry128[] local = [new FactorEntry128(prime)];
return new PartialFactorResult128(local, UInt128.One, true, 1);
FactorEntry128[] local = new FactorEntry128[1];
local[0] = new FactorEntry128(prime, 1);
return new PartialFactorResult128(local, UInt128.One, true, 1);
}
FactorEntry128[] extended = new FactorEntry128[Count + 1];
Array.Copy(Factors, extended, Count);
extended[Count] = new FactorEntry128(prime);
extended[Count] = new FactorEntry128(prime, 1);
return new PartialFactorResult128(extended, UInt128.One, true, Count + 1);
}
}
File diff suppressed because it is too large. Load diff
-72
View File
@@ -1,72 +0,0 @@
using System;
using System.Diagnostics;
namespace PerfectNumbers.Core;
internal readonly struct PrimeTestTimeLimit
{
private readonly long _deadlineTimestamp;
private PrimeTestTimeLimit(bool isActive, long deadlineTimestamp)
{
IsActive = isActive;
_deadlineTimestamp = deadlineTimestamp;
}
internal bool IsActive { get; }
internal static bool TryCreate(TimeSpan? timeLimit, out PrimeTestTimeLimit limit)
{
if (!timeLimit.HasValue)
{
limit = new PrimeTestTimeLimit(isActive: false, deadlineTimestamp: long.MaxValue);
return true;
}
TimeSpan limitValue = timeLimit.GetValueOrDefault();
if (limitValue <= TimeSpan.Zero)
{
limit = new PrimeTestTimeLimit(isActive: true, deadlineTimestamp: long.MinValue);
return false;
}
double seconds = limitValue.TotalSeconds;
if (double.IsInfinity(seconds))
{
limit = new PrimeTestTimeLimit(isActive: true, deadlineTimestamp: long.MaxValue);
return true;
}
double scaled = seconds * Stopwatch.Frequency;
long limitTicks = scaled >= long.MaxValue ? long.MaxValue : (long)scaled;
long startTimestamp = Stopwatch.GetTimestamp();
long deadline = startTimestamp <= long.MaxValue - limitTicks ? startTimestamp + limitTicks : long.MaxValue;
if (deadline < startTimestamp)
{
deadline = long.MaxValue;
}
limit = new PrimeTestTimeLimit(isActive: true, deadlineTimestamp: deadline);
return true;
}
internal bool HasExpired()
{
if (!IsActive)
{
return false;
}
if (_deadlineTimestamp == long.MaxValue)
{
return false;
}
if (_deadlineTimestamp == long.MinValue)
{
return true;
}
return Stopwatch.GetTimestamp() >= _deadlineTimestamp;
}
}
+1 -1
View File
@@ -176,7 +176,7 @@ public sealed class PrimeTester(bool useInternal = false)
DevicePrimes.View.CopyFromCPU(primes);
}
internal sealed class ScratchBuffers
internal sealed class ScratchBuffers : IDisposable
{
public MemoryBuffer1D<ulong, Stride1D.Dense> Input { get; private set; }
public MemoryBuffer1D<byte, Stride1D.Dense> Output { get; private set; }
+1 -1
View File
@@ -3,7 +3,7 @@ namespace PerfectNumbers.Core;
/// <summary>
/// Provides thread-safe file writing for both text and binary data.
/// </summary>
public sealed class TextFileWriter
public sealed class TextFileWriter : IDisposable
{
private readonly FileStream _stream;
private StreamWriter? _textWriter;
@@ -1,27 +0,0 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core;
internal static class ThreadLocalArrayPool<T>
{
[ThreadStatic]
private static ArrayPool<T>? s_threadPool;
public static ArrayPool<T> Shared
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
ArrayPool<T>? pool = s_threadPool;
if (pool is null)
{
pool = ArrayPool<T>.Create();
s_threadPool = pool;
}
return pool;
}
}
}
+8 -6
View File
@@ -218,7 +218,7 @@ public static class UInt128Extensions
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsBitSet(GpuUInt128 value, int bitIndex)
private static bool IsBitSet(in GpuUInt128 value, int bitIndex)
{
if (bitIndex >= 64)
{
@@ -229,16 +229,18 @@ public static class UInt128Extensions
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong ExtractWindowValue(GpuUInt128 exponent, int windowStart, int windowBitCount)
private static ulong ExtractWindowValue(in GpuUInt128 exponent, int windowStart, int windowBitCount)
{
ulong mask = (1UL << windowBitCount) - 1UL;
if (windowStart != 0)
{
exponent.ShiftRight(windowStart);
return exponent.Low & mask;
GpuUInt128 shifted = exponent;
shifted.ShiftRight(windowStart);
ulong mask = (1UL << windowBitCount) - 1UL;
return shifted.Low & mask;
}
return exponent.Low & mask;
ulong directMask = (1UL << windowBitCount) - 1UL;
return exponent.Low & directMask;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
+27 -548
View File
@@ -1,5 +1,4 @@
using System;
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using ILGPU.Algorithms;
@@ -12,17 +11,13 @@ public static class ULongExtensions
private const int Pow2WindowSize = 8;
private const ulong Pow2WindowFallbackThreshold = 32UL;
private const int Pow2WindowOddPowerCount = 1 << (Pow2WindowSize - 1);
private const int Pow2WindowSizeGpu = 5;
private const int Pow2WindowOddPowerCountGpu = 1 << (Pow2WindowSizeGpu - 1);
public static ulong CalculateOrder(this ulong q)
{
// Order computations only run on admissible divisors, so the small-prime guard remains commented out to keep the
// path branch-free.
// if (q <= 2UL)
// {
// return 0UL;
// }
if (q <= 2UL)
{
return 0UL;
}
ulong order = q - 1UL, prime, temp;
uint[] smallPrimes = PrimesGenerator.SmallPrimes;
@@ -292,220 +287,26 @@ public static class ULongExtensions
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong MulMod64(this ulong a, ulong b, ulong modulus)
{
// Scanner inputs always provide odd prime moduli, so the legacy modulus guard stays commented out to avoid extra
// branching.
// if (modulus <= 1UL)
// {
// return 0UL;
// }
// TODO: Replace this fallback with the UInt128 Montgomery helper measured fastest in
// MulMod64Benchmarks so CPU callers stop paying for triple modulo operations.
public static ulong MulMod64(this ulong a, ulong b, ulong modulus) => (ulong)(UInt128)(((a % modulus) * (b % modulus)) % modulus);
ulong reducedA = a >= modulus ? a % modulus : a;
ulong reducedB = b >= modulus ? b % modulus : b;
GpuUInt128 state = new(reducedA);
return state.MulMod(reducedB, modulus);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong MulMod64GpuCompatible(this ulong a, ulong b, ulong modulus)
{
// TODO: Remove this GPU-compatible shim from production once callers migrate to MulMod64,
// which the benchmarks show is roughly 6-7× faster on dense 64-bit inputs.
GpuUInt128 state = new(a % modulus);
return state.MulMod(b, modulus);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Pow2ModWindowedCpu(this ulong exponent, ulong modulus)
public static ulong MulMod64GpuCompatibleDeferred(this ulong a, ulong b, ulong modulus)
{
// The by-divisor pipeline only schedules odd prime moduli, so the defensive check below
// would never trigger on that path. Leaving it commented documents the invariant without
// forcing a branch in the hot loop.
// if (modulus <= 1UL)
// {
// return 0UL;
// }
// Exponents flow from the strictly positive candidate list on the by-divisor scanner, so
// this guard would be redundant there.
// if (exponent == 0UL)
// {
// return 1UL % modulus;
// }
// Even though the by-divisor pipeline only feeds very large exponents, the legacy diagnostic callers
// still hit this small-exponent fast path, so keep the fallback in place.
if (exponent <= Pow2WindowFallbackThreshold)
{
return Pow2ModBinaryFallback(exponent, modulus);
}
int bitLength = GetPortableBitLength(exponent);
int windowSize = GetWindowSize(bitLength);
int oddPowerCount = 1 << (windowSize - 1);
Span<ulong> oddPowerStorage = oddPowerCount <= PerfectNumberConstants.MaxOddPowersCount
? stackalloc ulong[PerfectNumberConstants.MaxOddPowersCount]
: new ulong[oddPowerCount];
oddPowerStorage = oddPowerStorage[..oddPowerCount];
InitializePlainOddPowers(modulus, oddPowerStorage);
// With modulus guaranteed to exceed one on the scanning path, the modulo operation would
// always yield one, so keep the literal to highlight the intended base value.
ulong result = 1UL;
int index = bitLength - 1;
while (index >= 0)
{
if (((exponent >> index) & 1UL) == 0UL)
{
result = result.MulMod64(result, modulus);
index--;
continue;
}
int windowStart = index - windowSize + 1;
if (windowStart < 0)
{
windowStart = 0;
}
while (((exponent >> windowStart) & 1UL) == 0UL)
{
windowStart++;
}
int windowLength = index - windowStart + 1;
for (int square = 0; square < windowLength; square++)
{
result = result.MulMod64(result, modulus);
}
ulong windowValue = (exponent >> windowStart) & ((1UL << windowLength) - 1UL);
int tableIndex = (int)((windowValue - 1UL) >> 1);
result = result.MulMod64(oddPowerStorage[tableIndex], modulus);
index = windowStart - 1;
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Pow2ModBinaryFallback(ulong exponent, ulong modulus)
{
// The scanning path keeps modulus strictly greater than one, but other utility callers still
// rely on this fallback for tiny exponents, so leave the modulo in place for safety.
ulong remainingExponent = exponent;
// Production moduli are always odd primes greater than two, so normalize the seed values directly.
ulong result = 1UL;
ulong baseValue = 2UL;
while (remainingExponent != 0UL)
{
if ((remainingExponent & 1UL) != 0UL)
{
result = result.MulMod64(baseValue, modulus);
}
remainingExponent >>= 1;
if (remainingExponent == 0UL)
{
break;
}
baseValue = baseValue.MulMod64(baseValue, modulus);
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Pow2ModWindowedGpu(this ulong exponent, ulong modulus)
{
// The GPU pipeline feeds only odd prime moduli, so the removed guard would never fire for
// EvenPerfectBitScanner workloads.
// if (modulus <= 1UL)
// {
// return 0UL;
// }
// The GPU pipeline only emits positive exponents on production workloads, so skip the zero guard to avoid an extra branch.
// Diagnostic harnesses must normalize their inputs before invoking this helper.
// if (exponent == 0UL)
// {
// return 1UL;
// }
// The GPU kernels mostly process large exponents as well, but the binary fallback still serves
// diagnostic harnesses that probe edge cases around the threshold.
if (exponent <= Pow2WindowFallbackThreshold)
{
return Pow2ModBinaryGpuFallback(exponent, modulus);
}
int bitLength = GetPortableBitLength(exponent);
int windowSize = GetGpuWindowSize(bitLength);
int oddPowerCount = 1 << (windowSize - 1);
PlainOddPowerTableGpu oddPowers = new PlainOddPowerTableGpu(modulus, oddPowerCount);
ulong result = 1UL;
int index = bitLength - 1;
while (index >= 0)
{
if (((exponent >> index) & 1UL) == 0UL)
{
result = result.MulMod64(result, modulus);
index--;
continue;
}
int windowStart = index - windowSize + 1;
if (windowStart < 0)
{
windowStart = 0;
}
while (((exponent >> windowStart) & 1UL) == 0UL)
{
windowStart++;
}
int windowLength = index - windowStart + 1;
for (int square = 0; square < windowLength; square++)
{
result = result.MulMod64(result, modulus);
}
ulong windowValue = (exponent >> windowStart) & ((1UL << windowLength) - 1UL);
int tableIndex = (int)((windowValue - 1UL) >> 1);
result = result.MulMod64(oddPowers.Get(tableIndex), modulus);
index = windowStart - 1;
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Pow2ModBinaryGpuFallback(ulong exponent, ulong modulus)
{
ulong remainingExponent = exponent;
// Production moduli are always odd primes greater than two, so normalize the seed values directly.
ulong result = 1UL;
ulong baseValue = 2UL;
while (remainingExponent != 0UL)
{
if ((remainingExponent & 1UL) != 0UL)
{
result = result.MulMod64(baseValue, modulus);
}
remainingExponent >>= 1;
if (remainingExponent == 0UL)
{
break;
}
baseValue = baseValue.MulMod64(baseValue, modulus);
}
return result;
// TODO: Move this deferred helper to the benchmark suite; the baseline MulMod64 avoids the
// 5-40× slowdown seen across real-world operand distributions.
GpuUInt128 state = new(a);
return state.MulModWithNativeModulo(b, modulus);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -525,14 +326,12 @@ public static class ULongExtensions
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Pow2MontgomeryModWindowedCpu(this ulong exponent, in MontgomeryDivisorData divisor, bool keepMontgomery)
public static ulong Pow2MontgomeryModWindowed(this ulong exponent, in MontgomeryDivisorData divisor, bool keepMontgomery)
{
ulong modulus = divisor.Modulus;
if (exponent == 0UL)
{
return keepMontgomery
? divisor.MontgomeryOne
: 1UL; // Montgomery moduli stay above one on the scanning path, so the modulo would always yield one.
return keepMontgomery ? divisor.MontgomeryOne : 1UL % modulus;
}
if (exponent <= Pow2WindowFallbackThreshold)
@@ -546,101 +345,7 @@ public static class ULongExtensions
ulong result = divisor.MontgomeryOne;
ulong nPrime = divisor.NPrime;
Span<ulong> oddPowerStorage = oddPowerCount <= PerfectNumberConstants.MaxOddPowersCount
? stackalloc ulong[PerfectNumberConstants.MaxOddPowersCount]
: default;
ulong[]? pooledOddPowers = null;
if (oddPowerCount <= PerfectNumberConstants.MaxOddPowersCount)
{
oddPowerStorage = oddPowerStorage[..oddPowerCount];
}
else if (oddPowerCount < PerfectNumberConstants.PooledArrayThreshold)
{
oddPowerStorage = new ulong[oddPowerCount];
}
else
{
pooledOddPowers = ArrayPool<ulong>.Shared.Rent(oddPowerCount);
oddPowerStorage = pooledOddPowers;
}
Span<ulong> oddPowers = oddPowerStorage[..oddPowerCount];
InitializeMontgomeryOddPowersCpu(divisor, modulus, nPrime, oddPowers);
int index = bitLength - 1;
while (index >= 0)
{
if (((exponent >> index) & 1UL) == 0UL)
{
result = result.MontgomeryMultiply(result, modulus, nPrime);
index--;
continue;
}
int windowStart = index - windowSize + 1;
if (windowStart < 0)
{
windowStart = 0;
}
while (((exponent >> windowStart) & 1UL) == 0UL)
{
windowStart++;
}
int windowLength = index - windowStart + 1;
for (int square = 0; square < windowLength; square++)
{
result = result.MontgomeryMultiply(result, modulus, nPrime);
}
ulong mask = (1UL << windowLength) - 1UL;
ulong windowValue = (exponent >> windowStart) & mask;
int tableIndex = (int)((windowValue - 1UL) >> 1);
ulong multiplier = oddPowers[tableIndex];
result = result.MontgomeryMultiply(multiplier, modulus, nPrime);
index = windowStart - 1;
}
if (pooledOddPowers is not null)
{
ArrayPool<ulong>.Shared.Return(pooledOddPowers);
}
if (keepMontgomery)
{
return result;
}
return result.MontgomeryMultiply(1UL, modulus, nPrime);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Pow2MontgomeryModWindowedGpu(this ulong exponent, in MontgomeryDivisorData divisor, bool keepMontgomery)
{
ulong modulus = divisor.Modulus;
if (exponent == 0UL)
{
return keepMontgomery
? divisor.MontgomeryOne
: 1UL; // Montgomery moduli stay above one on the scanning path, so the modulo would always yield one.
}
if (exponent <= Pow2WindowFallbackThreshold)
{
return Pow2MontgomeryModSingleBit(exponent, divisor, keepMontgomery);
}
int bitLength = GetPortableBitLength(exponent);
int windowSize = GetWindowSize(bitLength);
int oddPowerCount = 1 << (windowSize - 1);
ulong result = divisor.MontgomeryOne;
ulong nPrime = divisor.NPrime;
ulong[] oddPowers = InitializeMontgomeryOddPowersGpu(divisor, modulus, nPrime, oddPowerCount);
ulong[] oddPowers = InitializeMontgomeryOddPowers(divisor, modulus, nPrime, oddPowerCount);
int index = bitLength - 1;
while (index >= 0)
@@ -756,219 +461,7 @@ public static class ULongExtensions
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetGpuWindowSize(int bitLength)
{
int window = GetWindowSize(bitLength);
return window > Pow2WindowSizeGpu ? Pow2WindowSizeGpu : window;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void InitializePlainOddPowers(ulong modulus, Span<ulong> destination)
{
int oddPowerCount = destination.Length;
// The by-divisor kernels always request at least one odd power, but other helpers reuse this initializer
// for smaller workloads, so the early exit stays in place.
if (oddPowerCount == 0)
{
return;
}
ulong baseValue = 2UL; // Moduli arrive as odd primes greater than two, so the modulo would always return two.
destination[0] = baseValue;
if (oddPowerCount == 1)
{
return;
}
ulong square = baseValue.MulMod64(baseValue, modulus);
for (int i = 1; i < oddPowerCount; i++)
{
// Reuse baseValue to accumulate successive odd powers and avoid allocating another local.
baseValue = baseValue.MulMod64(square, modulus);
destination[i] = baseValue;
}
}
private struct PlainOddPowerTableGpu
{
private readonly int _count;
private ulong _value0;
private ulong _value1;
private ulong _value2;
private ulong _value3;
private ulong _value4;
private ulong _value5;
private ulong _value6;
private ulong _value7;
private ulong _value8;
private ulong _value9;
private ulong _value10;
private ulong _value11;
private ulong _value12;
private ulong _value13;
private ulong _value14;
private ulong _value15;
public PlainOddPowerTableGpu(ulong modulus, int requestedCount)
{
_count = requestedCount > Pow2WindowOddPowerCountGpu ? Pow2WindowOddPowerCountGpu : requestedCount;
_value0 = 0UL;
_value1 = 0UL;
_value2 = 0UL;
_value3 = 0UL;
_value4 = 0UL;
_value5 = 0UL;
_value6 = 0UL;
_value7 = 0UL;
_value8 = 0UL;
_value9 = 0UL;
_value10 = 0UL;
_value11 = 0UL;
_value12 = 0UL;
_value13 = 0UL;
_value14 = 0UL;
_value15 = 0UL;
// Window planning always requests at least one odd power, so keep the zero-count check commented out to avoid
// branching here.
// if (_count == 0)
// {
// return;
// }
ulong baseValue = 2UL;
_value0 = baseValue;
if (_count == 1)
{
return;
}
ulong square = baseValue.MulMod64(baseValue, modulus);
ulong current = baseValue;
for (int i = 1; i < _count; i++)
{
current = current.MulMod64(square, modulus);
SetValue(i, current);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong Get(int index)
{
// Windowed lookups never exceed the populated table bounds on production workloads, so leave the check commented
// out to remove the branch.
// if ((uint)index >= (uint)_count)
// {
// return 0UL;
// }
return index switch
{
0 => _value0,
1 => _value1,
2 => _value2,
3 => _value3,
4 => _value4,
5 => _value5,
6 => _value6,
7 => _value7,
8 => _value8,
9 => _value9,
10 => _value10,
11 => _value11,
12 => _value12,
13 => _value13,
14 => _value14,
_ => _value15,
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetValue(int index, ulong value)
{
switch (index)
{
case 1:
_value1 = value;
break;
case 2:
_value2 = value;
break;
case 3:
_value3 = value;
break;
case 4:
_value4 = value;
break;
case 5:
_value5 = value;
break;
case 6:
_value6 = value;
break;
case 7:
_value7 = value;
break;
case 8:
_value8 = value;
break;
case 9:
_value9 = value;
break;
case 10:
_value10 = value;
break;
case 11:
_value11 = value;
break;
case 12:
_value12 = value;
break;
case 13:
_value13 = value;
break;
case 14:
_value14 = value;
break;
case 15:
_value15 = value;
break;
}
}
}
private static void InitializeMontgomeryOddPowersCpu(in MontgomeryDivisorData divisor, ulong modulus, ulong nPrime, Span<ulong> destination)
{
int oddPowerCount = destination.Length;
if (oddPowerCount == 0)
{
return;
}
bool computedOnGpu = oddPowerCount <= PerfectNumberConstants.MaxOddPowersCount
&& MontgomeryOddPowerGpu.TryCompute(divisor, oddPowerCount, destination);
if (computedOnGpu)
{
return;
}
destination[0] = divisor.MontgomeryTwo;
if (oddPowerCount == 1)
{
return;
}
ulong square = divisor.MontgomeryTwoSquared;
ulong current = destination[0];
for (int i = 1; i < oddPowerCount; i++)
{
current = current.MontgomeryMultiply(square, modulus, nPrime);
destination[i] = current;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong[] InitializeMontgomeryOddPowersGpu(in MontgomeryDivisorData divisor, ulong modulus, ulong nPrime, int oddPowerCount)
private static ulong[] InitializeMontgomeryOddPowers(in MontgomeryDivisorData divisor, ulong modulus, ulong nPrime, int oddPowerCount)
{
ulong[] oddPowers = new ulong[PerfectNumberConstants.MaxOddPowersCount];
oddPowers[0] = divisor.MontgomeryTwo;
@@ -988,31 +481,17 @@ public static class ULongExtensions
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Pow2MontgomeryModWithCycleCpu(this ulong exponent, ulong cycleLength, in MontgomeryDivisorData divisor)
public static ulong Pow2MontgomeryModWithCycle(this ulong exponent, ulong cycleLength, in MontgomeryDivisorData divisor)
{
ulong rotationCount = exponent % cycleLength;
return Pow2MontgomeryModWindowedCpu(rotationCount, divisor, keepMontgomery: false);
return Pow2MontgomeryModWindowed(rotationCount, divisor, keepMontgomery: false);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Pow2MontgomeryModFromCycleRemainderCpu(this ulong reducedExponent, in MontgomeryDivisorData divisor)
public static ulong Pow2MontgomeryModFromCycleRemainder(this ulong reducedExponent, in MontgomeryDivisorData divisor)
{
return Pow2MontgomeryModWindowedCpu(reducedExponent, divisor, keepMontgomery: false);
return Pow2MontgomeryModWindowed(reducedExponent, divisor, keepMontgomery: false);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Pow2MontgomeryModWithCycleGpu(this ulong exponent, ulong cycleLength, in MontgomeryDivisorData divisor)
{
ulong rotationCount = exponent % cycleLength;
return Pow2MontgomeryModWindowedGpu(rotationCount, divisor, keepMontgomery: false);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Pow2MontgomeryModFromCycleRemainderGpu(this ulong reducedExponent, in MontgomeryDivisorData divisor)
{
return Pow2MontgomeryModWindowedGpu(reducedExponent, divisor, keepMontgomery: false);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static UInt128 PowMod(this ulong exponent, UInt128 modulus)
{
+4 -7
View File
@@ -335,13 +335,10 @@ if (patchResults.Any(static r => !r.Success))
static List<PatchFile> ParsePatchFromFile(string patchFilePath)
{
var stream = File.OpenRead(patchFilePath);
var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 16_384, leaveOpen: false);
var lineReader = new PatchLineReader(reader);
using var stream = File.OpenRead(patchFilePath);
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 16_384, leaveOpen: false);
using var lineReader = new PatchLineReader(reader);
var files = ParsePatch(lineReader);
lineReader.Dispose();
reader.Dispose();
stream.Dispose();
return files;
}
@@ -503,7 +500,7 @@ static void FinalizePatchFile(
files.Add(new PatchFile(originalPath, modifiedPath, hunks ?? new List<PatchHunk>(), type));
}
private sealed class PatchLineReader
private sealed class PatchLineReader : IDisposable
{
private const int ReadBufferSize = 4096;
private const int InitialLineBufferSize = 256;
+13
View File
@@ -122,6 +122,7 @@ PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:203:
PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:207: // TODO: When divisorCycle is zero (cache miss), compute only that single cycle on the device
PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:214: // TODO: Remove this no-cycle branch and require on-demand cycle computation (without
PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:240: // TODO: Replace this linear increment with the batched divisor-cycle walker validated in the
PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:262: : prime.Pow2MontgomeryMod(divisorData); // TODO: Switch to the upcoming ProcessEightBitWindows helper once the
PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:283: // TODO: Replace this modulo with the ring-buffer style counter (subtract loop) used in the fast CLI
PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:356: // TODO: Replace this guard with an on-demand CPU/GPU cycle computation path that skips
PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:396: // TODO: Remove this no-cycle fallback once divisor cycles are mandatory; the per-prime pow2 ladder here
@@ -265,6 +266,7 @@ todo_list.txt:121:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTeste
todo_list.txt:122:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:207: // TODO: When divisorCycle is zero (cache miss), compute only that single cycle on the device
todo_list.txt:123:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:214: // TODO: Remove this no-cycle branch and require on-demand cycle computation (without
todo_list.txt:124:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:240: // TODO: Replace this linear increment with the batched divisor-cycle walker validated in the
todo_list.txt:125:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:262: : prime.Pow2MontgomeryMod(divisorData); // TODO: Switch to the upcoming ProcessEightBitWindows helper once the
todo_list.txt:126:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:283: // TODO: Replace this modulo with the ring-buffer style counter (subtract loop) used in the fast CLI
todo_list.txt:127:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:356: // TODO: Replace this guard with an on-demand CPU/GPU cycle computation path that skips
todo_list.txt:128:PerfectNumbers.Core\Cpu\MersenneNumberDivisorByDivisorCpuTester.cs:396: // TODO: Remove this no-cycle fallback once divisor cycles are mandatory; the per-prime pow2 ladder here
@@ -405,6 +407,17 @@ PerfectNumbers.Core\ModResidueTracker.cs:257: // TODO: Us
PerfectNumbers.Core\ModResidueTracker.cs:263: // TODO: Consult MersenneDivisorCycles.Shared (or similar caches) here so we reuse precomputed cycle lengths
PerfectNumbers.Core\ModResidueTracker.cs:295: // TODO: Route this through the planned UInt128 windowed powmod so the inner MulMod adopts the
PerfectNumbers.Core\ModResidueTracker.cs:314: // TODO: Replace this double-and-add fallback with the UInt128 intrinsic-backed multiplier once the
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:49: set => _useDivisorCycles = value; // TODO: Remove this flag once GPU runs unconditionally fetch cycle lengths so every divisor check benefits from the measured speedups.
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:298: // TODO: Stage MontgomeryDivisorData in a GPU-ready cache keyed by divisor so we can copy
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:372: // TODO: Replace the block lease with a direct single-cycle lookup so the GPU path
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:395: // TODO: Drop this block hand-off once the cache exposes a direct
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:409: // TODO: Replace this `%` with the cycle-cache helper that subtracts precomputed
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:425: // TODO: Trigger an immediate GPU computation for the single missing cycle without storing
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:476: // TODO: Preload ProcessEightBitWindows-friendly divisor metadata for GPU batches so this
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:563: hits[index] = exponent.Pow2MontgomeryMod(divisor) == 1UL ? (byte)1 : (byte)0; // TODO: Replace Pow2MontgomeryMod with the ProcessEightBitWindows helper once available so GPU by-divisor scans reuse the benchmarked fast pow2 ladder.
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:632: // TODO: Replace this guard with an on-demand cycle computation that runs on the device selected
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:787: // TODO: Replace this `%` with the divisor-cycle remainder helper so we reuse cached
PerfectNumbers.Core\Gpu\MersenneNumberDivisorByDivisorGpuTester.cs:858: results[index] = exponent.Pow2MontgomeryModMontgomery(divisor); // TODO: Swap this Montgomery-domain pow2 with the windowed helper slated to replace the slower ladder in Pow2Minus1Mod benchmarks.
PerfectNumbers.Core\MersenneNumberDivisorByDivisorTester.cs:43: // TODO: Swap this per-run enumerator creation for the shared PrimeIterator so by-divisor scans reuse the
PerfectNumbers.Core\MersenneNumberDivisorByDivisorTester.cs:219: // TODO: Swap Task.Factory.StartNew for the shared low-overhead work queue once the scanner's
PerfectNumbers.Core\MersenneNumberDivisorByDivisorTester.cs:240: bool useDivisorCycles = tester.UseDivisorCycles; // TODO: Remove the conditional path once all testers always enable divisor cycles to keep every divisor scan on the faster cached-length track.
+4 -8
View File
@@ -54,11 +54,10 @@ static ProbeResult ProbeBigInteger(Accelerator accelerator)
try
{
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<int, Stride1D.Dense>>(BigIntegerKernel);
var buffer = accelerator.Allocate1D<int>(1);
using var buffer = accelerator.Allocate1D<int>(1);
var extent = new Index1D((int)buffer.Length);
kernel(extent, buffer.View);
accelerator.Synchronize();
buffer.Dispose();
return ProbeResult.Success(
"System.Numerics.BigInteger",
"Addition, subtraction, multiplication, division, and modulo compile on the CPU accelerator.");
@@ -74,10 +73,9 @@ static ProbeResult ProbeEInteger(Accelerator accelerator)
try
{
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<int, Stride1D.Dense>>(EIntegerKernel);
var buffer = accelerator.Allocate1D<int>(1);
using var buffer = accelerator.Allocate1D<int>(1);
kernel(new Index1D((int)buffer.Length), buffer.View);
accelerator.Synchronize();
buffer.Dispose();
return ProbeResult.Success(
"PeterO.Numbers.EInteger",
"Addition, subtraction, multiplication, division, and remainder compile on the CPU accelerator.");
@@ -95,7 +93,7 @@ static ProbeResult ProbeERational(Accelerator accelerator)
try
{
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<int, Stride1D.Dense>>(ERationalKernel);
var buffer = accelerator.Allocate1D<int>(1);
using var buffer = accelerator.Allocate1D<int>(1);
kernel(new Index1D((int)buffer.Length), buffer.View);
accelerator.Synchronize();
@@ -103,7 +101,6 @@ static ProbeResult ProbeERational(Accelerator accelerator)
details += hasRemainder
? " ERational exposes a remainder API that requires further investigation."
: " ERational does not expose a remainder/modulo API.";
buffer.Dispose();
return ProbeResult.Success("PeterO.Numbers.ERational", details);
}
catch (Exception ex)
@@ -123,10 +120,9 @@ static ProbeResult ProbeOpenNumericPrimesIsPrime(Accelerator accelerator)
try
{
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<int, Stride1D.Dense>>(OpenNumericPrimesIsPrimeKernel);
var buffer = accelerator.Allocate1D<int>(4);
using var buffer = accelerator.Allocate1D<int>(4);
kernel(new Index1D((int)buffer.Length), buffer.View);
accelerator.Synchronize();
buffer.Dispose();
return ProbeResult.Success(
"Open.Numeric.Primes.Prime.Numbers.IsPrime",
"IsPrime compiles and runs on the CPU accelerator kernel.");
@@ -8,7 +8,4 @@
<ItemGroup>
<ProjectReference Include="../../PerfectNumbers.Core/PerfectNumbers.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project>
</Project>