Compare commits

..
Author SHA1 Message Date
mlemanczyk 38b3df9405 GPU calculations disabled again 2025-10-17 05:09:13 +02:00
mlemanczyk d30a915890 Various optimizations 2025-10-17 05:07:11 +02:00
mlemanczyk 91dd373f43 Capacity increased 2025-10-17 05:06:44 +02:00
Marek Lemańczyk 6d82f5a608 Enable GPU execution for windowed Montgomery pow2 2025-10-17 04:11:16 +02:00
mlemanczyk f5c79c8807 Merge branch 'codex/analyze-gc-instances-in-checkdivisors-method' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into codex/analyze-gc-instances-in-checkdivisors-method 2025-10-17 03:51:09 +02:00
mlemanczyk b76ef0b11a Cache capacity initialized for better performance 2025-10-17 03:51:06 +02:00
Marek Lemańczyk 3b759c724e Merge pull request #182 from mlemanczyk/codex/locate-commit-adding-residue-tracking-arrays
Remove small prime stepper remainder cache
2025-10-17 03:50:20 +02:00
Marek Lemańczyk 534141e5a7 Remove small prime stepper remainder cache 2025-10-17 03:50:09 +02:00
Marek Lemańczyk 36a4617efe Merge pull request #181 from mlemanczyk/codex/revert-changes-from-commit-1b03fb3
Restore shared primality cache locking in CPU calculator
2025-10-17 03:30:54 +02:00
Marek Lemańczyk b985a46ed6 Restore cross-thread primality cache 2025-10-17 03:30:41 +02:00
mlemanczyk 56e413064a Using cleanup 2025-10-17 02:40:51 +02:00
mlemanczyk 495b3918c1 Compilation errors fixed 2025-10-17 02:40:15 +02:00
Marek Lemańczyk a0154dd587 Merge pull request #180 from mlemanczyk/codex/refactor-primeordercalculator-to-partial-class
Recycle small prime stepper arrays
2025-10-17 02:34:53 +02:00
Marek Lemańczyk ca0f498d0d Recycle small prime stepper arrays 2025-10-17 02:34:41 +02:00
mlemanczyk 06b08708d9 Merge branch 'codex/analyze-gc-instances-in-checkdivisors-method' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into codex/analyze-gc-instances-in-checkdivisors-method 2025-10-17 01:24:44 +02:00
Marek Lemańczyk 26894db181 Merge pull request #179 from mlemanczyk/codex/optimize-partialfactor-method-in-primeordercalculator
Hoist Pollard Rho deadline checks outside factoring loop
2025-10-17 01:15:04 +02:00
Marek Lemańczyk b81fe3de75 Hoist Pollard Rho deadline checks outside factoring loop 2025-10-17 01:14:51 +02:00
mlemanczyk d6a1fa0b51 Tiny cleanup 2025-10-17 01:05:13 +02:00
mlemanczyk 0a2e65e190 Small optimizations 2025-10-17 00:29:53 +02:00
mlemanczyk e63fba4df1 Merge branch 'codex/analyze-gc-instances-in-checkdivisors-method' of https://github.com/mlemanczyk/even-perfect-numbers-scanner into codex/analyze-gc-instances-in-checkdivisors-method 2025-10-17 00:11:47 +02:00
Marek Lemańczyk 14fcd715c1 Merge pull request #178 from mlemanczyk/codex/optimize-primeordercalculator-small-prime-factors
Optimize CPU small prime exponent extraction
2025-10-17 00:11:06 +02:00
mlemanczyk 1b03fb311a Cache switched to thread static for better performance 2025-10-17 00:10:49 +02:00
Marek Lemańczyk 1ce1622388 Optimize CPU small prime exponent extraction 2025-10-17 00:01:43 +02:00
mlemanczyk 140c291344 Small optimizations to limit code branches 2025-10-16 23:37:06 +02:00
Marek Lemańczyk b5f8ed786c Merge pull request #177 from mlemanczyk/codex/naprawic-bad-kompilacji-math.divrem
Replace invalid DivRem usage in prime factor extraction
2025-10-16 22:01:44 +02:00
Marek Lemańczyk ab86fc16fe Fix quotient and remainder calculation in factor extraction 2025-10-16 22:01:34 +02:00
Marek Lemańczyk 806dc96f8c Merge pull request #176 from mlemanczyk/codex/replace-concurrentdictionary-with-dictionary-and-lock
Refine primality caching and small factor loop
2025-10-16 21:54:29 +02:00
Marek Lemańczyk d2e7e4905a Streamline primality cache locking 2025-10-16 21:52:42 +02:00
Marek Lemańczyk 8d9b8b1829 Replace primality cache with lock-backed dictionary 2025-10-16 21:51:42 +02:00
Marek Lemańczyk e8c1069a77 Merge pull request #175 from mlemanczyk/codex/optimize-evenperfectbitscanner-for-reduced-gc
Reduce GC pressure in divisor checking pipeline
2025-10-16 21:33:52 +02:00
mlemanczyk 4c81f9e5a2 IsPrime cache reused 2025-10-16 21:21:40 +02:00
mlemanczyk 10751bf841 Concurrent dictionary added for IsPrime results caching to utilize the repetition of composites. 2025-10-16 21:10:58 +02:00
mlemanczyk aa4ff799f3 Small optimizations 2025-10-16 19:46:25 +02:00
mlemanczyk 07395830a5 Unnecessary cleanup commented out 2025-10-16 18:13:28 +02:00
Marek Lemańczyk e53bb6ba43 Track small-prime remainders with cycle steppers 2025-10-16 17:18:27 +02:00
Marek Lemańczyk fb08a61c66 Restore pooled small factor buffers 2025-10-16 17:18:19 +02:00
Marek Lemańczyk 949b5f891c Return windowed Montgomery odd powers to pooling 2025-10-16 04:52:22 +02:00
Marek Lemańczyk 3e7ee6ab46 Restore pooled buffers in partial factorization 2025-10-16 04:52:17 +02:00
Marek Lemańczyk ff724732f8 Allow test mode to seed by-divisor runs 2025-10-16 03:42:55 +02:00
Marek Lemańczyk 03751c27be Add timed test mode for prime candidate scans 2025-10-16 03:17:27 +02:00
Marek Lemańczyk 9c474fc241 Use stack buffers for small composite collections 2025-10-16 00:59:00 +02:00
Marek Lemańczyk 3e7bffc812 Refactor heuristic pooling to cut GC pressure 2025-10-16 00:37:01 +02:00
Marek Lemańczyk 0a23502441 Clear heuristic candidate list without try-finally 2025-10-15 23:57:46 +02:00
Marek Lemańczyk 51730bcba3 Reduce GC pressure in divisor checking pipeline 2025-10-15 23:43:27 +02:00
Marek Lemańczyk 571b5a9938 Replace divisor scan UInt128 usage with thread-static GpuUInt128 2025-10-15 22:48:36 +02:00
Marek Lemańczyk 9ab87a8b9b Streamline factor cache lease usage in CPU tester 2025-10-15 21:55:30 +02:00
Marek Lemańczyk 64c3721db4 Streamline divisor factor pooling for CPU hot path 2025-10-15 21:50:11 +02:00
Marek Lemańczyk c99aebdf68 Restore divisor cycle fallback and enforce session disposal guard 2025-10-15 21:29:24 +02:00
Marek Lemańczyk 763714307f Eliminate GC allocations from CPU divisor scan hot path 2025-10-15 20:48:42 +02:00
Marek Lemańczyk b0ce7152f7 Pool factor cache entries to keep CPU factoring allocation-free 2025-10-15 20:48:33 +02:00
Marek Lemańczyk 34fcd62c23 Eliminate GC pressure from divisor cycle factoring 2025-10-15 19:32:53 +02:00
Marek Lemańczyk c8f8132db7 Align divisor pooling with review feedback 2025-10-15 19:32:45 +02:00
Marek Lemańczyk e44662413a Use thread-static pools in CPU divisor testing 2025-10-15 18:10:39 +02:00
Marek Lemańczyk a70113a99d Merge pull request #173 from mlemanczyk/codex/analyze-and-reduce-gc-cost-in-primeordercalculator
Reduce Pollard Rho stopwatch allocations to lower GC pressure
2025-10-15 04:39:59 +02:00
Marek Lemańczyk 0b831b5a23 Reduce PartialFactor GC pressure 2025-10-15 04:39:46 +02:00
Marek Lemańczyk 77bc2601f1 Merge pull request #172 from mlemanczyk/codex/add-max-prime-parameter-to-evenperfectbitscanner
Add max prime filter option
2025-10-15 03:31:10 +02:00
Marek Lemańczyk 0055be366d Add max prime filter option 2025-10-15 03:30:57 +02:00
mlemanczyk 2f85636a2a Tiny cleanup 2025-10-15 03:21:38 +02:00
mlemanczyk d3695a3dc3 Pools made thread static. Various small optimizations. "in" parameter added to read-only method parameters 2025-10-15 02:17:13 +02:00
mlemanczyk 7c71d5165e TODO added 2025-10-13 21:16:03 +02:00
mlemanczyk e9373dd8c9 PollardRho extended to allow running with 10_240 threads. Configuration made static to prevent unnecessary GC pressure. 2025-10-13 20:44:15 +02:00
mlemanczyk 313286f70d Configuration adjusted 2025-10-13 20:13:44 +02:00
80 changed files with 4183 additions and 6198 deletions

No files matched your search

+5
View File
@@ -106,11 +106,16 @@ There is only one agents file - this one. Don't search for another one.
Always use indentation with four space characters in source code files, even if a file currently uses tabs or appears to rely on eight-space indentation. Increase indentation level by exactly one indentation unit (four spaces) per nested block. When parameters or long expressions wrap, indent the continuation with two indentation units for clarity.
Explicitly prefer reusing existing variables once their previous values are no longer required instead of declaring new locals of the same type. Call out the variable reuse in comments when its meaning changes so the intent stays clear and register pressure remains minimal.
Do not introduce try/finally blocks solely to release pooled resources. Restructure the code so that every return path releases the resources without relying on finally unless the control flow would become unclear or unmaintainable.
Keep the divisor cycle cache limited to a single block in memory (only the on-disk snapshot). When a lookup misses, compute the required cycle on the configured device and discard the transient result instead of scheduling background generation or caching additional blocks.
All code, comments, commit messages, branch names and PR descriptions must be written in American English.
- When modifying existing files, preserve all relevant TODO notes and explanatory comments unless they are no longer valid.
- Prefer defining data holders as structs or classes with public fields over auto-properties when performance on the hot path is a concern.
- Do not add or keep methods that simply wrap another method call without adding any logic; inline those calls or extend the wrapper with meaningful work.
## Performance implementation checklist
- Prefer dedicated kernels over flag-controlled or parameterized kernels. Avoid conditional branches in kernels and tight loops by creating separate kernels when necessary.
@@ -15,7 +15,4 @@
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
</ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project>
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
@@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
@@ -1,7 +1,6 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
@@ -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;
}
}
}
@@ -1,9 +1,7 @@
using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using System.Numerics;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
@@ -1,6 +1,4 @@
using System;
using System.Buffers.Binary;
using System.Threading;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -1,6 +1,4 @@
using System;
using System.Buffers.Binary;
using System.Numerics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
+8 -16
View File
@@ -1,5 +1,3 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
@@ -8,22 +6,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>
@@ -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>
+309 -150
View File
@@ -1,5 +1,4 @@
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -44,7 +43,11 @@ internal static class Program
private static string? _resultsPrefix;
private static readonly Optimized PrimeIterator = new();
private static ulong _byDivisorStartPrime;
private static TimeSpan? _primeTestLimit;
private static bool _testMode;
private static int _testTargetPrimeCount;
private static int _testProcessedPrimeCount;
private const string TestTimeFileName = "even_perfect_test_time.txt";
[ThreadStatic]
private static bool _lastCompositeP;
@@ -75,8 +78,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
@@ -84,6 +85,8 @@ internal static class Program
int scanBatchSize = 2_097_152, sliceSize = 32;
ulong residueKMax = 5_000_000UL;
string filterFile = string.Empty;
UInt128 maxPrimeLimit = UInt128.MaxValue;
bool maxPrimeConfigured = false;
string cyclesPath = DefaultCyclesPath;
int cyclesBatchSize = 512;
bool continueCyclesGeneration = false;
@@ -96,6 +99,10 @@ internal static class Program
ulong remainder = 0UL;
bool startPrimeProvided = false;
_testMode = false;
_testTargetPrimeCount = 0;
_testProcessedPrimeCount = 0;
// NTT backend selection (GPU): reference vs staged
for (; argIndex < args.Length; argIndex++)
{
@@ -112,6 +119,20 @@ internal static class Program
currentP = ulong.Parse(arg.AsSpan(arg.IndexOf('=') + 1));
startPrimeProvided = true;
}
else if (arg.StartsWith("--max-prime=", StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> value = arg.AsSpan(arg.IndexOf('=') + 1);
if (UInt128.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out UInt128 parsedMaxPrime))
{
maxPrimeLimit = parsedMaxPrime;
maxPrimeConfigured = true;
}
else
{
Console.WriteLine("Invalid value for --max-prime.");
return;
}
}
else if (arg.Equals("--increment=bit", StringComparison.OrdinalIgnoreCase))
{
useBitTransform = true;
@@ -184,55 +205,14 @@ 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;
}
else if (arg.Equals("--test", StringComparison.OrdinalIgnoreCase))
{
_testMode = true;
}
else if (arg.Equals("--use-order", StringComparison.OrdinalIgnoreCase))
{
useOrder = true;
@@ -425,7 +405,18 @@ internal static class Program
}
}
GpuKernelPool.CpuCycleUsesGpu = orderOnGpu;
int testPrimeCandidateLimit = 0;
if (_testMode)
{
currentP = 31UL;
remainder = currentP % 6UL;
filterFile = string.Empty;
maxPrimeLimit = UInt128.MaxValue;
maxPrimeConfigured = false;
startPrimeProvided = true;
testPrimeCandidateLimit = Math.Max(1, threadCount) * 3;
}
if (useByDivisor)
{
@@ -436,7 +427,7 @@ internal static class Program
_byDivisorStartPrime = 0UL;
}
if (useByDivisor && string.IsNullOrEmpty(filterFile))
if (useByDivisor && !_testMode && string.IsNullOrEmpty(filterFile))
{
Console.WriteLine("--mersenne=bydivisor requires --filter-p=<path>.");
return;
@@ -567,11 +558,6 @@ internal static class Program
? new MersenneNumberDivisorByDivisorGpuTester()
: new MersenneNumberDivisorByDivisorCpuTester();
_byDivisorTester.BatchSize = scanBatchSize;
if (_byDivisorTester is MersenneNumberDivisorByDivisorCpuTester cpuByDivisorTester)
{
cpuByDivisorTester.DeltasDevice = byDivisorDeltasDevice;
cpuByDivisorTester.MontgomeryDevice = byDivisorMontgomeryDevice;
}
}
// Load RLE blacklist (optional)
@@ -607,18 +593,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))
{
@@ -659,50 +645,101 @@ internal static class Program
File.WriteAllText(ResultsFileName, $"p,searchedMersenne,detailedCheck,passedAllTests{Environment.NewLine}");
}
bool useFilter = !string.IsNullOrEmpty(filterFile);
bool useFilter = !_testMode && !string.IsNullOrEmpty(filterFile);
HashSet<ulong> filter = [];
List<ulong> byDivisorCandidates = [];
ulong maxP = 0UL;
ulong[] localFilter = Array.Empty<ulong>();
int filterCount = 0;
if (useFilter)
if (_useByDivisorMode)
{
Console.WriteLine("Loading filter...");
if (_useByDivisorMode)
if (_testMode)
{
byDivisorCandidates = LoadByDivisorCandidates(filterFile);
byDivisorCandidates = BuildTestPrimeCandidates(testPrimeCandidateLimit);
}
else
{
// TODO: Rent this filter buffer from ArrayPool<ulong> and reuse it across reload batches so
// we do not allocate fresh arrays while replaying large result filters.
localFilter = new ulong[1024];
filterCount = 0;
LoadResultsFile(filterFile, (p, detailedCheck, passedAllTests) =>
Console.WriteLine("Loading filter...");
int skippedByLimit = 0;
byDivisorCandidates = LoadByDivisorCandidates(filterFile, maxPrimeLimit, maxPrimeConfigured, out skippedByLimit);
if (maxPrimeConfigured)
{
if (passedAllTests)
if (skippedByLimit > 0)
{
localFilter[filterCount++] = p;
if (p > maxP)
{
maxP = p;
}
if (filterCount == 1024)
{
filter.AddRange(localFilter[..filterCount]);
filterCount = 0;
Console.WriteLine($"Added {p}");
}
Console.WriteLine($"Skipped {skippedByLimit} by-divisor candidate(s) above --max-prime limit.");
}
});
if (filterCount > 0)
{
filter.AddRange(localFilter[..filterCount]);
if (byDivisorCandidates.Count == 0)
{
Console.WriteLine("No by-divisor candidates fall within the --max-prime limit.");
return;
}
}
}
}
else if (useFilter)
{
Console.WriteLine("Loading filter...");
// TODO: Rent this filter buffer from ArrayPool<ulong> and reuse it across reload batches so
// we do not allocate fresh arrays while replaying large result filters.
localFilter = new ulong[1024];
filterCount = 0;
bool addedWithinLimit = false;
int skippedByLimit = 0;
LoadResultsFile(filterFile, (p, detailedCheck, passedAllTests) =>
{
if (!passedAllTests)
{
return;
}
if (maxPrimeConfigured && (UInt128)p > maxPrimeLimit)
{
skippedByLimit++;
return;
}
localFilter[filterCount++] = p;
addedWithinLimit = true;
if (p > maxP)
{
maxP = p;
}
if (filterCount == 1024)
{
filter.AddRange(localFilter[..filterCount]);
filterCount = 0;
Console.WriteLine($"Added {p}");
}
});
if (filterCount > 0)
{
filter.AddRange(localFilter[..filterCount]);
}
if (maxPrimeConfigured)
{
if (!addedWithinLimit)
{
Console.WriteLine("No filter candidates fall within the --max-prime limit.");
return;
}
if (skippedByLimit > 0)
{
Console.WriteLine($"Skipped {skippedByLimit} filter candidate(s) above --max-prime limit.");
}
}
}
if (_testMode)
{
_testTargetPrimeCount = testPrimeCandidateLimit;
}
if (_primeCount >= 2)
{
@@ -716,13 +753,25 @@ internal static class Program
// Configure batch size for GPU primality sieve
PrimeTester.GpuBatchSize = gpuPrimeBatch;
Stopwatch? stopwatch = null;
if (_testMode)
{
stopwatch = Stopwatch.StartNew();
}
Console.WriteLine("Starting scan...");
if (_useByDivisorMode)
{
RunByDivisorMode(byDivisorCandidates, threadCount, _primeTestLimit);
RunByDivisorMode(byDivisorCandidates, threadCount);
FlushBuffer();
StringBuilderPool.Return(_outputBuilder!);
if (stopwatch is not null)
{
stopwatch.Stop();
ReportTestTime(stopwatch.Elapsed);
}
return;
}
@@ -737,7 +786,14 @@ internal static class Program
}
}
if (_testMode)
{
testPrimeCandidateLimit = Math.Max(1, threadCount) * 3;
_testTargetPrimeCount = testPrimeCandidateLimit;
}
_state = ((long)currentP << 3) | (long)remainder;
Task[] tasks = new Task[threadCount];
int taskIndex = 0;
@@ -807,13 +863,71 @@ internal static class Program
}
Task.WaitAll(tasks);
TimeSpan testElapsed = default;
bool reportTestTime = false;
if (stopwatch is not null)
{
stopwatch.Stop();
testElapsed = stopwatch.Elapsed;
reportTestTime = true;
}
FlushBuffer();
StringBuilderPool.Return(_outputBuilder!);
if (reportTestTime)
{
ReportTestTime(testElapsed);
}
}
private static List<ulong> LoadByDivisorCandidates(string candidateFile)
private static List<ulong> BuildTestPrimeCandidates(int targetCount)
{
if (targetCount <= 0)
{
return [];
}
List<ulong> candidates = new(targetCount);
ulong previous = 29UL;
int remaining = targetCount;
ulong nextPrime = 0UL;
while (remaining > 0)
{
try
{
nextPrime = PrimeIterator.Next(in previous);
}
catch (InvalidOperationException)
{
break;
}
if (nextPrime < 31UL)
{
previous = nextPrime;
continue;
}
candidates.Add(nextPrime);
remaining--;
previous = nextPrime;
}
if (remaining > 0)
{
Console.WriteLine("Unable to populate the requested number of test primes before reaching the 64-bit limit.");
}
return candidates;
}
private static List<ulong> LoadByDivisorCandidates(string candidateFile, UInt128 maxPrimeLimit, bool maxPrimeConfigured, out int skippedByLimit)
{
List<ulong> candidates = [];
skippedByLimit = 0;
using FileStream readStream = new(candidateFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using StreamReader reader = new(readStream);
string? line;
@@ -853,7 +967,14 @@ internal static class Program
// arrive so by-divisor candidate loading avoids per-value string allocations.
if (ulong.TryParse(span[start..index], NumberStyles.None, CultureInfo.InvariantCulture, out ulong parsed))
{
candidates.Add(parsed);
if (!maxPrimeConfigured || (UInt128)parsed <= maxPrimeLimit)
{
candidates.Add(parsed);
}
else
{
skippedByLimit++;
}
}
}
}
@@ -861,7 +982,7 @@ internal static class Program
return candidates;
}
private static void RunByDivisorMode(List<ulong> candidates, int threadCount, TimeSpan? primeTestLimit)
private static void RunByDivisorMode(List<ulong> candidates, int threadCount)
{
if (_byDivisorTester is null)
{
@@ -878,8 +999,7 @@ internal static class Program
static () => _lastCompositeP = true,
static () => _lastCompositeP = false,
PrintResult,
threadCount,
primeTestLimit);
threadCount);
}
private static unsafe void LoadResultsFile(string resultsFileName, Action<ulong, bool, bool> lineProcessorAction)
@@ -946,57 +1066,13 @@ 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]");
Console.WriteLine();
Console.WriteLine("Options:");
Console.WriteLine(" --prime=<value> starting exponent (p)");
Console.WriteLine(" --max-prime=<value> inclusive upper bound for primes from filter files");
Console.WriteLine(" --increment=bit|add exponent increment method");
Console.WriteLine(" --threads=<value> number of worker threads");
Console.WriteLine(" --block-size=<value> values processed per thread batch");
@@ -1024,9 +1100,6 @@ 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
@@ -1093,6 +1166,21 @@ internal static class Program
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void OnPrimeCandidateConfirmed()
{
if (!_testMode)
{
return;
}
int processed = Interlocked.Increment(ref _testProcessedPrimeCount);
if (processed == _testTargetPrimeCount)
{
Volatile.Write(ref _limitReached, true);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryAcquireConsoleSlot(bool primeCandidate, out bool primeFlag)
{
@@ -1229,6 +1317,75 @@ internal static class Program
}
}
private static void ReportTestTime(TimeSpan elapsed)
{
double elapsedSeconds = elapsed.TotalSeconds;
double elapsedMilliseconds = elapsed.TotalMilliseconds;
Console.WriteLine($"Test elapsed time: {elapsedSeconds:F3} s");
string filePath = GetTestTimeFilePath();
bool hasPrevious = TryReadTestTime(filePath, out double previousMilliseconds);
string elapsedText = elapsedMilliseconds.ToString("F3", CultureInfo.InvariantCulture);
if (!hasPrevious)
{
WriteTestTime(filePath, elapsedMilliseconds);
Console.WriteLine($"FIRST TIME: {elapsedText} ms");
return;
}
string previousText = previousMilliseconds.ToString("F3", CultureInfo.InvariantCulture);
if (elapsedMilliseconds < previousMilliseconds)
{
WriteTestTime(filePath, elapsedMilliseconds);
Console.WriteLine($"BETTER TIME: {elapsedText} ms (previous {previousText} ms)");
}
else
{
Console.WriteLine($"WORSE TIME: {elapsedText} ms (best {previousText} ms)");
}
}
private static string GetTestTimeFilePath()
{
if (string.IsNullOrEmpty(_resultsDir))
{
return TestTimeFileName;
}
return Path.Combine(_resultsDir!, TestTimeFileName);
}
private static bool TryReadTestTime(string filePath, out double milliseconds)
{
if (!File.Exists(filePath))
{
milliseconds = 0.0;
return false;
}
string content = File.ReadAllText(filePath).Trim();
if (content.Length == 0)
{
milliseconds = 0.0;
return false;
}
return double.TryParse(content, NumberStyles.Float, CultureInfo.InvariantCulture, out milliseconds);
}
private static void WriteTestTime(string filePath, double milliseconds)
{
string? directory = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
string formatted = milliseconds.ToString("F6", CultureInfo.InvariantCulture);
File.WriteAllText(filePath, formatted);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static ulong CountOnes(ulong value)
{
@@ -1459,6 +1616,8 @@ internal static class Program
_lastCompositeP = true;
return false;
}
OnPrimeCandidateConfirmed();
}
searchedMersenne = true;
@@ -1,10 +1,7 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using FluentAssertions;
using ILGPU;
using ILGPU.Runtime;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
using Xunit;
@@ -1,8 +1,6 @@
using System;
using System.Numerics;
using System.Reflection;
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
using Xunit;
@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using PerfectNumbers.Core;
using Xunit;
namespace PerfectNumbers.Core.Tests;
@@ -26,8 +23,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 +56,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;
@@ -71,7 +67,7 @@ public class MersenneNumberDivisorByDivisorTesterTests
return new DummySession();
}
public void PrepareCandidates(ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues)
public void PrepareCandidates(in ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues)
{
for (int i = 0; i < primes.Length; i++)
{
@@ -81,7 +77,7 @@ public class MersenneNumberDivisorByDivisorTesterTests
private sealed class DummySession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
{
public void CheckDivisor(ulong divisor, in MontgomeryDivisorData divisorData, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits)
public void CheckDivisor(ulong divisor, in MontgomeryDivisorData divisorData, ulong divisorCycle, in ReadOnlySpan<ulong> primes, Span<byte> hits)
{
throw new NotSupportedException();
}
@@ -1,4 +1,3 @@
using System;
using FluentAssertions;
using PerfectNumbers.Core.Cpu;
using Xunit;
@@ -7,88 +6,78 @@ namespace PerfectNumbers.Core.Tests;
public class MersenneNumberDivisorCpuTesterTests
{
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_tester_tracks_divisors_across_primes()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(11UL);
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_tester_tracks_divisors_across_primes()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(11UL);
tester.IsPrime(5UL, out bool divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
tester.IsPrime(5UL, out bool divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
tester.IsPrime(7UL, out 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();
}
tester.IsPrime(11UL, out divisorsExhausted).Should().BeFalse();
divisorsExhausted.Should().BeTrue();
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_prime_test_limit_disables_divisor_exhaustion()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(11UL);
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_checks_divisors_across_primes()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(13UL);
tester.IsPrime(5UL, out bool divisorsExhausted, TimeSpan.Zero).Should().BeTrue();
divisorsExhausted.Should().BeFalse();
}
using var session = tester.CreateDivisorSession();
ulong[] primes = { 5UL, 7UL, 11UL, 13UL };
byte[] hits = new byte[primes.Length];
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_checks_divisors_across_primes()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(13UL);
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 });
using var session = tester.CreateDivisorSession();
ulong[] primes = { 5UL, 7UL, 11UL, 13UL };
byte[] hits = new byte[primes.Length];
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 });
}
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 });
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_seven_as_composite()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(43UL);
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 });
}
using var session = tester.CreateDivisorSession();
ulong[] exponents = { 6UL, 7UL, 9UL, 10UL, 12UL };
byte[] hits = new byte[exponents.Length];
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_seven_as_composite()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(43UL);
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(7UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(7UL, divisorData);
session.CheckDivisor(7UL, divisorData, cycle, exponents, hits);
using var session = tester.CreateDivisorSession();
ulong[] exponents = { 6UL, 7UL, 9UL, 10UL, 12UL };
byte[] hits = new byte[exponents.Length];
hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 });
}
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(7UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(7UL, divisorData);
session.CheckDivisor(7UL, divisorData, cycle, exponents, hits);
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_eleven_as_composite()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(61UL);
hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 });
}
using var session = tester.CreateDivisorSession();
ulong[] exponents = { 10UL, 11UL, 20UL, 21UL, 30UL };
byte[] hits = new byte[exponents.Length];
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_eleven_as_composite()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(61UL);
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(11UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(11UL, divisorData);
session.CheckDivisor(11UL, divisorData, cycle, exponents, hits);
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(new byte[] { 1, 0, 1, 0, 1 });
}
}
@@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Cpu;
using PerfectNumbers.Core.Gpu;
using Xunit;
@@ -86,17 +83,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()
@@ -22,7 +22,4 @@
<ItemGroup>
<ProjectReference Include="../PerfectNumbers.Core/PerfectNumbers.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project>
@@ -1,5 +1,3 @@
using System.Collections.Generic;
using System.Numerics;
using FluentAssertions;
using Xunit;
@@ -7,9 +5,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 };
@@ -200,5 +195,4 @@ public class PrimeOrderCalculatorTests
return order;
}
*/
}
@@ -1,6 +1,5 @@
using System.Numerics;
using FluentAssertions;
using PerfectNumbers.Core;
using Xunit;
namespace PerfectNumbers.Core.Tests;
@@ -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,
}
@@ -0,0 +1,30 @@
namespace PerfectNumbers.Core.Cpu;
internal struct FactorCacheLease
{
private Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? _cache;
public void EnsureInitialized(ref Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? cache)
{
Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? current = cache;
if (current is null)
{
current = ThreadStaticPools.RentMersenneFactorCacheDictionary();
cache = current;
}
_cache = current;
}
public void Dispose()
{
Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? cache = _cache;
if (cache is null)
{
return;
}
ThreadStaticPools.ReturnMersenneFactorCacheDictionary(cache);
_cache = null;
}
}
@@ -0,0 +1,82 @@
namespace PerfectNumbers.Core.Cpu;
internal sealed class MersenneCpuDivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
{
private bool _disposed;
public void Reset()
{
_disposed = false;
}
public void CheckDivisor(
ulong divisor,
in MontgomeryDivisorData divisorData,
ulong divisorCycle,
in ReadOnlySpan<ulong> primes,
Span<byte> hits)
{
int length = primes.Length;
// EvenPerfectBitScanner always provides at least one exponent on this path.
// Keep the guard commented out for tests that might reuse the session differently.
// if (length == 0)
// {
// return;
// }
MontgomeryDivisorData cachedData = divisorData;
if (cachedData.Modulus != divisor)
{
cachedData = MontgomeryDivisorDataCache.Get(divisor);
}
if (divisorCycle == 0UL)
{
divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
if (divisorCycle == 0UL)
{
hits.Clear();
return;
}
}
// Keep these remainder steppers in place so future updates continue reusing the previously computed residues.
// They are critical for avoiding repeated full Montgomery exponentiation work when scanning divisors.
var exponentStepper = new ExponentRemainderStepper(cachedData);
if (!exponentStepper.IsValidModulus)
{
hits.Clear();
return;
}
var cycleStepper = new CycleRemainderStepper(divisorCycle);
ulong remainder = cycleStepper.Initialize(primes[0]);
hits[0] = remainder == 0UL
? (exponentStepper.ComputeNextIsUnity(primes[0]) ? (byte)1 : (byte)0)
: (byte)0;
for (int i = 1; i < length; i++)
{
remainder = cycleStepper.ComputeNext(primes[i]);
if (remainder != 0UL)
{
hits[i] = 0;
continue;
}
hits[i] = exponentStepper.ComputeNextIsUnity(primes[i]) ? (byte)1 : (byte)0;
}
}
public void Dispose()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(MersenneCpuDivisorScanSession), "Divisor scan session disposed twice.");
}
_disposed = true;
ThreadStaticPools.ReturnMersenneCpuDivisorSession(this);
}
}
@@ -1,9 +1,4 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using PerfectNumbers.Core;
using System.Runtime.CompilerServices;
using PerfectNumbers.Core.Gpu;
namespace PerfectNumbers.Core.Cpu;
@@ -11,120 +6,65 @@ namespace PerfectNumbers.Core.Cpu;
public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDivisorByDivisorTester
{
private readonly object _sync = new();
private readonly ConcurrentBag<DivisorScanSession> _sessionPool = new();
private ulong _divisorLimit;
private ulong _lastStatusDivisor;
private bool _isConfigured;
private int _batchSize = 1_024;
private ulong _configuredMaxPrime;
private ByDivisorDeltasDevice _deltasDevice = ByDivisorDeltasDevice.Cpu;
private ByDivisorMontgomeryDevice _montgomeryDevice = ByDivisorMontgomeryDevice.Cpu;
[ThreadStatic]
private static GpuUInt128WorkSet _divisorScanGpuWorkSet;
private struct GpuUInt128WorkSet
{
public GpuUInt128 Step;
public GpuUInt128 Divisor;
public GpuUInt128 Limit;
}
public int BatchSize
{
get => _batchSize;
set
{
int sanitized = Math.Max(1, value);
_batchSize = sanitized;
}
}
public ByDivisorDeltasDevice DeltasDevice
{
get => _deltasDevice;
set
{
lock (_sync)
{
_deltasDevice = value;
}
}
}
public ByDivisorMontgomeryDevice MontgomeryDevice
{
get => _montgomeryDevice;
set
{
lock (_sync)
{
_montgomeryDevice = value;
}
}
set => _batchSize = Math.Max(1, value);
}
public void ConfigureFromMaxPrime(ulong maxPrime)
{
lock (_sync)
{
_configuredMaxPrime = maxPrime;
_divisorLimit = ComputeDivisorLimitFromMaxPrime(maxPrime);
_lastStatusDivisor = 0UL;
_isConfigured = true;
}
_divisorLimit = ComputeDivisorLimitFromMaxPrime(maxPrime);
_lastStatusDivisor = 0UL;
}
public ulong DivisorLimit
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
lock (_sync)
{
if (!_isConfigured)
{
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
return _divisorLimit;
}
return _divisorLimit;
}
}
public ulong GetAllowedMaxDivisor(ulong prime)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ulong GetAllowedMaxDivisor(ulong prime) => ComputeAllowedMaxDivisor(prime, _divisorLimit);
public bool IsPrime(ulong prime, out bool divisorsExhausted)
{
lock (_sync)
{
if (!_isConfigured)
{
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
return ComputeAllowedMaxDivisor(prime, _divisorLimit);
}
}
public bool IsPrime(ulong prime, out bool divisorsExhausted, TimeSpan? timeLimit = null)
{
ulong allowedMax;
lock (_sync)
{
if (!_isConfigured)
{
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit);
}
ulong allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit);
if (allowedMax < 3UL)
{
// EvenPerfectBitScanner routes primes below the small-divisor cutoff to the GPU path, so the CPU path still sees
// trivial candidates during targeted tests. Short-circuit here to keep those runs aligned with the production flow.
divisorsExhausted = true;
return true;
}
ulong processedCount;
ulong lastProcessed;
bool processedAll;
bool composite = CheckDivisors(
prime,
allowedMax,
out lastProcessed,
out processedAll,
out processedCount,
timeLimit);
out processedCount);
if (processedCount > 0UL)
{
@@ -144,26 +84,11 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return true;
}
public void PrepareCandidates(ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues)
public void PrepareCandidates(in ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues)
{
if (allowedMaxValues.Length < primes.Length)
{
throw new ArgumentException("allowedMaxValues span must be at least as long as primes span.", nameof(allowedMaxValues));
}
ulong divisorLimit;
lock (_sync)
{
if (!_isConfigured)
{
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
divisorLimit = _divisorLimit;
}
for (int index = 0; index < primes.Length; index++)
ulong divisorLimit = _divisorLimit;
int length = primes.Length;
for (int index = 0; index < length; index++)
{
allowedMaxValues[index] = ComputeAllowedMaxDivisor(primes[index], divisorLimit);
}
@@ -171,118 +96,89 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession()
{
lock (_sync)
MersenneCpuDivisorScanSession? session = ThreadStaticPools.RentMersenneCpuDivisorSession();
if (session is not null)
{
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);
return session;
}
}
private void ReturnSession(DivisorScanSession session)
{
_sessionPool.Add(session);
return new MersenneCpuDivisorScanSession();
}
private bool CheckDivisors(
ulong prime,
ulong allowedMax,
out ulong lastProcessed,
out bool processedAll,
out ulong processedCount,
TimeSpan? timeLimit)
out ulong processedCount)
{
if (_deltasDevice == ByDivisorDeltasDevice.Cpu)
{
return CheckDivisorsCpu(prime, allowedMax, out lastProcessed, out processedAll, out processedCount, timeLimit);
}
return CheckDivisorsGpu(prime, allowedMax, out lastProcessed, out processedAll, out processedCount, timeLimit);
}
private bool CheckDivisorsCpu(
ulong prime,
ulong allowedMax,
out ulong lastProcessed,
out bool processedAll,
out ulong processedCount,
TimeSpan? timeLimit)
{
lastProcessed = 0UL;
processedCount = 0UL;
processedAll = false;
PrimeTestTimeLimit limitGuard;
if (!PrimeTestTimeLimit.TryCreate(timeLimit, out limitGuard))
{
return false;
}
// The EvenPerfectBitScanner feeds primes >= 138,000,000 here, so allowedMax >= 3 in production runs.
// Keeping the guard commented out documents the reasoning for benchmarks and tests.
// if (allowedMax < 3UL)
// {
// return false;
// }
bool enforceLimit = limitGuard.IsActive;
ulong iterationCounter = 0UL;
ref GpuUInt128WorkSet workSet = ref _divisorScanGpuWorkSet;
if (allowedMax < 3UL)
{
return false;
}
ref GpuUInt128 step = ref workSet.Step;
step.High = 0UL;
step.Low = prime;
step.ShiftLeft(1);
UInt128 step = (UInt128)prime << 1;
if (step == UInt128.Zero)
{
processedAll = true;
return false;
}
ref GpuUInt128 limit = ref workSet.Limit;
limit.High = 0UL;
limit.Low = allowedMax;
UInt128 limit = allowedMax;
UInt128 divisor = step + UInt128.One;
if (divisor > limit)
ref GpuUInt128 divisor = ref workSet.Divisor;
divisor.High = step.High;
divisor.Low = step.Low;
divisor.Add(1UL);
if (divisor.CompareTo(limit) > 0)
{
processedAll = true;
divisor.High = 0UL;
divisor.Low = 0UL;
step.High = 0UL;
step.Low = 0UL;
limit.High = 0UL;
limit.Low = 0UL;
return false;
}
FactorCacheLease factorCacheLease = new FactorCacheLease();
Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? factorCache = null;
DivisorCycleCache cycleCache = DivisorCycleCache.Shared;
byte step10 = (byte)(step % 10UL);
byte step8 = (byte)(step % 8UL);
byte step5 = (byte)(step % 5UL);
byte step3 = (byte)(step % 3UL);
byte step7 = (byte)(step % 7UL);
byte step11 = (byte)(step % 11UL);
ulong stepHigh = step.High;
ulong stepLow = step.Low;
byte step10 = (byte)((((stepHigh % 10UL) * 6UL) + (stepLow % 10UL)) % 10UL);
byte step8 = (byte)(stepLow % 8UL);
byte step5 = (byte)(((stepHigh % 5UL) + (stepLow % 5UL)) % 5UL);
byte step3 = (byte)(((stepHigh % 3UL) + (stepLow % 3UL)) % 3UL);
byte step7 = (byte)((((stepHigh % 7UL) * 2UL) + (stepLow % 7UL)) % 7UL);
byte step11 = (byte)((((stepHigh % 11UL) * 5UL) + (stepLow % 11UL)) % 11UL);
byte remainder10 = (byte)(divisor % 10UL);
byte remainder8 = (byte)(divisor % 8UL);
byte remainder5 = (byte)(divisor % 5UL);
byte remainder3 = (byte)(divisor % 3UL);
byte remainder7 = (byte)(divisor % 7UL);
byte remainder11 = (byte)(divisor % 11UL);
ulong divisorHigh = divisor.High;
ulong divisorLow = divisor.Low;
byte remainder10 = (byte)((((divisorHigh % 10UL) * 6UL) + (divisorLow % 10UL)) % 10UL);
byte remainder8 = (byte)(divisorLow % 8UL);
byte remainder5 = (byte)(((divisorHigh % 5UL) + (divisorLow % 5UL)) % 5UL);
byte remainder3 = (byte)(((divisorHigh % 3UL) + (divisorLow % 3UL)) % 3UL);
byte remainder7 = (byte)((((divisorHigh % 7UL) * 2UL) + (divisorLow % 7UL)) % 7UL);
byte remainder11 = (byte)((((divisorHigh % 11UL) * 5UL) + (divisorLow % 11UL)) % 11UL);
// Keep the divisibility filters aligned with the divisor-cycle generator so the
// CPU path never requests cycles that were skipped during cache creation.
bool lastIsSeven = (prime & 3UL) == 3UL;
while (divisor <= limit)
while (divisor.CompareTo(limit) <= 0)
{
if (enforceLimit && (iterationCounter == 0UL || (iterationCounter & 63UL) == 0UL) && limitGuard.HasExpired())
{
processedAll = false;
return false;
}
iterationCounter++;
ulong candidate = (ulong)divisor;
ulong candidate = divisor.Low;
processedCount++;
lastProcessed = candidate;
bool admissible = lastIsSeven
? (remainder10 == 3 || remainder10 == 7 || remainder10 == 9)
@@ -290,33 +186,46 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
if (admissible && (remainder8 == 1 || remainder8 == 7) && remainder3 != 0 && remainder5 != 0 && remainder7 != 0 && remainder11 != 0)
{
// Each divisor appears only once in this monotonically increasing sequence, so
// rebuilding the Montgomery data per candidate avoids caching entries that we
// would never hit again.
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(candidate);
MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(candidate);
ulong divisorCycle;
if (candidate <= PerfectNumberConstants.MaxQForDivisorCycles)
{
divisorCycle = cycleCache.GetCycleLength(candidate);
}
else
{
factorCache ??= new Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>(8);
// EvenPerfectBitScanner reaches this path only for divisors above the precomputed cycle snapshot.
// Those candidates follow q = 2 * prime * k + 1 with prime >= 5, so the downstream order calculator
// can rely on an odd divisor and a prime exponent when deriving the cycle length.
if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponent(candidate, prime, divisorData, factorCache, out divisorCycle) || divisorCycle == 0UL)
if (factorCache is null)
{
factorCacheLease.EnsureInitialized(ref factorCache);
}
if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponent(
candidate,
prime,
divisorData,
factorCache,
out ulong computedCycle) || computedCycle == 0UL)
{
divisorCycle = cycleCache.GetCycleLength(candidate);
}
else
{
divisorCycle = computedCycle;
}
}
if (divisorCycle == prime)
{
// A cycle equal to the tested exponent (which is prime in this path)
// guarantees that the candidate divides the corresponding Mersenne
// number because the order of 2 modulo the divisor is exactly p.
// A cycle equal to the tested exponent (which is prime in this path) guarantees that the candidate divides
// the corresponding Mersenne number because the order of 2 modulo the divisor is exactly p.
factorCacheLease.Dispose();
factorCache = null;
divisor.High = 0UL;
divisor.Low = 0UL;
step.High = 0UL;
step.Low = 0UL;
limit.High = 0UL;
limit.Low = 0UL;
processedAll = true;
return true;
}
@@ -327,7 +236,7 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
}
}
divisor += step;
divisor.Add(step);
remainder10 = AddMod(remainder10, step10, (byte)10);
remainder8 = AddMod(remainder8, step8, (byte)8);
remainder5 = AddMod(remainder5, step5, (byte)5);
@@ -336,249 +245,19 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
remainder11 = AddMod(remainder11, step11, (byte)11);
}
processedAll = divisor > limit;
factorCacheLease.Dispose();
factorCache = null; // The pooled dictionary was returned to the thread-static slot.
processedAll = divisor.CompareTo(limit) > 0;
divisor.High = 0UL;
divisor.Low = 0UL;
step.High = 0UL;
step.Low = 0UL;
limit.High = 0UL;
limit.Low = 0UL;
return false;
}
private bool CheckDivisorsGpu(
ulong prime,
ulong allowedMax,
out ulong lastProcessed,
out bool processedAll,
out ulong processedCount,
TimeSpan? timeLimit)
{
lastProcessed = 0UL;
processedCount = 0UL;
processedAll = false;
PrimeTestTimeLimit limitGuard;
if (!PrimeTestTimeLimit.TryCreate(timeLimit, out limitGuard))
{
return false;
}
bool enforceLimit = limitGuard.IsActive;
ulong iterationCounter = 0UL;
if (allowedMax < 3UL)
{
return false;
}
GpuUInt128 step128 = new GpuUInt128(prime);
step128.ShiftLeft(1);
if (step128.IsZero)
{
processedAll = true;
return false;
}
GpuUInt128 limit128 = new GpuUInt128(allowedMax);
GpuUInt128 divisor128 = step128;
divisor128.Add(1UL);
if (divisor128.CompareTo(limit128) > 0)
{
processedAll = true;
return false;
}
ulong step = step128.Low;
ulong limit64 = allowedMax;
int batchCapacity = Math.Max(1, _batchSize);
DivisorCycleCache cycleCache = DivisorCycleCache.Shared;
int preferredCycleBatch = cycleCache.PreferredBatchSize;
if (preferredCycleBatch > 0 && preferredCycleBatch < batchCapacity)
{
batchCapacity = preferredCycleBatch;
}
var candidateEvaluator = new MersenneNumberDivisorCandidateGpuEvaluator(batchCapacity);
ulong[] candidateBuffer = ArrayPool<ulong>.Shared.Rent(batchCapacity);
byte[] maskBuffer = ArrayPool<byte>.Shared.Rent(batchCapacity);
Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? factorCache = null;
const int RemainderTableSize = 6;
byte[] remainderBuffer = ArrayPool<byte>.Shared.Rent(RemainderTableSize);
byte[] stepBuffer = ArrayPool<byte>.Shared.Rent(RemainderTableSize);
Span<byte> remainderSpan = remainderBuffer.AsSpan(0, RemainderTableSize);
Span<byte> stepSpan = stepBuffer.AsSpan(0, RemainderTableSize);
// The remainder and step tables follow the fixed modulus order: 10, 8, 5, 3, 7, 11.
ulong startDivisor64 = divisor128.Low;
remainderSpan[0] = (byte)(startDivisor64 % 10UL);
remainderSpan[1] = (byte)(startDivisor64 % 8UL);
remainderSpan[2] = (byte)(startDivisor64 % 5UL);
remainderSpan[3] = (byte)(startDivisor64 % 3UL);
remainderSpan[4] = (byte)(startDivisor64 % 7UL);
remainderSpan[5] = (byte)(startDivisor64 % 11UL);
stepSpan[0] = (byte)(step % 10UL);
stepSpan[1] = (byte)(step % 8UL);
stepSpan[2] = (byte)(step % 5UL);
stepSpan[3] = (byte)(step % 3UL);
stepSpan[4] = (byte)(step % 7UL);
stepSpan[5] = (byte)(step % 11UL);
bool lastIsSeven = (prime & 3UL) == 3UL;
MersenneNumberDivisorRemainderGpuStepper remainderStepper = new(stepSpan);
MersenneNumberDivisorMontgomeryGpuBuilder? montgomeryBuilder = _montgomeryDevice == ByDivisorMontgomeryDevice.Gpu
? new MersenneNumberDivisorMontgomeryGpuBuilder(batchCapacity)
: null;
MontgomeryDivisorData[]? montgomeryBuffer = montgomeryBuilder != null
? ArrayPool<MontgomeryDivisorData>.Shared.Rent(batchCapacity)
: null;
ulong remainingCandidates = ComputeRemainingCandidateCount(startDivisor64, limit64, step);
try
{
while (remainingCandidates > 0UL && divisor128.CompareTo(limit128) <= 0)
{
if (divisor128.High != 0UL)
{
break;
}
ulong currentDivisor = divisor128.Low;
int chunkCount = remainingCandidates > (ulong)batchCapacity
? batchCapacity
: (int)remainingCandidates;
if (chunkCount <= 0)
{
break;
}
Span<ulong> candidateSpan = candidateBuffer.AsSpan(0, chunkCount);
Span<byte> maskSpan = maskBuffer.AsSpan(0, chunkCount);
candidateEvaluator.EvaluateCandidates(
currentDivisor,
step,
limit64,
remainderSpan,
stepSpan,
lastIsSeven,
candidateSpan,
maskSpan);
Span<MontgomeryDivisorData> montgomerySpan = default;
if (montgomeryBuilder != null && montgomeryBuffer != null)
{
montgomerySpan = montgomeryBuffer.AsSpan(0, chunkCount);
// Every candidate divisor is unique across the scan, so build Montgomery data
// for the current batch and drop it immediately instead of maintaining a cache.
montgomeryBuilder.Build(candidateSpan, montgomerySpan);
}
for (int i = 0; i < chunkCount; i++)
{
if (enforceLimit && (iterationCounter == 0UL || (iterationCounter & 63UL) == 0UL) && limitGuard.HasExpired())
{
processedAll = false;
return false;
}
iterationCounter++;
ulong candidate = candidateSpan[i];
processedCount++;
lastProcessed = candidate;
if (maskSpan[i] == 0)
{
continue;
}
MontgomeryDivisorData divisorData;
if (!montgomerySpan.IsEmpty)
{
divisorData = montgomerySpan[i];
if (divisorData.Modulus != candidate)
{
// The GPU builder might skip entries when masks zero them out; recompute on the CPU
// without caching because divisors never repeat within a session.
divisorData = MontgomeryDivisorData.FromModulus(candidate);
}
}
else
{
// Candidates remain unique even when GPU Montgomery support is disabled, so rebuild
// the divisor data inline instead of persisting unused cache entries.
divisorData = MontgomeryDivisorData.FromModulus(candidate);
}
ulong divisorCycle;
if (candidate <= PerfectNumberConstants.MaxQForDivisorCycles)
{
divisorCycle = cycleCache.GetCycleLength(candidate);
}
else
{
factorCache ??= new Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>(8);
if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponent(candidate, prime, divisorData, factorCache, out divisorCycle) || divisorCycle == 0UL)
{
divisorCycle = cycleCache.GetCycleLength(candidate);
}
}
if (divisorCycle == prime)
{
processedAll = true;
return true;
}
if (divisorCycle == 0UL)
{
Console.WriteLine($"Divisor cycle was not calculated for {prime}");
}
}
GpuUInt128 increment = step128;
increment.Mul((ulong)chunkCount);
divisor128.Add(increment);
remainderStepper.Advance(chunkCount, remainderSpan);
remainingCandidates -= (ulong)chunkCount;
}
}
finally
{
montgomeryBuilder?.Dispose();
if (montgomeryBuffer != null)
{
ArrayPool<MontgomeryDivisorData>.Shared.Return(montgomeryBuffer, clearArray: false);
}
remainderStepper.Dispose();
ArrayPool<byte>.Shared.Return(remainderBuffer, clearArray: false);
ArrayPool<byte>.Shared.Return(stepBuffer, clearArray: false);
ArrayPool<ulong>.Shared.Return(candidateBuffer, clearArray: false);
ArrayPool<byte>.Shared.Return(maskBuffer, clearArray: false);
candidateEvaluator.Dispose();
}
processedAll = remainingCandidates == 0UL && divisor128.CompareTo(limit128) > 0;
return false;
}
private static ulong ComputeRemainingCandidateCount(ulong currentDivisor, ulong limit, ulong step)
{
if (step == 0UL || currentDivisor > limit)
{
return 0UL;
}
ulong distance = limit - currentDivisor;
ulong stepsAvailable = distance / step;
return stepsAvailable + 1UL;
}
private static byte CheckDivisor(ulong prime, ulong divisorCycle, in MontgomeryDivisorData divisorData)
{
ulong residue = prime.Pow2MontgomeryModWithCycleCpu(divisorCycle, divisorData);
@@ -646,287 +325,5 @@ public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDiv
return Math.Min((1UL << (int)(prime - 1UL)) - 1UL, divisorLimit);
}
private sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
{
private readonly MersenneNumberDivisorByDivisorCpuTester _owner;
private bool _disposed;
private ulong[]? _primeDeltas;
private ulong[]? _cycleRemainders;
private ulong[]? _montgomeryDeltas;
private int _bufferCapacity;
private MersenneNumberDivisorResidueGpuEvaluator? _gpuResidueEvaluator;
internal DivisorScanSession(MersenneNumberDivisorByDivisorCpuTester owner)
{
_owner = owner;
}
internal void Reset()
{
_disposed = false;
}
public void CheckDivisor(ulong divisor, in MontgomeryDivisorData divisorData, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(DivisorScanSession));
}
int length = primes.Length;
if (length == 0)
{
return;
}
MontgomeryDivisorData effectiveDivisorData = divisorData;
if (effectiveDivisorData.Modulus != divisor)
{
// Each divisor surfaces only once across the monotonic scan, so rebuild the Montgomery data
// instead of retaining a cache entry that would never be reused within future checks.
effectiveDivisorData = MontgomeryDivisorData.FromModulus(divisor);
}
ulong modulus = effectiveDivisorData.Modulus;
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
hits.Clear();
return;
}
if (divisorCycle == 0UL)
{
divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
if (divisorCycle == 0UL)
{
hits.Clear();
return;
}
}
if (_owner._deltasDevice == ByDivisorDeltasDevice.Cpu)
{
CheckDivisorCpu(effectiveDivisorData, divisorCycle, primes, hits);
return;
}
CheckDivisorGpu(effectiveDivisorData, divisorCycle, primes, hits, length);
}
private static void CheckDivisorCpu(
in MontgomeryDivisorData divisorData,
ulong divisorCycle,
ReadOnlySpan<ulong> primes,
Span<byte> hits)
{
// Keep these remainder steppers in place so future updates continue reusing the previously computed residues.
// They are critical for avoiding repeated full Montgomery exponentiation work when scanning divisors.
var exponentStepper = new ExponentRemainderStepper(divisorData);
if (!exponentStepper.IsValidModulus)
{
hits.Clear();
return;
}
var cycleStepper = new CycleRemainderStepper(divisorCycle);
ulong remainder = cycleStepper.Initialize(primes[0]);
hits[0] = remainder == 0UL
? (exponentStepper.ComputeNextIsUnity(primes[0]) ? (byte)1 : (byte)0)
: (byte)0;
int length = primes.Length;
for (int i = 1; i < length; i++)
{
remainder = cycleStepper.ComputeNext(primes[i]);
if (remainder != 0UL)
{
hits[i] = 0;
continue;
}
hits[i] = exponentStepper.ComputeNextIsUnity(primes[i]) ? (byte)1 : (byte)0;
}
}
private void CheckDivisorGpu(
in MontgomeryDivisorData divisorData,
ulong divisorCycle,
ReadOnlySpan<ulong> primes,
Span<byte> hits,
int length)
{
EnsureCapacity(length);
Span<ulong> deltaSpan = _primeDeltas!.AsSpan(0, length);
ComputePrimeDeltas(primes, deltaSpan);
Span<ulong> remainderSpan = _cycleRemainders!.AsSpan(0, length);
ComputeCycleRemainders(divisorCycle, primes, deltaSpan, remainderSpan);
EnsureGpuEvaluator();
Span<ulong> residueSpan = _montgomeryDeltas!.AsSpan(0, length);
_gpuResidueEvaluator!.ComputeResidues(primes, divisorData, residueSpan);
ulong montgomeryOne = divisorData.MontgomeryOne;
for (int i = 0; i < length; i++)
{
hits[i] = remainderSpan[i] == 0UL && residueSpan[i] == montgomeryOne ? (byte)1 : (byte)0;
}
}
private void EnsureGpuEvaluator()
{
if (_gpuResidueEvaluator is null)
{
_gpuResidueEvaluator = new MersenneNumberDivisorResidueGpuEvaluator(_owner._batchSize);
}
}
private void ReleaseGpuEvaluator()
{
if (_gpuResidueEvaluator is null)
{
return;
}
_gpuResidueEvaluator.Dispose();
_gpuResidueEvaluator = null;
}
private void EnsureCapacity(int requiredLength)
{
if (requiredLength <= 0)
{
return;
}
if (requiredLength <= _bufferCapacity && _primeDeltas is not null && _cycleRemainders is not null && _montgomeryDeltas is not null)
{
return;
}
int newCapacity = _bufferCapacity;
if (newCapacity == 0)
{
newCapacity = 1;
}
while (newCapacity < requiredLength)
{
newCapacity <<= 1;
}
ReturnBuffers();
_primeDeltas = ArrayPool<ulong>.Shared.Rent(newCapacity);
_cycleRemainders = ArrayPool<ulong>.Shared.Rent(newCapacity);
_montgomeryDeltas = ArrayPool<ulong>.Shared.Rent(newCapacity);
_bufferCapacity = newCapacity;
}
private void ReturnBuffers()
{
if (_primeDeltas is not null)
{
ArrayPool<ulong>.Shared.Return(_primeDeltas, clearArray: false);
_primeDeltas = null;
}
if (_cycleRemainders is not null)
{
ArrayPool<ulong>.Shared.Return(_cycleRemainders, clearArray: false);
_cycleRemainders = null;
}
if (_montgomeryDeltas is not null)
{
ArrayPool<ulong>.Shared.Return(_montgomeryDeltas, clearArray: false);
_montgomeryDeltas = null;
}
_bufferCapacity = 0;
}
private static void ComputePrimeDeltas(ReadOnlySpan<ulong> primes, Span<ulong> deltas)
{
if (primes.Length == 0)
{
return;
}
ulong previous = primes[0];
deltas[0] = previous;
for (int i = 1; i < primes.Length; i++)
{
ulong current = primes[i];
if (current <= previous)
{
throw new ArgumentOutOfRangeException(nameof(primes), "Primes must be strictly increasing.");
}
deltas[i] = current - previous;
previous = current;
}
}
private static void ComputeCycleRemainders(ulong cycleLength, ReadOnlySpan<ulong> primes, ReadOnlySpan<ulong> deltas, Span<ulong> remainders)
{
if (cycleLength == 0UL || primes.Length == 0)
{
remainders.Clear();
return;
}
ulong remainder = primes[0] % cycleLength;
remainders[0] = remainder;
for (int i = 1; i < primes.Length; i++)
{
remainder = AddMod(remainder, deltas[i], cycleLength);
remainders[i] = remainder;
}
}
private static ulong AddMod(ulong value, ulong delta, ulong modulus)
{
if (modulus == 0UL)
{
return 0UL;
}
if (ulong.MaxValue - value < delta)
{
UInt128 extended = (UInt128)value + delta;
return (ulong)(extended % modulus);
}
ulong sum = value + delta;
if (sum >= modulus)
{
sum -= modulus;
if (sum >= modulus)
{
sum %= modulus;
}
}
return sum;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
ReleaseGpuEvaluator();
_owner.ReturnSession(this);
}
}
}
+12 -12
View File
@@ -1,9 +1,8 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core;
internal struct CycleRemainderStepper
public struct CycleRemainderStepper
{
private readonly ulong _cycleLength;
private ulong _previousPrime;
@@ -23,15 +22,16 @@ internal struct CycleRemainderStepper
_hasState = false;
}
public void Reset()
{
// TODO: Inline this reset at the call sites so the hot loops reuse struct reinitialization
// measured fastest in MersenneDivisorCycleLengthGpuBenchmarks, avoiding the extra
// method call when scanners need to restart stepping.
_previousPrime = 0UL;
_currentRemainder = 0UL;
_hasState = false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
// TODO: Inline this reset at the call sites so the hot loops reuse struct reinitialization
// measured fastest in MersenneDivisorCycleLengthGpuBenchmarks, avoiding the extra
// method call when scanners need to restart stepping.
_previousPrime = 0UL;
_currentRemainder = 0UL;
_hasState = false;
}
public ulong Initialize(ulong prime)
{
@@ -1,6 +1,3 @@
using System;
using System.Threading;
namespace PerfectNumbers.Core;
internal static class DeterministicRandom
+14 -47
View File
@@ -1,10 +1,7 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using ILGPU;
using ILGPU.Runtime;
using PerfectNumbers.Core.Gpu;
@@ -63,9 +60,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;
@@ -108,7 +103,7 @@ public sealed class DivisorCycleCache
if (rentedMissing is not null)
{
ThreadLocalArrayPool<int>.Shared.Return(rentedMissing, clearArray: false);
ArrayPool<int>.Shared.Return(rentedMissing, clearArray: false);
}
}
@@ -130,16 +125,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 +146,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 +169,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 +231,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();
+5 -16
View File
@@ -1,19 +1,8 @@
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;
}
}
public readonly struct FactorEntry(ulong value, int exponent)
{
public readonly ulong Value = value;
public readonly int Exponent = exponent;
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace PerfectNumbers.Core;
public readonly struct FactorEntry128(in UInt128 value, int exponent)
{
public readonly UInt128 Value = value;
public readonly int Exponent = exponent;
}
+131 -134
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
{
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;
}
}
}
+30 -87
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;
@@ -110,15 +109,15 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
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(IDisposable limiter, GpuContextLease gpu, Kerne
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(IDisposable limiter, GpuContextLease gpu, Kerne
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(IDisposable limiter, GpuContextLease gpu, Kerne
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(IDisposable limiter, GpuContextLease gpu, Kerne
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(IDisposable limiter, GpuContextLease gpu, Kerne
_stream?.Dispose();
_stream = null;
if (_gpu.HasValue)
{
_gpu.Value.ReturnToPool();
}
_gpu?.Dispose();
_gpu = null;
_executionLock = null;
@@ -739,18 +684,18 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
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)
-2
View File
@@ -1,5 +1,3 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core.Gpu;
-1
View File
@@ -1,4 +1,3 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using ILGPU.Algorithms;
@@ -1,13 +1,9 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Numerics;
using ILGPU;
using ILGPU.Runtime;
using MontgomeryDivisorData = PerfectNumbers.Core.MontgomeryDivisorData;
using CycleRemainderStepper = PerfectNumbers.Core.CycleRemainderStepper;
namespace PerfectNumbers.Core.Gpu;
@@ -71,7 +67,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
}
}
public bool IsPrime(ulong prime, out bool divisorsExhausted, TimeSpan? timeLimit = null)
public bool IsPrime(ulong prime, out bool divisorsExhausted)
{
ulong allowedMax;
int batchCapacity;
@@ -108,7 +104,6 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
composite = CheckDivisors(
prime,
allowedMax,
timeLimit,
accelerator,
kernel,
resources.DivisorsBuffer,
@@ -148,7 +143,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
return true;
}
public void PrepareCandidates(ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues)
public void PrepareCandidates(in ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues)
{
if (allowedMaxValues.Length < primes.Length)
{
@@ -176,7 +171,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,
@@ -204,15 +198,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;
@@ -288,12 +273,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)
@@ -412,8 +391,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);
}
@@ -569,7 +546,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
}
ulong exponent = exponents[index];
hits[index] = exponent.Pow2MontgomeryModWindowedGpu(divisor, keepMontgomery: false) == 1UL ? (byte)1 : (byte)0;
hits[index] = ULongExtensions.Pow2MontgomeryModWindowedKernel(exponent, divisor, keepMontgomery: false) == 1UL ? (byte)1 : (byte)0;
}
public sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
@@ -601,7 +578,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
_disposed = false;
}
public void CheckDivisor(ulong divisor, in MontgomeryDivisorData divisorData, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits)
public void CheckDivisor(ulong divisor, in MontgomeryDivisorData divisorData, ulong divisorCycle, in ReadOnlySpan<ulong> primes, Span<byte> hits)
{
if (_disposed)
{
@@ -788,7 +765,7 @@ public sealed class MersenneNumberDivisorByDivisorGpuTester : IMersenneNumberDiv
ulong exponent = exponents[index];
results[index] = exponent.Pow2MontgomeryModWindowedGpu(divisor, keepMontgomery: true);
results[index] = ULongExtensions.Pow2MontgomeryModWindowedKernel(exponent, divisor, keepMontgomery: true);
}
public ulong DivisorLimit
@@ -1,262 +0,0 @@
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using ILGPU;
using ILGPU.Runtime;
namespace PerfectNumbers.Core.Gpu;
internal sealed class MersenneNumberDivisorCandidateGpuEvaluator : IDisposable
{
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;
if (count == 0)
{
return;
}
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 == 0 ? 1 : _capacity;
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)
{
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,179 +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 : IDisposable
{
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;
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 == 0 ? 1 : _capacity;
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;
if (idx >= moduli.Length)
{
return;
}
ulong modulus = moduli[idx];
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
destination[idx] = default;
return;
}
ulong nPrime = ComputeMontgomeryNPrime(modulus);
ulong montgomeryOne = ComputeMontgomeryResidue64(modulus);
ulong montgomeryTwo = montgomeryOne.MulMod64GpuCompatible(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,140 +0,0 @@
using System;
using System.Buffers;
using System.Runtime.InteropServices;
using ILGPU;
using ILGPU.Runtime;
namespace PerfectNumbers.Core.Gpu;
internal sealed class MersenneNumberDivisorRemainderGpuStepper : IDisposable
{
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)
{
return;
}
_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));
}
if (advanceCount <= 0 || remainders.Length == 0)
{
return;
}
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;
if (idx >= remainders.Length)
{
return;
}
int modulus = GetModulus(idx);
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,114 +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 : IDisposable
{
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)
{
return;
}
_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;
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;
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;
if (idx >= length)
{
return;
}
ulong exponent = exponents[idx];
results[idx] = exponent.Pow2MontgomeryModWindowedGpu(divisorData, keepMontgomery: true);
}
}
@@ -1,8 +1,5 @@
using System;
using System.Buffers;
using System.Threading;
using System.Runtime.InteropServices;
using ILGPU;
using ILGPU.Runtime;
namespace PerfectNumbers.Core.Gpu;
@@ -1,6 +1,5 @@
using System.Buffers;
using System.Runtime.InteropServices;
using System.Threading;
using ILGPU.Runtime;
namespace PerfectNumbers.Core.Gpu;
@@ -1,94 +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;
using var divisorBuffer = accelerator.Allocate1D<MontgomeryDivisorData>(divisorCount);
using var countBuffer = accelerator.Allocate1D<int>(divisorCount);
using var resultBuffer = accelerator.Allocate1D<ulong>(divisorCount * stride);
try
{
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);
return true;
}
catch
{
return false;
}
finally
{
execution.Dispose();
lease.Dispose();
}
}
}
@@ -1,10 +1,8 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using ILGPU;
using ILGPU.Runtime;
using System.Numerics;
using PerfectNumbers.Core;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using ILGPU.Runtime.OpenCL;
@@ -811,7 +809,7 @@ internal static partial class PrimeOrderGpuHeuristics
}
ulong reduced = phi / factor;
if (reduced.Pow2MontgomeryModWindowedGpu(divisor, keepMontgomery: false) == 1UL)
if (ULongExtensions.Pow2MontgomeryModWindowedKernel(reduced, divisor, keepMontgomery: false) == 1UL)
{
return false;
}
@@ -1595,7 +1593,7 @@ internal static partial class PrimeOrderGpuHeuristics
private static bool Pow2EqualsOneKernel(ulong exponent, in MontgomeryDivisorData divisor)
{
return exponent.Pow2MontgomeryModWindowedGpu(divisor, keepMontgomery: false) == 1UL;
return ULongExtensions.Pow2MontgomeryModWindowedKernel(exponent, divisor, keepMontgomery: false) == 1UL;
}
private static ulong CalculateByDoublingKernel(ulong prime)
@@ -1715,10 +1713,10 @@ internal static partial class PrimeOrderGpuHeuristics
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);
@@ -1755,6 +1753,7 @@ internal static partial class PrimeOrderGpuHeuristics
exponentBuffer.Dispose();
remainderBuffer.Dispose();
execution.Dispose();
return true;
}
catch (Exception)
@@ -1774,7 +1773,6 @@ internal static partial class PrimeOrderGpuHeuristics
ArrayPool<GpuUInt128>.Shared.Return(rentedResults, clearArray: false);
}
execution.Dispose();
lease.Dispose();
}
}
@@ -1795,7 +1793,7 @@ internal static partial class PrimeOrderGpuHeuristics
return 0UL;
}
return exponent.Pow2MontgomeryModWindowedGpu(divisorData, keepMontgomery: false);
return ULongExtensions.Pow2MontgomeryModWindowedKernel(exponent, divisorData, keepMontgomery: false);
}
private static Action<AcceleratorStream, Index1D, ArrayView1D<ulong, Stride1D.Dense>, MontgomeryDivisorData, ArrayView1D<ulong, Stride1D.Dense>> GetPow2ModKernel(Accelerator accelerator)
@@ -1811,57 +1809,7 @@ internal static partial class PrimeOrderGpuHeuristics
private static void Pow2ModKernel(Index1D index, ArrayView1D<ulong, Stride1D.Dense> exponents, MontgomeryDivisorData divisor, ArrayView1D<ulong, Stride1D.Dense> remainders)
{
ulong exponent = exponents[index];
ulong modulus = divisor.Modulus;
// ulong nPrime = divisor.NPrime;
// ulong result = divisor.MontgomeryOne;
// ulong baseValue = divisor.MontgomeryTwo;
// ulong remainingExponent = exponent;
// if (remainingExponent == 0UL)
// {
// remainders[index] = result.MontgomeryMultiply(1UL, modulus, nPrime);
// return;
// }
// while (remainingExponent != 0UL)
// {
// if ((remainingExponent & 1UL) != 0UL)
// {
// result = result.MontgomeryMultiply(baseValue, modulus, nPrime);
// }
// remainingExponent >>= 1;
// if (remainingExponent == 0UL)
// {
// break;
// }
// // Reusing baseValue to hold base^2 so we avoid allocating another register for the squared term.
// baseValue = baseValue.MontgomeryMultiply(baseValue, modulus, nPrime);
// }
// remainders[index] = result.MontgomeryMultiply(1UL, modulus, nPrime);
// Plain modular exponentiation without Montgomery reduction kept for reference.
ulong plainResult = 1UL;// % modulus;
ulong plainBase = 2UL;// % modulus;
while (exponent != 0UL)
{
if ((exponent & 1UL) != 0UL)
{
plainResult = (plainResult * plainBase) % modulus;
}
exponent >>= 1;
if (exponent == 0UL)
{
break;
}
plainBase = (plainBase * plainBase) % modulus;
}
remainders[index] = plainResult;
remainders[index] = ULongExtensions.Pow2MontgomeryModWindowedKernel(exponent, divisor, keepMontgomery: false);
}
private static Action<AcceleratorStream, Index1D, ArrayView1D<GpuUInt128, Stride1D.Dense>, GpuUInt128, ArrayView1D<GpuUInt128, Stride1D.Dense>> GetPow2ModWideKernel(Accelerator accelerator)
@@ -1,4 +1,3 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -1,5 +1,3 @@
using System;
namespace PerfectNumbers.Core;
public interface IMersenneNumberDivisorByDivisorTester
@@ -12,14 +10,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);
void PrepareCandidates(in ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues);
public interface IDivisorScanSession : IDisposable
{
void CheckDivisor(ulong divisor, in MontgomeryDivisorData divisorData, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits);
void CheckDivisor(ulong divisor, in MontgomeryDivisorData divisorData, ulong divisorCycle, in ReadOnlySpan<ulong> primes, Span<byte> hits);
}
}
+192 -248
View File
@@ -1,12 +1,9 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using PerfectNumbers.Core.Gpu;
using ILGPU;
using ILGPU.Runtime;
@@ -18,13 +15,6 @@ public class MersenneDivisorCycles
private List<(ulong divisor, ulong cycleLength)> _table = [];
// 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()
@@ -272,7 +262,7 @@ public class MersenneDivisorCycles
}
public static bool TryCalculateCycleLengthForExponent(
ulong divisor,
ulong divisor,
ulong exponent,
in MontgomeryDivisorData divisorData,
Dictionary<ulong, FactorCacheEntry>? factorCache,
@@ -280,24 +270,17 @@ public class MersenneDivisorCycles
{
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(
divisor,
@@ -312,9 +295,8 @@ public class MersenneDivisorCycles
}
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.
// A zero totient would imply divisor == 0, which never occurs on the tested path. Keep the guard documented without
// branching in the hot loop.
// if (phi == 0UL)
// {
// return false;
@@ -322,43 +304,38 @@ public class MersenneDivisorCycles
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.
// k == 0 would indicate reducedPhi < exponent, which the EvenPerfectBitScanner call flow prevents. Document the
// assumption instead of branching.
// if (k == 0UL)
// {
// return false;
// }
Dictionary<ulong, int> factorCounts = AcquireFactorCountDictionary(8);
try
Dictionary<ulong, int> factorCounts = ThreadStaticPools.RentFactorCountDictionary();
bool success = false;
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
bool exponentAccumulated = AccumulateFactors(exponent, factorCache, factorCounts, cacheResult: true);
bool factorsAccumulated = exponentAccumulated && AccumulateFactors(k, factorCache, factorCounts, cacheResult: false);
if (factorsAccumulated)
{
ReleaseFactorCountDictionary(factorCounts);
cycleLength = ReduceOrder(divisorData, phi, factorCounts);
success = true;
}
ThreadStaticPools.ReturnFactorCountDictionary(factorCounts);
return success;
}
private static bool AccumulateFactors(
@@ -372,127 +349,99 @@ public class MersenneDivisorCycles
return true;
}
FactorCacheEntry cachedFactorization;
PrimeFactorPartialResult? lease = null;
bool fromCache;
try
if (!TryGetFactorization(value, cache, cacheResult, out FactorCacheEntry? factorization, out bool releaseAfterUse) || factorization is null)
{
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
FactorCacheEntry actualFactorization = factorization;
KeyValuePair<ulong, int>[]? pairs = actualFactorization.Pairs;
if (pairs is not null)
{
lease?.Dispose();
int length = actualFactorization.Count;
for (int i = 0; i < length; i++)
{
KeyValuePair<ulong, int> pair = pairs[i];
AddFactor(counts, pair.Key, pair.Value);
}
}
if (releaseAfterUse)
{
actualFactorization.ReturnToPool();
}
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,
out bool releaseAfterUse)
{
factorization = default;
lease = null;
fromCache = false;
releaseAfterUse = false;
if (value <= 1UL)
if (cache is not null && cache.TryGetValue(value, out FactorCacheEntry? cachedEntry))
{
factorization = cachedEntry;
return true;
}
if (cache is not null && cache.TryGetValue(value, out factorization))
Dictionary<ulong, int> scratch = ThreadStaticPools.RentFactorScratchDictionary();
bool factored = TryFactorIntoCountsInternal(value, scratch);
if (!factored)
{
fromCache = true;
return true;
ThreadStaticPools.ReturnFactorScratchDictionary(scratch);
factorization = null;
return false;
}
Dictionary<ulong, int> scratch = AcquireFactorScratchDictionary(8);
try
int count = scratch.Count;
FactorCacheEntry entry = ThreadStaticPools.RentFactorCacheEntry();
if (count == 0)
{
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();
entry.Initialize(null, 0, fromPool: false);
factorization = entry;
releaseAfterUse = cache is null || !cacheResult;
if (cache is not null && cacheResult)
{
FactorCacheEntry cacheEntry = result.ToCacheEntry();
cache[value] = cacheEntry;
cache[value] = entry;
}
ThreadStaticPools.ReturnFactorScratchDictionary(scratch);
return true;
}
finally
KeyValuePair<ulong, int>[] pairs = ThreadStaticPools.FactorKeyValuePairPool.Rent(count);
((ICollection<KeyValuePair<ulong, int>>)scratch).CopyTo(pairs, 0);
ThreadStaticPools.ReturnFactorScratchDictionary(scratch);
Array.Sort(pairs, 0, count, FactorPairValueComparer.Instance);
entry.Initialize(pairs, count, fromPool: true);
factorization = entry;
if (cache is not null && cacheResult)
{
ReleaseFactorScratchDictionary(scratch);
cache[value] = entry;
}
else
{
releaseAfterUse = true;
}
return true;
}
private static bool TryFactorIntoCountsInternal(ulong value, Dictionary<ulong, int> counts)
{
if (value <= 1UL)
{
return true;
}
// The EvenPerfectBitScanner only requests factors for values >= 2 here. Leave the guard documented without executing it.
// if (value <= 1UL)
// {
// return true;
// }
ulong remaining = value;
uint[] smallPrimes = PrimesGenerator.SmallPrimes;
@@ -507,6 +456,7 @@ public class MersenneDivisorCycles
break;
}
// TODO: Reuse the remainder-tracking update from the cycle heuristic so repeated divisions disappear from this loop.
while ((remaining % prime) == 0UL)
{
AddFactor(counts, prime);
@@ -519,20 +469,43 @@ public class MersenneDivisorCycles
return true;
}
// TODO: Benchmark PrimeTester.IsPrimeInternal against Open.Numeric.Primes on the EvenPerfectBitScanner workloads to confirm the faster option for wide composites.
if (PrimeTester.IsPrimeInternal(remaining, CancellationToken.None))
{
AddFactor(counts, remaining);
return true;
}
// TODO: Evaluate folding the remainder-tracking mechanism into PollardRho64 so the walk leverages previously computed residues.
ulong factor = PollardRho64(remaining);
if (factor == 0UL || factor == remaining)
{
// Pollard-Rho can still stall on rare stubborn composites during test and benchmark runs, so keep the failure guard in place
// and let the caller fall back to the cached cycle calculator.
return false;
}
// TODO: Investigate computing this quotient via remainder tracking or reciprocal multiplication to avoid the division.
ulong quotient = remaining / factor;
// Pollard-Rho never returns factors <= 1 on the EvenPerfectBitScanner path; keep the old guard documented
// for completeness without branching in the hot path.
// if (factor > 1UL && !TryFactorIntoCountsInternal(factor, counts))
// {
// return false;
// }
if (!TryFactorIntoCountsInternal(factor, counts))
{
return false;
}
ulong quotient = remaining / factor;
return TryFactorIntoCountsInternal(factor, counts) && TryFactorIntoCountsInternal(quotient, counts);
// Guard the recursive calls to terminate once Pollard-Rho reduces either branch to 1.
if (quotient > 1UL && !TryFactorIntoCountsInternal(quotient, counts))
{
return false;
}
return true;
}
private static void AddFactor(Dictionary<ulong, int> counts, ulong factor)
@@ -559,22 +532,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)
{
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;
@@ -603,13 +569,6 @@ public class MersenneDivisorCycles
if (candidate.Pow2MontgomeryModWindowedCpu(divisorData, keepMontgomery: false) == 1UL)
{
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;
}
@@ -621,10 +580,7 @@ public class MersenneDivisorCycles
}
finally
{
if (rentFromPool)
{
ThreadLocalArrayPool<ulong>.Shared.Return(primes, clearArray: false);
}
ArrayPool<ulong>.Shared.Return(primes, clearArray: false);
}
}
@@ -697,53 +653,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];
@@ -751,19 +660,66 @@ public class MersenneDivisorCycles
return BinaryPrimitives.ReadUInt64LittleEndian(buffer);
}
public readonly struct FactorCacheEntry
private sealed class FactorPairValueComparer : IComparer<KeyValuePair<ulong, int>>
{
public FactorCacheEntry(ulong[] primes, byte[] exponents, int count)
public static readonly FactorPairValueComparer Instance = new FactorPairValueComparer();
private FactorPairValueComparer()
{
Primes = primes;
Exponents = exponents;
Count = count;
}
public ulong[] Primes { get; }
public byte[] Exponents { get; }
public int Compare(KeyValuePair<ulong, int> x, KeyValuePair<ulong, int> y)
{
if (x.Key < y.Key)
{
return -1;
}
public int Count { get; }
if (x.Key > y.Key)
{
return 1;
}
return 0;
}
}
public sealed class FactorCacheEntry
{
internal FactorCacheEntry? Next;
internal KeyValuePair<ulong, int>[]? Pairs;
internal int Count;
private bool _fromPool;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Reset()
{
Pairs = null;
Count = 0;
_fromPool = false;
Next = null;
}
internal void Initialize(KeyValuePair<ulong, int>[]? pairs, int count, bool fromPool)
{
Pairs = pairs;
Count = count;
_fromPool = fromPool;
Next = null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void ReturnToPool()
{
KeyValuePair<ulong, int>[]? pairs = Pairs;
if (_fromPool && pairs is not null)
{
ThreadStaticPools.FactorKeyValuePairPool.Return(pairs, clearArray: false);
}
Reset();
ThreadStaticPools.ReturnFactorCacheEntry(this);
}
}
public static (long nextPosition, long completeCount) FindLast(string path)
@@ -810,11 +766,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
@@ -862,10 +814,7 @@ public class MersenneDivisorCycles
}
finally
{
if (rentLocalCycles)
{
ThreadLocalArrayPool<(ulong, ulong)>.Shared.Return(localCycles, clearArray: false);
}
ArrayPool<(ulong, ulong)>.Shared.Return(localCycles, clearArray: false);
}
});
}
@@ -880,7 +829,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;
@@ -1085,22 +1034,17 @@ public class MersenneDivisorCycles
internal static bool TryCalculateCycleLengthHeuristic(ulong divisor, in MontgomeryDivisorData divisorData, out ulong cycleLength)
{
// 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))
{
@@ -1,139 +1,129 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.Threading.Tasks;
using Open.Numeric.Primes;
using System.Runtime.InteropServices;
namespace PerfectNumbers.Core;
public static class MersenneNumberDivisorByDivisorTester
{
public static void Run(
List<ulong> candidates,
IMersenneNumberDivisorByDivisorTester tester,
Dictionary<ulong, (bool DetailedCheck, bool PassedAllTests)>? previousResults,
ulong startPrime,
Action markComposite,
Action clearComposite,
Action<ulong, bool, bool, bool> printResult,
int threadCount,
TimeSpan? primeTestLimit)
{
if (candidates.Count == 0)
{
Console.WriteLine("No candidates were provided for --mersenne=bydivisor.");
return;
}
public static void Run(
List<ulong> candidates,
IMersenneNumberDivisorByDivisorTester tester,
Dictionary<ulong, (bool DetailedCheck, bool PassedAllTests)>? previousResults,
ulong startPrime,
Action markComposite,
Action clearComposite,
Action<ulong, bool, bool, bool> printResult,
int threadCount)
{
if (candidates.Count == 0)
{
Console.WriteLine("No candidates were provided for --mersenne=bydivisor.");
return;
}
_ = threadCount;
candidates.Sort();
Span<ulong> candidateSpan = CollectionsMarshal.AsSpan(candidates);
int candidateCount = candidateSpan.Length;
candidates.Sort();
Span<ulong> candidateSpan = CollectionsMarshal.AsSpan(candidates);
int candidateCount = candidateSpan.Length;
bool applyStartPrime = startPrime > 0UL;
int skippedByPreviousResults = 0;
bool applyStartPrime = startPrime > 0UL;
int skippedByPreviousResults = 0;
ArrayPool<ulong> pool = ThreadStaticPools.UlongPool;
if (previousResults is not null && previousResults.Count > 0)
{
int recordedCount = previousResults.Count;
ulong[] recordedCandidates = pool.Rent(recordedCount);
if (previousResults is not null && previousResults.Count > 0)
{
int recordedCount = previousResults.Count;
ulong[] recordedCandidates = ArrayPool<ulong>.Shared.Rent(recordedCount);
try
{
Span<ulong> recordedSpan = recordedCandidates.AsSpan(0, recordedCount);
int recordedIndex = 0;
foreach (ulong value in previousResults.Keys)
{
recordedSpan[recordedIndex++] = value;
}
Span<ulong> recordedSpan = recordedCandidates.AsSpan(0, recordedCount);
int recordedIndex = 0;
foreach (ulong value in previousResults.Keys)
{
recordedSpan[recordedIndex++] = value;
}
recordedSpan.Sort();
recordedSpan.Sort();
int writeIndex = 0;
int skipIndex = 0;
int writeIndex = 0;
int skipIndex = 0;
for (int readIndex = 0; readIndex < candidateCount; readIndex++)
{
ulong candidate = candidateSpan[readIndex];
for (int readIndex = 0; readIndex < candidateCount; readIndex++)
{
ulong candidate = candidateSpan[readIndex];
while (skipIndex < recordedCount && recordedSpan[skipIndex] < candidate)
{
skipIndex++;
}
while (skipIndex < recordedCount && recordedSpan[skipIndex] < candidate)
{
skipIndex++;
}
if (skipIndex < recordedCount && recordedSpan[skipIndex] == candidate)
{
skippedByPreviousResults++;
skipIndex++;
continue;
}
if (skipIndex < recordedCount && recordedSpan[skipIndex] == candidate)
{
skippedByPreviousResults++;
skipIndex++;
continue;
}
candidateSpan[writeIndex++] = candidate;
}
candidateSpan[writeIndex++] = candidate;
}
if (writeIndex < candidateCount)
{
candidates.RemoveRange(writeIndex, candidateCount - writeIndex);
candidateSpan = CollectionsMarshal.AsSpan(candidates);
candidateCount = candidateSpan.Length;
}
}
finally
{
ArrayPool<ulong>.Shared.Return(recordedCandidates, clearArray: true);
}
}
if (writeIndex < candidateCount)
{
candidates.RemoveRange(writeIndex, candidateCount - writeIndex);
candidateSpan = CollectionsMarshal.AsSpan(candidates);
candidateCount = candidateSpan.Length;
}
if (skippedByPreviousResults > 0)
{
Console.WriteLine($"Skipped {skippedByPreviousResults.ToString(CultureInfo.InvariantCulture)} candidates excluded by previous results.");
}
pool.Return(recordedCandidates, clearArray: true);
}
int startIndex = 0;
if (applyStartPrime)
{
startIndex = candidates.BinarySearch(startPrime);
if (startIndex < 0)
{
startIndex = ~startIndex;
}
else
{
while (startIndex > 0 && candidates[startIndex - 1] >= startPrime)
{
startIndex--;
}
}
}
if (skippedByPreviousResults > 0)
{
Console.WriteLine($"Skipped {skippedByPreviousResults.ToString(CultureInfo.InvariantCulture)} candidates excluded by previous results.");
}
List<ulong> primesToTest = candidates;
Span<ulong> primesSpan = CollectionsMarshal.AsSpan(primesToTest);
ulong maxPrime = 0UL;
int primeWriteIndex = 0;
int startIndex = 0;
if (applyStartPrime)
{
startIndex = candidates.BinarySearch(startPrime);
if (startIndex < 0)
{
startIndex = ~startIndex;
}
else
{
while (startIndex > 0 && candidates[startIndex - 1] >= startPrime)
{
startIndex--;
}
}
}
List<ulong> primesToTest = candidates;
Span<ulong> primesSpan = CollectionsMarshal.AsSpan(primesToTest);
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++)
{
ulong candidate = primesSpan[index];
for (int index = startIndex; index < candidateCount; index++)
{
ulong candidate = primesSpan[index];
if (candidate <= 1UL)
{
markComposite();
printResult(candidate, false, false, false);
continue;
}
if (candidate <= 1UL)
{
markComposite();
printResult(candidate, false, false, false);
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();
@@ -155,154 +145,118 @@ public static class MersenneNumberDivisorByDivisorTester
primesSpan[primeWriteIndex++] = candidate;
if (candidate > maxPrime)
{
maxPrime = candidate;
}
}
if (candidate > maxPrime)
{
maxPrime = candidate;
}
}
if (primeWriteIndex < primesToTest.Count)
{
primesToTest.RemoveRange(primeWriteIndex, primesToTest.Count - primeWriteIndex);
}
if (primeWriteIndex < primesToTest.Count)
{
primesToTest.RemoveRange(primeWriteIndex, primesToTest.Count - primeWriteIndex);
}
if (primesToTest.Count == 0)
{
if (applyStartPrime)
{
Console.WriteLine($"No primes greater than or equal to {startPrime.ToString(CultureInfo.InvariantCulture)} were found for --mersenne=bydivisor.");
}
else
{
Console.WriteLine("No prime candidates remain for --mersenne=bydivisor after filtering.");
}
if (primesToTest.Count == 0)
{
if (applyStartPrime)
{
Console.WriteLine($"No primes greater than or equal to {startPrime.ToString(CultureInfo.InvariantCulture)} were found for --mersenne=bydivisor.");
}
else
{
Console.WriteLine("No prime candidates remain for --mersenne=bydivisor after filtering.");
}
return;
}
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;
}
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);
tester.ConfigureFromMaxPrime(maxPrime);
int primeCount = primesToTest.Count;
ulong[] primeBatch = ArrayPool<ulong>.Shared.Rent(primeCount);
ulong[] allowedMaxBatch = ArrayPool<ulong>.Shared.Rent(primeCount);
List<ulong> filteredPrimes = new(primeCount);
int primeCount = primesToTest.Count;
ulong[] primeBatch = pool.Rent(primeCount);
ulong[] allowedMaxBatch = pool.Rent(primeCount);
List<ulong> filteredPrimes = new(primeCount);
try
{
Span<ulong> primeSpan = primeBatch.AsSpan(0, primeCount);
Span<ulong> allowedMaxSpan = allowedMaxBatch.AsSpan(0, primeCount);
Span<ulong> primeSpan = primeBatch.AsSpan(0, primeCount);
Span<ulong> allowedMaxSpan = allowedMaxBatch.AsSpan(0, primeCount);
for (int i = 0; i < primeCount; i++)
{
primeSpan[i] = primesToTest[i];
}
for (int i = 0; i < primeCount; i++)
{
primeSpan[i] = primesToTest[i];
}
tester.PrepareCandidates(primeSpan, allowedMaxSpan);
tester.PrepareCandidates(primeSpan, allowedMaxSpan);
for (int i = 0; i < primeCount; i++)
{
ulong prime = primeSpan[i];
ulong allowedMax = allowedMaxSpan[i];
for (int i = 0; i < primeCount; i++)
{
ulong prime = primeSpan[i];
ulong allowedMax = allowedMaxSpan[i];
if (allowedMax < 3UL)
{
clearComposite();
printResult(prime, true, true, true);
continue;
}
if (allowedMax < 3UL)
{
clearComposite();
printResult(prime, true, true, true);
continue;
}
filteredPrimes.Add(prime);
}
}
finally
{
ArrayPool<ulong>.Shared.Return(allowedMaxBatch, clearArray: true);
ArrayPool<ulong>.Shared.Return(primeBatch, clearArray: true);
}
filteredPrimes.Add(prime);
}
if (filteredPrimes.Count == 0)
{
if (applyStartPrime)
{
Console.WriteLine($"No primes greater than or equal to {startPrime.ToString(CultureInfo.InvariantCulture)} were found for --mersenne=bydivisor.");
}
pool.Return(allowedMaxBatch, clearArray: true);
pool.Return(primeBatch, clearArray: true);
return;
}
if (filteredPrimes.Count == 0)
{
if (applyStartPrime)
{
Console.WriteLine($"No primes greater than or equal to {startPrime.ToString(CultureInfo.InvariantCulture)} were found for --mersenne=bydivisor.");
}
int workerCount = threadCount <= 0 ? Environment.ProcessorCount : threadCount;
if (workerCount < 1)
{
workerCount = 1;
}
return;
}
void ProcessPrime(ulong prime)
{
bool isPrime = tester.IsPrime(prime, out bool divisorsExhausted, primeTestLimit);
int workerCount = threadCount <= 0 ? Environment.ProcessorCount : threadCount;
if (workerCount < 1)
{
workerCount = 1;
}
if (!isPrime)
{
markComposite();
printResult(prime, true, true, false);
return;
}
void ProcessPrime(ulong prime)
{
bool isPrime = tester.IsPrime(prime, out bool divisorsExhausted);
clearComposite();
printResult(prime, true, divisorsExhausted, true);
}
if (!isPrime)
{
markComposite();
printResult(prime, true, true, false);
return;
}
if (workerCount > filteredPrimes.Count)
{
workerCount = filteredPrimes.Count;
}
clearComposite();
printResult(prime, true, divisorsExhausted, true);
}
if (workerCount <= 1)
{
foreach (ulong prime in filteredPrimes)
{
ProcessPrime(prime);
}
if (workerCount == 1)
{
foreach (ulong prime in filteredPrimes)
{
ProcessPrime(prime);
}
}
else
{
ParallelOptions options = new()
{
MaxDegreeOfParallelism = workerCount
};
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++)
{
int groupSize = baseGroupSize;
if (workerIndex < remaining)
{
groupSize++;
}
startIndex = groupStartIndex;
int endIndex = startIndex + groupSize;
workerRanges[workerIndex] = (startIndex, endIndex);
groupStartIndex = endIndex;
}
ParallelOptions parallelOptions = new()
{
MaxDegreeOfParallelism = workerCount
};
Parallel.ForEach(workerRanges, parallelOptions, range =>
{
for (int candidateIndex = range.Start; candidateIndex < range.End; candidateIndex++)
{
ProcessPrime(filteredPrimes[candidateIndex]);
}
});
}
Parallel.ForEach(filteredPrimes, options, ProcessPrime);
}
}
}
+28 -2
View File
@@ -1,5 +1,3 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core;
@@ -45,4 +43,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);
}
}
}
}
+24 -38
View File
@@ -1,4 +1,4 @@
using System;
using System.Buffers;
namespace PerfectNumbers.Core
{
@@ -7,20 +7,12 @@ namespace PerfectNumbers.Core
[ThreadStatic]
private static PartialFactorResult? s_poolHead;
public static readonly PartialFactorResult Empty = new()
{
Cofactor = 1UL,
FullyFactored = true,
};
public FactorEntry[]? Factors;
private PartialFactorResult? _next;
public ulong Cofactor;
public bool FullyFactored;
public int Count;
private PartialFactorResult? _next;
private bool _factorsFromPool;
private PartialFactorResult()
{
}
@@ -37,58 +29,52 @@ namespace PerfectNumbers.Core
s_poolHead = instance._next;
}
FactorEntry[]? factorArray = factors;
instance._next = null;
instance.Factors = factorArray;
instance.Factors = factors;
instance.Cofactor = cofactor;
instance.FullyFactored = fullyFactored;
instance.Count = count;
instance._factorsFromPool = factorArray is not null && factorArray.Length >= PerfectNumberConstants.PooledArrayThreshold;
return instance;
}
public static readonly PartialFactorResult Empty = new()
{
Cofactor = 1UL,
FullyFactored = true,
};
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);
FactorEntry[] local = ArrayPool<FactorEntry>.Shared.Rent(1);
local[0] = new FactorEntry(prime, 1);
return Rent(local, 1UL, true, 1);
}
int newCount = Count + 1;
FactorEntry[] extended = newCount >= PerfectNumberConstants.PooledArrayThreshold
? ThreadLocalArrayPool<FactorEntry>.Shared.Rent(newCount)
: new FactorEntry[newCount];
FactorEntry[] extended = ArrayPool<FactorEntry>.Shared.Rent(Count + 1);
Array.Copy(source, 0, extended, 0, Count);
extended[Count] = new FactorEntry(prime);
Span<FactorEntry> span = extended.AsSpan(0, newCount);
extended[Count] = new FactorEntry(prime, 1);
Span<FactorEntry> span = extended.AsSpan(0, Count + 1);
span.Sort(static (a, b) => a.Value.CompareTo(b.Value));
return Rent(extended, 1UL, true, newCount);
return Rent(extended, 1UL, true, Count + 1);
}
public void Dispose()
{
if (this == Empty)
if (this != Empty)
{
return;
}
FactorEntry[]? factors = Factors;
if (factors is not null)
{
Factors = null;
if (_factorsFromPool)
FactorEntry[]? factors = Factors;
if (factors is not null)
{
ThreadLocalArrayPool<FactorEntry>.Shared.Return(factors, clearArray: false);
Factors = null;
ThreadStaticPools.FactorEntryPool.Return(factors, clearArray: false);
}
}
_factorsFromPool = false;
_next = s_poolHead;
s_poolHead = this;
_next = s_poolHead;
s_poolHead = this;
}
}
}
}
@@ -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 : IDisposable
{
[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;
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,45 @@
using PerfectNumbers.Core.Gpu;
namespace PerfectNumbers.Core;
internal static partial class PrimeOrderCalculator
{
private static bool IsGpuPow2Allowed => s_pow2ModeInitialized && s_allowGpuPow2;
private const int GpuSmallPrimeFactorSlots = 64;
private static bool TryPopulateSmallPrimeFactorsGpu(ulong value, uint limit, Dictionary<ulong, int> counts, out ulong remaining)
{
ulong[] primeBufferArray = ThreadStaticPools.UlongPool.Rent(GpuSmallPrimeFactorSlots);
int[] exponentBufferArray = ThreadStaticPools.IntPool.Rent(GpuSmallPrimeFactorSlots);
Span<ulong> primeBuffer = primeBufferArray.AsSpan(0, GpuSmallPrimeFactorSlots);
Span<int> exponentBuffer = exponentBufferArray.AsSpan(0, GpuSmallPrimeFactorSlots);
primeBuffer.Clear();
exponentBuffer.Clear();
if (!PrimeOrderGpuHeuristics.TryPartialFactor(value, limit, primeBuffer, exponentBuffer, out int factorCount, out ulong gpuRemaining, out _))
{
remaining = value;
ThreadStaticPools.UlongPool.Return(primeBufferArray);
ThreadStaticPools.IntPool.Return(exponentBufferArray);
return false;
}
remaining = gpuRemaining;
for (int i = 0; i < factorCount; i++)
{
ulong primeValue = primeBuffer[i];
int exponent = exponentBuffer[i];
if (primeValue == 0UL || exponent == 0)
{
continue;
}
counts[primeValue] = exponent;
}
ThreadStaticPools.UlongPool.Return(primeBufferArray);
ThreadStaticPools.IntPool.Return(exponentBufferArray);
return true;
}
}
@@ -1,55 +1,88 @@
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using System.Threading;
using PerfectNumbers.Core.Gpu;
namespace PerfectNumbers.Core;
internal static partial class PrimeOrderCalculator
{
public static UInt128 Calculate(
UInt128 prime,
UInt128? previousOrder,
PrimeOrderSearchConfig config,
PrimeOrderHeuristicDevice device)
{
var scope = UsePow2Mode(device);
MontgomeryDivisorData divisorData;
UInt128 result;
if (prime <= ulong.MaxValue)
{
ulong? previous = null;
if (previousOrder.HasValue)
{
UInt128 previousValue = previousOrder.Value;
if (previousValue <= ulong.MaxValue)
{
previous = (ulong)previousValue;
}
else
{
previous = ulong.MaxValue;
}
}
[ThreadStatic]
private static bool s_pow2ModeInitialized;
ulong prime64 = (ulong)prime;
divisorData = MontgomeryDivisorData.FromModulus(prime64);
ulong order64 = Calculate(prime64, previous, divisorData, config, device);
result = order64 == 0UL ? UInt128.Zero : (UInt128)order64;
}
else
[ThreadStatic]
private static bool s_allowGpuPow2;
[ThreadStatic]
private static PrimeOrderHeuristicDevice s_deviceMode;
[ThreadStatic]
private static bool s_debugLoggingEnabled;
private static bool IsGpuHeuristicDevice => s_pow2ModeInitialized && s_deviceMode == PrimeOrderHeuristicDevice.Gpu;
private readonly struct Pow2ModeScope
{
divisorData = default;
result = CalculateWideInternal(prime, previousOrder, divisorData, config);
private readonly bool _previousInitialized;
private readonly bool _previousAllow;
private readonly PrimeOrderHeuristicDevice _previousDevice;
public Pow2ModeScope(PrimeOrderHeuristicDevice device)
{
_previousInitialized = s_pow2ModeInitialized;
_previousAllow = s_allowGpuPow2;
_previousDevice = s_deviceMode;
s_pow2ModeInitialized = true;
s_allowGpuPow2 = device == PrimeOrderHeuristicDevice.Gpu;
s_deviceMode = device;
}
public void Dispose()
{
s_allowGpuPow2 = _previousAllow;
s_pow2ModeInitialized = _previousInitialized;
s_deviceMode = _previousDevice;
}
}
scope.Dispose();
return result;
}
private readonly struct DebugLoggingScope
{
private readonly bool _previous;
private static UInt128 CalculateWideInternal(UInt128 prime, UInt128? previousOrder, in MontgomeryDivisorData divisorData, PrimeOrderSearchConfig config)
public DebugLoggingScope(bool enabled)
{
_previous = s_debugLoggingEnabled;
s_debugLoggingEnabled = enabled;
}
public void Dispose()
{
s_debugLoggingEnabled = _previous;
}
}
private static Pow2ModeScope UsePow2Mode(PrimeOrderHeuristicDevice device) => new(device);
private static DebugLoggingScope UseDebugLogging(bool enabled) => new(enabled);
[Conditional("DEBUG")]
private static void DebugLog(string message)
{
if (s_debugLoggingEnabled)
{
Console.WriteLine(message);
}
}
[Conditional("DEBUG")]
private static void DebugLog(Func<string> messageFactory)
{
if (s_debugLoggingEnabled)
{
Console.WriteLine(messageFactory());
}
}
private static UInt128 CalculateWideInternal(in UInt128 prime, in UInt128? previousOrder, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config)
{
if (prime <= UInt128.One)
{
@@ -79,12 +112,12 @@ internal static partial class PrimeOrderCalculator
}
private static UInt128 RunHeuristicPipelineWide(
UInt128 prime,
UInt128? previousOrder,
in UInt128 prime,
in UInt128? previousOrder,
in MontgomeryDivisorData divisorData,
PrimeOrderSearchConfig config,
UInt128 phi,
PartialFactorResult128 phiFactors)
in PrimeOrderSearchConfig config,
in UInt128 phi,
in PartialFactorResult128 phiFactors)
{
if (phiFactors.FullyFactored && TrySpecialMaxWide(phi, prime, divisorData, phiFactors))
{
@@ -112,12 +145,12 @@ internal static partial class PrimeOrderCalculator
return candidateOrder;
}
private static UInt128 FinishStrictlyWide(UInt128 prime, in MontgomeryDivisorData divisorData)
private static UInt128 FinishStrictlyWide(in UInt128 prime, in MontgomeryDivisorData divisorData)
{
return CalculateByFactorizationWide(prime, divisorData);
}
private static bool TrySpecialMaxWide(UInt128 phi, UInt128 prime, in MontgomeryDivisorData divisorData, PartialFactorResult128 factors)
private static bool TrySpecialMaxWide(in UInt128 phi, in UInt128 prime, in MontgomeryDivisorData divisorData, in PartialFactorResult128 factors)
{
ReadOnlySpan<FactorEntry128> factorSpan = factors.Factors;
int length = factors.Count;
@@ -134,7 +167,7 @@ internal static partial class PrimeOrderCalculator
return true;
}
private static UInt128 InitializeStartingOrderWide(UInt128 prime, UInt128 phi, in MontgomeryDivisorData divisorData)
private static UInt128 InitializeStartingOrderWide(in UInt128 prime, in UInt128 phi, in MontgomeryDivisorData divisorData)
{
UInt128 order = phi;
UInt128 mod8 = prime & (UInt128)7UL;
@@ -150,19 +183,20 @@ internal static partial class PrimeOrderCalculator
return order;
}
private static UInt128 ExponentLoweringWide(UInt128 order, UInt128 prime, in MontgomeryDivisorData divisorData, PartialFactorResult128 factors)
private static UInt128 ExponentLoweringWide(UInt128 order, in UInt128 prime, in MontgomeryDivisorData divisorData, in PartialFactorResult128 factors)
{
FactorEntry128[]? tempArray = null;
ArrayPool<FactorEntry128> pool = ThreadStaticPools.FactorEntry128Pool;
ReadOnlySpan<FactorEntry128> factorSpan = factors.Factors;
int length = factors.Count;
// TODO: This should never trigger from production code - check
// if (length == 0)
// {
// return order;
// }
FactorEntry128[]? tempArray = pool.Rent(length + 1);
try
{
ReadOnlySpan<FactorEntry128> factorSpan = factors.Factors;
int length = factors.Count;
if (length == 0)
{
return order;
}
tempArray = ThreadLocalArrayPool<FactorEntry128>.Shared.Rent(length + 1);
Span<FactorEntry128> buffer = tempArray.AsSpan(0, length);
factorSpan.CopyTo(buffer);
@@ -198,14 +232,11 @@ internal static partial class PrimeOrderCalculator
}
finally
{
if (tempArray is not null)
{
ThreadLocalArrayPool<FactorEntry128>.Shared.Return(tempArray, clearArray: false);
}
pool.Return(tempArray, clearArray: false);
}
}
private static bool TryConfirmOrderWide(UInt128 prime, UInt128 order, in MontgomeryDivisorData divisorData, PrimeOrderSearchConfig config)
private static bool TryConfirmOrderWide(in UInt128 prime, in UInt128 order, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config)
{
if (order == UInt128.Zero)
{
@@ -263,11 +294,11 @@ internal static partial class PrimeOrderCalculator
}
private static bool TryHeuristicFinishWide(
UInt128 prime,
UInt128 order,
UInt128? previousOrder,
in UInt128 prime,
in UInt128 order,
in UInt128? previousOrder,
in MontgomeryDivisorData divisorData,
PrimeOrderSearchConfig config,
in PrimeOrderSearchConfig config,
out UInt128 result)
{
result = UInt128.Zero;
@@ -339,7 +370,7 @@ internal static partial class PrimeOrderCalculator
return false;
}
private static void SortCandidatesWide(UInt128 prime, UInt128? previousOrder, List<UInt128> candidates)
private static void SortCandidatesWide(UInt128 prime, in UInt128? previousOrder, List<UInt128> candidates)
{
UInt128 previous = previousOrder ?? UInt128.Zero;
int previousGroup = previousOrder.HasValue ? GetGroupWide(previousOrder.Value, prime) : 1;
@@ -367,9 +398,9 @@ internal static partial class PrimeOrderCalculator
}
private static CandidateKey128 BuildKeyWide(
UInt128 value,
UInt128 prime,
UInt128 previous,
in UInt128 value,
in UInt128 prime,
in UInt128 previous,
int previousGroup,
bool hasPrevious)
{
@@ -391,7 +422,7 @@ internal static partial class PrimeOrderCalculator
return new CandidateKey128(primary, false, distance, false, value);
}
private static int CompareComponents(bool descendingX, UInt128 valueX, bool descendingY, UInt128 valueY)
private static int CompareComponents(bool descendingX, in UInt128 valueX, bool descendingY, in UInt128 valueY)
{
if (descendingX == descendingY)
{
@@ -416,7 +447,7 @@ internal static partial class PrimeOrderCalculator
return valueX > valueY ? 1 : -1;
}
private static int GetGroupWide(UInt128 value, UInt128 prime)
private static int GetGroupWide(in UInt128 value, in UInt128 prime)
{
UInt128 threshold1 = prime >> 3;
if (value <= threshold1)
@@ -439,7 +470,7 @@ internal static partial class PrimeOrderCalculator
return 0;
}
private static void BuildCandidatesWide(UInt128 order, FactorEntry128[] factors, int count, List<UInt128> candidates, int limit)
private static void BuildCandidatesWide(in UInt128 order, FactorEntry128[] factors, int count, List<UInt128> candidates, int limit)
{
if (count == 0)
{
@@ -452,10 +483,10 @@ internal static partial class PrimeOrderCalculator
}
private static void BuildCandidatesRecursiveWide(
UInt128 order,
ReadOnlySpan<FactorEntry128> factors,
in UInt128 order,
in ReadOnlySpan<FactorEntry128> factors,
int index,
UInt128 divisorProduct,
in UInt128 divisorProduct,
List<UInt128> candidates,
int limit)
{
@@ -511,7 +542,7 @@ internal static partial class PrimeOrderCalculator
}
}
private static bool TryConfirmCandidateWide(UInt128 prime, UInt128 candidate, PrimeOrderSearchConfig config, ref int powUsed, int powBudget, in MontgomeryDivisorData divisorData)
private static bool TryConfirmCandidateWide(in UInt128 prime, in UInt128 candidate, in PrimeOrderSearchConfig config, ref int powUsed, int powBudget, in MontgomeryDivisorData divisorData)
{
PartialFactorResult128 factorization = PartialFactorWide(candidate, config);
if (factorization.Factors is null)
@@ -564,7 +595,7 @@ internal static partial class PrimeOrderCalculator
return true;
}
private static PartialFactorResult128 PartialFactorWide(UInt128 value, PrimeOrderSearchConfig config)
private static PartialFactorResult128 PartialFactorWide(in UInt128 value, in PrimeOrderSearchConfig config)
{
if (value <= UInt128.One)
{
@@ -605,12 +636,12 @@ internal static partial class PrimeOrderCalculator
if (config.PollardRhoMilliseconds > 0 && pending.Count > 0)
{
Stopwatch stopwatch = Stopwatch.StartNew();
long budgetTicks = TimeSpan.FromMilliseconds(config.PollardRhoMilliseconds).Ticks;
long deadlineTimestamp = CreateDeadlineTimestamp(config.PollardRhoMilliseconds);
Stack<UInt128> stack = new();
stack.Push(remaining);
pending.Clear();
long timestamp = 0L; // reused for deadline checks.
while (stack.Count > 0)
{
UInt128 composite = stack.Pop();
@@ -625,13 +656,14 @@ internal static partial class PrimeOrderCalculator
continue;
}
if (stopwatch.ElapsedTicks > budgetTicks)
timestamp = Stopwatch.GetTimestamp();
if (timestamp > deadlineTimestamp)
{
pending.Add(composite);
continue;
}
if (!TryPollardRhoWide(composite, stopwatch, budgetTicks, out UInt128 factor))
if (!TryPollardRhoWide(composite, deadlineTimestamp, out UInt128 factor))
{
pending.Add(composite);
continue;
@@ -664,7 +696,8 @@ internal static partial class PrimeOrderCalculator
return new PartialFactorResult128(null, value, false, 0);
}
FactorEntry128[]? rented = counts.Count > 0 ? ThreadLocalArrayPool<FactorEntry128>.Shared.Rent(counts.Count) : null;
ArrayPool<FactorEntry128>? pool = null;
FactorEntry128[]? rented = counts.Count > 0 ? (pool = ThreadStaticPools.FactorEntry128Pool).Rent(counts.Count) : null;
int index = 0;
if (rented is not null)
{
@@ -676,20 +709,20 @@ internal static partial class PrimeOrderCalculator
}
FactorEntry128[]? resultArray = null;
if (rented is not null)
if (pool is not null)
{
Span<FactorEntry128> span = rented.AsSpan(0, index);
span.Sort(static (a, b) => a.Value.CompareTo(b.Value));
resultArray = new FactorEntry128[index];
span.CopyTo(resultArray);
ThreadLocalArrayPool<FactorEntry128>.Shared.Return(rented, clearArray: false);
pool.Return(rented!, clearArray: false);
}
bool fullyFactored = cofactor == UInt128.One;
return new PartialFactorResult128(resultArray, cofactor, fullyFactored, index);
}
private static UInt128 PopulateSmallPrimeFactorsCpuWide(UInt128 value, uint limit, Dictionary<UInt128, int> counts)
private static UInt128 PopulateSmallPrimeFactorsCpuWide(in UInt128 value, uint limit, Dictionary<UInt128, int> counts)
{
UInt128 remaining = value;
uint[] primes = PrimesGenerator.SmallPrimes;
@@ -729,7 +762,7 @@ internal static partial class PrimeOrderCalculator
return remaining;
}
private static bool TryPollardRhoWide(UInt128 n, Stopwatch stopwatch, long budgetTicks, out UInt128 factor)
private static bool TryPollardRhoWide(in UInt128 n, long deadlineTimestamp, out UInt128 factor)
{
factor = UInt128.Zero;
if ((n & UInt128.One) == UInt128.Zero)
@@ -741,10 +774,12 @@ internal static partial class PrimeOrderCalculator
UInt128 c = UInt128.One;
UInt128 x = (UInt128)2UL;
UInt128 y = x;
long timestamp = 0L; // reused for deadline checks.
while (true)
{
if (stopwatch.ElapsedTicks > budgetTicks)
timestamp = Stopwatch.GetTimestamp();
if (timestamp > deadlineTimestamp)
{
return false;
}
@@ -771,7 +806,7 @@ internal static partial class PrimeOrderCalculator
}
}
private static UInt128 AdvancePolynomialWide(UInt128 x, UInt128 c, UInt128 modulus)
private static UInt128 AdvancePolynomialWide(in UInt128 x, in UInt128 c, in UInt128 modulus)
{
BigInteger value = (BigInteger)x;
value = (value * value + (BigInteger)c) % (BigInteger)modulus;
@@ -809,7 +844,7 @@ internal static partial class PrimeOrderCalculator
}
}
private static int TrailingZeroCountWide(UInt128 value)
private static int TrailingZeroCountWide(in UInt128 value)
{
if (value == UInt128.Zero)
{
@@ -865,7 +900,7 @@ internal static partial class PrimeOrderCalculator
return order;
}
private static void FactorCompletelyWide(UInt128 value, Dictionary<UInt128, int> counts)
private static void FactorCompletelyWide(in UInt128 value, Dictionary<UInt128, int> counts)
{
if (value <= UInt128.One)
{
@@ -883,7 +918,7 @@ internal static partial class PrimeOrderCalculator
FactorCompletelyWide(value / factor, counts);
}
private static UInt128 PollardRhoWide(UInt128 n)
private static UInt128 PollardRhoWide(in UInt128 n)
{
if ((n & UInt128.One) == UInt128.Zero)
{
@@ -916,7 +951,7 @@ internal static partial class PrimeOrderCalculator
}
}
private static bool IsPrimeWide(UInt128 value)
private static bool IsPrimeWide(in UInt128 value)
{
if (value <= ulong.MaxValue)
{
@@ -996,7 +1031,7 @@ internal static partial class PrimeOrderCalculator
return true;
}
private static UInt128 Pow2ModWide(UInt128 exponent, in UInt128 modulus, in MontgomeryDivisorData divisorData)
private static UInt128 Pow2ModWide(in UInt128 exponent, in UInt128 modulus, in MontgomeryDivisorData divisorData)
{
if (modulus == UInt128.One)
{
@@ -1026,7 +1061,7 @@ internal static partial class PrimeOrderCalculator
return exponent.Pow2MontgomeryModWindowed(modulus);
}
private static void AddFactor(Dictionary<UInt128, int> counts, UInt128 prime, int exponent)
private static void AddFactor(Dictionary<UInt128, int> counts, in UInt128 prime, int exponent)
{
if (counts.TryGetValue(prime, out int existing))
{
@@ -1038,77 +1073,43 @@ internal static partial class PrimeOrderCalculator
}
}
private readonly struct CandidateKey128
{
public CandidateKey128(int primary, bool secondaryDescending, UInt128 secondary, bool tertiaryDescending, UInt128 tertiary)
{
Primary = primary;
SecondaryDescending = secondaryDescending;
Secondary = secondary;
TertiaryDescending = tertiaryDescending;
Tertiary = tertiary;
}
private readonly struct CandidateKey128(int primary, bool secondaryDescending, in UInt128 secondary, bool tertiaryDescending, in UInt128 tertiary)
{
public int Primary { get; } = primary;
public int Primary { get; }
public bool SecondaryDescending { get; } = secondaryDescending;
public bool SecondaryDescending { get; }
public UInt128 Secondary { get; } = secondary;
public UInt128 Secondary { get; }
public bool TertiaryDescending { get; } = tertiaryDescending;
public bool TertiaryDescending { get; }
public UInt128 Tertiary { get; } = tertiary;
}
public UInt128 Tertiary { get; }
}
private readonly struct PartialFactorResult128(FactorEntry128[]? factors, UInt128 cofactor, bool fullyFactored, int count)
{
public FactorEntry128[]? Factors { get; } = factors;
private readonly struct FactorEntry128
{
public FactorEntry128(UInt128 value)
{
Value = value;
}
public UInt128 Cofactor { get; } = cofactor;
public FactorEntry128(UInt128 value, int exponent)
{
Value = value;
Exponent = exponent;
}
public bool FullyFactored { get; } = fullyFactored;
public readonly UInt128 Value;
public int Count { get; } = count;
public readonly int Exponent = 1;
}
private readonly struct PartialFactorResult128
{
public PartialFactorResult128(FactorEntry128[]? factors, UInt128 cofactor, bool fullyFactored, int count)
{
Factors = factors;
Cofactor = cofactor;
FullyFactored = fullyFactored;
Count = count;
}
public readonly FactorEntry128[]? Factors;
public readonly UInt128 Cofactor;
public readonly bool FullyFactored;
public readonly int Count;
public static PartialFactorResult128 Empty => new(null, UInt128.One, true, 0);
public static PartialFactorResult128 Empty => new(null, UInt128.One, true, 0);
public PartialFactorResult128 WithAdditionalPrime(UInt128 prime)
{
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,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;
}
}
}
+320
View File
@@ -0,0 +1,320 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using PerfectNumbers.Core.Cpu;
namespace PerfectNumbers.Core
{
public readonly struct ThreadStaticPools
{
[ThreadStatic]
private static ArrayPool<FactorEntry>? _factorEntryPool;
public static ArrayPool<FactorEntry> FactorEntryPool
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return _factorEntryPool ??= ArrayPool<FactorEntry>.Create();
}
}
[ThreadStatic]
private static ArrayPool<FactorEntry128>? _factorEntry128Pool;
public static ArrayPool<FactorEntry128> FactorEntry128Pool
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return _factorEntry128Pool ??= ArrayPool<FactorEntry128>.Create();
}
}
[ThreadStatic]
private static ArrayPool<KeyValuePair<ulong, int>>? _factorKeyValuePairPool;
public static ArrayPool<KeyValuePair<ulong, int>> FactorKeyValuePairPool
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return _factorKeyValuePairPool ??= ArrayPool<KeyValuePair<ulong, int>>.Create();
}
}
[ThreadStatic]
private static MersenneCpuDivisorScanSession? _mersenneCpuDivisorSession;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static MersenneCpuDivisorScanSession? RentMersenneCpuDivisorSession()
{
MersenneCpuDivisorScanSession? session = _mersenneCpuDivisorSession;
if (session is null)
{
return null;
}
_mersenneCpuDivisorSession = null;
session.Reset();
return session;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ReturnMersenneCpuDivisorSession(MersenneCpuDivisorScanSession session)
{
_mersenneCpuDivisorSession = session;
}
[ThreadStatic]
private static ArrayPool<ulong>? _ulongPool;
public static ArrayPool<ulong> UlongPool
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return _ulongPool ??= ArrayPool<ulong>.Create();
}
}
[ThreadStatic]
private static ArrayPool<int>? _intPool;
public static ArrayPool<int> IntPool
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return _intPool ??= ArrayPool<int>.Create();
}
}
[ThreadStatic]
private static List<List<ulong>>? _ulongListPool;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static List<ulong> RentUlongList(int capacityHint)
{
List<List<ulong>>? pool = _ulongListPool;
if (pool is not null && pool.Count > 0)
{
int lastIndex = pool.Count - 1;
List<ulong> list = pool[lastIndex];
pool.RemoveAt(lastIndex);
if (list.Count > 0)
{
list.Clear();
}
if (list.Capacity < capacityHint)
{
list.Capacity = capacityHint;
}
return list;
}
return new List<ulong>(capacityHint);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReturnUlongList(List<ulong> list)
{
List<List<ulong>>? pool = _ulongListPool;
if (pool is null)
{
pool = new List<List<ulong>>(4);
_ulongListPool = pool;
}
pool.Add(list);
}
[ThreadStatic]
private static List<Stack<ulong>>? _ulongStackPool;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Stack<ulong> RentUlongStack(int capacityHint)
{
List<Stack<ulong>>? pool = _ulongStackPool;
if (pool is not null && pool.Count > 0)
{
int lastIndex = pool.Count - 1;
Stack<ulong> stack = pool[lastIndex];
pool.RemoveAt(lastIndex);
if (stack.Count > 0)
{
stack.Clear();
}
if (stack.Count < capacityHint)
{
stack.EnsureCapacity(capacityHint);
}
return stack;
}
return new Stack<ulong>(capacityHint);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReturnUlongStack(Stack<ulong> stack)
{
List<Stack<ulong>>? pool = _ulongStackPool;
if (pool is null)
{
pool = new List<Stack<ulong>>(4);
_ulongStackPool = pool;
}
pool.Add(stack);
}
[ThreadStatic]
private static List<Dictionary<ulong, int>>? _ulongIntDictionaryPool;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Dictionary<ulong, int> RentUlongIntDictionary(int capacityHint)
{
List<Dictionary<ulong, int>>? pool = _ulongIntDictionaryPool;
if (pool is not null && pool.Count > 0)
{
int lastIndex = pool.Count - 1;
Dictionary<ulong, int> dictionary = pool[lastIndex];
pool.RemoveAt(lastIndex);
if (dictionary.Count > 0)
{
dictionary.Clear();
}
if (dictionary.Count < capacityHint)
{
dictionary.EnsureCapacity(capacityHint);
}
return dictionary;
}
return new Dictionary<ulong, int>(capacityHint);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReturnUlongIntDictionary(Dictionary<ulong, int> dictionary)
{
List<Dictionary<ulong, int>>? pool = _ulongIntDictionaryPool;
if (pool is null)
{
pool = new List<Dictionary<ulong, int>>(4);
_ulongIntDictionaryPool = pool;
}
pool.Add(dictionary);
}
[ThreadStatic]
private static Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? _mersenneFactorCacheDictionary;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry> RentMersenneFactorCacheDictionary()
{
Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? cache = _mersenneFactorCacheDictionary;
if (cache is null)
{
return new Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>(8);
}
_mersenneFactorCacheDictionary = null;
cache.Clear();
return cache;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReturnMersenneFactorCacheDictionary(Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry> cache)
{
for (Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>.Enumerator enumerator = cache.GetEnumerator(); enumerator.MoveNext();)
{
enumerator.Current.Value.ReturnToPool();
}
cache.Clear();
_mersenneFactorCacheDictionary = cache;
}
[ThreadStatic]
private static MersenneDivisorCycles.FactorCacheEntry? _factorCacheEntryPoolHead;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static MersenneDivisorCycles.FactorCacheEntry RentFactorCacheEntry()
{
MersenneDivisorCycles.FactorCacheEntry? entry = _factorCacheEntryPoolHead;
if (entry is null)
{
entry = new MersenneDivisorCycles.FactorCacheEntry();
}
else
{
_factorCacheEntryPoolHead = entry.Next;
}
entry.Reset();
return entry;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReturnFactorCacheEntry(MersenneDivisorCycles.FactorCacheEntry entry)
{
entry.Next = _factorCacheEntryPoolHead;
_factorCacheEntryPoolHead = entry;
}
[ThreadStatic]
private static Dictionary<ulong, int>? _factorCountDictionary;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Dictionary<ulong, int> RentFactorCountDictionary()
{
Dictionary<ulong, int>? dictionary = _factorCountDictionary;
if (dictionary is null)
{
return new Dictionary<ulong, int>(8);
}
_factorCountDictionary = null;
dictionary.Clear();
return dictionary;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReturnFactorCountDictionary(Dictionary<ulong, int> dictionary)
{
dictionary.Clear();
_factorCountDictionary = dictionary;
}
[ThreadStatic]
private static Dictionary<ulong, int>? _factorScratchDictionary;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Dictionary<ulong, int> RentFactorScratchDictionary()
{
Dictionary<ulong, int>? dictionary = _factorScratchDictionary;
if (dictionary is null)
{
return new Dictionary<ulong, int>(8);
}
_factorScratchDictionary = null;
dictionary.Clear();
return dictionary;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ReturnFactorScratchDictionary(Dictionary<ulong, int> dictionary)
{
dictionary.Clear();
_factorScratchDictionary = dictionary;
}
}
}
-1
View File
@@ -1,4 +1,3 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using PerfectNumbers.Core.Gpu;
File diff suppressed because it is too large. Load diff
@@ -8,7 +8,4 @@
<ItemGroup>
<ProjectReference Include="../../PerfectNumbers.Core/PerfectNumbers.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove=".temp.cs" />
</ItemGroup>
</Project>
</Project>