Compare commits

..
Author SHA1 Message Date
Marek Lemańczyk 73608e0c2f Enable by divisor mode to honor starting prime 2025-09-22 23:59:26 +02:00
151 changed files with 5668 additions and 27577 deletions

No files matched your search

+3 -16
View File
@@ -20,22 +20,9 @@
"request": "launch",
"preLaunchTask": "Build Debug",
"program": "${workspaceFolder}/${input:projectName}/bin/Debug/net8.0/${input:projectName}.dll",
"args": [
"--increment=add",
"--mersenne=bydivisor",
"--threads=1",
"--gpu-scan-batch=256000",
"--primes-device=gpu",
"--mersenne-device=gpu",
"--mod-reduction=barrett128",
"--block-size=1000000",
"--gpu-prime-threads=64",
"--write-batch-size=10000",
"--divisor-cycles-device=gpu",
"--prime=31",
"--results-prefix=test-",
"--filter-p=sorted-primes-passed-residue-_even_perfect_bit_scan_inc-add_thr-16_blk-10000000_mers-incremental_mersdev-gpu_ntt-reference_red-barrett128_primesdev-gpu_order-off_orderdev-gpu_gcd-off_modfix-off_gputh-64_llslice-32_scanb-512000_warm-5000000.csv"
],
"args": [
"--increment=add --mersenne=residue --threads=128 --gpu-scan-batch=512000 --primes-device=gpu --mersenne-device=gpu --mod-reduction=barrett128 --block-size=10000000 --gpu-prime-threads=32 --write-batch-size=10000 --divisor-cycles-device=gpu"
],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "internalConsole"
+1 -7
View File
@@ -2,12 +2,6 @@
"dotnet.defaultSolution": "FlexiCoder.sln",
"chatgpt.openOnStartup": true,
"cSpell.words": [
"CCCCCCCDUL",
"cofactor",
"ILGPU",
"mers",
"Mersenne",
"primality",
"suff"
"ILGPU"
]
}
+4 -96
View File
@@ -14,61 +14,14 @@ To build `EvenPerfectScanner` run:
dotnet build EvenPerfectScanner.sln
```
Run only the tests related to the solution you modify. Always run tests without --no-build parameter to avoid run failures due to missing assembly files. Tests should be always written using XUnit with FluentAssertions. Before executing any tests, make sure their running time is reasonable; skip test suites that are expected to take excessively long. When running tests, always provide both an explicit timeout guard and the Category=Fast filter so that only the fast suite executes.
## Making Changes in the Repository
To make any changes to the files in the repository, you should always utilize a script created for it to overcome environment / scripting issues. The script is called `apply_patch.cs` and has the following syntax:
```bash
dotnet script apply_patch.cs -- <patch-file> [target-directory]
```
> **Temporary workflow notice:** Until the pending apply_patch.cs performance and correctness TODOs are complete, do not invoke the script in this repository. Edit files directly with shell tools (sed, tee, cat, python, etc.) and stage the results via git. Re-enable the helper only after the TODO tracker explicitly calls out that the ban has been lifted.
It supports the standard `git diff` patch files as input. You should be able to run it both in the local and remote `Codex` environments. You should expect it to work both in `Windows` and `Linux` environments. Make sure that you include `diff --git` lines, headers and file prefixes in the patch files to avoid applying issues.
`apply_patch.cs` supports several parameters, depending on your needs:
* `--check` - allows checking if the patch will work without making changes to the files
* `--ignore-whitespace`
* `--ignore-eol`
* `--ignore-line-endings`
* `--target` - allows specifying target root directory for the patch
* `--profile` - capture read/parse/apply timings for each patch file.
* `--replace-line <path> <line-number> <expected-text> <new-text>` - replace a single line by absolute number (use `_` as the expected text to skip validation)
* Build patches in standard `git diff` format.
* It supports `---`, `+++`, and `@@`.
* It doesn't support `***` or `*** Begin/End Patch` wrappers.
* It requires `diff --git` at the beginning.
* Patch header needs the actual line numbers.
* When hand-writing a diff, always include the numeric ranges in every `@@ -start,count +start,count @@` header; missing or zero counts will cause `--check` to fail immediately.
* Always check if the patch will work by using `--check` parameter, without making changes to the files, which would force patch changes. Try applying the patch only when `--check` passed.
* Build patches in small, line-precise hunks; confirm target lines with `Get-Content` or `nl -ba` before writing the diff to avoid context mismatches.
* Capture the exact start lines for each hunk while drafting the `@@ -start,length +start,length @@` header; double-check them with `nl -ba` so the first verification succeeds.
* For one-off line edits, prefer the `--replace-line <path> <line-number> <expected-text> <new-text>` option. Pass `_` as the expected text to skip validation when necessary.
* Before calling `--replace-line`, capture the target number with `nl -ba` (or an equivalent command) so the replacement lands exactly once without retrying.
* After every successful patch application, regenerate a fresh diff from the updated files before preparing the next patch so the hunk headers and context reflect the latest line numbers.
* If the patch script flags repeated mismatches, regenerate a fresh diff for the updated file instead of tweaking stale context blocks.
* When `--check` reveals that only whitespace or EOL normalization differs, rerun it with `--ignore-whitespace` and/or `--ignore-line-endings` so validation concentrates on the meaningful edits.
* Regenerate the diff whenever the file shifts—never reuse an old hunk after other edits.
* Keep patch filenames unique and delete applied files to prevent accidental re-use.
* When a hunk fails, inspect the around-lines immediately and adjust rather than retrying the same diff.
* Prefer single-purpose patches (one logical change per file) so rollbacks or fixes stay focused.
* Use `nl -ba` to show line numbers, when needed.
* Always use 4 spaces for indentation of code blocks.
If you want to make any changes, create a patch file and run the script to apply it. Always run it from the root directory and reference any modified files with relative paths respectively in the patch.
Do **not** attempt to apply the changes manually, or with `Python`, or with `PowerShell` scripts, or `cat` or any other way, unless you apply changes to `apply_patch.cs` script itself. If you identify any issues with `apply_patch.cs` script, you should propose fixes to resolve them and continue using the script after updates. If you spot any missing, but required features, propose enhancements and continue using the script after updates, too.
Run only the tests related to the solution you modify. Always run tests without --no-build parameter to avoid run failures due to missing assembly files. Tests should be always written using XUnit with FluentAssertions. Before executing any tests, make sure their running time is reasonable; skip test suites that are expected to take excessively long.
### Test execution time policy
- Always estimate runtime before launching any tests or long-running samples. If a test or ad-hoc run is likely to exceed 2 minutes, do not run it unless the user explicitly requests a longer run and provides justification.
- Never apply the two-minute guard to benchmark or profiling runs; allow them to finish and capture the summary unless the console log shows the job is clearly stuck.
- Do not hardcode timeouts inside test code. Enforce time limits from the runner/shell layer so developers can run tests locally without artificial constraints.
- Prefer targeted filters over full suites to keep runs short: `--filter FullyQualifiedName~<ClassOrMethod>` or `--filter "FullyQualifiedName=Namespace.Class.Method"`.
- Keep GPU-heavy tests minimal and scoped. If a kernel compile or warmup is expected to take >2 minutes, skip running it during regular iterations.
- When working on a task in a local Codex environment, do not run any existing unit tests, except for fast, new, added tests, unless you're instructed otherwise. In the task summary list the names of the recommended unit tests to validate the changes, instead.
#### How to run tests within the 2-minute limit
@@ -82,8 +35,7 @@ Do **not** attempt to apply the changes manually, or with `Python`, or with `Pow
- Linux/macOS (bash):
- Use GNU timeout (install coreutils if needed on macOS):
- `timeout 120s dotnet test <path-to>.csproj -c Debug --filter "Category=Fast&FullyQualifiedName~SomeTests"`
- Always specify `timeout 120s` (or a shorter limit when appropriate) and include `Category=Fast` in every filter.
- `timeout 120s dotnet test <path-to>.csproj -c Debug --filter "FullyQualifiedName~SomeTests"`
- Note: exit code 124 indicates timeout; handle accordingly in scripts.
- Codex/agent runs: when invoked as separate steps where inline time control isn’t available, wrap calls through the shell using the guards above rather than calling `dotnet test` directly from the tool.
@@ -100,14 +52,11 @@ The only .Net SDK available in Codex environment is .Net 8.0. It'll be periodica
Always enter blank line after closing "}" parenthesis, unless there is a consecutive closing "}" bracket. IF's, loops, in practice all code blocks should be put in their "{ (...) }" blocks. Even if there is only a single line in such a code block. Always use the latest officially released language features. All .csproj files should define nullable = enabled and we should always use "?" and/or "!" where appropriate.
Never ever run tests marked with trait Category = Slow. Always invoke `dotnet test` (or equivalent wrappers) with both an explicit timeout and the `Category=Fast` filter; never run tests without specifying these arguments. If there are no categories assigned yet, categorize them before running or skip the execution.
Never ever run tests marked with trait Category = Slow. Only run tests with category "Fast", when you want to validate if you haven't broken anything. If there are no categories assigned to the tests, when you attempt running them always watch out the 2 mins execution time, to prevent hanging up on test execution. In such case, marked all timeout out tests with category Slow and tests which executed within time limit, even if they failed, mark with category Fast, so that we can recognize them in the future.
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.
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.
Always use indentation with 4 spaces and / or 1 tab in source code files.
All code, comments, commit messages, branch names and PR descriptions must be written in American English.
@@ -139,48 +88,7 @@ The following checks were executed with ILGPU 1.5.3 on CPU and OpenCL accelerato
- [x] `UInt128` bit shifts: attempts failed in kernels; avoid inside device code.
- [x] GPU context pooling: supported and recommended (validated in a separate program).
Additional numeric-type probe results (`tools/GpuNumericTypeProbe.csx`, ILGPU 1.5.3 on the CPU accelerator):
- `System.Numerics.BigInteger`: unsupported. Kernels that exercise addition, subtraction, multiplication, division, modulo, and `CompareTo` all fail with `InternalCompilerException`, indicating fundamental codegen gaps for the type.
- `PeterO.Numbers.EInteger`: unsupported. Attempts to compile kernels that call addition, subtraction, multiplication, division, remainder, and `CompareTo` throw `InternalCompilerException`, so none of the arithmetic helpers are currently usable on the device.
- `PeterO.Numbers.ERational`: unsupported. Kernels that call addition, subtraction, multiplication, division, and `CompareTo` trigger `InternalCompilerException`; the type also lacks a remainder/modulo API for device usage.
Patch-preparation checklist for reliable `--check` runs:
- Manually review every patch file before running `--check`; confirm the hunks reflect only the intended edits and eliminate accidental whitespace-only noise ahead of time.
- Always capture context with `nl -ba` before composing each hunk so the `@@` header line numbers match the target file on the first try.
- Ensure every hunk carries at least three unchanged context lines above and below the edits whenever the surrounding file provides them; regenerate the diff if the file changed since the previous attempt so the context stays accurate.
- Trim or normalize trailing whitespace while drafting the patch, or rely on `--ignore-whitespace` during verification when formatting noise is unavoidable, then re-run without ignores to confirm the final diff matches exactly.
- Prefer regenerating the patch from scratch whenever the working tree changes instead of editing previous diffs; stale context is the most common source of mismatches.
- The patch parser now ignores blank metadata lines between `diff --git`, `---`, and `+++` headers and skips stray blank separators between hunks, so you no longer need to insert dummy empty lines to satisfy the checker. Keep the header compact unless your diff tool emits those blank lines naturally.
### apply_patch.cs profiling snapshot (2025-10-04)
- Latest timing check: `dotnet script apply_patch.cs -- --profile --check /tmp/agents_todo_update.diff`.
- Patch characteristics: 2 files, 4 hunks, 31 lines, 3468 bytes.
- Timings: read 0.45 ms, parse 2.96 ms, apply 2.96 ms.
- Focus next on streaming the patch reader and pooled buffer reuse to reduce the parse APPLY bottleneck highlighted above.
Mersenne divisor size estimates:
- Any prime divisor `q` of a Mersenne number `2^p - 1` must satisfy `q ≡ 1 (mod 2p)`. The minimal `q` therefore appears at `q = 2p + 1` when the multiplier `k = 1` yields a prime.
- For candidate exponents `p` in the `[138,000,000, 200,000,000]` range, the minimal compliant divisor lands below `2 · 200,000,000 + 1 = 400,000,001`, which fits in 29 bits (`2^29 = 536,870,912`).
- Extending the search up to `p = 600,000,000` pushes the minimal divisor to `1,200,000,001`, requiring 31 bits (`2^31 = 2,147,483,648`).
- Larger `k` values (and thus larger `q`) are possible—up to nearly `2^p - 1`—so the heuristics and storage should plan for wider integers when deep trial factoring is necessary, but the noted bit widths cover the first admissible divisors for these exponent bands.
- Because every proper divisor of `2^p - 1` is at most `(2^p - 1) / 2`, the worst-case bit-width for any admissible `q` is `p - 1` bits (since `⌊(2^p - 1) / 2⌋ = 2^{p-1} - 1`). Consequently we need roughly `199,999,999` bits when `p ≤ 200,000,000` and `599,999,999` bits when `p ≤ 600,000,000`. In terms of the `q = 2kp + 1` form, this translates to `k_max = ⌊((2^p - 1) / 2 - 1) / (2p)⌋`, which is approximately `2^{p-2} / p` for large `p`.
Practical guidance:
- Use native `%` and `/` in kernels when working with 32-bit or 64-bit integers if it is the most efficient option for the target backend.
- For `UInt128` in kernels, avoid `%`/`/`; implement reductions via Montgomery/Barrett, or use Mersenne-specific folding for mod `2^p-1`.
- Prefer Mersenne folding over general modulo for `2^p-1` moduli to remove branches in hot loops.
- Heuristic multiply-shift benchmarks (2025-10-04):
- Run `dotnet run -c Release --project tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj` to compare the `ULongExtensions` helpers without the BenchmarkDotNet harness.
- Timings below report the average nanoseconds per call over 200,000,000 iterations; all variants produced identical checksums, confirming consistent arithmetic.
- `NearOverflowShift`: `MultiplyShiftRight` ≈ 2.37 ns, shift-first ≈ 4.71 ns, naive `(value * multiplier) >> shift` ≈ 0.85 ns.
- `HalfRange`: `MultiplyShiftRight` ≈ 2.11 ns, shift-first ≈ 4.15 ns, naive ≈ 0.70 ns.
- `MixedBits`: `MultiplyShiftRight` ≈ 1.43 ns, shift-first ≈ 2.83 ns, naive ≈ 0.60 ns.
- `Sparse`: `MultiplyShiftRight` ≈ 1.57 ns, shift-first ≈ 2.84 ns, naive ≈ 0.59 ns.
- The shift-first rewrite roughly doubles the per-call cost on CPU, but it stays within 64-bit arithmetic and is the preferred fallback for GPU heuristics when avoiding `UInt128` overflow is mandatory.
-3124
View File
File diff suppressed because it is too large. Load diff
@@ -1,3 +1,4 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
@@ -1,85 +0,0 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class GpuPow2ModBenchmarks
{
private static readonly Pow2ModInput[] Inputs = new Pow2ModInput[]
{
new(63UL, new GpuUInt128(0UL, 97UL), "SmallExponentSmallModulus"),
new(1_048_575UL, new GpuUInt128(0UL, 0xFFFF_FFFBUL), "MediumExponentPrimeModulus"),
new(6_710_886_400UL, new GpuUInt128(0x0000_0000_0000_0001UL, 0xFFFF_FFFF_FFFF_FFC3UL), "LargeExponentHighWordModulus"),
new(ulong.MaxValue, new GpuUInt128(0xFFFF_FFFF_FFFF_FFFBUL, 0xFFFF_FFFF_FFFF_FFC5UL), "FullWidthExponentFullWidthModulus"),
};
[ParamsSource(nameof(GetInputs))]
public Pow2ModInput Input { get; set; }
public static IEnumerable<Pow2ModInput> GetInputs() => Inputs;
/// <summary>
/// Windowed (8-bit) exponentiation that dominated every dataset: 21.5 μs for the full-width modulus, 7.87 μs for the large
/// set, 265 ns for medium, and 82.97 ns for the small modulus sample.
/// </summary>
/// <remarks>
/// Observed means: FullWidthExponentFullWidthModulus 21,481.71 ns (1.00×), LargeExponentHighWordModulus 7,874.85 ns,
/// MediumExponentPrimeModulus 264.97 ns, SmallExponentSmallModulus 82.97 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public GpuUInt128 ProcessEightBitWindows()
{
return GpuUInt128.Pow2Mod(Input.Exponent, Input.Modulus);
}
/// <summary>
/// Bit-by-bit fallback; competitive only on medium/small moduli (266 ns / 85.39 ns) while trailing badly on the widest
/// modulus (50.97 μs, 2.37× slower) and large input (8.85 μs, 1.12× slower).
/// </summary>
/// <remarks>
/// Observed means: FullWidthExponentFullWidthModulus 50,967.07 ns (2.37×), LargeExponentHighWordModulus 8,851.95 ns,
/// MediumExponentPrimeModulus 265.98 ns, SmallExponentSmallModulus 85.39 ns.
/// </remarks>
[Benchmark]
public GpuUInt128 ProcessSingleBits()
{
return Pow2ModSingleBit(Input.Exponent, Input.Modulus);
}
private static GpuUInt128 Pow2ModSingleBit(ulong exponent, in GpuUInt128 modulus)
{
if (modulus.IsZero || modulus == GpuUInt128.One)
{
return new GpuUInt128();
}
GpuUInt128 result = new(1UL);
GpuUInt128 baseVal = new(2UL);
ulong e = exponent;
while (e != 0UL)
{
if ((e & 1UL) != 0UL)
{
result.MulMod(baseVal, modulus);
}
baseVal.MulMod(baseVal, modulus);
e >>= 1;
}
return result;
}
public readonly record struct Pow2ModInput(ulong Exponent, GpuUInt128 Modulus, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,29 +0,0 @@
using BenchmarkDotNet.Attributes;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class GpuRationalBenchmarks
{
private static readonly GpuRational Left = new(new GpuUInt128(3UL), new GpuUInt128(4UL));
private static readonly GpuRational Right = new(new GpuUInt128(5UL), new GpuUInt128(6UL));
private static readonly GpuUInt128 IntegerLeft = new(3UL);
private static readonly GpuUInt128 IntegerRight = new(5UL);
[Benchmark]
public GpuRational AddRational()
{
GpuRational value = Left;
value = value + Right;
return value;
}
[Benchmark]
public GpuUInt128 AddInteger()
{
GpuUInt128 value = IntegerLeft;
value.Add(IntegerRight);
return value;
}
}
@@ -1,71 +0,0 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class GpuUInt128AddBenchmarks
{
private static readonly AddInput[] Inputs = new AddInput[]
{
new(new GpuUInt128(0UL, 0UL), new GpuUInt128(0UL, 1UL), "IncrementLow"),
new(new GpuUInt128(0UL, ulong.MaxValue), new GpuUInt128(0UL, 1UL), "CarryIntoHigh"),
new(new GpuUInt128(1UL, ulong.MaxValue), new GpuUInt128(3UL, 7UL), "MixedOperands"),
new(new GpuUInt128(ulong.MaxValue, ulong.MaxValue), new GpuUInt128(ulong.MaxValue, ulong.MaxValue), "AllBitsSet"),
};
[ParamsSource(nameof(GetInputs))]
public AddInput Input { get; set; }
public static IEnumerable<AddInput> GetInputs() => Inputs;
/// <summary>
/// Materializes the carry in a local before updating the struct; stayed between 0.459 ns and 0.474 ns across all operand
/// patterns, giving it the edge on carry-heavy cases.
/// </summary>
/// <remarks>
/// Observed means: AllBitsSet 0.4737 ns (1.00×), CarryIntoHigh 0.4586 ns, IncrementLow 0.4653 ns, MixedOperands 0.4628 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public GpuUInt128 CarryMaterialisedInLocal()
{
GpuUInt128 value = Input.Left;
value.Add(Input.Right);
return value;
}
/// <summary>
/// Computes the carry inline within the expression; matches the baseline on balanced additions (0.474 ns mixed) but trails
/// slightly when the carry bubbles into the high word (0.491 ns) or chains through increments (0.508 ns).
/// </summary>
/// <remarks>
/// Observed means: AllBitsSet 0.4676 ns (0.98×), CarryIntoHigh 0.4907 ns, IncrementLow 0.5082 ns, MixedOperands 0.4741 ns.
/// </remarks>
[Benchmark]
public GpuUInt128 CarryInExpression()
{
GpuUInt128 value = Input.Left;
AddInline(ref value, Input.Right);
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AddInline(ref GpuUInt128 left, GpuUInt128 right)
{
ulong low = left.Low + right.Low;
left.High = left.High + right.High + (low < left.Low ? 1UL : 0UL);
left.Low = low;
}
public readonly record struct AddInput(GpuUInt128 Left, GpuUInt128 Right, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,100 +0,0 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class GpuUInt128BinaryGcdBenchmarks
{
private static readonly BinaryGcdInput[] Inputs = new BinaryGcdInput[]
{
new(new GpuUInt128(0UL, 48UL), new GpuUInt128(0UL, 18UL), "SmallOperands"),
new(
new GpuUInt128(0x0123_4567_89AB_CDEFUL, 0xFEDC_BA98_7654_3210UL),
new GpuUInt128(0x0FED_CBA9_8765_4321UL, 0x0123_4567_89AB_CDEFUL),
"HighEntropy"),
new(new GpuUInt128(0UL, ulong.MaxValue), new GpuUInt128(0UL, ulong.MaxValue - 1UL), "LowWordHeavy"),
new(new GpuUInt128(0x8000_0000_0000_0000UL, 0UL), new GpuUInt128(0x7FFF_FFFF_FFFF_FFFFUL, 0UL), "HighWordOnly"),
};
[ParamsSource(nameof(GetInputs))]
public BinaryGcdInput Input { get; set; }
public static IEnumerable<BinaryGcdInput> GetInputs() => Inputs;
/// <summary>
/// Baseline that reuses locals between iterations; measured 195–196 ns on high-entropy/high-word-only inputs, 157.8 ns on
/// low-word-heavy pairs, and 7.32 ns on tiny operands.
/// </summary>
/// <remarks>
/// Observed means: HighEntropy 195.593 ns (1.00×), HighWordOnly 196.218 ns, LowWordHeavy 157.787 ns, SmallOperands 7.317 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public GpuUInt128 ReusedVariables()
{
return BinaryGcdWithReusedVariables(Input.Left, Input.Right);
}
/// <summary>
/// Allocates a temporary struct per loop iteration; trims 4–9% off the large inputs (186–186 ns) and 18% off small operands
/// (5.99 ns), making it the better all-around choice when GC pressure is acceptable.
/// </summary>
/// <remarks>
/// Observed means: HighEntropy 186.615 ns (0.95×), HighWordOnly 185.849 ns, LowWordHeavy 143.686 ns, SmallOperands 5.987 ns.
/// </remarks>
[Benchmark]
public GpuUInt128 TemporaryStructPerIteration()
{
return GpuUInt128.BinaryGcd(Input.Left, Input.Right);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static GpuUInt128 BinaryGcdWithReusedVariables(GpuUInt128 u, GpuUInt128 v)
{
if (u.IsZero)
{
return v;
}
if (v.IsZero)
{
return u;
}
ulong combinedLow = u.Low | v.Low;
ulong combinedHigh = u.High | v.High;
int shift = combinedLow != 0UL
? BitOperations.TrailingZeroCount(combinedLow)
: 64 + BitOperations.TrailingZeroCount(combinedHigh);
int zu = GpuUInt128.TrailingZeroCount(u);
u >>= zu;
do
{
int zv = GpuUInt128.TrailingZeroCount(v);
v >>= zv;
if (u > v)
{
(u, v) = (v, u);
}
v -= u;
}
while (!v.IsZero);
return u << shift;
}
public readonly record struct BinaryGcdInput(GpuUInt128 Left, GpuUInt128 Right, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,92 +0,0 @@
using System;
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class GpuUInt128Montgomery64Benchmarks
{
private static readonly MontgomeryInput[] Inputs = new MontgomeryInput[]
{
Create(0x0000_0000_0000_0065UL, 0x0000_0000_0000_0037UL, 0x0000_0000_0000_007FUL, "TinyOperands"),
Create(0x7FFF_FFFF_FFFF_FFABUL, 0x7FFF_FFFF_FFFF_FFC5UL, 0xFFFF_FFFF_FFFF_FFC5UL, "NearModulus"),
Create(0x0123_4567_89AB_CDEFUL, 0x0FED_CBA9_8765_4321UL, 0xFFFF_FFFF_FFFF_FF4FUL, "MixedMagnitude"),
Create(0xFFFF_FFFF_FFFF_FF01UL, 0xFFFF_FFFF_FFFF_FDCBUL, 0xFFFF_FFFF_FFFF_FFCFUL, "DenseOperands"),
};
[ParamsSource(nameof(GetInputs))]
public MontgomeryInput Input { get; set; }
public static IEnumerable<MontgomeryInput> GetInputs() => Inputs;
/// <summary>
/// Extension-based Montgomery multiply that ran in 3.64 ns across every operand mix (dense, mixed, near-modulus, tiny),
/// making it the reference CPU helper.
/// </summary>
/// <remarks>
/// Observed means: DenseOperands 3.663 ns (1.00×), MixedMagnitude 3.636 ns, NearModulus 3.651 ns, TinyOperands 3.639 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public ulong ExtensionMontgomeryMultiply()
{
return Input.Left.MontgomeryMultiply(Input.Right, Input.Modulus, Input.NPrime);
}
/// <summary>
/// Struct-based GPU-style multiply; matches device arithmetic but costs 24.9 ns regardless of the operand pattern,
/// approximately 6.8× slower than the extension helper.
/// </summary>
/// <remarks>
/// Observed means: DenseOperands 24.904 ns (6.80×), MixedMagnitude 24.886 ns, NearModulus 24.893 ns, TinyOperands 24.869 ns.
/// </remarks>
[Benchmark]
public ulong GpuStructMontgomeryMultiply()
{
// TODO: Migrate remaining callers of MulModMontgomery64 to the extension helper; the struct
// emulation is ~6.8× slower but kept for GPU parity benchmarks.
GpuUInt128 state = new(Input.Left);
return state.MulModMontgomery64(Input.Right, Input.Modulus, Input.NPrime, Input.R2);
}
private static MontgomeryInput Create(ulong left, ulong right, ulong modulus, string name)
{
ulong adjustedModulus = (modulus & 1UL) == 0UL ? modulus | 1UL : modulus;
ulong nPrime = ComputeMontgomeryNPrime(adjustedModulus);
ulong r2 = ComputeMontgomeryR2(adjustedModulus);
return new MontgomeryInput(left, right, adjustedModulus, nPrime, r2, name);
}
private static ulong ComputeMontgomeryR2(ulong modulus)
{
UInt128 r = UInt128.One << 64;
return (ulong)((r * r) % modulus);
}
private static ulong ComputeMontgomeryNPrime(ulong modulus)
{
ulong inv = modulus;
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
return unchecked(0UL - inv);
}
public readonly record struct MontgomeryInput(
ulong Left,
ulong Right,
ulong Modulus,
ulong NPrime,
ulong R2,
string Name)
{
public override string ToString() => Name;
}
}
@@ -1,70 +0,0 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class GpuUInt128Mul64Benchmarks
{
private static readonly Mul64Input[] Inputs = new Mul64Input[]
{
new(new GpuUInt128(0UL, 1UL), new GpuUInt128(0UL, 1UL), "TinyOperands"),
new(new GpuUInt128(0UL, ulong.MaxValue), new GpuUInt128(0UL, ulong.MaxValue), "LowWordMax"),
new(new GpuUInt128(1UL, 0UL), new GpuUInt128(0UL, ulong.MaxValue), "HighByLow"),
new(new GpuUInt128(ulong.MaxValue, ulong.MaxValue), new GpuUInt128(ulong.MaxValue, ulong.MaxValue), "AllBitsSet"),
};
[ParamsSource(nameof(GetInputs))]
public Mul64Input Input { get; set; }
public static IEnumerable<Mul64Input> GetInputs() => Inputs;
/// <summary>
/// Baseline that materializes the high product in a local; measured 1.53–1.55 ns across all operand patterns, keeping it in
/// front of the inline variant.
/// </summary>
/// <remarks>
/// Observed means: AllBitsSet 1.551 ns (1.00×), HighByLow 1.532 ns, LowWordMax 1.548 ns, TinyOperands 1.529 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public GpuUInt128 HighProductMaterializedInLocal()
{
GpuUInt128 value = Input.Multiplicand;
value.Mul64(Input.Multiplier);
return value;
}
/// <summary>
/// Inline variant that avoids the temporary struct field update, but costs roughly 4–5% extra work with 1.59–1.63 ns means.
/// </summary>
/// <remarks>
/// Observed means: AllBitsSet 1.631 ns (1.05×), HighByLow 1.594 ns, LowWordMax 1.623 ns, TinyOperands 1.602 ns.
/// </remarks>
[Benchmark]
public GpuUInt128 HighProductInline()
{
GpuUInt128 value = Input.Multiplicand;
Mul64Inline(ref value, Input.Multiplier);
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Mul64Inline(ref GpuUInt128 left, GpuUInt128 right)
{
ulong operand = left.Low;
left.Low = operand * right.Low;
left.High = operand * right.High + GpuUInt128.MulHigh(operand, right.Low);
}
public readonly record struct Mul64Input(GpuUInt128 Multiplicand, GpuUInt128 Multiplier, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,118 +0,0 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class GpuUInt128MulModBenchmarks
{
private static readonly MulModInput[] Inputs =
[
new(
new GpuUInt128(0UL, 5UL),
new GpuUInt128(0UL, 7UL),
new GpuUInt128(0UL, 97UL),
"TinyOperands"),
new(
new GpuUInt128(0UL, ulong.MaxValue - 3UL),
new GpuUInt128(0UL, ulong.MaxValue - 7UL),
new GpuUInt128(0UL, ulong.MaxValue - 11UL),
"LowWordHeavy"),
new(
new GpuUInt128(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL),
new GpuUInt128(0x0ACE_BDF0_1357_9BDFUL, 0x0246_8ACE_ECA8_6421UL),
new GpuUInt128(0x1FFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFF7UL),
"HighWordModulus"),
new(
new GpuUInt128(0x8000_0000_0000_0000UL, 0x0000_0000_0000_0001UL),
new GpuUInt128(0x7FFF_FFFF_FFFF_FFFFUL, 0x8000_0000_0000_0000UL),
new GpuUInt128(0x7FFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFF1UL),
"MixedMagnitude"),
];
[ParamsSource(nameof(GetInputs))]
public MulModInput Input { get; set; }
public static IEnumerable<MulModInput> GetInputs() => Inputs;
/// <summary>
/// In-place multiply-modulo; great for low-word-heavy operands (6.88 ns) and tiny pairs (7.15 ns) but notably slower on
/// large moduli (193–201 ns).
/// </summary>
/// <remarks>
/// Observed means: HighWordModulus 192.794 ns (1.00×), LowWordHeavy 6.883 ns, MixedMagnitude 201.468 ns, TinyOperands 7.146 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public GpuUInt128 InPlaceMulMod()
{
// TODO: Switch the extension method to allocate per iteration, as it's faster. Keep the benchmarks.
GpuUInt128 value = Input.Left;
value.MulMod(Input.Right, Input.Modulus);
return value;
}
/// <summary>
/// Allocates fresh temporaries per iteration; pays off on every dataset with 4.66 ns on tiny operands, 117 ns on
/// low-word-heavy inputs, 138 ns on mixed magnitude, and 184 ns on high-word moduli.
/// </summary>
/// <remarks>
/// Observed means: HighWordModulus 184.322 ns (0.96×), LowWordHeavy 116.993 ns, MixedMagnitude 138.684 ns, TinyOperands 4.659 ns.
/// </remarks>
[Benchmark]
public GpuUInt128 AllocatePerIteration()
{
return MulModInline(Input.Left, Input.Right, Input.Modulus);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static GpuUInt128 MulModInline(GpuUInt128 left, GpuUInt128 right, GpuUInt128 modulus)
{
// Reuse the variables to new instances and lower registry pressure, if possible.
GpuUInt128 a = left;
GpuUInt128 b = right;
GpuUInt128 result = new();
while (!b.IsZero)
{
if ((b.Low & 1UL) != 0UL)
{
result.Add(a);
if (result.CompareTo(modulus) >= 0)
{
result.Sub(modulus);
}
}
a <<= 1;
if (a.CompareTo(modulus) >= 0)
{
a.Sub(modulus);
}
if (b.High == 0UL)
{
b = new GpuUInt128(b.Low >> 1);
}
else
{
ulong low = (b.Low >> 1) | (b.High << 63);
ulong high = b.High >> 1;
b = new GpuUInt128(high, low);
}
}
return result;
}
public readonly record struct MulModInput(GpuUInt128 Left, GpuUInt128 Right, GpuUInt128 Modulus, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,159 +0,0 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class GpuUInt128MulModByLimbBenchmarks
{
private static readonly MulModByLimbInput[] Inputs = new MulModByLimbInput[]
{
new(
new GpuUInt128(0UL, 17UL),
new GpuUInt128(0UL, 19UL),
new GpuUInt128(0x0000_0000_0000_0001UL, 0x0000_0000_0000_00FFUL),
"TinyOperands"),
new(
new GpuUInt128(0x0123_4567_89AB_CDEFUL, 0xFEDC_BA98_7654_3210UL),
new GpuUInt128(0x0F1E_2D3C_4B5A_6978UL, 0x8776_6554_4332_2110UL),
new GpuUInt128(0x0FFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFC3UL),
"MixedMagnitude"),
new(
new GpuUInt128(0x8000_0000_0000_0000UL, 0x0000_0000_0000_0001UL),
new GpuUInt128(0x7FFF_FFFF_FFFF_FFFFUL, 0x8000_0000_0000_0000UL),
new GpuUInt128(0x7FFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFF1UL),
"HighWordDominant"),
};
[ParamsSource(nameof(GetInputs))]
public MulModByLimbInput Input { get; set; }
public static IEnumerable<MulModByLimbInput> GetInputs() => Inputs;
/// <summary>
/// Legacy implementation that allocates intermediate limbs; delivered 10.7 ns on tiny operands, 12.7 ns on high-word-dominant
/// pairs, and 16.3 ns on mixed magnitude, making it the faster choice overall.
/// </summary>
/// <remarks>
/// Observed means: HighWordDominant 12.69 ns (1.00×), MixedMagnitude 16.26 ns, TinyOperands 10.74 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public GpuUInt128 LegacyAllocating()
{
return MulModByLimbLegacy(Input.Left, Input.Right, Input.Modulus);
}
/// <summary>
/// In-place reduction variant; avoids allocations but costs 12.9–25.1 ns, trailing the legacy path by 20–55% depending on the
/// operand mix.
/// </summary>
/// <remarks>
/// Observed means: HighWordDominant 17.88 ns (1.41×), MixedMagnitude 25.14 ns, TinyOperands 12.92 ns.
/// </remarks>
[Benchmark]
public GpuUInt128 InPlaceReduction()
{
// TODO: Switch the extension method back to legacy method, as it's faster. Keep the benchmarks.
GpuUInt128 value = Input.Left;
value.MulModByLimb(Input.Right, Input.Modulus);
return value;
}
private static GpuUInt128 MulModByLimbLegacy(GpuUInt128 left, GpuUInt128 right, GpuUInt128 modulus)
{
var (p3, p2, p1, p0) = MultiplyFullLegacy(left, right);
GpuUInt128 remainder = new(p3, p2);
while (remainder.CompareTo(modulus) >= 0)
{
remainder.Sub(modulus);
}
ulong limb = p1;
for (int i = 0; i < 2; i++)
{
remainder <<= 64;
remainder = new GpuUInt128(remainder.High, limb);
while (remainder.CompareTo(modulus) >= 0)
{
remainder.Sub(modulus);
}
limb = p0;
}
return remainder;
}
private static (ulong P3, ulong P2, ulong P1, ulong P0) MultiplyFullLegacy(GpuUInt128 left, GpuUInt128 right)
{
var (h0, l0) = Mul64Legacy(left.Low, right.Low);
var (h1, l1) = Mul64Legacy(left.Low, right.High);
var (h2, l2) = Mul64Legacy(left.High, right.Low);
var (h3, l3) = Mul64Legacy(left.High, right.High);
ulong carry = 0UL;
ulong sum0 = l0;
ulong sum1 = h0;
sum1 += l1;
if (sum1 < l1)
{
carry++;
}
sum1 += l2;
if (sum1 < l2)
{
carry++;
}
ulong sum2 = h1 + h2;
ulong carry2 = sum2 < h2 ? 1UL : 0UL;
sum2 += l3;
if (sum2 < l3)
{
carry2++;
}
sum2 += carry;
if (sum2 < carry)
{
carry2++;
}
ulong p1 = sum1;
ulong p2 = sum2;
ulong p3 = h3 + carry2;
return (p3, p2, p1, sum0);
}
private static (ulong High, ulong Low) Mul64Legacy(ulong left, ulong right)
{
ulong a0 = (uint)left;
ulong a1 = left >> 32;
ulong b0 = (uint)right;
ulong b1 = right >> 32;
ulong lo = a0 * b0;
ulong mid1 = a1 * b0;
b0 = a0 * b1;
b1 *= a1;
a0 = (lo >> 32) + (uint)mid1 + (uint)b0;
a1 = (lo & 0xFFFF_FFFFUL) | (a0 << 32);
b1 += (mid1 >> 32) + (b0 >> 32) + (a0 >> 32);
return (b1, a1);
}
public readonly record struct MulModByLimbInput(GpuUInt128 Left, GpuUInt128 Right, GpuUInt128 Modulus, string Name)
{
public override string ToString() => Name;
}
}
@@ -1,59 +0,0 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class GpuUInt128NativeModuloBenchmarks
{
private static readonly NativeModuloInput[] Inputs = new NativeModuloInput[]
{
new(0x0000_0000_0000_0003UL, 0x0000_0000_0000_0005UL, 0x0000_0000_0000_007FUL, "TinyOperands"),
new(0x7FFF_FFFF_FFFF_FFC3UL, 0x7FFF_FFFF_FFFF_FF21UL, 0x7FFF_FFFF_FFFF_FFC5UL, "NearModulus"),
new(0xFFFF_FFFF_FFFF_FFFBUL, 0xFFFF_FFFF_FFFF_FFCFUL, 0xFFFF_FFFF_FFFF_FFF5UL, "DenseOperands"),
new(0x0123_4567_89AB_CDEFUL, 0x0FED_CBA9_8765_4321UL, 0x1FFF_FFFF_FFFF_FFFBUL, "MixedMagnitude"),
};
[ParamsSource(nameof(GetInputs))]
public NativeModuloInput Input { get; set; }
public static IEnumerable<NativeModuloInput> GetInputs() => Inputs;
/// <summary>
/// Performs the modulo immediately after loading the left operand; runs in 7.28 ns on tiny inputs and 38–58 ns on dense or
/// mixed workloads, keeping it well ahead of the deferred path.
/// </summary>
/// <remarks>
/// Observed means: DenseOperands 38.127 ns (1.00×), MixedMagnitude 58.555 ns, NearModulus 54.691 ns, TinyOperands 7.278 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public ulong ImmediateModulo()
{
GpuUInt128 state = new(Input.Left % Input.Modulus);
return state.MulMod(Input.Right, Input.Modulus);
}
/// <summary>
/// Defers the modulo until after the multiply; only helps on tiny operands (18.15 ns) but is 4–8× slower on dense or
/// near-modulus workloads at 256–299 ns.
/// </summary>
/// <remarks>
/// Observed means: DenseOperands 298.926 ns (7.83×), MixedMagnitude 255.624 ns, NearModulus 293.280 ns, TinyOperands 18.149 ns.
/// </remarks>
[Benchmark]
public ulong DeferredNativeModulo()
{
// TODO: Drop MulModWithNativeModulo from production once all callers use the immediate
// reduction path; benchmarks show it is 4–8× slower on large 128-bit operands.
GpuUInt128 state = new(Input.Left);
return state.MulModWithNativeModulo(Input.Right, Input.Modulus);
}
public readonly record struct NativeModuloInput(ulong Left, ulong Right, ulong Modulus, string Name)
{
public override string ToString() => Name;
}
}
@@ -1,70 +0,0 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class GpuUInt128ScalarMulModBenchmarks
{
private static readonly ScalarMulModInput[] Inputs = new ScalarMulModInput[]
{
new(
new GpuUInt128(0x0000_0000_0000_0000UL, 0x0000_0000_0000_0011UL),
0x0000_0000_0000_0013UL,
new GpuUInt128(0x0000_0000_0000_0001UL, 0x0000_0000_0000_00FFUL),
"TinyOperands"),
new(
new GpuUInt128(0x0123_4567_89AB_CDEFUL, 0xFEDC_BA98_7654_3210UL),
0x0F1E_2D3C_4B5A_6978UL,
new GpuUInt128(0x0FFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFC3UL),
"MixedMagnitude"),
new(
new GpuUInt128(0x8000_0000_0000_0000UL, 0x0000_0000_0000_0001UL),
0x7FFF_FFFF_FFFF_FFFBUL,
new GpuUInt128(0x7FFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFF1UL),
"HighWordDominant"),
};
[ParamsSource(nameof(GetInputs))]
public ScalarMulModInput Input { get; set; }
public static IEnumerable<ScalarMulModInput> GetInputs() => Inputs;
/// <summary>
/// Allocates a temporary <see cref="GpuUInt128"/> for the scalar operand; landed at 164–194 ns across the three workloads,
/// making it the slower option.
/// </summary>
/// <remarks>
/// Observed means: HighWordDominant 193.7 ns (1.00×), MixedMagnitude 180.7 ns, TinyOperands 164.0 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public GpuUInt128 StructAllocating()
{
GpuUInt128 value = Input.Left;
value.MulMod(new GpuUInt128(Input.Right), Input.Modulus);
return value;
}
/// <summary>
/// Specialized scalar path that multiplies directly by the 64-bit factor; saves 6–13% by avoiding the temporary (154–192 ns).
/// </summary>
/// <remarks>
/// Observed means: HighWordDominant 192.1 ns (0.99×), MixedMagnitude 168.5 ns, TinyOperands 154.2 ns.
/// </remarks>
[Benchmark]
public GpuUInt128 SpecializedScalar()
{
GpuUInt128 value = Input.Left;
value.MulMod(Input.Right, Input.Modulus);
return value;
}
public readonly record struct ScalarMulModInput(GpuUInt128 Left, ulong Right, GpuUInt128 Modulus, string Name)
{
public override string ToString() => Name;
}
}
@@ -1,82 +0,0 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class GpuUInt128SubModScalarBenchmarks
{
private static readonly SubModInput[] Inputs = new SubModInput[]
{
new(new GpuUInt128(0UL, 3UL), 5UL, new GpuUInt128(0UL, 11UL), "UnderflowAddsModulus"),
new(new GpuUInt128(0UL, ulong.MaxValue - 3UL), 7UL, new GpuUInt128(0UL, ulong.MaxValue - 11UL), "LargeLowWord"),
new(new GpuUInt128(17UL, 2UL), 1UL, new GpuUInt128(3UL, 7UL), "HighWordBorrow"),
};
[ParamsSource(nameof(GetInputs))]
public SubModInput Input { get; set; }
public static IEnumerable<SubModInput> GetInputs() => Inputs;
/// <summary>
/// In-place subtraction that hits 0.509–0.717 ns across the scenarios, edging out the legacy helper especially when a borrow
/// is required.
/// </summary>
/// <remarks>
/// Observed means: HighWordBorrow 0.5112 ns (1.00×), LargeLowWord 0.5092 ns, UnderflowAddsModulus 0.7169 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public GpuUInt128 CurrentInPlace()
{
GpuUInt128 value = Input.Left;
value.SubMod(Input.Scalar, Input.Modulus);
return value;
}
/// <summary>
/// Legacy version that stages intermediates in temporaries; nearly matches the in-place path but remains 1–4% slower on every
/// case (0.514–0.722 ns).
/// </summary>
/// <remarks>
/// Observed means: HighWordBorrow 0.5308 ns (1.04×), LargeLowWord 0.5144 ns, UnderflowAddsModulus 0.7224 ns.
/// </remarks>
[Benchmark]
public GpuUInt128 LegacyTemporaries()
{
GpuUInt128 value = Input.Left;
LegacySubMod(ref value, Input.Scalar, Input.Modulus);
return value;
}
private static void LegacySubMod(ref GpuUInt128 value, ulong scalar, GpuUInt128 modulus)
{
ulong low;
ulong high;
if (value.High == 0UL && value.Low < scalar)
{
low = value.Low + modulus.Low;
ulong carry = low < value.Low ? 1UL : 0UL;
high = value.High + modulus.High + carry;
}
else
{
high = value.High;
low = value.Low;
}
ulong borrow = low < scalar ? 1UL : 0UL;
value.High = high - borrow;
value.Low = low - scalar;
}
public readonly record struct SubModInput(GpuUInt128 Left, ulong Scalar, GpuUInt128 Modulus, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,53 +0,0 @@
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class HeuristicMultiplyShiftBenchmarks
{
private static readonly MultiplyShiftInput[] Inputs =
[
new(0x3FFF_FFFF_FFFF_FFFFUL, 3UL, 3, "NearOverflowShift"),
new(0x2000_0000_0000_0000UL, 7UL, 4, "HalfRange"),
new(0x1234_5678_9ABC_DEF0UL, 5UL, 5, "MixedBits"),
new(0x0000_0000_1000_0000UL, 11UL, 3, "Sparse"),
];
[ParamsSource(nameof(GetInputs))]
public MultiplyShiftInput Input { get; set; }
public static IEnumerable<MultiplyShiftInput> GetInputs()
{
return Inputs;
}
[Benchmark(Baseline = true)]
public ulong SafeMultiplyShift()
{
return ULongExtensions.MultiplyShiftRight(Input.Value, Input.Multiplier, Input.Shift);
}
[Benchmark]
public ulong ShiftFirstMultiplyShift()
{
return ULongExtensions.MultiplyShiftRightShiftFirst(Input.Value, Input.Multiplier, Input.Shift);
}
[Benchmark]
public ulong NaiveMultiplyShift()
{
return (Input.Value * Input.Multiplier) >> Input.Shift;
}
public readonly record struct MultiplyShiftInput(ulong Value, ulong Multiplier, int Shift, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,511 +0,0 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 3)]
[MemoryDiagnoser]
public class MersenneDivisorCycleLengthGpuBenchmarks
{
private const int Iterations = 64;
private static readonly ulong[] StepOffsets = [0UL, 2UL, 4UL, 6UL, 8UL, 10UL, 12UL, 14UL];
private const int StepMask = 7;
private static readonly ulong[] Divisors =
[
17UL,
31UL,
8191UL,
131071UL,
524287UL,
2097151UL,
8388607UL,
];
[ParamsSource(nameof(GetDivisors))]
public ulong Divisor { get; set; }
public IEnumerable<ulong> GetDivisors() => Divisors;
/// <summary>
/// Baseline single-step GPU loop; spans from 6.74 ns at divisor 17 to 4.95 ms at divisor 8,388,607 and serves as the ratio
/// reference for the other variants.
/// </summary>
[Benchmark(Baseline = true, OperationsPerInvoke = Iterations)]
public ulong CurrentImplementation()
{
ulong checksum = 0UL;
ulong baseDivisor = Divisor | 1UL;
for (int i = 0; i < Iterations; i++)
{
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
checksum ^= CalculateCycleLengthGpu_Current(candidate);
}
return checksum;
}
/// <summary>
/// Do-while style loop; nearly matches the baseline (5.88 ns at 17, 9.41 ns at 31) and trims ~5% off the largest divisor with
/// 4.95 ms at 8,388,607.
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
public ulong DoWhileImplementation()
{
ulong checksum = 0UL;
ulong baseDivisor = Divisor | 1UL;
for (int i = 0; i < Iterations; i++)
{
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
checksum ^= CalculateCycleLengthGpu_DoWhile(candidate);
}
return checksum;
}
/// <summary>
/// Double-subtract variant retained for comparison; ranges from 6.30 ns at divisor 17 to 4.92 ms at 8,388,607, generally matching
/// or slightly trailing the baseline.
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
public ulong DoubleSubtractImplementation()
{
ulong checksum = 0UL;
ulong baseDivisor = Divisor | 1UL;
for (int i = 0; i < Iterations; i++)
{
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
checksum ^= CalculateCycleLengthGpu_DoubleSubtract(candidate);
}
return checksum;
}
/// <summary>
/// UInt128 modulo helper used for comparison; incurs 42.36 ns at divisor 17 and stretches to 6.17 ms at 8,388,607, making it the
/// slowest option across the board.
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
public ulong UInt128ModuloImplementation()
{
ulong checksum = 0UL;
ulong baseDivisor = Divisor | 1UL;
for (int i = 0; i < Iterations; i++)
{
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
checksum ^= CalculateCycleLengthGpu_UInt128Modulo(candidate);
}
return checksum;
}
/// <summary>
/// Two-step unrolled loop; improves small divisors to 6.79 ns (17) and large ones to 4.78 ms (8,388,607), shaving ~3–5% off the
/// baseline at the top end.
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
public ulong UnrolledPairImplementation()
{
ulong checksum = 0UL;
ulong baseDivisor = Divisor | 1UL;
for (int i = 0; i < Iterations; i++)
{
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
checksum ^= CalculateCycleLengthGpu_UnrolledPair(candidate);
}
return checksum;
}
/// <summary>
/// Four-step unrolled loop; posts 5.83 ns at divisor 17 and 4.75 ms at 8,388,607, outperforming the pair loop especially on large
/// divisors (52.18 μs at 131,071 vs. 52.65 μs).
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
public ulong UnrolledQuadImplementation()
{
ulong checksum = 0UL;
ulong baseDivisor = Divisor | 1UL;
for (int i = 0; i < Iterations; i++)
{
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
checksum ^= CalculateCycleLengthGpu_UnrolledQuad(candidate);
}
return checksum;
}
/// <summary>
/// Eight-step unrolled loop; 5.45 ns at divisor 17, 582 ns at 8,191, 50.45 μs at 131,071, and 4.67 ms at 8,388,607—slightly behind the
/// hex loop on the largest case but faster on mid-range divisors.
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
public ulong UnrolledOctImplementation()
{
ulong checksum = 0UL;
ulong baseDivisor = Divisor | 1UL;
for (int i = 0; i < Iterations; i++)
{
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
checksum ^= CalculateCycleLengthGpu_UnrolledOct(candidate);
}
return checksum;
}
/// <summary>
/// Sixteen-step unrolled loop (hex); leads the pack at the largest divisor with 4.59 ms and remains competitive elsewhere (4.83 ns at 17,
/// 5.13 ns at 31, 50.52 μs at 131,071).
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
public ulong UnrolledHexImplementation()
{
ulong checksum = 0UL;
ulong baseDivisor = Divisor | 1UL;
for (int i = 0; i < Iterations; i++)
{
ulong candidate = ((baseDivisor + StepOffsets[i & StepMask]) | 1UL);
checksum ^= CalculateCycleLengthGpu_UnrolledHex(candidate);
}
return checksum;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_Current(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
ulong order = 1UL;
ulong pow = 2UL;
while (pow != 1UL)
{
pow <<= 1;
if (pow >= divisor)
{
pow -= divisor;
}
order++;
}
return order;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_DoWhile(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
ulong order = 1UL;
ulong pow = 2UL;
while (true)
{
pow <<= 1;
if (pow >= divisor)
{
pow -= divisor;
}
order++;
if (pow == 1UL)
{
return order;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_DoubleSubtract(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
ulong order = 1UL;
ulong pow = 2UL;
while (pow != 1UL)
{
pow += pow;
if (pow >= divisor)
{
pow -= divisor;
if (pow >= divisor)
{
pow -= divisor;
}
}
order++;
}
return order;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_UInt128Modulo(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
ulong order = 1UL;
ulong pow = 2UL;
while (pow != 1UL)
{
System.UInt128 doubled = (System.UInt128)pow << 1;
pow = (ulong)(doubled % divisor);
order++;
}
return order;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_UnrolledPair(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
ulong order = 1UL;
ulong pow = 2UL;
while (true)
{
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_UnrolledQuad(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
ulong order = 1UL;
ulong pow = 2UL;
while (true)
{
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_UnrolledOct(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
ulong order = 1UL;
ulong pow = 2UL;
while (true)
{
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong CalculateCycleLengthGpu_UnrolledHex(ulong divisor)
{
if ((divisor & (divisor - 1UL)) == 0UL)
{
return 1UL;
}
ulong order = 1UL;
ulong pow = 2UL;
while (true)
{
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
if (Step(ref pow, divisor, ref order))
{
return order;
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool Step(ref ulong pow, ulong divisor, ref ulong order)
{
pow += pow;
if (pow >= divisor)
{
pow -= divisor;
}
order++;
return pow == 1UL;
}
}
@@ -1,66 +1,52 @@
using BenchmarkDotNet.Attributes;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod10UInt128Benchmarks
{
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
public UInt128 Value { get; set; }
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
public UInt128 Value { get; set; }
/// <summary>
/// Direct `% 10` using the <see cref="UInt128"/> operator; lands between 3.25 ns and 3.38 ns (3.378 ns at value 3, 3.269 ns at
/// 8191, 3.248 ns at 131071, 3.281 ns at 2147483647) and serves as the baseline.
/// </summary>
[Benchmark(Baseline = true)]
public UInt128 StandardModulo()
{
return Value % 10UL;
}
[Benchmark(Baseline = true)]
public UInt128 StandardModulo()
{
return Value % 10UL;
}
/// <summary>
/// Splits the value into high/low halves and folds with the 2^64 ≡ 6 (mod 10) rule; fastest option at 1.78-1.82 ns (1.804 ns at
/// value 3, 1.798 ns at 8191, 1.817 ns at 131071, 1.779 ns at 2147483647), beating the baseline by ~1.9x.
/// </summary>
[Benchmark]
public ulong ModWithHighLowModulo()
{
return ModWithHighLowModuloHelper(Value);
}
[Benchmark]
public ulong ModWithHighLowModulo()
{
// Split and fold under mod 10: 2^64 ≡ 6 (mod 10)
ulong value = (ulong)Value;
ulong high = value >> 64;
ulong low = value;
ulong highRem = high % 10UL;
ulong lowRem = low % 10UL;
ulong combined = lowRem + highRem * 6UL;
return combined % 10UL;
}
private static ulong ModWithHighLowModuloHelper(UInt128 value128)
{
ulong value = (ulong)value128;
return ((value % 10UL) + ((value >> 64) % 10UL) * 6UL) % 10UL;
}
/// <summary>
/// Loop-based folding that repeatedly applies the 2^64 ≡ 6 rule; measures 2.20-2.27 ns (2.204 ns at value 3, 2.234 ns at 8191,
/// 2.268 ns at 131071, 2.216 ns at 2147483647), situating it between the high/low helper and the raw modulo.
/// </summary>
[Benchmark]
public ulong ModWithLoopModulo()
{
return ModWithLoopModuloHelper(Value);
}
UInt128 value = Value;
UInt128 zero = UInt128.Zero;
if (value == zero)
return 0UL;
private static ulong ModWithLoopModuloHelper(UInt128 value)
{
UInt128 zero = UInt128.Zero;
if (value == zero)
{
return 0UL;
}
ulong result = (ulong)value;
UInt128 high = value >> 64;
ulong result = (ulong)value;
value >>= 64;
while (high != zero)
{
// 2^64 ≡ 6 (mod 10)
result = (result + (ulong)high * 6UL) % 10UL;
high >>= 64;
}
while (value != zero)
{
result = (result + (ulong)value * 6UL) % 10UL;
value >>= 64;
}
return result % 10UL;
return result % 10UL;
}
}
@@ -1,4 +1,3 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -9,30 +8,19 @@ namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod10UIntBenchmarks
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint Mod10(uint value) => value - (uint)((value * UIntExtensions.Mod5Mask) >> 35) * 10U;
[Params(10U, 2047U, 65535U, 2147483647U)]
public uint Value { get; set; }
/// <summary>
/// Direct `% 10` on <see cref="uint"/>; BenchmarkDotNet flagged near-zero timings, but the measured means were 0.0407 ns at
/// value 10, 0.3224 ns at 2047, 0.2382 ns at 65535, and 0.0555 ns at 2147483647.
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 10U;
}
/// <summary>
/// Optimized extension method that folds with a precomputed mask; still costs 0.77 ns on the tiny value 10 but drops to
/// 0.0448 ns at 2047, 0.0405 ns at 65535, and 0.0372 ns at 2147483647 (roughly 6-9x faster where the baseline exceeded the
/// empty-loop noise floor).
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return Mod10(Value);
return Value.Mod10();
}
}
@@ -1,3 +1,4 @@
using System.Numerics;
using BenchmarkDotNet.Attributes;
using PerfectNumbers.Core;
@@ -9,23 +10,16 @@ public class Mod10ULongBenchmarks
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
public ulong Value { get; set; }
/// <summary>
/// Scalar `% 10` baseline; consistently measured around 0.26 ns (0.2591 ns at value 3, 0.2597 ns at 8191, 0.2605 ns at 131071,
/// 0.2598 ns at 2147483647).
/// </summary>
[Benchmark(Baseline = true)]
public ulong LegacyModulo()
{
return Value % 10UL;
return Value % 10UL;
}
/// <summary>
/// Extension helper that applies the multiply-high mask; 0.73-0.75 ns across the suite (0.7503 ns at value 3, 0.7320 ns at
/// 8191, 0.7346 ns at 131071, 0.7411 ns at 2147483647), roughly 2.8-2.9x slower than the raw modulo on `ulong`.
/// </summary>
[Benchmark]
public ulong ModMethodModulo()
{
return Value.Mod10();
}
}
@@ -1,3 +1,4 @@
using System.Numerics;
using BenchmarkDotNet.Attributes;
using PerfectNumbers.Core;
@@ -9,10 +10,6 @@ public class Mod10_8_5_3Benchmarks
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
public ulong Value { get; set; }
/// <summary>
/// Baseline scalar modulo path; held steady between 1.13 ns and 1.14 ns across every value (1.138 ns at 3, 1.143 ns at 8191,
/// 1.134 ns at 131071, 1.141 ns at 2147483647), making it the fastest option overall.
/// </summary>
[Benchmark(Baseline = true)]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) ModuloOperator()
{
@@ -20,51 +17,41 @@ public class Mod10_8_5_3Benchmarks
return (value % 10UL, value % 8UL, value % 3UL, value % 5UL);
}
/// <summary>
/// Legacy folding helper; useful for cross-checks but stays at 2.83-3.07 ns (2.834 ns at 3, 3.038 ns at 8191, 3.067 ns at
/// 131071, 3.039 ns at 2147483647), roughly 2.5-2.7x slower than the baseline.
/// </summary>
[Benchmark]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) LegacyModulo()
{
ulong mod10;
ulong mod8;
ulong mod3;
ulong mod5;
UInt128 zero = UInt128.Zero;
if (Value == zero)
{
mod3 = 0UL;
mod5 = 0UL;
mod8 = 0UL;
mod10 = 0UL;
return (mod10, mod8, mod3, mod5);
}
ulong mod10, mod8, mod3, mod5;
UInt128 zero = UInt128.Zero;
if (Value == zero)
{
mod3 = 0UL;
mod5 = 0UL;
mod8 = 0UL;
mod10 = 0UL;
return (mod10, mod8, mod3, mod5);
}
UInt128 high = Value >> 64;
ulong result = Value;
mod8 = result & 7UL;
UInt128 high = Value >> 64;
ulong result = Value;
mod8 = result & 7UL;
// 2^64 ≡ 1 (mod 3)
ulong modRem = (result % 3UL) + ((ulong)high % 3UL);
mod3 = modRem >= 3UL ? modRem - 3UL : modRem;
// 2^64 ≡ 1 (mod 5)
modRem = (result % 5UL) + ((ulong)high % 5UL);
mod5 = modRem >= 5UL ? modRem - 5UL : modRem;
ulong modRem = (result % 3UL) + ((ulong)high % 3UL);
mod3 = modRem >= 3UL ? modRem - 3UL : modRem;
while (high != zero)
{
// 2^64 ≡ 6 (mod 10)
result = (result + (ulong)high * 6UL) % 10UL;
high >>= 64;
}
modRem = (result % 5UL) + ((ulong)high % 5UL);
mod5 = modRem >= 5UL ? modRem - 5UL : modRem;
while (high != zero)
{
result = (result + (ulong)high * 6UL) % 10UL;
high >>= 64;
}
mod10 = result % 10UL;
mod10 = result % 10UL;
return (mod10, mod8, mod3, mod5);
}
/// <summary>
/// Method-based optimized helper; clocks in at 1.63 ns for value 3, 1.64 ns at 8191, 1.875 ns at 131071, and 2.164 ns at
/// 2147483647 (1.43-1.90x the baseline) so it trails the raw `%` but remains 1.6x quicker than the legacy fold.
/// </summary>
[Benchmark]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) ModMethodModulo()
{
@@ -72,3 +59,4 @@ public class Mod10_8_5_3Benchmarks
return (mod10, mod8, mod3, mod5);
}
}
@@ -1,3 +1,4 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using PerfectNumbers.Core;
@@ -7,10 +8,10 @@ namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod10_8_5_3PropertiesBenchmarks
{
public ulong Mod10R;
public ulong Mod8R;
public ulong Mod3R;
public ulong Mod5R;
public ulong Mod10R;
public ulong Mod8R;
public ulong Mod3R;
public ulong Mod5R;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod8(UInt128 value) => (ulong)value & 7UL;
@@ -18,6 +19,7 @@ public class Mod10_8_5_3PropertiesBenchmarks
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod3(UInt128 value)
{
// 2^64 ≡ 1 (mod 3)
ulong high = (ulong)(value >> 64);
ulong low = (ulong)value;
ulong rem = (low % 3UL) + (high % 3UL);
@@ -27,55 +29,37 @@ public class Mod10_8_5_3PropertiesBenchmarks
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod5(UInt128 value)
{
// 2^64 ≡ 1 (mod 5)
ulong high = (ulong)(value >> 64);
ulong low = (ulong)value;
ulong rem = (low % 5UL) + (high % 5UL);
return rem >= 5UL ? rem - 5UL : rem;
}
[Params(
3UL,
6UL,
128UL,
256UL,
2047UL,
8191UL,
65535UL,
131071UL,
2147483647UL,
ulong.MaxValue - 1024UL,
ulong.MaxValue - 511UL,
ulong.MaxValue - 255UL,
ulong.MaxValue - 127UL,
ulong.MaxValue - 63UL,
ulong.MaxValue - 31UL,
ulong.MaxValue - 17UL)]
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
public ulong Value { get; set; }
/// <summary>
/// Legacy property-based helper; stayed within 2.35-2.46 ns on the small inputs and 2.36-2.46 ns across the near-ulong.Max
/// cases (e.g. 2.413 ns at value 3, 2.378 ns at 6, 2.458 ns at 2147483647, 2.371 ns near ulong.Max), keeping it in front.
/// </summary>
[Benchmark(Baseline = true)]
public (ulong Mod10, ulong Mod8, ulong Mod5, ulong Mod3) LegacyModulo()
{
UInt128 value = Value;
UInt128 value = Value;
Mod10R = value.Mod10();
Mod8R = Mod8(value);
Mod5R = Mod5(value);
Mod3R = Mod3(value);
return (Mod10R, Mod8R, Mod5R, Mod3R);
Mod8R = Mod8(value);
Mod5R = Mod5(value);
Mod3R = Mod3(value);
return (Mod10R, Mod8R, Mod5R, Mod3R);
}
/// <summary>
/// Method variant that routes through <see cref="UInt128Extensions.Mod10_8_5_3"/>; ranged from 2.79 ns at value 3 to 4.34 ns on
/// the largest operands (3.135 ns at 6, 3.033 ns at 2047, 3.441 ns at 2147483647, 4.289 ns near ulong.Max), roughly 1.16-1.83x
/// slower than the legacy path.
/// </summary>
[Benchmark]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) ModMethodModulo()
{
Value.Mod10_8_5_3(out Mod10R, out Mod8R, out Mod5R, out Mod3R);
Value.Mod10_8_5_3(out var mod10, out ulong mod8, out ulong mod5, out ulong mod3);
Mod10R = mod10;
Mod8R = mod8;
Mod5R = mod5;
Mod3R = mod3;
return (Mod10R, Mod8R, Mod3R, Mod5R);
}
}
@@ -1,3 +1,4 @@
using System.Numerics;
using BenchmarkDotNet.Attributes;
using PerfectNumbers.Core;
@@ -9,29 +10,24 @@ public class Mod10_8_5_3StepsBenchmarks
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
public ulong Value { get; set; }
/// <summary>
/// Legacy step-per-bit helper; required 11.89-13.40 ns per call (12.052 ns at value 3, 11.891 ns at 8191, 13.401 ns at 131071,
/// 12.114 ns at 2147483647), making it 3-4x slower than the streamlined method variant.
/// </summary>
[Benchmark(Baseline = true)]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) LegacyModulo()
{
UInt128 value = Value;
ulong mod10 = (((ulong)(value % 10UL)) << 1) % 10UL;
ulong mod8 = (((ulong)(value & 7UL)) << 1) & 7UL;
ulong mod5 = (((ulong)(value % 5UL)) << 1) % 5UL;
ulong mod3 = (((ulong)(value % 3UL)) << 1) % 3UL;
UInt128 value = Value;
ulong mod10 = (ulong)(value % 10UL) % 10UL;
ulong mod8 = (ulong)(value % 8UL);
ulong mod3 = (ulong)(value % 3UL);
ulong mod5 = (ulong)(value % 5UL);
return (mod10, mod8, mod3, mod5);
}
/// <summary>
/// Optimized step helper exposed on <see cref="UInt128Extensions.Mod10_8_5_3Steps"/>; trimmed execution to 3.58-3.77 ns
/// (3.576 ns at value 3, 3.682 ns at 8191, 3.773 ns at 131071, 3.649 ns at 2147483647), giving a 3.2-3.6x speedup.
/// </summary>
[Benchmark]
public (ulong Mod10, ulong Mod8, ulong Mod3, ulong Mod5) ModMethodModulo()
{
Value.Mod10_8_5_3Steps(out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3);
Value.Mod10_8_5_3(out ulong mod10, out ulong mod8, out ulong mod5, out ulong mod3);
// Step residues for q += 2*p
mod10 %= 10UL;
return (mod10, mod8, mod3, mod5);
}
}
@@ -1,3 +1,4 @@
using System.Numerics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -11,23 +12,16 @@ public class Mod11UInt128Benchmarks
[Params(11UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 255UL)]
public UInt128 Value { get; set; }
/// <summary>
/// Direct `% 11` for <see cref="UInt128"/>; ran in ~2.85-2.87 ns on every dataset (2.8719 ns at value 11, 2.8715 ns at 8191,
/// 2.8534 ns at 131071, 2.8716 ns at 2147483647, 2.8715 ns near ulong.Max).
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return (ulong)(Value % 11UL);
}
/// <summary>
/// Extension helper that folds digits with lookup coefficients; 0.66-0.68 ns across the inputs (0.6721 ns at value 11, 0.6765 ns
/// at 8191, 0.6760 ns at 131071, 0.6614 ns at 2147483647, 0.6661 ns near ulong.Max), about 4.2x faster than the baseline.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod11();
}
}
@@ -1,6 +1,6 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
@@ -8,40 +8,19 @@ namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod11UIntBenchmarks
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint Mod11(uint value)
{
uint remainder = 0U;
while (value != 0U)
{
remainder += value & 1023U;
value >>= 10;
remainder -= 11U * (remainder / 11U);
}
return remainder;
}
[Params(11U, 2047U, 65535U, 2147483647U)]
public uint Value { get; set; }
/// <summary>
/// `% 11` baseline; essentially free at this scale with means of 0.0299 ns (value 11), 0.0333 ns (2047), 0.0393 ns (65535), and
/// 0.0367 ns (2147483647).
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 11U;
}
/// <summary>
/// Looping extension method; useful for validation but slower across the board at 0.286 ns (value 11), 0.957 ns (2047),
/// 0.922 ns (65535), and 3.170 ns (2147483647), i.e. 9-86x the direct modulo.
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return Mod11(Value);
return Value.Mod11();
}
}
@@ -1,4 +1,3 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -12,35 +11,16 @@ public class Mod11ULongBenchmarks
[Params(11UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 127UL)]
public ulong Value { get; set; }
/// <summary>
/// `% 11` baseline for `ulong`; timings hovered near 0.26-0.27 ns on the large inputs (0.2647 ns at 2147483647, 0.2709 ns near
/// ulong.Max) with the smaller samples in the same 0.23-0.27 ns band.
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return Value % 11UL;
}
/// <summary>
/// Lookup-based helper mirroring `UIntExtensions.Mod11`; costs 0.66-0.75 ns on 32-bit scale inputs and rises to 1.15 ns near
/// ulong.Max, remaining 2.5-4.3x slower than the `%` baseline.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Mod11(Value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Mod11(ulong value)
{
ulong remainder = ((uint)value % 11U) + ((uint)(value >> 32) % 11U) << 2;
while (remainder >= 11UL)
{
remainder -= 11UL;
}
return remainder;
return Value.Mod11();
}
}
@@ -1,81 +1,14 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod11_7_5_3Benchmarks
{
private static readonly byte[] Mod7ByteCoefficients = [1, 4, 2, 1, 4, 2, 1, 4];
private static readonly byte[] Mod11ByteCoefficients = [1, 3, 9, 5, 4, 1, 3, 9];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong ReduceMod7(uint value)
{
while (value >= 7U)
{
value = (value >> 3) + (value & 7U);
if (value >= 7U && value < 14U)
{
value -= 7U;
}
}
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong ReduceMod11(uint value)
{
while (value >= 11U)
{
value = (value & 15U) + ((value >> 4) * 5U);
if (value >= 11U && value < 22U)
{
value -= 11U;
}
}
return value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Mod11_7_5_3(ulong value, out ulong mod11, out ulong mod7, out ulong mod5, out ulong mod3)
{
uint mod3And5Accumulator = 0U;
uint mod7Accumulator = 0U;
uint mod11Accumulator = 0U;
int index = 0;
byte[] mod7ByteCoefficients = Mod7ByteCoefficients;
byte[] mod11ByteCoefficients = Mod11ByteCoefficients;
ulong temp = value;
do
{
byte current = (byte)temp;
mod3And5Accumulator += current;
mod7Accumulator += (uint)(current * mod7ByteCoefficients[index]);
mod11Accumulator += (uint)(current * mod11ByteCoefficients[index]);
temp >>= 8;
index++;
}
while (temp != 0UL);
mod3 = mod3And5Accumulator % 3UL;
mod5 = mod3And5Accumulator % 5UL;
mod7 = ReduceMod7(mod7Accumulator);
mod11 = ReduceMod11(mod11Accumulator);
}
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
public ulong Value { get; set; }
/// <summary>
/// Plain `%` baseline; timings hovered between 1.56 ns and 1.61 ns (1.611 ns at value 3, 1.563 ns at 8191, 1.561 ns at 131071,
/// 1.572 ns at 2147483647).
/// </summary>
[Benchmark(Baseline = true)]
public (ulong Mod11, ulong Mod7, ulong Mod5, ulong Mod3) ModuloOperator()
{
@@ -83,14 +16,17 @@ public class Mod11_7_5_3Benchmarks
return (value % 11UL, value % 7UL, value % 5UL, value % 3UL);
}
/// <summary>
/// Combined coefficient method; simplifies residue reuse but costs 2.10 ns at value 3, 4.93 ns at 8191, 7.10 ns at 131071, and
/// 8.97 ns at 2147483647 (5.7x slower than the scalar `%` baseline on the largest input).
/// </summary>
[Benchmark]
public (ulong Mod11, ulong Mod7, ulong Mod5, ulong Mod3) ExtensionMethods()
{
ulong value = Value;
return (value.Mod11(), value.Mod7(), value.Mod5(), value.Mod3());
}
[Benchmark]
public (ulong Mod11, ulong Mod7, ulong Mod5, ulong Mod3) CombinedMethod()
{
Mod11_7_5_3(Value, out ulong mod11, out ulong mod7, out ulong mod5, out ulong mod3);
Value.Mod11_7_5_3(out ulong mod11, out ulong mod7, out ulong mod5, out ulong mod3);
return (mod11, mod7, mod5, mod3);
}
}
@@ -1,3 +1,4 @@
using System.Numerics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -11,23 +12,16 @@ public class Mod128UInt128Benchmarks
[Params(128UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 511UL)]
public UInt128 Value { get; set; }
/// <summary>
/// `% 128` baseline for <see cref="UInt128"/>; measured 2.95-3.42 ns across the suite (3.423 ns at value 128, 3.025 ns at
/// 8191, 2.950 ns at 131071, 2.948 ns at 2147483647, 2.967 ns near ulong.Max).
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return (ulong)(Value % 128UL);
}
/// <summary>
/// Bitmask helper (`value & 127UL`); durations were indistinguishable from the empty-loop cost at 0.004-0.025 ns—roughly
/// 150-1000x faster than the modulo baseline on every sample.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod128();
}
}
@@ -1,6 +1,6 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
@@ -8,52 +8,19 @@ namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod128UIntBenchmarks
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint Mod128(uint value) => value & 127U;
[Params(
3U,
5U,
6U,
8U,
10U,
11U,
32U,
64U,
128U,
256U,
2047U,
8191U,
65535U,
131071U,
2147483647U,
uint.MaxValue - 1024U,
uint.MaxValue - 511U,
uint.MaxValue - 255U,
uint.MaxValue - 127U,
uint.MaxValue - 63U,
uint.MaxValue - 31U,
uint.MaxValue - 17U,
uint.MaxValue)]
[Params(128U, 2047U, 65535U, 2147483647U)]
public uint Value { get; set; }
/// <summary>
/// `% 128` baseline; BenchmarkDotNet reported near-empty-loop costs with means from 0.010-0.026 ns across the domain (e.g.
/// 0.0191 ns at value 3, 0.0136 ns at 5, 0.0170 ns at 32, 0.0108 ns at 256, 0.0138 ns at 2147483647).
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 128U;
}
/// <summary>
/// Bitmask helper that returns `value & 127`; generally matches the baseline at 0.006-0.023 ns (0.0163 ns at value 3, 0.0064 ns at
/// 6, 0.0152 ns at 10, 0.0156 ns at 2147483647) but regresses on 64 due to the benchmark harness (0.9496 ns, 67x slower).
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return Mod128(Value);
return Value.Mod128();
}
}
@@ -8,49 +8,19 @@ namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod128ULongBenchmarks
{
[Params(
3UL,
5UL,
6UL,
8UL,
10UL,
11UL,
32UL,
64UL,
128UL,
256UL,
2047UL,
8191UL,
65535UL,
131071UL,
2147483647UL,
4294966271UL,
4294966784UL,
4294967040UL,
4294967168UL,
4294967232UL,
4294967264UL,
4294967278UL,
4294967295UL)]
[Params(128UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 255UL)]
public ulong Value { get; set; }
/// <summary>
/// `% 128` baseline; timings stayed below 0.03 ns on every dataset (0.0227 ns at value 3, 0.0108 ns at 256, 0.0251 ns at 2047,
/// 0.0190 ns at 2147483647, 0.0165 ns at 4294966271).
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return Value % 128UL;
}
/// <summary>
/// Bitmask helper (`value & 127UL`); typically 0.006-0.015 ns (0.0113 ns at value 3, 0.0094 ns at 64, 0.0109 ns at 2147483647),
/// but spikes to 0.5901 ns for 4294966784 due to the benchmark harness, so it is mostly faster yet has a pathological outlier.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod128();
}
}
@@ -1,3 +1,4 @@
using System.Numerics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -11,23 +12,16 @@ public class Mod3UInt128Benchmarks
[Params(3UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 17UL)]
public UInt128 Value { get; set; }
/// <summary>
/// Direct `% 3` on <see cref="UInt128"/>; measured 2.88-2.97 ns (2.969 ns at value 3, 2.887 ns at 8191, 2.885 ns at 131071,
/// 2.884 ns at 2147483647, 2.933 ns near ulong.Max).
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return (ulong)(Value % 3UL);
}
/// <summary>
/// Extension helper folding the high and low halves; trimmed timings to 0.455-0.475 ns (0.4549 ns at value 3, 0.4562 ns at
/// 8191, 0.4656 ns at 131071, 0.4710 ns at 2147483647, 0.4750 ns near ulong.Max), about 6.4x faster than the baseline.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod3();
}
}
@@ -11,23 +11,16 @@ public class Mod3UIntBenchmarks
[Params(3U, 2047U, 65535U, 2147483647U)]
public uint Value { get; set; }
/// <summary>
/// `% 3` baseline; the near-zero timings (0.0320 ns at value 3, 0.0918 ns at 2047, 0.0355 ns at 65535, 0.0309 ns at
/// 2147483647) reflect the noise floor flagged by BenchmarkDotNet.
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 3U;
}
/// <summary>
/// Extension helper that folds digits; produced 0.0217 ns at value 3, 0.0303 ns at 2047, 0.0597 ns at 65535, and 0.0350 ns at
/// 2147483647, tracking the baseline within measurement noise.
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return UIntExtensions.Mod3(Value);
return Value.Mod3();
}
}
@@ -1,4 +1,3 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -9,33 +8,19 @@ namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod3ULongBenchmarks
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod3(ulong value) =>
((uint)(value & ULongExtensions.WordBitMask)
+ (uint)((value >> 16) & ULongExtensions.WordBitMask)
+ (uint)((value >> 32) & ULongExtensions.WordBitMask)
+ (uint)((value >> 48) & ULongExtensions.WordBitMask)) % 3UL;
[Params(3UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 17UL)]
public ulong Value { get; set; }
/// <summary>
/// `% 3` baseline; delivered 0.259-0.286 ns for the standard values (0.2640 ns at 3, 0.2599 ns at 8191, 0.2620 ns at 131071,
/// 0.2858 ns at 2147483647) and 0.4916 ns near ulong.Max.
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return Value % 3UL;
}
/// <summary>
/// Byte-slice helper; measured 0.441 ns at value 3, 0.437 ns at 8191, 0.438 ns at 131071, 0.436 ns at 2147483647, and 0.448 ns
/// near ulong.Max—about 1.5-1.7x slower than the `%` baseline but still sub-nanosecond.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Mod3(Value);
return Value.Mod3();
}
}
@@ -1,3 +1,4 @@
using System.Numerics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -11,23 +12,16 @@ public class Mod5UInt128Benchmarks
[Params(5UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 31UL)]
public UInt128 Value { get; set; }
/// <summary>
/// `% 5` baseline; ran at 2.84-2.93 ns (2.840 ns at value 5, 2.874 ns at 8191, 2.870 ns at 131071, 2.895 ns at 2147483647,
/// 2.927 ns near ulong.Max).
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return (ulong)(Value % 5UL);
}
/// <summary>
/// Extension helper that folds high and low halves; trimmed execution to 0.47-0.49 ns (0.4706 ns at value 5, 0.4691 ns at
/// 8191, 0.4644 ns at 131071, 0.4847 ns at 2147483647, 0.4669 ns near ulong.Max), about 6x faster.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod5();
}
}
@@ -8,49 +8,19 @@ namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod5UIntBenchmarks
{
[Params(
3U,
5U,
6U,
8U,
10U,
11U,
32U,
64U,
128U,
256U,
2047U,
8191U,
65535U,
131071U,
2147483647U,
uint.MaxValue - 1024U,
uint.MaxValue - 511U,
uint.MaxValue - 255U,
uint.MaxValue - 127U,
uint.MaxValue - 63U,
uint.MaxValue - 31U,
uint.MaxValue - 17U,
uint.MaxValue)]
[Params(5U, 2047U, 65535U, 2147483647U)]
public uint Value { get; set; }
/// <summary>
/// `% 5` baseline; times hovered around 0.03 ns with the expected zero-measurement noise (0.0296 ns at value 5, 0.0671 ns at 6,
/// 0.0326 ns at 8, 0.0289 ns at 10, 0.1236 ns at 11, dropping back to ~0.03 ns for large values).
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 5U;
}
/// <summary>
/// Extension helper; typically within 0.03-0.04 ns (0.0349 ns at value 3, 0.0294 ns at 8, 0.0305 ns at 65535, 0.0270 ns at 131071,
/// 0.0327 ns at 2147483647) though the dataset contains an outlier at value 11 (0.0315 ns) and value 64 (0.0356 ns).
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return Value.Mod5();
}
}
@@ -1,4 +1,3 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -9,33 +8,19 @@ namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod5ULongBenchmarks
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Mod5(ulong value) =>
((uint)(value & ULongExtensions.WordBitMask)
+ (uint)((value >> 16) & ULongExtensions.WordBitMask)
+ (uint)((value >> 32) & ULongExtensions.WordBitMask)
+ (uint)((value >> 48) & ULongExtensions.WordBitMask)) % 5UL;
[Params(5UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 31UL)]
public ulong Value { get; set; }
/// <summary>
/// `% 5` baseline; measured 0.2657 ns at value 5, 0.2622 ns at 8191, 0.2640 ns at 131071, 0.2645 ns at 2147483647, and 0.2696 ns
/// near ulong.Max.
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return Value % 5UL;
}
/// <summary>
/// Extension helper; slower at 0.431-0.448 ns (0.4305 ns at value 5, 0.4404 ns at 8191, 0.4359 ns at 131071, 0.4361 ns at
/// 2147483647, 0.4481 ns near ulong.Max), roughly 1.6-1.7x the `%` cost.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Mod5(Value);
return Value.Mod5();
}
}
@@ -1,4 +1,4 @@
using System.Runtime.CompilerServices;
using System.Numerics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -9,11 +9,6 @@ namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod6ComparisonBenchmarks
{
private static readonly byte[] Mod6Lookup = [0, 3, 4, 1, 2, 5];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Mod6(ulong value) => Mod6Lookup[(int)(((value % 3UL) << 1) | (value & 1UL))];
private const int Iterations = 10_000_000;
private const ulong Divisor = 6UL;
private static readonly ulong FastDivMul = (ulong)(((UInt128)1 << 64) / Divisor);
@@ -21,9 +16,6 @@ public class Mod6ComparisonBenchmarks
[Params(3UL, 131071UL, ulong.MaxValue - 1024UL)]
public ulong StartValue { get; set; }
/// <summary>
/// `% 6` baseline inside the loop; measured 1.02 ns at StartValue 3, 1.035 ns at 131071, and 1.011 ns near ulong.Max.
/// </summary>
[Benchmark(Baseline = true, OperationsPerInvoke = Iterations)]
public ulong ModuloOperator()
{
@@ -39,10 +31,6 @@ public class Mod6ComparisonBenchmarks
return checksum;
}
/// <summary>
/// `value / 6` with a multiply back; varied between 0.962 ns and 1.063 ns (0.9915 ns at StartValue 3, 1.0633 ns at 131071,
/// 0.9617 ns near ulong.Max), roughly matching the baseline.
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
public ulong DivisionBased()
{
@@ -59,10 +47,6 @@ public class Mod6ComparisonBenchmarks
return checksum;
}
/// <summary>
/// FastDivHigh helper; landed at 1.67-1.71 ns (1.691 ns at StartValue 3, 1.706 ns at 131071, 1.673 ns near ulong.Max), making it
/// ~1.65x slower than `%`.
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
public ulong FastDivHigh()
{
@@ -79,10 +63,6 @@ public class Mod6ComparisonBenchmarks
return checksum;
}
/// <summary>
/// Lookup-based helper argued for adoption; ran in 1.06-1.07 ns (1.0605 ns at StartValue 3, 1.0696 ns at 131071, 1.0604 ns near
/// ulong.Max), essentially matching the baseline with slightly higher stability.
/// </summary>
[Benchmark(OperationsPerInvoke = Iterations)]
public ulong ExtensionMethod()
{
@@ -91,10 +71,11 @@ public class Mod6ComparisonBenchmarks
for (int i = 0; i < Iterations; i++)
{
checksum += Mod6(value);
checksum += value.Mod6();
value += (ulong)((i & 15) + 1);
}
return checksum;
}
}
@@ -1,3 +1,4 @@
using System.Numerics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -11,22 +12,16 @@ public class Mod6UInt128Benchmarks
[Params(6UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 63UL)]
public UInt128 Value { get; set; }
/// <summary>
/// `% 6` baseline; recorded 2.89-2.91 ns on the smaller values and 2.874 ns at 2147483647, 2.908 ns near ulong.Max.
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return (ulong)(Value % 6UL);
}
/// <summary>
/// Extension helper (<see cref="UInt128Extensions.Mod6"/>); reduced the timings to 1.16-1.24 ns (1.180 ns at value 6, 1.164 ns at
/// 8191, 1.158 ns at 131071, 1.241 ns at 2147483647, 1.167 ns near ulong.Max), roughly 2.4-2.5x faster.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod6();
}
}
@@ -1,6 +1,6 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
@@ -8,31 +8,19 @@ namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod6UIntBenchmarks
{
private static readonly byte[] Mod6Lookup = [0, 3, 4, 1, 2, 5];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint Mod6(uint value) => Mod6Lookup[(int)(((value % 3U) << 1) | (value & 1U))];
[Params(6U, 2047U, 65535U, 2147483647U)]
public uint Value { get; set; }
/// <summary>
/// `% 6` baseline; BenchmarkDotNet highlighted zero-measurement noise with means of 0.0348 ns at value 6, 0.0277 ns at 2047,
/// 0.0492 ns at 65535, and 0.0385 ns at 2147483647.
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 6U;
}
/// <summary>
/// Lookup-based helper; significantly slower at 0.433 ns (value 6), 0.452 ns (2047), 0.454 ns (65535), and 0.459 ns
/// (2147483647), about 11-19x the raw modulo cost.
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return Mod6(Value);
return Value.Mod6();
}
}
@@ -1,4 +1,4 @@
using System.Runtime.CompilerServices;
using System.Numerics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -9,49 +9,35 @@ namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod6ULongBenchmarks
{
private static readonly byte[] Mod6Lookup = [0, 3, 4, 1, 2, 5];
private static readonly ulong Divisor = 6UL;
private static readonly ulong FastDivMul = (ulong)(((UInt128)1 << 64) / Divisor);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Mod6(ulong value) => Mod6Lookup[(int)(((value % 3UL) << 1) | (value & 1UL))];
[Params(3UL, 131071UL, ulong.MaxValue - 1024UL)]
[Params(3UL, 131071UL, ulong.MaxValue)]
public ulong Value { get; set; }
/// <summary>
/// `% 6` baseline; recorded 0.264 ns at Value 3, 0.262 ns at 131071, and 0.271 ns near ulong.Max.
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return Value % 6UL;
return Value % Divisor;
}
/// <summary>
/// Division-based reduction (`value / 6`); timings were 0.277 ns, 0.284 ns, and 0.287 ns across the three start values.
/// </summary>
[Benchmark]
public ulong DivisionBased()
{
ulong quotient = Value / 6UL;
return Value - quotient * 6UL;
ulong quotient = Value / Divisor;
return Value - quotient * Divisor;
}
/// <summary>
/// FastDivHigh helper; significantly slower at 1.24 ns (Value 3), 1.04 ns (131071), and 1.17 ns (near ulong.Max).
/// </summary>
[Benchmark]
public ulong FastDivHigh()
{
ulong quotient = Value.FastDiv64(6UL, (ulong)(((UInt128)1 << 64) / 6UL));
return Value - quotient * 6UL;
ulong quotient = Value.FastDiv64(Divisor, FastDivMul);
return Value - quotient * Divisor;
}
/// <summary>
/// Lookup-based helper; cost 0.461 ns at Value 3, 0.467 ns at 131071, 0.467 ns near ulong.Max—about 1.7x slower than the modulo.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Mod6(Value);
return Value.Mod6();
}
}
@@ -1,3 +1,4 @@
using System.Numerics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -11,23 +12,16 @@ public class Mod7UInt128Benchmarks
[Params(7UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 127UL)]
public UInt128 Value { get; set; }
/// <summary>
/// `% 7` baseline; timings clustered around 2.86-2.91 ns (2.860 ns at value 7, 2.887 ns at 8191, 2.873 ns at 131071,
/// 2.878 ns at 2147483647, 2.915 ns near ulong.Max).
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return (ulong)(Value % 7UL);
}
/// <summary>
/// Extension helper; achieved 0.676 ns (value 7), 0.675 ns (8191), 0.722 ns (131071), 0.882 ns (2147483647), and 0.676 ns near
/// ulong.Max—between 3.2x and 4.2x faster than the baseline.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod7();
}
}
@@ -1,6 +1,6 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
@@ -8,43 +8,19 @@ namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod7UIntBenchmarks
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static uint Mod7(uint value)
{
uint remainder = 0U;
while (value != 0U)
{
remainder += value & 7U;
value >>= 3;
if (remainder >= 7U)
{
remainder -= 7U;
}
}
return remainder >= 7U ? remainder - 7U : remainder;
}
[Params(7U, 2047U, 65535U, 2147483647U)]
public uint Value { get; set; }
/// <summary>
/// `% 7` baseline; again near the noise floor with means of 0.0354 ns (value 7), 0.0276 ns (2047), 0.0282 ns (65535), and
/// 0.0422 ns (2147483647).
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 7U;
}
/// <summary>
/// Extension helper; substantially slower at 0.268 ns (value 7), 1.217 ns (2047), 2.334 ns (65535), and 4.579 ns (2147483647)—up to
/// 110x the baseline.
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return Mod7(Value);
return Value.Mod7();
}
}
@@ -1,6 +1,6 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
@@ -8,37 +8,19 @@ namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
public class Mod7ULongBenchmarks
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Mod7(ulong value)
{
ulong remainder = ((uint)value % 7U) + (((uint)(value >> 32) % 7U) << 2);
while (remainder >= 7UL)
{
remainder -= 7UL;
}
return remainder;
}
[Params(7UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 63UL)]
public ulong Value { get; set; }
/// <summary>
/// `% 7` baseline; 0.274 ns at value 7, 0.270 ns at 8191, 0.268 ns at 131071, 0.270 ns at 2147483647, and 0.275 ns near ulong.Max.
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return Value % 7UL;
}
/// <summary>
/// Extension helper; 0.433 ns at value 7, 0.443 ns at 8191, 0.436 ns at 131071, 0.438 ns at 2147483647, but 0.684 ns near
/// ulong.Max—about 1.6-2.5x slower overall.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Mod7(Value);
return Value.Mod7();
}
}
@@ -1,3 +1,4 @@
using System.Numerics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
@@ -11,23 +12,16 @@ public class Mod8UInt128Benchmarks
[Params(8UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 255UL)]
public UInt128 Value { get; set; }
/// <summary>
/// `% 8` baseline; 2.86-2.89 ns across the values (2.864 ns at value 8, 2.878 ns at 8191, 2.895 ns at 131071, 2.890 ns at
/// 2147483647, 2.894 ns near ulong.Max).
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return (ulong)(Value % 8UL);
}
/// <summary>
/// Bitmask helper; reduced the cost to 0.010-0.020 ns (0.0170 ns at value 8, 0.0144 ns at 8191, 0.0134 ns at 131071, 0.0105 ns at
/// 2147483647, 0.0203 ns near ulong.Max), delivering a 140-280x speedup.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Value.Mod8();
}
}
@@ -11,23 +11,16 @@ public class Mod8UIntBenchmarks
[Params(8U, 2047U, 65535U, 2147483647U)]
public uint Value { get; set; }
/// <summary>
/// `% 8` baseline; means stayed around 0.011-0.015 ns (0.0109 ns at value 8, 0.0151 ns at 2047, 0.0123 ns at 65535, 0.0122 ns at
/// 2147483647), i.e. indistinguishable from the empty-loop costs.
/// </summary>
[Benchmark(Baseline = true)]
public uint ModuloOperator()
{
return Value % 8U;
}
/// <summary>
/// Bitmask helper (`value & 7U`); returned 0.0032 ns at value 8, 0.0113 ns at 2047, 0.0191 ns at 65535, and 0.0150 ns at
/// 2147483647—roughly matching the baseline within measurement noise.
/// </summary>
[Benchmark]
public uint ExtensionMethod()
{
return Value.Mod8();
}
}
@@ -1,41 +1,26 @@
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
public class Mod8ULongBenchmarks
{
[Params(8UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 127UL)]
public ulong Value { get; set; }
/// <summary>
/// Uses the built-in <c>% 8</c>; measured 0.0224 ns on Value=8, ~0 ns within the measurement noise floor for 8,191,
/// 0.0069 ns for 131,071, 0.0117 ns for 2,147,483,647, and 0.107 ns for the near-maximum value, making it the fastest
/// option except when the branch-free mask wins on huge operands.
/// </summary>
[Benchmark(Baseline = true)]
public ulong ModuloOperator()
{
return Value % 8UL;
}
/// <summary>
/// Applies the branch-free mask helper; costs 0.0344 ns on Value=8, 0.0296 ns on 8,191, 0.0114 ns on 131,071,
/// 0.0106 ns on 2,147,483,647, and just 0.0109 ns on the near-maximum sample, overtaking <c>%</c> once the divisor
/// approaches 2<sup>64</sup>.
/// </summary>
[Benchmark]
public ulong ExtensionMethod()
{
return Mod8(Value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod8(ulong value)
{
return value & 7UL;
return Value.Mod8();
}
}
@@ -32,12 +32,6 @@ public class MontgomeryMultiplyBenchmarks
}
}
/// <summary>
/// Baseline Montgomery multiply used in earlier revisions; measured 247 ns (64 batch), 974 ns (256), and 3.94 μs (1024).
/// </summary>
/// <remarks>
/// Observed means: BatchSize 64 → 247.0 ns (1.00×), 256 → 974.2 ns, 1024 → 3,940.6 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public ulong OriginalImplementation()
{
@@ -50,12 +44,6 @@ public class MontgomeryMultiplyBenchmarks
return checksum;
}
/// <summary>
/// Optimized Montgomery multiply; consistently 3–5% faster with 234.9 ns (64), 942.6 ns (256), and 3.75 μs (1024).
/// </summary>
/// <remarks>
/// Observed means: BatchSize 64 → 234.9 ns (0.95×), 256 → 942.6 ns, 1024 → 3,751.6 ns.
/// </remarks>
[Benchmark]
public ulong OptimizedImplementation()
{
@@ -1,126 +0,0 @@
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class Mul64Benchmarks
{
private static readonly BigInteger ProductMask = (BigInteger.One << 128) - BigInteger.One;
private static readonly Mul64Input[] Inputs =
[
new(Create(0UL, 0x0000_0000_0000_0001UL), Create(0UL, 0x0000_0000_0000_0001UL), "TinyOperands"),
new(Create(0UL, 0xFFFF_FFFF_FFFF_FFFBUL), Create(0UL, 0xFFFF_FFFF_FFFF_FFFFUL), "LowWordMax"),
new(Create(0x0000_0000_0000_0001UL, 0x0000_0000_0000_0000UL), Create(0x0000_0000_0000_0001UL, 0x0000_0000_0000_0000UL), "HighWordOnly"),
new(Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), "AllBitsSet"),
new(Create(0x1234_5678_9ABC_DEF0UL, 0x1357_9BDF_2468_ACEEUL), Create(0x0FED_CBA9_8765_4321UL, 0x0246_8ACE_1357_9BDFUL), "InterleavedHighLow"),
new(Create(0x0000_0000_0000_0000UL, 0x0000_0001_0000_0000UL), Create(0xFFFF_FFFF_0000_0000UL, 0x0000_0000_FFFF_FFFFUL), "ShiftHeavyMixed"),
];
[ParamsSource(nameof(GetInputs))]
public Mul64Input Input { get; set; }
public static IEnumerable<Mul64Input> GetInputs() => Inputs;
/// <summary>
/// Mul64 layout that keeps the high-word accumulation in locals; measured 1.49–1.57 ns across all operand mixes, making it
/// the fastest CPU layout we profiled.
/// </summary>
/// <remarks>
/// Observed means: AllBitsSet 1.568 ns, HighWordOnly 1.495 ns, InterleavedHighLow 1.526 ns,
/// LowWordMax 1.506 ns, ShiftHeavyMixed 1.481 ns, TinyOperands 1.503 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public UInt128 HighWordAccumulatedInLocals()
{
return Input.Left.Mul64(Input.Right);
}
/// <summary>
/// Mul64 layout that folds the high-word expression into a single return value; costs roughly 3–5% extra with 1.54–1.58 ns
/// runtimes depending on the operand mix.
/// </summary>
/// <remarks>
/// Observed means: AllBitsSet 1.583 ns, HighWordOnly 1.542 ns, InterleavedHighLow 1.575 ns,
/// LowWordMax 1.581 ns, ShiftHeavyMixed 1.562 ns, TinyOperands 1.546 ns.
/// </remarks>
[Benchmark]
public UInt128 FoldedHighReturnValue()
{
return Mul64FoldedHighReturn(Input.Left, Input.Right);
}
/// <summary>
/// Mul64 layout shaped for the GPU helper to keep the cross products in registers; roughly 2.35× slower than the baseline at
/// 3.68–3.72 ns, but useful when mirroring GPU arithmetic on the CPU.
/// </summary>
/// <remarks>
/// Observed means: AllBitsSet 3.682 ns, HighWordOnly 3.679 ns, InterleavedHighLow 3.719 ns,
/// LowWordMax 3.684 ns, ShiftHeavyMixed 3.707 ns, TinyOperands 3.683 ns.
/// </remarks>
[Benchmark]
public UInt128 GpuFriendlyLayout()
{
return Mul64GpuFriendly(Input.Left, Input.Right);
}
/// <summary>
/// Reference implementation that multiplies with <see cref="BigInteger"/> to expose the full 256-bit product; handy for
/// validation but 22–57× slower at 34–87 ns depending on the operand distribution.
/// </summary>
/// <remarks>
/// Observed means: AllBitsSet 86.087 ns, HighWordOnly 78.860 ns, InterleavedHighLow 87.367 ns,
/// LowWordMax 74.418 ns, ShiftHeavyMixed 81.020 ns, TinyOperands 34.286 ns.
/// </remarks>
[Benchmark]
public UInt128 BigIntegerReference()
{
return Mul64WithBigInteger(Input.Left, Input.Right);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static UInt128 Mul64FoldedHighReturn(UInt128 left, UInt128 right)
{
ulong leftLow = (ulong)left;
ulong rightLow = (ulong)right;
return ((UInt128)(leftLow * (ulong)(right >> 64) + leftLow.MulHigh(rightLow)) << 64) | (UInt128)(leftLow * rightLow);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static UInt128 Mul64GpuFriendly(UInt128 left, UInt128 right)
{
GpuUInt128 gpuLeft = new((ulong)(left >> 64), (ulong)left);
GpuUInt128 gpuRight = new((ulong)(right >> 64), (ulong)right);
gpuLeft.Mul(gpuRight);
return (UInt128)gpuLeft;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static UInt128 Mul64WithBigInteger(UInt128 left, UInt128 right)
{
BigInteger product = (BigInteger)left * (BigInteger)right;
return (UInt128)(product & ProductMask);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static UInt128 Create(ulong high, ulong low)
{
return ((UInt128)high << 64) | low;
}
public readonly record struct Mul64Input(UInt128 Left, UInt128 Right, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,163 +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;
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MulHighBenchmarks
{
private static readonly MulHighInput[] Inputs =
[
new(0x0000000100000000UL, 0x0000000100000000UL, "LowHighWord"),
new(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL, "Mixed"),
new(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL, "AllBitsSet"),
new(0x8000_0000_0000_0000UL, 0x8000_0000_0000_0000UL, "HighPowersOfTwo"),
new(0x0000_0000_0001_0000UL, 0xFFFF_FFFF_0000_0000UL, "ShiftHeavy"),
new(0x0000_0000_0000_0000UL, 0xFFFF_FFFF_FFFF_FFFFUL, "IncludesZero"),
];
[ParamsSource(nameof(GetInputs))]
public MulHighInput Input { get; set; }
public static IEnumerable<MulHighInput> GetInputs() => Inputs;
/// <summary>
/// Baseline MulHigh implementation used across the scanner; measured 0.73–0.78 ns with minimal variance between operand
/// mixes, yet still slower than the UInt128 intrinsic.
/// </summary>
/// <remarks>
/// Observed means: AllBitsSet 0.7577 ns, HighPowersOfTwo 0.7352 ns, IncludesZero 0.7497 ns,
/// LowHighWord 0.7296 ns, Mixed 0.7365 ns, ShiftHeavy 0.7747 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public ulong CurrentImplementation()
{
// TODO: Replace ULongExtensions.MulHigh with the UInt128-based helper on CPU paths; it was
// 11–14× faster in the latest benchmarks while the GPU-specific variant remains available
// separately.
return ULongExtensions.MulHigh(Input.X, Input.Y);
}
/// <summary>
/// Uses UInt128 to compute the high word directly; fastest option at 0.054–0.067 ns across all mixes, beating the baseline
/// by roughly 11–14×.
/// </summary>
/// <remarks>
/// Observed means: AllBitsSet 0.0641 ns, HighPowersOfTwo 0.0674 ns, IncludesZero 0.0541 ns,
/// LowHighWord 0.0576 ns, Mixed 0.0634 ns, ShiftHeavy 0.0591 ns.
/// </remarks>
[Benchmark]
public ulong UInt128BuiltIn()
{
return MulHighWithUInt128(Input.X, Input.Y);
}
/// <summary>
/// Emulates UInt128 multiplication using the GPU-friendly <see cref="GpuUInt128"/> type; lands between 0.854 ns and 0.869 ns,
/// which is ~12–17% slower than the baseline but mirrors GPU arithmetic exactly.
/// </summary>
/// <remarks>
/// Observed means: AllBitsSet 0.8643 ns, HighPowersOfTwo 0.8584 ns, IncludesZero 0.8538 ns,
/// LowHighWord 0.8576 ns, Mixed 0.8693 ns, ShiftHeavy 0.8625 ns.
/// </remarks>
[Benchmark]
public ulong GpuUInt128Style()
{
return MulHighWithGpuUInt128(Input.X, Input.Y);
}
/// <summary>
/// Reduces trailing zeros before multiplying smaller operands; excels on high-power-of-two inputs (1.142 ns) but remains
/// 20–55% slower than the baseline elsewhere.
/// </summary>
/// <remarks>
/// Observed means: AllBitsSet 0.9894 ns, HighPowersOfTwo 1.1421 ns, IncludesZero 0.9270 ns,
/// LowHighWord 0.9195 ns, Mixed 1.2042 ns, ShiftHeavy 1.1909 ns.
/// </remarks>
[Benchmark]
public ulong ShiftThenMultiply()
{
return MulHighWithTrailingZeroReduction(Input.X, Input.Y);
}
/// <summary>
/// Schoolbook multiply splitting inputs into 32-bit halves; tracks the baseline closely at 0.739–0.748 ns with its best
/// showing on mixed inputs.
/// </summary>
/// <remarks>
/// Observed means: AllBitsSet 0.7414 ns, HighPowersOfTwo 0.7459 ns, IncludesZero 0.7442 ns,
/// LowHighWord 0.7441 ns, Mixed 0.7395 ns, ShiftHeavy 0.7485 ns.
/// </remarks>
[Benchmark]
public ulong SchoolbookVariant()
{
return MulHighSchoolbook(Input.X, Input.Y);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulHighWithUInt128(ulong x, ulong y)
{
return (ulong)(((UInt128)x * y) >> 64);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulHighWithGpuUInt128(ulong x, ulong y)
{
return ULongExtensions.MulHighGpuCompatible(x, y);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulHighWithTrailingZeroReduction(ulong x, ulong y)
{
if ((x | y) == 0UL)
{
return 0UL;
}
int shiftX = BitOperations.TrailingZeroCount(x);
int shiftY = BitOperations.TrailingZeroCount(y);
x >>= shiftX;
y >>= shiftY;
UInt128 product = (UInt128)x * y;
int totalShift = shiftX + shiftY;
if (totalShift >= 64)
{
return (ulong)(product << (totalShift - 64));
}
return (ulong)(product >> (64 - totalShift));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulHighSchoolbook(ulong x, ulong y)
{
ulong xLow = (uint)x;
ulong xHigh = x >> 32;
ulong yLow = (uint)y;
ulong yHigh = y >> 32;
ulong w1 = xLow * yHigh;
ulong w2 = xHigh * yLow;
ulong w3 = xLow * yLow;
ulong result = (xHigh * yHigh) + (w1 >> 32) + (w2 >> 32);
result += ((w3 >> 32) + (uint)w1 + (uint)w2) >> 32;
return result;
}
public readonly record struct MulHighInput(ulong X, ulong Y, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,248 +0,0 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80)]
[MemoryDiagnoser]
public class MulMod64Benchmarks
{
private static readonly MulMod64Input[] Inputs =
[
new(0UL, 0UL, 3UL, "ZeroOperands"),
new(ulong.MaxValue, ulong.MaxValue - 1UL, ulong.MaxValue - 58UL, "NearFullRange"),
new(0xFFFF_FFFF_0000_0000UL, 0x0000_0000_FFFF_FFFFUL, 0xFFFF_FFFF_0000_002BUL, "CrossWordBlend"),
new(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL, 0x1FFF_FFFF_FFFF_FFFBUL, "MixedBitPattern"),
new(0x0000_0001_0000_0000UL, 0x0000_0000_0000_0003UL, 0x0000_0000_FFFF_FFC3UL, "SparseOperands"),
new(0x7FFF_FFFF_FFFF_FFA3UL, 0x6FFF_FFFF_FFFF_FF81UL, 0x7FFF_FFFF_FFFF_FFE7UL, "PrimeSizedModulus"),
];
[ParamsSource(nameof(GetInputs))]
public MulMod64Input Input { get; set; }
public static IEnumerable<MulMod64Input> GetInputs()
{
return Inputs;
}
/// <summary>
/// Calls the UInt128-based <see cref="ULongExtensions.MulMod64(ulong, ulong, ulong)"/> helper; every input landed in the
/// 3.56–3.61 ns window, keeping this baseline consistently ahead of the other CPU implementations.
/// </summary>
/// <remarks>
/// Observed means: CrossWordBlend 3.606 ns (1.00×), MixedBitPattern 3.564 ns, NearFullRange 3.583 ns,
/// PrimeSizedModulus 3.593 ns, SparseOperands 3.555 ns, ZeroOperands 3.599 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public ulong ExtensionBaseline()
{
return Input.Left.MulMod64(Input.Right, Input.Modulus);
}
/// <summary>
/// Calls the UInt128-based <see cref="ULongExtensions.MulMod(ulong, ulong, ulong)"/> helper; sparse operands (NearFullRange
/// 5.71 ns, SparseOperands 5.67 ns) and zeros (1.98 ns) benefit, but dense patterns cost 22.9–64.8 ns.
/// </summary>
/// <remarks>
/// Observed means: CrossWordBlend 22.870 ns (6.34×), MixedBitPattern 64.800 ns, NearFullRange 5.708 ns,
/// PrimeSizedModulus 52.766 ns, SparseOperands 5.671 ns, ZeroOperands 1.976 ns.
/// </remarks>
[Benchmark]
public ulong GpuCompatibleMulModExtension()
{
GpuUInt128 gpuUInt128 = new(Input.Left);
return gpuUInt128.MulMod(Input.Right, Input.Modulus);
}
/// <summary>
/// Calls the UInt128-based <see cref="ULongExtensions.MulModSimplified(ulong, ulong, ulong)"/> helper; performance mirrors
/// the full helper with 5.24–5.95 ns on sparse inputs and 1.99 ns when zeros dominate, but 22.8–59.2 ns on dense blends.
/// </summary>
/// <remarks>
/// Observed means: CrossWordBlend 22.780 ns (6.32×), MixedBitPattern 59.165 ns, NearFullRange 5.952 ns,
/// PrimeSizedModulus 50.490 ns, SparseOperands 5.239 ns, ZeroOperands 1.986 ns.
/// </remarks>
[Benchmark]
public ulong GpuCompatibleMulModSimplifiedExtension()
{
GpuUInt128 gpuUInt128 = new(Input.Left);
return gpuUInt128.MulModSimplified(Input.Right, Input.Modulus);
}
/// <summary>
/// Uses manual UInt128 multiplication inside the benchmark; dense operands land between 22.2 ns and 31.4 ns, while sparse
/// or zero inputs stay near the 3.54–3.56 ns baseline.
/// </summary>
/// <remarks>
/// Observed means: CrossWordBlend 22.218 ns (6.16×), MixedBitPattern 30.539 ns, NearFullRange 27.159 ns,
/// PrimeSizedModulus 31.409 ns, SparseOperands 3.563 ns, ZeroOperands 3.542 ns.
/// </remarks>
[Benchmark]
public ulong InlineUInt128Operands()
{
return MulMod64Inline(Input.Left, Input.Right, Input.Modulus);
}
/// <summary>
/// Reduces both operands before the inline UInt128 multiply; reductions shine on NearFullRange (4.36 ns) and sparse inputs
/// (4.62 ns) but dense mixes still take 22.6–32.2 ns.
/// </summary>
/// <remarks>
/// Observed means: CrossWordBlend 22.626 ns (6.27×), MixedBitPattern 31.237 ns, NearFullRange 4.357 ns,
/// PrimeSizedModulus 32.176 ns, SparseOperands 4.617 ns, ZeroOperands 4.286 ns.
/// </remarks>
[Benchmark]
public ulong InlineUInt128OperandsWithReductionFirst()
{
return MulMod64InlineWithReductionFirst(Input.Left, Input.Right, Input.Modulus);
}
/// <summary>
/// Reduces each operand individually before multiplication; behaves like the reduction-first path with 4.28–4.32 ns on
/// sparse and NearFullRange inputs, yet still spends 22.5–31.8 ns on dense patterns.
/// </summary>
/// <remarks>
/// Observed means: CrossWordBlend 22.548 ns (6.25×), MixedBitPattern 30.210 ns, NearFullRange 4.319 ns,
/// PrimeSizedModulus 31.786 ns, SparseOperands 4.282 ns, ZeroOperands 4.256 ns.
/// </remarks>
[Benchmark]
public ulong InlineUInt128OperandsWithOperandReduction()
{
return MulMod64InlineWithOperandReduction(Input.Left, Input.Right, Input.Modulus);
}
/// <summary>
/// Splits the UInt128 product across locals; dense workloads cost 22.2–31.4 ns, while SparseOperands and ZeroOperands stay
/// near 3.57–3.59 ns.
/// </summary>
/// <remarks>
/// Observed means: CrossWordBlend 22.226 ns (6.16×), MixedBitPattern 30.882 ns, NearFullRange 27.137 ns,
/// PrimeSizedModulus 31.351 ns, SparseOperands 3.566 ns, ZeroOperands 3.586 ns.
/// </remarks>
[Benchmark]
public ulong InlineUInt128WithLocals()
{
return MulMod64InlineWithLocals(Input.Left, Input.Right, Input.Modulus);
}
/// <summary>
/// Combines locals with operand reduction; reductions help sparsity (11.86–15.10 ns) but dense cases inflate to 29.7–40.5 ns
/// making it the slowest inline variant.
/// </summary>
/// <remarks>
/// Observed means: CrossWordBlend 29.746 ns (8.24×), MixedBitPattern 38.013 ns, NearFullRange 12.187 ns,
/// PrimeSizedModulus 40.459 ns, SparseOperands 15.100 ns, ZeroOperands 11.863 ns.
/// </remarks>
[Benchmark]
public ulong InlineUInt128WithLocalsAndOperandReduction()
{
return MulMod64InlineWithLocalsAndOperandReduction(Input.Left, Input.Right, Input.Modulus);
}
/// <summary>
/// Reconstructs the 128-bit product from MulHigh/MulLow pieces; dense blends remain costly at 23.7–32.0 ns but sparse and
/// NearFullRange inputs land around 5.20–5.55 ns.
/// </summary>
/// <remarks>
/// Observed means: CrossWordBlend 23.685 ns (6.57×), MixedBitPattern 30.114 ns, NearFullRange 5.221 ns,
/// PrimeSizedModulus 31.950 ns, SparseOperands 5.204 ns, ZeroOperands 5.171 ns.
/// </remarks>
[Benchmark]
public ulong MultiplyHighDecomposition()
{
return MulMod64MulHigh(Input.Left, Input.Right, Input.Modulus);
}
/// <summary>
/// Uses the GPU-friendly <see cref="ULongExtensions.MulMod64GpuCompatible(ulong, ulong, ulong)"/> helper; dense inputs now
/// cost 24.4–61.9 ns, while NearFullRange and SparseOperands finish near 7.27 ns and ZeroOperands stay baseline-fast at
/// 3.59 ns.
/// </summary>
/// <remarks>
/// Observed means: CrossWordBlend 24.422 ns (6.86×), MixedBitPattern 61.850 ns, NearFullRange 7.267 ns,
/// PrimeSizedModulus 55.114 ns, SparseOperands 7.261 ns, ZeroOperands 3.587 ns.
/// </remarks>
[Benchmark]
public ulong GpuCompatibleBaseline()
{
// TODO: Callers in production should migrate to ULongExtensions.MulMod64 where GPU parity
// is not required; that baseline stayed 6–17× faster for dense 64-bit operands.
return Input.Left.MulMod64GpuCompatible(Input.Right, Input.Modulus);
}
/// <summary>
/// Uses the deferred native-modulo helper <see cref="ULongExtensions.MulMod64GpuCompatibleDeferred(ulong, ulong, ulong)"/>;
/// excels when operands are tiny (2.01 ns on ZeroOperands) yet remains the slowest choice on dense data at 146–295 ns.
/// </summary>
/// <remarks>
/// Observed means: CrossWordBlend 146.559 ns (40.63×), MixedBitPattern 255.555 ns, NearFullRange 21.390 ns,
/// PrimeSizedModulus 294.486 ns, SparseOperands 18.182 ns, ZeroOperands 2.009 ns.
/// </remarks>
[Benchmark]
public ulong GpuCompatibleDeferred()
{
// TODO: Retire the deferred GPU shim from runtime paths and keep it only for benchmarks;
// ULongExtensions.MulMod64 avoids the 6×–82× slowdown on real-world operand mixes.
return Input.Left.MulMod64GpuCompatibleDeferred(Input.Right, Input.Modulus);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulMod64Inline(ulong a, ulong b, ulong modulus) => (ulong)(((UInt128)a * b) % modulus);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulMod64InlineWithReductionFirst(ulong a, ulong b, ulong modulus) => (ulong)(((UInt128)(a % modulus) * (b % modulus)) % modulus);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulMod64InlineWithOperandReduction(ulong a, ulong b, ulong modulus)
{
ulong left = a % modulus;
ulong right = b % modulus;
return (ulong)(((UInt128)left * right) % modulus);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulMod64InlineWithLocals(ulong a, ulong b, ulong modulus)
{
UInt128 left128 = a;
UInt128 right128 = b;
UInt128 product = left128 * right128;
UInt128 reduced = product % modulus;
return (ulong)reduced;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulMod64InlineWithLocalsAndOperandReduction(ulong a, ulong b, ulong modulus)
{
UInt128 left128 = a;
UInt128 right128 = b;
left128 %= modulus;
right128 %= modulus;
UInt128 product = left128 * right128;
UInt128 reduced = product % modulus;
return (ulong)reduced;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulMod64MulHigh(ulong a, ulong b, ulong modulus)
{
ulong left = a % modulus;
ulong right = b % modulus;
ulong low = left * right;
ulong high = left.MulHigh(right);
UInt128 product = ((UInt128)high << 64) | low;
return (ulong)(product % modulus);
}
public readonly record struct MulMod64Input(ulong Left, ulong Right, ulong Modulus, string Name)
{
public override string ToString()
{
return Name;
}
}
}
@@ -1,481 +0,0 @@
using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using System.Numerics;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
public class Pow2MontgomeryModBenchmarks
{
private const int SampleCount = 256;
private readonly MontgomeryDivisorData[] _smallDivisors = new MontgomeryDivisorData[SampleCount];
private readonly MontgomeryDivisorData[] _largeDivisors = new MontgomeryDivisorData[SampleCount];
private readonly ulong[] _smallExponents = new ulong[SampleCount];
private readonly ulong[] _largeExponents = new ulong[SampleCount];
private readonly ulong[] _smallCycles = new ulong[SampleCount];
private readonly ulong[] _largeCycles = new ulong[SampleCount];
private readonly Random _random = new(13);
public enum InputScale
{
Small,
Large
}
[ParamsAllValues]
public InputScale Scale { get; set; }
[GlobalSetup]
public void Setup()
{
for (int i = 0; i < SampleCount; i++)
{
ulong smallModulus = NextSmallOddModulus();
_smallDivisors[i] = CreateMontgomeryDivisorData(smallModulus);
_smallExponents[i] = NextSmallExponent();
_smallCycles[i] = MersenneDivisorCycles.CalculateCycleLength(smallModulus, MontgomeryDivisorData.FromModulus(smallModulus));
(ulong largeModulus, ulong largeCycle) = NextLargeModulusAndCycle();
_largeDivisors[i] = CreateMontgomeryDivisorData(largeModulus);
_largeExponents[i] = NextLargeExponent();
_largeCycles[i] = largeCycle;
}
}
/// <summary>
/// Baseline right-to-left Montgomery ladder; measured 36.36 μs on the small sample set and 117.32 μs on the large one.
/// </summary>
[Benchmark(Baseline = true)]
public ulong Baseline()
{
GetData(out var exponents, out var divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWindowed(divisors[i], keepMontgomery: false);
}
return checksum;
}
/// <summary>
/// Uses a known divisor cycle to fold the exponent first, dropping runtimes to 26.04 μs on small inputs (0.72× baseline) and 9.53 μs on large inputs (0.08× baseline).
/// </summary>
[Benchmark]
public ulong BaselineWithKnownCycle()
{
GetData(out var exponents, out var divisors, out var cycles);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWithCycle(cycles[i], divisors[i]);
}
return checksum;
}
/// <summary>
/// Left-to-right Montgomery scan that trails the baseline: 39.11 μs on small inputs (1.08×) and 126.38 μs on large ones (1.08×).
/// </summary>
[Benchmark]
public ulong LeftToRight()
{
GetData(out var exponents, out var divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= Pow2MontgomeryModLeftToRight(exponents[i], divisors[i]);
}
return checksum;
}
/// <summary>
/// Processes four exponent bits per loop, finishing at 38.74 μs on small cases (1.07× baseline) and 120.11 μs on large ones (1.02×).
/// </summary>
[Benchmark]
public ulong Batched4()
{
GetData(out var exponents, out var divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= Pow2MontgomeryModBatched4(exponents[i], divisors[i]);
}
return checksum;
}
/// <summary>
/// Windowed Montgomery ladder (width 5) that lags behind the baseline at 52.82 μs on small samples (1.45×) and 124.13 μs on large samples (1.06×).
/// </summary>
[Benchmark]
public ulong SlidingWindow()
{
GetData(out var exponents, out var divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= Pow2MontgomeryModSlidingWindow(exponents[i], divisors[i]);
}
return checksum;
}
/// <summary>
/// Classic binary ladder without Montgomery reduction; runs 24.75 μs on small inputs (0.68× baseline) but collapses to 295.29 μs on large ones (2.52×).
/// </summary>
[Benchmark]
public ulong BinaryMod()
{
GetData(out var exponents, out var divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= Pow2ModBinary(exponents[i], divisors[i].Modulus);
}
return checksum;
}
/// <summary>
/// Precomputes all squarings once; 30.18 μs on small inputs (0.83× baseline) but 289.80 μs on large ones (2.47×).
/// </summary>
[Benchmark]
public ulong PrecomputedTableMod()
{
GetData(out var exponents, out var divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= Pow2ModPrecomputedTable(exponents[i], divisors[i].Modulus);
}
return checksum;
}
/// <summary>
/// Relies on <see cref="BigInteger.ModPow"/>; fastest on small inputs at 17.21 μs (0.47× baseline) but slowest on large ones at 562.78 μs (4.80×).
/// </summary>
[Benchmark]
public ulong BigIntegerMod()
{
GetData(out var exponents, out var divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= Pow2ModBigInteger(exponents[i], divisors[i].Modulus);
}
return checksum;
}
private void GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out ulong[] cycles)
{
if (Scale == InputScale.Small)
{
exponents = _smallExponents;
divisors = _smallDivisors;
cycles = _smallCycles;
}
else
{
exponents = _largeExponents;
divisors = _largeDivisors;
cycles = _largeCycles;
}
}
private ulong NextSmallExponent() => (ulong)_random.NextInt64(1L, 1_000_000L);
private ulong NextLargeExponent() => (ulong)_random.NextInt64(1L << 60, long.MaxValue);
private ulong NextSmallOddModulus()
{
ulong value = (ulong)_random.NextInt64(3L, 1_000_000L);
return value | 1UL;
}
private (ulong modulus, ulong cycleLength) NextLargeModulusAndCycle()
{
int bitLength = _random.Next(48, 64);
ulong modulus = (1UL << bitLength) - 1UL;
return (modulus, (ulong)bitLength);
}
private static MontgomeryDivisorData CreateMontgomeryDivisorData(ulong modulus)
{
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return new MontgomeryDivisorData(modulus, 0UL, 0UL, 0UL, 0UL);
}
ulong nPrime = ComputeMontgomeryNPrime(modulus);
ulong montgomeryOne = ComputeMontgomeryResidue(1UL, modulus);
ulong montgomeryTwo = ComputeMontgomeryResidue(2UL, modulus);
ulong montgomeryTwoSquared = ULongExtensions.MontgomeryMultiply(montgomeryTwo, montgomeryTwo, modulus, nPrime);
return new MontgomeryDivisorData(
modulus,
nPrime,
montgomeryOne,
montgomeryTwo,
montgomeryTwoSquared);
}
private static ulong ComputeMontgomeryResidue(ulong value, ulong modulus) => (ulong)((UInt128)value * (UInt128.One << 64) % modulus);
private static ulong ComputeMontgomeryNPrime(ulong modulus)
{
ulong inv = modulus;
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
return unchecked(0UL - inv);
}
private static ulong Pow2MontgomeryModLeftToRight(ulong exponent, in MontgomeryDivisorData divisor)
{
ulong modulus = divisor.Modulus;
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return 0UL;
}
ulong result = divisor.MontgomeryOne;
ulong baseVal = divisor.MontgomeryTwo;
ulong nPrime = divisor.NPrime;
if (exponent == 0UL)
{
return result.MontgomeryMultiply(1UL, modulus, nPrime);
}
int msbIndex = 63 - BitOperations.LeadingZeroCount(exponent);
for (int bitIndex = msbIndex; bitIndex >= 0; bitIndex--)
{
result = result.MontgomeryMultiply(result, modulus, nPrime);
if (((exponent >> bitIndex) & 1UL) != 0UL)
{
result = result.MontgomeryMultiply(baseVal, modulus, nPrime);
}
}
return result.MontgomeryMultiply(1UL, modulus, nPrime);
}
private static ulong Pow2MontgomeryModBatched4(ulong exponent, in MontgomeryDivisorData divisor)
{
ulong modulus = divisor.Modulus;
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return 0UL;
}
ulong result = divisor.MontgomeryOne;
ulong baseVal = divisor.MontgomeryTwo;
ulong nPrime = divisor.NPrime;
ulong remainingExponent = exponent;
while (remainingExponent != 0UL)
{
ulong nibble = remainingExponent & 0xFUL;
ulong base0 = baseVal;
ulong base1 = base0.MontgomeryMultiply(base0, modulus, nPrime);
ulong base2 = base1.MontgomeryMultiply(base1, modulus, nPrime);
ulong base3 = base2.MontgomeryMultiply(base2, modulus, nPrime);
if ((nibble & 1UL) != 0UL)
{
result = result.MontgomeryMultiply(base0, modulus, nPrime);
}
if ((nibble & 2UL) != 0UL)
{
result = result.MontgomeryMultiply(base1, modulus, nPrime);
}
if ((nibble & 4UL) != 0UL)
{
result = result.MontgomeryMultiply(base2, modulus, nPrime);
}
if ((nibble & 8UL) != 0UL)
{
result = result.MontgomeryMultiply(base3, modulus, nPrime);
}
baseVal = base3.MontgomeryMultiply(base3, modulus, nPrime);
remainingExponent >>= 4;
}
return result.MontgomeryMultiply(1UL, modulus, nPrime);
}
private static ulong Pow2MontgomeryModSlidingWindow(ulong exponent, in MontgomeryDivisorData divisor)
{
ulong modulus = divisor.Modulus;
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return 0UL;
}
ulong nPrime = divisor.NPrime;
ulong result = divisor.MontgomeryOne;
ulong baseVal = divisor.MontgomeryTwo;
if (exponent == 0UL)
{
return result.MontgomeryMultiply(1UL, modulus, nPrime);
}
const int WindowSize = 5;
Span<ulong> oddPowers = stackalloc ulong[1 << (WindowSize - 1)];
oddPowers[0] = baseVal;
ulong baseSquared = baseVal.MontgomeryMultiply(baseVal, modulus, nPrime);
for (int i = 1; i < oddPowers.Length; i++)
{
oddPowers[i] = oddPowers[i - 1].MontgomeryMultiply(baseSquared, modulus, nPrime);
}
int bitIndex = 63 - BitOperations.LeadingZeroCount(exponent);
while (bitIndex >= 0)
{
if (((exponent >> bitIndex) & 1UL) == 0UL)
{
result = result.MontgomeryMultiply(result, modulus, nPrime);
bitIndex--;
continue;
}
int windowStart = Math.Max(bitIndex - WindowSize + 1, 0);
ulong windowValue = (exponent >> windowStart) & ((1UL << (bitIndex - windowStart + 1)) - 1UL);
while ((windowValue & 1UL) == 0UL)
{
windowValue >>= 1;
windowStart++;
}
int squares = bitIndex - windowStart + 1;
for (int i = 0; i < squares; i++)
{
result = result.MontgomeryMultiply(result, modulus, nPrime);
}
result = result.MontgomeryMultiply(oddPowers[(int)(windowValue >> 1)], modulus, nPrime);
bitIndex = windowStart - 1;
}
return result.MontgomeryMultiply(1UL, modulus, nPrime);
}
private static ulong Pow2ModBinary(ulong exponent, ulong modulus)
{
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return 0UL;
}
ulong result = 1UL % modulus;
ulong baseVal = 2UL % modulus;
ulong remainingExponent = exponent;
while (remainingExponent > 0UL)
{
if ((remainingExponent & 1UL) != 0UL)
{
result = MultiplyMod(result, baseVal, modulus);
}
remainingExponent >>= 1;
if (remainingExponent == 0UL)
{
break;
}
baseVal = MultiplyMod(baseVal, baseVal, modulus);
}
return result;
}
private static ulong Pow2ModPrecomputedTable(ulong exponent, ulong modulus)
{
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return 0UL;
}
if (exponent == 0UL)
{
return 1UL % modulus;
}
Span<ulong> powers = stackalloc ulong[64];
powers[0] = 2UL % modulus;
int highestBit = 63 - BitOperations.LeadingZeroCount(exponent);
for (int index = 1; index <= highestBit; index++)
{
powers[index] = MultiplyMod(powers[index - 1], powers[index - 1], modulus);
}
ulong result = 1UL % modulus;
ulong remainingExponent = exponent;
int bitIndex = 0;
while (remainingExponent > 0UL)
{
if ((remainingExponent & 1UL) != 0UL)
{
result = MultiplyMod(result, powers[bitIndex], modulus);
}
remainingExponent >>= 1;
bitIndex++;
}
return result;
}
private static ulong Pow2ModBigInteger(ulong exponent, ulong modulus)
{
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return 0UL;
}
BigInteger mod = modulus;
BigInteger result = BigInteger.ModPow(2, exponent, mod);
return (ulong)result;
}
private static ulong MultiplyMod(ulong x, ulong y, ulong modulus)
{
UInt128 product = (UInt128)x * y;
return (ulong)(product % modulus);
}
}
@@ -1,341 +0,0 @@
using System;
using System.Buffers.Binary;
using System.Threading;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
/// <remarks>
/// Benchmark results collected on AMD Ryzen 5 5625U (.NET 8.0, launchCount: 1, warmupCount: 1, iterationCount: 5):
/// Small sample (~1e6 random odd moduli)
/// - MontgomeryWithoutCycle: 35.41 µs (baseline)
/// - MontgomeryWithPrecomputedCycle: 25.81 µs (0.73× baseline)
/// - MontgomeryWithGpuCycleComputation: 54.09 ms (1,527× baseline, includes GPU cycle calculation)
/// Large sample (Mersenne-like moduli from 2^48-1 to 2^63-1)
/// - MontgomeryWithoutCycle: 117.33 µs (baseline)
/// - MontgomeryWithPrecomputedCycle: 9.12 µs (0.08× baseline)
/// - MontgomeryWithGpuCycleComputation: 12.91 µs (0.11× baseline)
/// </remarks>
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
public class Pow2MontgomeryModCycleComputationBenchmarks
{
private const int SampleCount = 256;
private const ulong OneShiftedLeft63 = 1UL << 63;
private const long OneShiftedLeft60 = 1L << 60;
private readonly MontgomeryDivisorData[] _smallDivisors = new MontgomeryDivisorData[SampleCount];
private readonly MontgomeryDivisorData[] _largeDivisors = new MontgomeryDivisorData[SampleCount];
private readonly MontgomeryDivisorData[] _veryLargeDivisors = new MontgomeryDivisorData[SampleCount];
private readonly UInt128[] _wideModuli = new UInt128[SampleCount];
private readonly ulong[] _smallExponents = new ulong[SampleCount];
private readonly ulong[] _largeExponents = new ulong[SampleCount];
private readonly ulong[] _veryLargeExponents = new ulong[SampleCount];
private readonly ulong[] _smallCycles = new ulong[SampleCount];
private readonly ulong[] _largeCycles = new ulong[SampleCount];
private readonly ulong[] _veryLargeCycles = new ulong[SampleCount];
private readonly UInt128[] _wideCycles = new UInt128[SampleCount];
private readonly Random _random = new(113);
private ulong? _previousPrimeOrder;
private UInt128? _previousWidePrimeOrder;
public enum InputScale
{
Small,
Large,
VeryLarge
}
[ParamsAllValues]
public InputScale Scale { get; set; }
[GlobalSetup]
public void Setup()
{
for (int i = 0; i < SampleCount; i++)
{
#if DEBUG
Console.WriteLine($"Generating sample {i}");
#endif
ulong smallModulus = NextSmallOddModulus();
_smallDivisors[i] = CreateMontgomeryDivisorData(smallModulus);
_smallExponents[i] = NextSmallExponent();
_smallCycles[i] = CalculateCycleLengthWithHeuristics(smallModulus);
(ulong largeModulus, ulong largeCycle) = NextLargeModulusAndCycle();
_largeDivisors[i] = CreateMontgomeryDivisorData(largeModulus);
_largeExponents[i] = NextLargeExponent();
_largeCycles[i] = CalculateCycleLengthWithHeuristics(largeModulus, largeCycle);
ulong veryLargeModulus = NextVeryLargeOddModulus();
_veryLargeDivisors[i] = CreateMontgomeryDivisorData(veryLargeModulus);
_veryLargeExponents[i] = NextVeryLargeExponent();
#if DEBUG
Console.WriteLine($"Calculating cycle length {veryLargeModulus}");
#endif
_veryLargeCycles[i] = CalculateCycleLengthWithHeuristics(veryLargeModulus);
Console.WriteLine($"Generating wide sample {i}");
UInt128 wideModulus = NextWideOddModulus();
_wideModuli[i] = wideModulus;
_wideCycles[i] = CalculateCycleLengthWithHeuristics(wideModulus);
}
}
/// <summary>
/// Baseline Montgomery reduction without precomputed cycle data; measured 35.41 μs on the small sample and 117.33 μs on the
/// large set.
/// </summary>
// [Benchmark(Baseline = true)]
public ulong MontgomeryWithoutCycle()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWindowed(divisors[i], keepMontgomery: false);
}
return checksum;
}
/// <summary>
/// Montgomery reduction with a known cycle length (cycle lookup only); 25.81 μs on the small set (0.73× baseline) and 9.12 μs
/// on the large one (0.08×).
/// </summary>
[Benchmark]
public ulong MontgomeryWithPrecomputedCycle()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out ulong[] cycles);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= exponents[i].Pow2MontgomeryModWithCycle(cycles[i], divisors[i]);
}
return checksum;
}
/// <summary>
/// Montgomery reduction with the cycle computed on the fly using the GPU helper; costs 54.09 ms on the small benchmark due to
/// cycle discovery, but drops to 12.91 μs on the large set (0.11× baseline).
/// </summary>
// [Benchmark]
public ulong MontgomeryWithGpuCycleComputation()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
ulong cycle = MersenneDivisorCycles.CalculateCycleLengthGpu(divisors[i].Modulus);
checksum ^= exponents[i].Pow2MontgomeryModWithCycle(cycle, divisors[i]);
}
return checksum;
}
[Benchmark]
public ulong MontgomeryWithHeuristicCycleComputation()
{
GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out _);
ulong checksum = 0UL;
for (int i = 0; i < SampleCount; i++)
{
ulong cycle = CalculateCycleLengthWithHeuristics(divisors[i].Modulus);
checksum ^= exponents[i].Pow2MontgomeryModWithCycle(cycle, divisors[i]);
}
return checksum;
}
[Benchmark]
public UInt128 HeuristicWideCycleComputation()
{
UInt128 checksum = UInt128.Zero;
for (int i = 0; i < SampleCount; i++)
{
UInt128 cycle = CalculateCycleLengthWithHeuristics(_wideModuli[i]);
_wideCycles[i] = cycle;
checksum ^= cycle;
}
return checksum;
}
private void GetData(out ulong[] exponents, out MontgomeryDivisorData[] divisors, out ulong[] cycles)
{
switch (Scale)
{
case InputScale.Small:
exponents = _smallExponents;
divisors = _smallDivisors;
cycles = _smallCycles;
break;
case InputScale.Large:
exponents = _largeExponents;
divisors = _largeDivisors;
cycles = _largeCycles;
break;
case InputScale.VeryLarge:
exponents = _veryLargeExponents;
divisors = _veryLargeDivisors;
cycles = _veryLargeCycles;
break;
default:
throw new ArgumentOutOfRangeException(nameof(Scale), Scale, null);
}
}
private ulong NextSmallExponent() => (ulong)_random.NextInt64(1L, 1_000_000L);
private ulong NextLargeExponent() => (ulong)_random.NextInt64(OneShiftedLeft60, long.MaxValue);
private ulong NextVeryLargeExponent()
{
Span<byte> buffer = stackalloc byte[8];
_random.NextBytes(buffer);
ulong value = checked(BinaryPrimitives.ReadUInt64LittleEndian(buffer) | OneShiftedLeft63);
return value;
}
private ulong NextSmallOddModulus()
{
ulong value = (ulong)_random.NextInt64(3L, 1_000_000L);
return value | 1UL;
}
private (ulong modulus, ulong cycleLength) NextLargeModulusAndCycle()
{
int bitLength = _random.Next(48, 64);
ulong modulus = (1UL << bitLength) - 1UL;
return (modulus, (ulong)bitLength);
}
private ulong NextVeryLargeOddModulus()
{
Span<byte> buffer = stackalloc byte[8];
_random.NextBytes(buffer);
ulong value = BinaryPrimitives.ReadUInt64LittleEndian(buffer) | OneShiftedLeft63 | 1UL;
return value;
}
private UInt128 NextWideOddModulus()
{
Span<byte> buffer = stackalloc byte[16];
_random.NextBytes(buffer);
ulong low = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
Span<byte> highSlice = buffer.Slice(8, 8);
ulong high = BinaryPrimitives.ReadUInt64LittleEndian(highSlice) | (1UL << 63);
UInt128 value = ((UInt128)high << 64) | low;
return value | UInt128.One;
}
private static MontgomeryDivisorData CreateMontgomeryDivisorData(ulong modulus)
{
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return new MontgomeryDivisorData(modulus, 0UL, 0UL, 0UL, 0UL);
}
ulong nPrime = ComputeMontgomeryNPrime(modulus);
ulong montgomeryOne = ComputeMontgomeryResidue(1UL, modulus);
ulong montgomeryTwo = ComputeMontgomeryResidue(2UL, modulus);
ulong montgomeryTwoSquared = ULongExtensions.MontgomeryMultiply(montgomeryTwo, montgomeryTwo, modulus, nPrime);
return new MontgomeryDivisorData(
modulus,
nPrime,
montgomeryOne,
montgomeryTwo,
montgomeryTwoSquared);
}
private ulong CalculateCycleLengthWithHeuristics(ulong modulus, ulong fallbackOrder = 0UL)
{
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(modulus);
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
{
return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLength(modulus, divisorData);
}
// When the below is uncommented, the benchmark never completes due to the primality test cost on very large random set.
// bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(modulus);
// // bool isPrime = PrimeTester.IsPrimeInternal(modulus, CancellationToken.None);
// if (!isPrime)
// {
// return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLength(modulus);
// }
#if DEBUG
Console.WriteLine("Trying heuristic. Prime order calculation");
#endif
PrimeOrderCalculator.PrimeOrderResult orderResult = PrimeOrderCalculator.Calculate(
modulus,
_previousPrimeOrder,
divisorData,
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault);
if (orderResult.Order != 0UL)
{
_previousPrimeOrder = orderResult.Order;
return orderResult.Order;
}
_previousPrimeOrder = null;
Console.WriteLine($"Heuristic failed for {modulus}, falling back to full cycle calculation");
return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLength(modulus, divisorData);
}
private UInt128 CalculateCycleLengthWithHeuristics(UInt128 modulus)
{
if (modulus <= UInt128.One || (modulus & UInt128.One) == UInt128.Zero)
{
return MersenneDivisorCycles.GetCycle(modulus);
}
#if DEBUG
Console.WriteLine("Trying heuristic. Wide prime order calculation");
#endif
PrimeOrderCalculator.PrimeOrderResultWide orderResult = PrimeOrderCalculator.Calculate(
modulus,
_previousWidePrimeOrder,
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault);
if (orderResult.Order != UInt128.Zero)
{
_previousWidePrimeOrder = orderResult.Order;
return orderResult.Order;
}
_previousWidePrimeOrder = null;
Console.WriteLine($"Wide heuristic failed for {modulus}, falling back to full cycle calculation");
return MersenneDivisorCycles.GetCycle(modulus);
}
private static ulong ComputeMontgomeryResidue(ulong value, ulong modulus) => (ulong)((UInt128)value * (UInt128.One << 64) % modulus);
private static ulong ComputeMontgomeryNPrime(ulong modulus)
{
ulong inv = modulus;
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
inv *= unchecked(2UL - modulus * inv);
return unchecked(0UL - inv);
}
}
@@ -1,283 +0,0 @@
using System;
using System.Buffers.Binary;
using System.Numerics;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
public class Pow2MontgomeryModUInt128Benchmarks
{
private const int SampleCount = 256;
private const ulong Pow2WindowFallbackThreshold = 32UL;
private readonly UInt128[] _moduli = new UInt128[SampleCount];
private readonly UInt128[] _exponents = new UInt128[SampleCount];
private readonly Random _random = new(19);
[GlobalSetup]
public void Setup()
{
for (int i = 0; i < SampleCount; i++)
{
UInt128 modulus = NextWideOddModulus();
_moduli[i] = modulus;
UInt128 exponent = NextWideExponent();
_exponents[i] = exponent;
}
}
/// <summary>
/// Current UInt128-native windowed Montgomery ladder that avoids GpuUInt128 on CPU paths.
/// </summary>
[Benchmark(Baseline = true)]
public UInt128 UInt128Windowed()
{
UInt128 checksum = UInt128.Zero;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= _exponents[i].Pow2MontgomeryModWindowed(_moduli[i]);
}
return checksum;
}
/// <summary>
/// Previous CPU shim that reused the GPU-oriented GpuUInt128 implementation.
/// </summary>
[Benchmark]
public UInt128 GpuStructShim()
{
UInt128 checksum = UInt128.Zero;
for (int i = 0; i < SampleCount; i++)
{
checksum ^= Pow2MontgomeryModWindowedGpuShim(_exponents[i], _moduli[i]);
}
return checksum;
}
private UInt128 NextWideOddModulus()
{
Span<byte> buffer = stackalloc byte[16];
_random.NextBytes(buffer);
ulong low = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
ulong high = BinaryPrimitives.ReadUInt64LittleEndian(buffer[8..]);
if (high == 0UL)
{
high = 1UL;
}
UInt128 modulus = ((UInt128)high << 64) | low;
modulus |= UInt128.One;
if (modulus <= UInt128.One)
{
modulus += 2UL;
}
return modulus;
}
private UInt128 NextWideExponent()
{
Span<byte> buffer = stackalloc byte[16];
_random.NextBytes(buffer);
ulong low = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
ulong high = BinaryPrimitives.ReadUInt64LittleEndian(buffer[8..]);
if (high == 0UL)
{
high = 1UL;
}
return ((UInt128)high << 64) | low;
}
private static UInt128 Pow2MontgomeryModWindowedGpuShim(UInt128 exponent, UInt128 modulus)
{
if (modulus == UInt128.One)
{
return UInt128.Zero;
}
if (exponent == UInt128.Zero)
{
return UInt128.One % modulus;
}
GpuUInt128 modulusGpu = new(modulus);
GpuUInt128 exponentGpu = new(exponent);
GpuUInt128 baseValue = new(2UL);
if (baseValue.CompareTo(modulusGpu) >= 0)
{
baseValue.Sub(modulusGpu);
}
if (ShouldUseSingleBit(exponentGpu))
{
GpuUInt128 singleBitResult = Pow2MontgomeryModSingleBit(exponentGpu, modulusGpu, baseValue);
return (UInt128)singleBitResult;
}
int bitLength = exponentGpu.GetBitLength();
int windowSize = GetWindowSize(bitLength);
int oddPowerCount = 1 << (windowSize - 1);
Span<GpuUInt128> oddPowers = stackalloc GpuUInt128[oddPowerCount];
InitializeOddPowers(baseValue, modulusGpu, oddPowers);
GpuUInt128 result = GpuUInt128.One;
int index = bitLength - 1;
while (index >= 0)
{
if (!IsBitSet(exponentGpu, index))
{
result.MulMod(result, modulusGpu);
index--;
continue;
}
int windowStart = index - windowSize + 1;
if (windowStart < 0)
{
windowStart = 0;
}
while (!IsBitSet(exponentGpu, windowStart))
{
windowStart++;
}
int windowBitCount = index - windowStart + 1;
for (int square = 0; square < windowBitCount; square++)
{
result.MulMod(result, modulusGpu);
}
ulong windowValue = ExtractWindowValue(exponentGpu, windowStart, windowBitCount);
int tableIndex = (int)((windowValue - 1UL) >> 1);
GpuUInt128 factor = oddPowers[tableIndex];
result.MulMod(factor, modulusGpu);
index = windowStart - 1;
}
return (UInt128)result;
}
private static bool ShouldUseSingleBit(in GpuUInt128 exponent)
{
if (exponent.High == 0UL && exponent.Low <= Pow2WindowFallbackThreshold)
{
return true;
}
return false;
}
private static int GetWindowSize(int bitLength)
{
if (bitLength <= 8)
{
return Math.Max(bitLength, 1);
}
if (bitLength <= 23)
{
return 4;
}
if (bitLength <= 79)
{
return 5;
}
if (bitLength <= 239)
{
return 6;
}
if (bitLength <= 671)
{
return 7;
}
return 8;
}
private static void InitializeOddPowers(GpuUInt128 baseValue, GpuUInt128 modulus, Span<GpuUInt128> oddPowers)
{
oddPowers[0] = baseValue;
if (oddPowers.Length == 1)
{
return;
}
// Reusing baseValue to hold base^2 for the shared odd-power ladder.
baseValue.MulMod(baseValue, modulus);
for (int i = 1; i < oddPowers.Length; i++)
{
oddPowers[i] = oddPowers[i - 1];
oddPowers[i].MulMod(baseValue, modulus);
}
}
private static GpuUInt128 Pow2MontgomeryModSingleBit(GpuUInt128 exponent, GpuUInt128 modulus, GpuUInt128 baseValue)
{
GpuUInt128 result = GpuUInt128.One;
while (!exponent.IsZero)
{
if ((exponent.Low & 1UL) != 0UL)
{
result.MulMod(baseValue, modulus);
}
exponent.ShiftRight(1);
if (exponent.IsZero)
{
break;
}
// Reusing baseValue to store the squared base for the next iteration.
baseValue.MulMod(baseValue, modulus);
}
return result;
}
private static bool IsBitSet(in GpuUInt128 value, int bitIndex)
{
if (bitIndex >= 64)
{
return ((value.High >> (bitIndex - 64)) & 1UL) != 0UL;
}
return ((value.Low >> bitIndex) & 1UL) != 0UL;
}
private static ulong ExtractWindowValue(in GpuUInt128 exponent, int windowStart, int windowBitCount)
{
if (windowStart != 0)
{
GpuUInt128 shifted = exponent;
shifted.ShiftRight(windowStart);
ulong mask = (1UL << windowBitCount) - 1UL;
return shifted.Low & mask;
}
ulong directMask = (1UL << windowBitCount) - 1UL;
return exponent.Low & directMask;
}
}
File diff suppressed because it is too large. Load diff
+2 -20
View File
@@ -1,6 +1,5 @@
using BenchmarkDotNet.Configs;
using System;
using BenchmarkDotNet.Running;
using PerfectNumbers.Core.Gpu;
namespace EvenPerfectBitScanner.Benchmarks;
@@ -8,24 +7,7 @@ public static class Program
{
public static void Main(string[] args)
{
// var config = DefaultConfig.Instance.WithOptions(ConfigOptions.JoinSummary | ConfigOptions.DisableOptimizationsValidator);
// BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
Pow2MontgomeryModCycleComputationBenchmarks benchmarks = new()
{
Scale = Pow2MontgomeryModCycleComputationBenchmarks.InputScale.Large
};
GpuPrimeWorkLimiter.SetLimit(1);
benchmarks.Setup();
// Console.WriteLine("Press ENTER to run");
// Console.ReadLine();
// foreach (int i in Enumerable.Range(0, 100_000))
// {
// benchmarks.MontgomeryWithPrecomputedCycle();
// }
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
}
}
@@ -1,3 +1,4 @@
using System.Numerics;
using BenchmarkDotNet.Attributes;
using PerfectNumbers.Core;
@@ -9,12 +10,6 @@ public class ResidueComputationBenchmarks
[Params(3UL, 8191UL, 131071UL, 2147483647UL)]
public ulong Exponent { get; set; }
/// <summary>
/// Legacy modulo pipeline; stays in the 2.41–2.43 ns range even for the 31-bit exponent (2,147,483,647).
/// </summary>
/// <remarks>
/// Observed means: Exponent 3 → 2.412 ns (1.00×), 8,191 → 2.405 ns, 131,071 → 2.416 ns, 2,147,483,647 → 2.427 ns.
/// </remarks>
[Benchmark(Baseline = true)]
public (ulong Step10, ulong Step8, ulong Step3, ulong Step5) LegacyModulo()
{
@@ -26,12 +21,6 @@ public class ResidueComputationBenchmarks
return (step10, step8, step3, step5);
}
/// <summary>
/// <see cref="PerfectNumbers.Core.ExponentResidues.Mod10_8_5_3"/> helper; 2.80 ns at exponent 3 but stretching to 3.36 ns at the 31-bit case (1.16–1.38× slower than legacy).
/// </summary>
/// <remarks>
/// Observed means: Exponent 3 → 2.799 ns (1.16×), 8,191 → 2.842 ns, 131,071 → 3.111 ns, 2,147,483,647 → 3.355 ns.
/// </remarks>
[Benchmark]
public (ulong Step10, ulong Step8, ulong Step3, ulong Step5) ModMethodModulo()
{
+85 -140
View File
@@ -1,5 +1,6 @@
using System.Globalization;
using System.Text;
using System.Threading.Tasks;
using Open.Numeric.Primes;
namespace EvenPerfectBitScanner.ResultsParser;
@@ -149,25 +150,22 @@ internal static class Program
return true;
}
private static ulong ParseRangeValue(in string option, in string value)
{
// TODO: Replace TryParse with the Utf8Parser-based span helper once the results parser accepts ReadOnlySpan<char>
// inputs so numeric option parsing matches the fastest CLI path identified in benchmarks.
if (!ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong parsed))
{
throw new FormatException($"Value '{value}' for option '{option}' is not a valid non-negative integer.");
}
private static ulong ParseRangeValue(in string option, in string value)
{
if (!ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong parsed))
{
throw new FormatException($"Value '{value}' for option '{option}' is not a valid non-negative integer.");
}
return parsed;
}
return parsed;
}
private static int ParsePositiveInt(in string option, in string value)
{
// TODO: Switch to the Utf8Parser-based fast path so integer arguments avoid transient strings while parsing.
if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed) || parsed < 1)
{
throw new FormatException($"Value '{value}' for option '{option}' is not a valid positive integer.");
}
private static int ParsePositiveInt(in string option, in string value)
{
if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed) || parsed < 1)
{
throw new FormatException($"Value '{value}' for option '{option}' is not a valid positive integer.");
}
return parsed;
}
@@ -178,24 +176,22 @@ internal static class Program
out string option,
out string value)
{
option = string.Empty;
value = string.Empty;
string current = arguments[index];
int equalsIndex = current.IndexOf('=');
if (equalsIndex <= 0 || equalsIndex == current.Length - 1)
{
Console.Error.WriteLine($"Option '{current}' must use the --name=value syntax.");
PrintHelp();
return false;
}
option = string.Empty;
value = string.Empty;
string current = arguments[index];
int equalsIndex = current.IndexOf('=');
if (equalsIndex <= 0 || equalsIndex == current.Length - 1)
{
Console.Error.WriteLine($"Option '{current}' must use the --name=value syntax.");
PrintHelp();
return false;
}
// TODO: Swap this string slicing for ReadOnlySpan<char>-based parsing once the CLI helpers expose
// Utf8Parser-compatible overloads so option handling avoids allocating substrings for every argument.
option = current[..equalsIndex];
value = current[(equalsIndex + 1)..];
index++;
return true;
}
option = current[..equalsIndex];
value = current[(equalsIndex + 1)..];
index++;
return true;
}
private static void PrintHelp()
{
@@ -230,10 +226,7 @@ internal static class Program
private static void ProcessFile(in string inputPath, ulong pMin, ulong pMax, int threadCount, int bufferSize)
{
using StreamReader reader = new(inputPath);
// TODO: Replace this vanilla StreamReader with a pooled FileStreamOptions + ArrayPool-backed reader so large
// reloads reuse the benchmarked zero-allocation buffered pipeline instead of allocating new decoder buffers per
// run.
using StreamReader reader = new(inputPath);
string? headerLine = reader.ReadLine();
if (headerLine is null)
{
@@ -247,10 +240,7 @@ internal static class Program
string header = string.IsNullOrWhiteSpace(headerLine) ? DefaultHeader : headerLine.Trim();
// TODO: Stream candidates through a pooled buffer pipeline so we can filter and dispatch without allocating
// the entire list upfront; the benchmarks showed that retaining every row simultaneously increases GC pressure
// during large result replays.
List<CandidateResult> candidates = LoadCandidates(reader, pMin, pMax, threadCount, bufferSize);
List<CandidateResult> candidates = LoadCandidates(reader, pMin, pMax, threadCount, bufferSize);
Console.WriteLine("Building sorted prime results...");
List<CandidateResult> sortedPrimeResults = ExtractSortedPrimeCandidates(candidates);
Console.WriteLine("Building RAW prime results...");
@@ -314,29 +304,23 @@ internal static class Program
throw new FormatException("Input line does not contain expected comma separators.");
}
// TODO: Replace ulong.Parse with the Utf8Parser-based span helper so CSV reload stays on the zero-allocation
// path identified as fastest in the shared CLI benchmarks.
ulong p = ulong.Parse(span[..firstCommaIndex], NumberStyles.Integer, CultureInfo.InvariantCulture);
ulong p = ulong.Parse(span[..firstCommaIndex], NumberStyles.Integer, CultureInfo.InvariantCulture);
if (p < pMin || p > pMax)
{
return CandidateResult.Empty;
}
// TODO: Swap bool.Parse for the Utf8Parser-powered boolean reader once exposed so reload avoids allocating
// temporary strings per record.
bool passedAllTests = bool.Parse(span[(lastCommaIndex + 1)..]);
bool passedAllTests = bool.Parse(span[(lastCommaIndex + 1)..]);
return new CandidateResult(p, passedAllTests, line);
}
private static string BuildOutputPath(in string inputPath, in string prefix)
{
// TODO: Switch this path composition to Span<char>-based stack buffers once we expose the pooled
// formatter helpers so output names avoid intermediate strings for directory/file splits.
string fileName = Path.GetFileName(inputPath);
string prefixedName = prefix + fileName;
string? directory = Path.GetDirectoryName(inputPath);
if (string.IsNullOrEmpty(directory))
{
private static string BuildOutputPath(in string inputPath, in string prefix)
{
string fileName = Path.GetFileName(inputPath);
string prefixedName = prefix + fileName;
string? directory = Path.GetDirectoryName(inputPath);
if (string.IsNullOrEmpty(directory))
{
return prefixedName;
}
@@ -345,49 +329,39 @@ internal static class Program
private static void WriteCsv(in string path, in string header, bool append, IEnumerable<CandidateResult> entries)
{
using StreamWriter writer = new(path, append, Encoding.UTF8);
// TODO: Switch WriteCsv to a span-based Utf8Formatter pipeline backed by ArrayPool-rented buffers so
// appends reuse the zero-allocation formatting path highlighted in the results writer benchmarks.
if (!append)
{
writer.WriteLine(header);
}
using StreamWriter writer = new(path, append, Encoding.UTF8);
if (!append)
{
writer.WriteLine(header);
}
foreach (CandidateResult entry in entries)
{
// TODO: Replace this per-entry WriteLine with a batched chunk writer that reuses pooled
// StringBuilder instances so large files avoid the per-line flush overhead observed in the
// chunked writer benchmarks.
writer.WriteLine(entry.Csv);
}
foreach (CandidateResult entry in entries)
{
writer.WriteLine(entry.Csv);
}
}
}
private static List<CandidateResult> LoadCandidates(
private static List<CandidateResult> LoadCandidates(
StreamReader reader,
ulong pMin,
ulong pMax,
int threadCount,
int bufferSize)
{
List<CandidateResult> candidates = [];
if (bufferSize < 1)
{
return candidates;
}
List<CandidateResult> candidates = [];
if (bufferSize < 1)
{
return candidates;
}
int workerLimit = Math.Max(1, threadCount);
// TODO: Replace the Task-based scheduling with the pooled work queue used in the scanner so candidate
// parsing leverages the same low-overhead thread handoff profiled as fastest in the CLI benchmarks.
List<Task<ChunkResult>> activeChunks = new(workerLimit);
List<Task<ChunkResult>> activeChunks = new(workerLimit);
Dictionary<int, ChunkResult> completedChunks = new();
int nextChunkToFlush = 0;
int consoleProgress = 0;
ulong lastReportedCandidate = 0UL;
// TODO: Rent this line buffer from ArrayPool<string> to eliminate repeated allocations when scanning
// large result files.
string[] buffer = new string[bufferSize];
string[] buffer = new string[bufferSize];
int buffered = 0;
int chunkIndex = 0;
@@ -446,18 +420,14 @@ internal static class Program
void DispatchBuffer(int length)
{
string[] chunkLines;
if (length == buffer.Length)
{
chunkLines = buffer;
// TODO: Rent this full-capacity buffer from ArrayPool<string> and reuse it between reads instead
// of allocating a fresh array each time steady-state batches are dispatched.
buffer = new string[bufferSize];
}
if (length == buffer.Length)
{
chunkLines = buffer;
buffer = new string[bufferSize];
}
else
{
// TODO: Rent the partial chunk buffer from ArrayPool<string> instead of allocating a fresh
// array for every remainder dispatch.
chunkLines = new string[length];
chunkLines = new string[length];
Array.Copy(buffer, chunkLines, length);
}
@@ -496,9 +466,7 @@ internal static class Program
private static ChunkResult ProcessChunk(string[] lines, int index, ulong pMin, ulong pMax)
{
// TODO: Rent the candidate array from ArrayPool<CandidateResult> so chunk processing remains allocation-free
// for hot reload paths.
CandidateResult[] items = new CandidateResult[lines.Length];
CandidateResult[] items = new CandidateResult[lines.Length];
int count = 0;
ulong pEmpty = CandidateResult.Empty.P;
for (int i = 0; i < lines.Length; i++)
@@ -528,15 +496,11 @@ internal static class Program
return [];
}
// TODO: Replace this copy with an in-place filtering pipeline that reuses pooled buffers; duplicating the list
// doubled working-set usage in the reload benchmarks when processing multi-million-entry snapshots.
List<CandidateResult> sortedCandidates = [.. candidates];
List<CandidateResult> sortedCandidates = [.. candidates];
sortedCandidates.Sort(CandidateResultComparer.Instance);
List<CandidateResult> primeCandidates = [];
// TODO: Keep using the Open.Numeric prime enumerator for the merge walk but hoist a shared instance so reloads
// reuse it instead of allocating a brand new enumerator for every pass.
using IEnumerator<ulong> primeEnumerator = Prime.Numbers.GetEnumerator();
List<CandidateResult> primeCandidates = [];
using IEnumerator<ulong> primeEnumerator = Prime.Numbers.GetEnumerator();
if (!primeEnumerator.MoveNext())
{
throw new InvalidOperationException("Prime generator did not provide any values.");
@@ -575,9 +539,7 @@ internal static class Program
return [];
}
// TODO: Replace this Dictionary allocation with the pooled span-based frequency map once the ValueListBuilder
// helper lands so raw prime reconstruction stops allocating temporary hash tables for every reload.
Dictionary<ulong, int> remainingCounts = new(primeCount);
Dictionary<ulong, int> remainingCounts = new(primeCount);
for (int i = 0; i < primeCount; i++)
{
CandidateResult candidate = sortedPrimeResults[i];
@@ -590,9 +552,7 @@ internal static class Program
remainingCounts.Add(candidate.P, 1);
}
// TODO: Rent the raw prime accumulator from ArrayPool<List<CandidateResult>> or migrate to a pooled struct
// builder so large reloads avoid repeated List allocations along this path.
List<CandidateResult> rawPrimes = new(primeCount);
List<CandidateResult> rawPrimes = new(primeCount);
for (int i = 0; i < candidates.Count; i++)
{
CandidateResult candidate = candidates[i];
@@ -638,10 +598,10 @@ internal static class Program
return;
}
if (threadCount <= 1 || count < threadCount * 4)
{
passedResults = new List<CandidateResult>(count);
rejectedResults = new List<CandidateResult>(count);
if (threadCount <= 1 || count < threadCount * 4)
{
passedResults = new List<CandidateResult>(count);
rejectedResults = new List<CandidateResult>(count);
for (int i = 0; i < count; i++)
{
CandidateResult candidate = sortedResults[i];
@@ -658,20 +618,13 @@ internal static class Program
}
int workerCount = Math.Min(threadCount, count);
// TODO: Rent these segment arrays from ArrayPool<List<CandidateResult>> so parallel splitting avoids allocating
// new jagged arrays on every reload; the pooled work queue already used by the scanner benchmarks faster.
List<CandidateResult>[] passedSegments = new List<CandidateResult>[workerCount];
List<CandidateResult>[] rejectedSegments = new List<CandidateResult>[workerCount];
Task[] tasks = new Task[workerCount];
// TODO: Rent the tasks array from ArrayPool<Task> so repeated reloads avoid allocating new
// scheduling buffers; the pooled queue used by the scanner benchmarks noticeably faster.
List<CandidateResult>[] passedSegments = new List<CandidateResult>[workerCount];
List<CandidateResult>[] rejectedSegments = new List<CandidateResult>[workerCount];
Task[] tasks = new Task[workerCount];
int start = 0;
// TODO: Replace the separate division/modulo here with Math.DivRem (or the branchless chunk
// planner from the parallel partition benchmarks) so we avoid paying for two divisions when
// distributing work across threads.
int baseChunk = count / workerCount;
int remainder = count % workerCount;
int start = 0;
int baseChunk = count / workerCount;
int remainder = count % workerCount;
for (int worker = 0; worker < workerCount; worker++)
{
int length = baseChunk + (worker < remainder ? 1 : 0);
@@ -687,15 +640,10 @@ internal static class Program
}
int workerIndex = worker;
// TODO: Replace Task.Run with the pooled work queue from the scanner so scheduling
// overhead matches the benchmarked fast path instead of spawning transient threads.
tasks[workerIndex] = Task.Run(() =>
{
List<CandidateResult> localPassed = new(length);
List<CandidateResult> localRejected = new(length);
// TODO: Rent these per-worker lists from a dedicated pool so parallel splits reuse
// pre-sized buffers; allocating new List instances showed up heavily in the multi-
// threaded reload benchmarks once files crossed tens of millions of entries.
int end = chunkStart + length;
for (int i = chunkStart; i < end; i++)
{
@@ -716,13 +664,10 @@ internal static class Program
Task.WaitAll(tasks);
passedResults = new List<CandidateResult>(count);
rejectedResults = new List<CandidateResult>(count);
// TODO: Promote these final result lists to pooled builders so we can reuse the buffers
// across reloads; the pooled aggregators from the scanner benchmarks avoid repeated large
// allocations during long parsing sessions.
for (int worker = 0; worker < workerCount; worker++)
{
passedResults = new List<CandidateResult>(count);
rejectedResults = new List<CandidateResult>(count);
for (int worker = 0; worker < workerCount; worker++)
{
if (passedSegments[worker].Count > 0)
{
passedResults.AddRange(passedSegments[worker]);
@@ -1,6 +1,10 @@
using FluentAssertions;
using Xunit;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Reflection;
using System.Text;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
File diff suppressed because it is too large. Load diff
@@ -5,6 +5,7 @@ namespace PerfectNumbers.Core.Tests;
public class AlphaCacheTests
{
[Fact(Skip = "TODO: implement test")]
[Trait("Category", "Fast")]
public void Placeholder()
{
}
@@ -5,6 +5,7 @@ namespace PerfectNumbers.Core.Tests;
public class AlphaCalculationsTests
{
[Fact(Skip = "TODO: implement test")]
[Trait("Category", "Fast")]
public void Placeholder()
{
}
@@ -0,0 +1,22 @@
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
[Trait("Category", "Fast")]
public class EulerPrimeTesterTests
{
private readonly EulerPrimeTester _tester = new(new PrimeTester());
[Theory]
[InlineData(13UL, true)]
[InlineData(29UL, true)]
[InlineData(3UL, false)]
[InlineData(21UL, false)]
[InlineData(1UL, false)]
public void IsEulerPrime_returns_expected(ulong n, bool expected)
{
_tester.IsEulerPrime(n).Should().Be(expected);
}
}
@@ -1,418 +0,0 @@
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;
namespace PerfectNumbers.Core.Tests.Gpu;
[Collection("GpuNtt")]
public class GpuKernelCompilationTests
{
private static readonly MethodInfo DivisorCycleCacheLoadKernelMethod =
typeof(DivisorCycleCache).GetMethod("LoadKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("DivisorCycleCache.LoadKernel not found.");
private static readonly MethodInfo NttGetBitReverseKernelMethod =
typeof(NttGpuMath).GetMethod("GetBitReverseKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("NttGpuMath.GetBitReverseKernel not found.");
private static readonly MethodInfo NttGetMulKernelMethod =
typeof(NttGpuMath).GetMethod("GetMulKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("NttGpuMath.GetMulKernel not found.");
private static readonly MethodInfo NttGetStageKernelMethod =
typeof(NttGpuMath).GetMethod("GetStageKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("NttGpuMath.GetStageKernel not found.");
private static readonly MethodInfo NttGetScaleKernelMethod =
typeof(NttGpuMath).GetMethod("GetScaleKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("NttGpuMath.GetScaleKernel not found.");
private static readonly MethodInfo NttGetStageMontKernelMethod =
typeof(NttGpuMath).GetMethod("GetStageMontKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("NttGpuMath.GetStageMontKernel not found.");
private static readonly MethodInfo NttGetStageBarrettKernelMethod =
typeof(NttGpuMath).GetMethod("GetStageBarrett128Kernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("NttGpuMath.GetStageBarrett128Kernel not found.");
private static readonly MethodInfo NttGetScaleBarrettKernelMethod =
typeof(NttGpuMath).GetMethod("GetScaleBarrett128Kernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("NttGpuMath.GetScaleBarrett128Kernel not found.");
private static readonly MethodInfo NttGetSquareBarrettKernelMethod =
typeof(NttGpuMath).GetMethod("GetSquareBarrett128Kernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("NttGpuMath.GetSquareBarrett128Kernel not found.");
private static readonly MethodInfo NttGetToMont64KernelMethod =
typeof(NttGpuMath).GetMethod("GetToMont64Kernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("NttGpuMath.GetToMont64Kernel not found.");
private static readonly MethodInfo NttGetFromMont64KernelMethod =
typeof(NttGpuMath).GetMethod("GetFromMont64Kernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("NttGpuMath.GetFromMont64Kernel not found.");
private static readonly MethodInfo NttGetSquareMont64KernelMethod =
typeof(NttGpuMath).GetMethod("GetSquareMont64Kernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("NttGpuMath.GetSquareMont64Kernel not found.");
private static readonly MethodInfo NttGetScaleMont64KernelMethod =
typeof(NttGpuMath).GetMethod("GetScaleMont64Kernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("NttGpuMath.GetScaleMont64Kernel not found.");
private static readonly MethodInfo NttGetForwardKernelMethod =
typeof(NttGpuMath).GetMethod("GetForwardKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("NttGpuMath.GetForwardKernel not found.");
private static readonly MethodInfo NttGetInverseKernelMethod =
typeof(NttGpuMath).GetMethod("GetInverseKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("NttGpuMath.GetInverseKernel not found.");
private static readonly MethodInfo DivisorByDivisorGetKernelMethod =
typeof(MersenneNumberDivisorByDivisorGpuTester).GetMethod("GetKernel", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberDivisorByDivisorGpuTester.GetKernel not found.");
private static readonly MethodInfo DivisorByDivisorGetExponentKernelMethod =
typeof(MersenneNumberDivisorByDivisorGpuTester).GetMethod("GetKernelByPrimeExponent", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberDivisorByDivisorGpuTester.GetKernelByPrimeExponent not found.");
private static readonly MethodInfo DivisorByDivisorGetRemainderDeltaKernelMethod =
typeof(MersenneNumberDivisorByDivisorGpuTester).GetMethod("GetRemainderDeltaKernel", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberDivisorByDivisorGpuTester.GetRemainderDeltaKernel not found.");
private static readonly MethodInfo DivisorByDivisorGetRemainderScanKernelMethod =
typeof(MersenneNumberDivisorByDivisorGpuTester).GetMethod("GetRemainderScanKernel", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberDivisorByDivisorGpuTester.GetRemainderScanKernel not found.");
private static readonly MethodInfo DivisorTesterGetKernelMethod =
typeof(MersenneNumberDivisorGpuTester).GetMethod("GetKernel", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberDivisorGpuTester.GetKernel not found.");
private static readonly MethodInfo LucasLehmerGetKernelMethod =
typeof(MersenneNumberLucasLehmerGpuTester).GetMethod("GetKernel", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberLucasLehmerGpuTester.GetKernel not found.");
private static readonly MethodInfo LucasLehmerGetAddSmallKernelMethod =
typeof(MersenneNumberLucasLehmerGpuTester).GetMethod("GetAddSmallKernel", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberLucasLehmerGpuTester.GetAddSmallKernel not found.");
private static readonly MethodInfo LucasLehmerGetSubSmallKernelMethod =
typeof(MersenneNumberLucasLehmerGpuTester).GetMethod("GetSubSmallKernel", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberLucasLehmerGpuTester.GetSubSmallKernel not found.");
private static readonly MethodInfo LucasLehmerGetReduceKernelMethod =
typeof(MersenneNumberLucasLehmerGpuTester).GetMethod("GetReduceKernel", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberLucasLehmerGpuTester.GetReduceKernel not found.");
private static readonly MethodInfo LucasLehmerGetIsZeroKernelMethod =
typeof(MersenneNumberLucasLehmerGpuTester).GetMethod("GetIsZeroKernel", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberLucasLehmerGpuTester.GetIsZeroKernel not found.");
private static readonly MethodInfo LucasLehmerGetBatchKernelMethod =
typeof(MersenneNumberLucasLehmerGpuTester).GetMethod("GetBatchKernel", BindingFlags.NonPublic | BindingFlags.Instance)
?? throw new InvalidOperationException("MersenneNumberLucasLehmerGpuTester.GetBatchKernel not found.");
private static readonly MethodInfo PrimeOrderGetPartialFactorKernelMethod =
typeof(PrimeOrderGpuHeuristics).GetMethod("GetPartialFactorKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("PrimeOrderGpuHeuristics.GetPartialFactorKernel not found.");
private static readonly MethodInfo PrimeOrderGetOrderKernelMethod =
typeof(PrimeOrderGpuHeuristics).GetMethod("GetOrderKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("PrimeOrderGpuHeuristics.GetOrderKernel not found.");
private static readonly MethodInfo PrimeOrderGetPow2ModKernelMethod =
typeof(PrimeOrderGpuHeuristics).GetMethod("GetPow2ModKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("PrimeOrderGpuHeuristics.GetPow2ModKernel not found.");
private static readonly MethodInfo PrimeOrderGetPow2ModWideKernelMethod =
typeof(PrimeOrderGpuHeuristics).GetMethod("GetPow2ModWideKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("PrimeOrderGpuHeuristics.GetPow2ModWideKernel not found.");
private static readonly Action<Index1D, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>> MersenneDivisorCyclesKernel =
(Action<Index1D, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>)Delegate.CreateDelegate(
typeof(Action<Index1D, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>),
typeof(MersenneDivisorCycles).GetMethod("GpuDivisorCycleKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("MersenneDivisorCycles.GpuDivisorCycleKernel not found."));
private static readonly Action<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<byte>> PrimeTesterSmallPrimeKernel =
(Action<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<byte>>)Delegate.CreateDelegate(
typeof(Action<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<byte>>),
typeof(PrimeTester).GetMethod("SmallPrimeSieveKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("PrimeTester.SmallPrimeSieveKernel not found."));
private static readonly Action<Index1D, ArrayView<ulong>, ArrayView<byte>> PrimeTesterSharesFactorKernel =
(Action<Index1D, ArrayView<ulong>, ArrayView<byte>>)Delegate.CreateDelegate(
typeof(Action<Index1D, ArrayView<ulong>, ArrayView<byte>>),
typeof(PrimeTester).GetMethod("SharesFactorKernel", BindingFlags.NonPublic | BindingFlags.Static)
?? throw new InvalidOperationException("PrimeTester.SharesFactorKernel not found."));
public static IEnumerable<object[]> KernelLoaders()
{
yield return Loader("DivisorCycleCache.GpuAdvanceDivisorCyclesKernel", CompileDivisorCycleCacheKernel);
yield return Loader("NttGpuMath.BitReverseKernel", CompileNttBitReverseKernel);
yield return Loader("NttGpuMath.MulKernel", CompileNttMulKernel);
yield return Loader("NttGpuMath.StageKernel", CompileNttStageKernel);
yield return Loader("NttGpuMath.ScaleKernel", CompileNttScaleKernel);
yield return Loader("NttGpuMath.StageMontKernel", CompileNttStageMontKernel);
yield return Loader("NttGpuMath.StageBarrett128Kernel", CompileNttStageBarrettKernel);
yield return Loader("NttGpuMath.ScaleBarrett128Kernel", CompileNttScaleBarrettKernel);
yield return Loader("NttGpuMath.SquareBarrett128Kernel", CompileNttSquareBarrettKernel);
yield return Loader("NttGpuMath.ToMont64Kernel", CompileNttToMont64Kernel);
yield return Loader("NttGpuMath.FromMont64Kernel", CompileNttFromMont64Kernel);
yield return Loader("NttGpuMath.SquareMont64Kernel", CompileNttSquareMont64Kernel);
yield return Loader("NttGpuMath.ScaleMont64Kernel", CompileNttScaleMont64Kernel);
yield return Loader("NttGpuMath.ForwardKernel", CompileNttForwardKernel);
yield return Loader("NttGpuMath.InverseKernel", CompileNttInverseKernel);
yield return Loader("MersenneNumberDivisorByDivisorGpuTester.CheckKernel", CompileDivisorByDivisorCheckKernel);
yield return Loader("MersenneNumberDivisorByDivisorGpuTester.ComputeMontgomeryExponentKernel", CompileDivisorByDivisorExponentKernel);
yield return Loader("MersenneNumberDivisorByDivisorGpuTester.ComputeRemainderDeltasKernel", CompileDivisorByDivisorRemainderDeltaKernel);
yield return Loader("MersenneNumberDivisorByDivisorGpuTester.AccumulateRemaindersKernel", CompileDivisorByDivisorRemainderScanKernel);
yield return Loader("MersenneNumberDivisorGpuTester.Kernel", CompileDivisorTesterKernel);
yield return Loader("MersenneNumberLucasLehmerGpuTester.Kernel", CompileLucasLehmerKernel);
yield return Loader("MersenneNumberLucasLehmerGpuTester.AddSmallKernel", CompileLucasLehmerAddSmallKernel);
yield return Loader("MersenneNumberLucasLehmerGpuTester.SubtractSmallKernel", CompileLucasLehmerSubSmallKernel);
yield return Loader("MersenneNumberLucasLehmerGpuTester.ReduceModMersenneKernel", CompileLucasLehmerReduceKernel);
yield return Loader("MersenneNumberLucasLehmerGpuTester.IsZeroKernel", CompileLucasLehmerIsZeroKernel);
yield return Loader("MersenneNumberLucasLehmerGpuTester.KernelBatch", CompileLucasLehmerBatchKernel);
yield return Loader("PrimeOrderGpuHeuristics.PartialFactorKernel", CompilePrimeOrderPartialFactorKernel);
yield return Loader("PrimeOrderGpuHeuristics.CalculateOrderKernel", CompilePrimeOrderKernel);
yield return Loader("PrimeOrderGpuHeuristics.Pow2ModKernel", CompilePrimeOrderPow2ModKernel);
yield return Loader("PrimeOrderGpuHeuristics.Pow2ModKernelWide", CompilePrimeOrderPow2ModWideKernel);
yield return Loader("MersenneDivisorCycles.GpuDivisorCycleKernel", CompileMersenneDivisorCyclesKernel);
yield return Loader("PrimeTester.SmallPrimeSieveKernel", CompilePrimeTesterSmallPrimeKernel);
yield return Loader("PrimeTester.SharesFactorKernel", CompilePrimeTesterSharesFactorKernel);
yield return Loader("GpuKernelPool.KernelLeaseKernels", CompileGpuKernelPoolKernels);
}
private static object[] Loader(string name, Action<Accelerator> loader)
{
return new object[] { name, loader };
}
[Theory]
[Trait("Category", "Fast")]
[MemberData(nameof(KernelLoaders))]
public void Kernel_compiles_without_internal_compiler_errors(string name, Action<Accelerator> compile)
{
using var lease = GpuContextPool.RentPreferred(preferCpu: true);
var accelerator = lease.Accelerator;
Action action = () => compile(accelerator);
action.Should().NotThrow($"kernel {name} should compile");
}
private static void CompileDivisorCycleCacheKernel(Accelerator accelerator)
{
_ = DivisorCycleCacheLoadKernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileNttBitReverseKernel(Accelerator accelerator)
{
_ = NttGetBitReverseKernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileNttMulKernel(Accelerator accelerator)
{
_ = NttGetMulKernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileNttStageKernel(Accelerator accelerator)
{
_ = NttGetStageKernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileNttScaleKernel(Accelerator accelerator)
{
_ = NttGetScaleKernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileNttStageMontKernel(Accelerator accelerator)
{
_ = NttGetStageMontKernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileNttStageBarrettKernel(Accelerator accelerator)
{
_ = NttGetStageBarrettKernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileNttScaleBarrettKernel(Accelerator accelerator)
{
_ = NttGetScaleBarrettKernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileNttSquareBarrettKernel(Accelerator accelerator)
{
_ = NttGetSquareBarrettKernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileNttToMont64Kernel(Accelerator accelerator)
{
_ = NttGetToMont64KernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileNttFromMont64Kernel(Accelerator accelerator)
{
_ = NttGetFromMont64KernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileNttSquareMont64Kernel(Accelerator accelerator)
{
_ = NttGetSquareMont64KernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileNttScaleMont64Kernel(Accelerator accelerator)
{
_ = NttGetScaleMont64KernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileNttForwardKernel(Accelerator accelerator)
{
_ = NttGetForwardKernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileNttInverseKernel(Accelerator accelerator)
{
_ = NttGetInverseKernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileDivisorByDivisorCheckKernel(Accelerator accelerator)
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
_ = DivisorByDivisorGetKernelMethod.Invoke(tester, new object[] { accelerator });
}
private static void CompileDivisorByDivisorExponentKernel(Accelerator accelerator)
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
_ = DivisorByDivisorGetExponentKernelMethod.Invoke(tester, new object[] { accelerator });
}
private static void CompileDivisorByDivisorRemainderDeltaKernel(Accelerator accelerator)
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
_ = DivisorByDivisorGetRemainderDeltaKernelMethod.Invoke(tester, new object[] { accelerator });
}
private static void CompileDivisorByDivisorRemainderScanKernel(Accelerator accelerator)
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
_ = DivisorByDivisorGetRemainderScanKernelMethod.Invoke(tester, new object[] { accelerator });
}
private static void CompileDivisorTesterKernel(Accelerator accelerator)
{
var tester = new MersenneNumberDivisorGpuTester();
_ = DivisorTesterGetKernelMethod.Invoke(tester, new object[] { accelerator });
}
private static void CompileLucasLehmerKernel(Accelerator accelerator)
{
var tester = new MersenneNumberLucasLehmerGpuTester();
_ = LucasLehmerGetKernelMethod.Invoke(tester, new object[] { accelerator });
}
private static void CompileLucasLehmerAddSmallKernel(Accelerator accelerator)
{
var tester = new MersenneNumberLucasLehmerGpuTester();
_ = LucasLehmerGetAddSmallKernelMethod.Invoke(tester, new object[] { accelerator });
}
private static void CompileLucasLehmerSubSmallKernel(Accelerator accelerator)
{
var tester = new MersenneNumberLucasLehmerGpuTester();
_ = LucasLehmerGetSubSmallKernelMethod.Invoke(tester, new object[] { accelerator });
}
private static void CompileLucasLehmerReduceKernel(Accelerator accelerator)
{
var tester = new MersenneNumberLucasLehmerGpuTester();
_ = LucasLehmerGetReduceKernelMethod.Invoke(tester, new object[] { accelerator });
}
private static void CompileLucasLehmerIsZeroKernel(Accelerator accelerator)
{
var tester = new MersenneNumberLucasLehmerGpuTester();
_ = LucasLehmerGetIsZeroKernelMethod.Invoke(tester, new object[] { accelerator });
}
private static void CompileLucasLehmerBatchKernel(Accelerator accelerator)
{
var tester = new MersenneNumberLucasLehmerGpuTester();
_ = LucasLehmerGetBatchKernelMethod.Invoke(tester, new object[] { accelerator });
}
private static void CompilePrimeOrderPartialFactorKernel(Accelerator accelerator)
{
_ = PrimeOrderGetPartialFactorKernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompilePrimeOrderKernel(Accelerator accelerator)
{
_ = PrimeOrderGetOrderKernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompilePrimeOrderPow2ModKernel(Accelerator accelerator)
{
_ = PrimeOrderGetPow2ModKernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompilePrimeOrderPow2ModWideKernel(Accelerator accelerator)
{
_ = PrimeOrderGetPow2ModWideKernelMethod.Invoke(null, new object[] { accelerator });
}
private static void CompileMersenneDivisorCyclesKernel(Accelerator accelerator)
{
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>(MersenneDivisorCyclesKernel);
_ = kernel;
}
private static void CompilePrimeTesterSmallPrimeKernel(Accelerator accelerator)
{
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<uint>, ArrayView<byte>>(PrimeTesterSmallPrimeKernel);
_ = kernel;
}
private static void CompilePrimeTesterSharesFactorKernel(Accelerator accelerator)
{
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView<ulong>, ArrayView<byte>>(PrimeTesterSharesFactorKernel);
_ = kernel;
}
private static void CompileGpuKernelPoolKernels(Accelerator accelerator)
{
_ = accelerator;
var lease = GpuKernelPool.GetKernel(useGpuOrder: false);
try
{
_ = lease.OrderKernel;
_ = lease.IncrementalKernel;
_ = lease.Pow2ModKernel;
_ = lease.IncrementalOrderKernel;
_ = lease.Pow2ModOrderKernel;
}
finally
{
lease.Dispose();
}
var orderLease = GpuKernelPool.GetKernel(useGpuOrder: true);
try
{
_ = orderLease.OrderKernel;
_ = orderLease.IncrementalKernel;
_ = orderLease.Pow2ModKernel;
_ = orderLease.IncrementalOrderKernel;
_ = orderLease.Pow2ModOrderKernel;
}
finally
{
orderLease.Dispose();
}
}
}
@@ -1,82 +0,0 @@
using FluentAssertions;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests.Gpu;
public class GpuRationalTests
{
[Fact, Trait("Category", "Fast")]
public void Constructor_ReducesFraction()
{
GpuRational value = new(6UL, 8UL);
value.Numerator.Should().Be(new GpuUInt128(3UL));
value.Denominator.Should().Be(new GpuUInt128(4UL));
}
[Fact, Trait("Category", "Fast")]
public void Addition_ReturnsExpectedValue()
{
GpuRational left = new(1UL, 3UL);
GpuRational right = new(1UL, 6UL);
GpuRational sum = left + right;
sum.Numerator.Should().Be(new GpuUInt128(1UL));
sum.Denominator.Should().Be(new GpuUInt128(2UL));
}
[Fact, Trait("Category", "Fast")]
public void Subtraction_ReturnsExpectedValue()
{
GpuRational left = new(5UL, 6UL);
GpuRational right = new(1UL, 6UL);
GpuRational difference = left - right;
difference.Numerator.Should().Be(new GpuUInt128(2UL));
difference.Denominator.Should().Be(new GpuUInt128(3UL));
}
[Fact, Trait("Category", "Fast")]
public void Multiplication_ReturnsExpectedValue()
{
GpuRational left = new(3UL, 4UL);
GpuRational right = new(2UL, 3UL);
GpuRational product = left * right;
product.Numerator.Should().Be(new GpuUInt128(1UL));
product.Denominator.Should().Be(new GpuUInt128(2UL));
}
[Fact, Trait("Category", "Fast")]
public void Division_ReturnsExpectedValue()
{
GpuRational left = new(3UL, 5UL);
GpuRational right = new(9UL, 10UL);
GpuRational quotient = left / right;
quotient.Numerator.Should().Be(new GpuUInt128(2UL));
quotient.Denominator.Should().Be(new GpuUInt128(3UL));
}
[Fact, Trait("Category", "Fast")]
public void CompareTo_DetectsOrdering()
{
GpuRational smaller = new(1UL, 4UL);
GpuRational larger = new(2UL, 3UL);
smaller.CompareTo(larger).Should().BeLessThan(0);
larger.CompareTo(smaller).Should().BeGreaterThan(0);
smaller.CompareTo(new GpuRational(1UL, 4UL)).Should().Be(0);
}
[Fact, Trait("Category", "Fast")]
public void ToDouble_ReturnsExpectedResult()
{
GpuRational value = new(1UL, 5UL);
value.ToDouble().Should().BeApproximately(0.2, 1e-12);
}
}
@@ -79,8 +79,8 @@ public class GpuUInt128Tests
var a = new GpuUInt128(aVal);
var b = new GpuUInt128(bVal);
UInt128 expected = ((UInt128)aVal * bVal) % modulus;
ulong result = a.MulMod(b, modulus);
((UInt128)result).Should().Be(expected);
a.MulMod(b, modulus);
((UInt128)a).Should().Be(expected);
}
}
@@ -1,372 +0,0 @@
using System;
using System.Numerics;
using System.Reflection;
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
using Xunit;
namespace PerfectNumbers.Core.Tests;
[Trait("Category", "Fast")]
public class PrimeOrderGpuHeuristicsTests
{
public PrimeOrderGpuHeuristicsTests()
{
PrimeOrderGpuHeuristics.OverflowRegistry.Clear();
PrimeOrderGpuHeuristics.OverflowRegistryWide.Clear();
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
}
[Fact]
public void TryPow2Mod_returns_overflow_for_marked_prime()
{
const ulong prime = 97UL;
try
{
PrimeOrderGpuHeuristics.OverflowRegistry[prime] = 0;
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, prime, out ulong remainder, divisorData);
status.Should().Be(GpuPow2ModStatus.Overflow);
remainder.Should().Be(0UL);
}
finally
{
PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _);
}
}
[Fact]
public void TryPow2Mod_returns_success_for_supported_prime()
{
const ulong prime = 193UL;
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, prime, out ulong remainder, divisorData);
status.Should().Be(GpuPow2ModStatus.Success);
remainder.Should().Be(2UL);
}
[Fact]
public void TryPow2Mod_returns_overflow_when_modulus_exceeds_capability()
{
const ulong prime = 97UL;
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(6, 64);
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
try
{
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, prime, out ulong remainder, divisorData);
status.Should().Be(GpuPow2ModStatus.Overflow);
remainder.Should().Be(0UL);
}
finally
{
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _);
}
}
[Fact]
public void TryPow2Mod_returns_overflow_when_exponent_exceeds_capability()
{
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(64, 3);
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
try
{
const ulong prime = 193UL;
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(16UL, prime, out ulong remainder, divisorData);
status.Should().Be(GpuPow2ModStatus.Overflow);
remainder.Should().Be(0UL);
GpuPow2ModStatus followUp = PrimeOrderGpuHeuristics.TryPow2Mod(4UL, prime, out ulong followUpRemainder, divisorData);
followUp.Should().Be(GpuPow2ModStatus.Success);
followUpRemainder.Should().Be(16UL);
}
finally
{
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
}
}
[Fact]
public void TryPow2ModBatch_returns_overflow_for_marked_prime()
{
const ulong prime = 193UL;
try
{
PrimeOrderGpuHeuristics.OverflowRegistry[prime] = 0;
ulong[] remainders = new ulong[2];
ulong[] exponents = { 1UL, 2UL };
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders, divisorData);
status.Should().Be(GpuPow2ModStatus.Overflow);
remainders[0].Should().Be(0UL);
remainders[1].Should().Be(0UL);
}
finally
{
PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _);
}
}
[Fact]
public void TryPow2ModBatch_returns_success_for_supported_prime()
{
ulong[] remainders = { 123UL, 456UL };
ulong[] exponents = { 1UL, 2UL };
const ulong prime = 257UL;
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders, divisorData);
status.Should().Be(GpuPow2ModStatus.Success);
remainders[0].Should().Be(2UL);
remainders[1].Should().Be(4UL);
}
[Fact]
public void TryPow2ModBatch_returns_overflow_when_modulus_exceeds_capability()
{
const ulong prime = 97UL;
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(6, 64);
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
try
{
ulong[] exponents = { 1UL, 2UL };
ulong[] remainders = { 5UL, 6UL };
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders, divisorData);
status.Should().Be(GpuPow2ModStatus.Overflow);
remainders[0].Should().Be(0UL);
remainders[1].Should().Be(0UL);
}
finally
{
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _);
}
}
[Fact]
public void TryPow2ModBatch_returns_overflow_when_any_exponent_exceeds_capability()
{
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(64, 3);
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
try
{
ulong[] exponents = { 2UL, 16UL };
ulong[] remainders = { 9UL, 11UL };
const ulong prime = 193UL;
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders, divisorData);
status.Should().Be(GpuPow2ModStatus.Overflow);
remainders[0].Should().Be(0UL);
remainders[1].Should().Be(0UL);
}
finally
{
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
}
}
[Fact]
public void TryPow2ModWide_returns_overflow_for_marked_prime()
{
UInt128 prime = ((UInt128)1 << 80) + 7;
try
{
PrimeOrderGpuHeuristics.OverflowRegistryWide[prime] = 0;
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(UInt128.One, prime, out UInt128 remainder);
status.Should().Be(GpuPow2ModStatus.Overflow);
remainder.Should().Be(UInt128.Zero);
}
finally
{
PrimeOrderGpuHeuristics.OverflowRegistryWide.TryRemove(prime, out _);
}
}
[Fact]
public void TryPow2ModWide_returns_success_for_supported_prime()
{
UInt128 prime = ((UInt128)1 << 96) + 11;
UInt128 exponent = ((UInt128)1 << 64) + 3;
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(exponent, prime, out UInt128 remainder);
status.Should().Be(GpuPow2ModStatus.Success);
UInt128 expected = (UInt128)BigInteger.ModPow(2, (BigInteger)exponent, (BigInteger)prime);
remainder.Should().Be(expected);
}
[Theory]
[InlineData("193", "31")]
[InlineData("193", "255")]
[InlineData("79228162514264337593543950347", "36893488147419103251")]
public void Pow2ModKernelCore_matches_cpu_windowed_path(string modulusText, string exponentText)
{
UInt128 modulus = UInt128.Parse(modulusText);
UInt128 exponent = UInt128.Parse(exponentText);
var method = typeof(PrimeOrderGpuHeuristics).GetMethod("Pow2ModKernelCore", BindingFlags.NonPublic | BindingFlags.Static);
method.Should().NotBeNull();
object? gpuResultObject = method!.Invoke(null, new object[] { new GpuUInt128(exponent), new GpuUInt128(modulus) });
GpuUInt128 gpuResult = (GpuUInt128)gpuResultObject!;
UInt128 expected = exponent.Pow2MontgomeryModWindowed(modulus);
((UInt128)gpuResult).Should().Be(expected);
}
[Fact]
public void TryPow2ModWide_returns_overflow_when_modulus_exceeds_capability()
{
UInt128 prime = ((UInt128)1 << 80) + 9;
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(64, 128);
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
try
{
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(UInt128.One, prime, out UInt128 remainder);
status.Should().Be(GpuPow2ModStatus.Overflow);
remainder.Should().Be(UInt128.Zero);
}
finally
{
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
PrimeOrderGpuHeuristics.OverflowRegistryWide.TryRemove(prime, out _);
}
}
[Fact]
public void TryPow2ModWide_returns_overflow_when_exponent_exceeds_capability()
{
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(128, 4);
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
try
{
UInt128 exponent = UInt128.One << 8;
UInt128 prime = ((UInt128)1 << 70) + 3;
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(exponent, prime, out UInt128 remainder);
status.Should().Be(GpuPow2ModStatus.Overflow);
remainder.Should().Be(UInt128.Zero);
}
finally
{
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
}
}
[Fact]
public void TryPow2ModBatchWide_returns_success_for_supported_prime()
{
UInt128 prime = ((UInt128)1 << 90) + 19;
UInt128[] exponents = { UInt128.One, ((UInt128)1 << 64) + 5 };
UInt128[] remainders = { UInt128.Zero, UInt128.Zero };
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders);
status.Should().Be(GpuPow2ModStatus.Success);
for (int i = 0; i < exponents.Length; i++)
{
UInt128 expected = (UInt128)BigInteger.ModPow(2, (BigInteger)exponents[i], (BigInteger)prime);
remainders[i].Should().Be(expected);
}
}
[Fact]
public void TryPow2ModBatchWide_returns_overflow_when_modulus_exceeds_capability()
{
UInt128 prime = ((UInt128)1 << 88) + 13;
UInt128[] exponents = { UInt128.One, (UInt128)5 };
UInt128[] remainders = { UInt128.Zero, UInt128.Zero };
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(64, 128);
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
try
{
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders);
status.Should().Be(GpuPow2ModStatus.Overflow);
remainders[0].Should().Be(UInt128.Zero);
remainders[1].Should().Be(UInt128.Zero);
}
finally
{
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
PrimeOrderGpuHeuristics.OverflowRegistryWide.TryRemove(prime, out _);
}
}
[Fact]
public void TryPow2ModBatchWide_returns_overflow_when_any_exponent_exceeds_capability()
{
UInt128 prime = ((UInt128)1 << 72) + 23;
UInt128[] exponents = { UInt128.One << 5, UInt128.One << 9 };
UInt128[] remainders = { UInt128.Zero, UInt128.Zero };
var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(128, 6);
PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability);
try
{
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders);
status.Should().Be(GpuPow2ModStatus.Overflow);
remainders[0].Should().Be(UInt128.Zero);
remainders[1].Should().Be(UInt128.Zero);
}
finally
{
PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting();
}
}
// This will never happen in production code because the caller ensures the remainder span is large enough.
// [Fact]
// public void TryPow2ModBatch_throws_when_remainder_span_is_too_small()
// {
// ulong[] remainders = new ulong[1];
// ulong[] exponents = { 1UL, 2UL };
// Action act = () =>
// {
// const ulong prime = 5UL;
// MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
// PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders, divisorData);
// };
// act.Should().Throw<ArgumentException>();
// }
[Fact]
public void Calculate_falls_back_to_cpu_when_gpu_overflow_is_marked()
{
const ulong prime = 7UL;
try
{
PrimeOrderGpuHeuristics.OverflowRegistry[prime] = 0;
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(prime);
PrimeOrderCalculator.PrimeOrderResult result = PrimeOrderCalculator.Calculate(prime, null, divisorData, PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault);
result.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found);
result.Order.Should().Be(3UL);
}
finally
{
PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _);
}
}
}
@@ -1,4 +1,6 @@
using System.Threading;
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
using Xunit;
@@ -5,6 +5,7 @@ namespace PerfectNumbers.Core.Tests;
public class InputParserTests
{
[Fact(Skip = "TODO: implement test")]
[Trait("Category", "Fast")]
public void Placeholder()
{
}
@@ -5,6 +5,7 @@ namespace PerfectNumbers.Core.Tests;
public class KRangeFinderTests
{
[Fact(Skip = "TODO: implement test")]
[Trait("Category", "Fast")]
public void Placeholder()
{
}
@@ -1,4 +1,7 @@
using System.Collections.Generic;
using System.IO;
using FluentAssertions;
using PerfectNumbers.Core;
using Xunit;
namespace PerfectNumbers.Core.Tests;
@@ -38,8 +41,7 @@ public class MersenneDivisorCyclesGenerateGpuTests
foreach (var pair in pairs)
{
expected.Should().Contain(pair.divisor);
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(pair.divisor);
pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLength(pair.divisor, divisorData));
pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLength(pair.divisor));
}
}
finally
@@ -50,58 +52,4 @@ public class MersenneDivisorCyclesGenerateGpuTests
}
}
}
[Fact]
[Trait("Category", "Fast")]
public void Generate_produces_unique_cycles_for_expected_divisors()
{
string path = Path.GetTempFileName();
try
{
MersenneDivisorCycles.Generate(path, maxDivisor: 50UL, threads: 4);
var pairs = new List<(ulong divisor, ulong cycle)>();
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
using var reader = new BinaryReader(stream);
while (stream.Position < stream.Length)
{
ulong divisor = reader.ReadUInt64();
ulong cycle = reader.ReadUInt64();
pairs.Add((divisor, cycle));
}
var expected = new List<ulong>();
for (ulong divisor = 3; divisor <= 50; divisor++)
{
if ((divisor & 1UL) == 0UL || divisor % 3UL == 0UL || divisor % 5UL == 0UL || divisor % 7UL == 0UL || divisor % 11UL == 0UL)
{
continue;
}
expected.Add(divisor);
}
pairs.Should().HaveCount(expected.Count);
HashSet<ulong> uniqueDivisors = new();
foreach (var pair in pairs)
{
uniqueDivisors.Add(pair.divisor).Should().BeTrue();
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(pair.divisor);
pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLength(pair.divisor, divisorData));
}
uniqueDivisors.Should().BeEquivalentTo(expected);
var (_, completeCount) = MersenneDivisorCycles.FindLast(path);
completeCount.Should().Be(expected.Count);
}
finally
{
if (File.Exists(path))
{
File.Delete(path);
}
}
}
}
@@ -18,8 +18,7 @@ public class MersenneDivisorCyclesTests
[MemberData(nameof(Divisors))]
public void CalculateCycleLength_returns_expected_value(ulong divisor, ulong expected)
{
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(divisor);
MersenneDivisorCycles.CalculateCycleLength(divisor, divisorData).Should().Be(expected);
MersenneDivisorCycles.CalculateCycleLength(divisor).Should().Be(expected);
MersenneDivisorCycles.CalculateCycleLengthGpu(divisor).Should().Be(expected);
}
}
@@ -1,93 +0,0 @@
using System;
using System.Collections.Generic;
using FluentAssertions;
using PerfectNumbers.Core;
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class MersenneNumberDivisorByDivisorTesterTests
{
[Fact]
[Trait("Category", "Fast")]
public void Run_marks_candidates_divisible_by_seven_or_eleven_as_composite()
{
var candidates = new List<ulong> { 5UL, 7UL, 11UL, 13UL };
var tester = new FakeTester();
var results = new List<(ulong Prime, bool Searched, bool Detailed, bool Passed)>();
int compositeMarks = 0;
int clearedMarks = 0;
MersenneNumberDivisorByDivisorTester.Run(
candidates,
tester,
previousResults: null,
startPrime: 0UL,
markComposite: () => compositeMarks++,
clearComposite: () => clearedMarks++,
printResult: (prime, searched, detailed, passed) => results.Add((prime, searched, detailed, passed)),
threadCount: 1);
tester.ConfiguredMaxPrime.Should().Be(13UL);
compositeMarks.Should().Be(2);
clearedMarks.Should().Be(2);
results.Should().Equal(
new[]
{
(5UL, true, true, true),
(7UL, true, true, false),
(11UL, true, true, false),
(13UL, true, true, true),
});
}
private sealed class FakeTester : IMersenneNumberDivisorByDivisorTester
{
public ulong ConfiguredMaxPrime { get; private set; }
public int BatchSize { get; set; }
public ulong DivisorLimit => ConfiguredMaxPrime + 100UL;
public void ConfigureFromMaxPrime(ulong maxPrime)
{
ConfiguredMaxPrime = maxPrime;
}
public ulong GetAllowedMaxDivisor(ulong prime)
{
return DivisorLimit;
}
public bool IsPrime(ulong prime, out bool divisorsExhausted)
{
divisorsExhausted = true;
return prime % 7UL != 0UL && prime % 11UL != 0UL;
}
public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession()
{
return new DummySession();
}
public void PrepareCandidates(ReadOnlySpan<ulong> primes, Span<ulong> allowedMaxValues)
{
for (int i = 0; i < primes.Length; i++)
{
allowedMaxValues[i] = 100UL;
}
}
private sealed class DummySession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
{
public void CheckDivisor(ulong divisor, in MontgomeryDivisorData divisorData, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits)
{
throw new NotSupportedException();
}
public void Dispose()
{
}
}
}
}
@@ -1,84 +0,0 @@
using System;
using FluentAssertions;
using PerfectNumbers.Core.Cpu;
using Xunit;
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);
tester.IsPrime(5UL, out bool divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
tester.IsPrime(7UL, out divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
tester.IsPrime(11UL, out divisorsExhausted).Should().BeFalse();
divisorsExhausted.Should().BeTrue();
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_checks_divisors_across_primes()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(13UL);
using var session = tester.CreateDivisorSession();
ulong[] primes = { 5UL, 7UL, 11UL, 13UL };
byte[] hits = new byte[primes.Length];
ulong cycle23 = MersenneDivisorCycles.CalculateCycleLength(23UL, MontgomeryDivisorData.FromModulus(23UL));
session.CheckDivisor(23UL, MontgomeryDivisorData.FromModulus(23UL), cycle23, primes, hits);
hits.Should().ContainInOrder(new byte[] { 0, 0, 1, 0 });
Array.Fill(hits, (byte)0);
ulong cycle31 = MersenneDivisorCycles.CalculateCycleLength(31UL, MontgomeryDivisorData.FromModulus(31UL));
session.CheckDivisor(31UL, MontgomeryDivisorData.FromModulus(31UL), cycle31, primes, hits);
hits.Should().ContainInOrder(new byte[] { 1, 0, 0, 0 });
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_seven_as_composite()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(43UL);
using var session = tester.CreateDivisorSession();
ulong[] exponents = { 6UL, 7UL, 9UL, 10UL, 12UL };
byte[] hits = new byte[exponents.Length];
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(7UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(7UL, divisorData);
session.CheckDivisor(7UL, divisorData, cycle, exponents, hits);
hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 });
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_eleven_as_composite()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester();
tester.ConfigureFromMaxPrime(61UL);
using var session = tester.CreateDivisorSession();
ulong[] exponents = { 10UL, 11UL, 20UL, 21UL, 30UL };
byte[] hits = new byte[exponents.Length];
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(11UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(11UL, divisorData);
session.CheckDivisor(11UL, divisorData, cycle, exponents, hits);
hits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 });
}
}
@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Cpu;
using PerfectNumbers.Core.Gpu;
using System;
using System.Numerics;
using System.Reflection;
using Xunit;
namespace PerfectNumbers.Core.Tests;
@@ -12,18 +10,15 @@ namespace PerfectNumbers.Core.Tests;
public class MersenneNumberDivisorGpuTesterTests
{
[Theory]
[InlineData(33UL, 7UL, true)]
[InlineData(35UL, 31UL, true)]
[InlineData(37UL, 223UL, true)]
[InlineData(33UL, 13UL, false)]
[InlineData(35UL, 73UL, false)]
[InlineData(37UL, 227UL, false)]
[InlineData(3UL, 7UL, true)]
[InlineData(11UL, 23UL, true)]
[InlineData(7UL, 35UL, false)]
[InlineData(13UL, 23UL, false)]
[Trait("Category", "Fast")]
public void IsDivisible_returns_expected(ulong exponent, ulong divisor, bool expected)
{
var tester = new MersenneNumberDivisorGpuTester();
ReadOnlyGpuUInt128 divisorValue = new ReadOnlyGpuUInt128(divisor);
tester.IsDivisible(exponent, in divisorValue).Should().Be(expected);
tester.IsDivisible(exponent, divisor).Should().Be(expected);
}
[Fact]
@@ -32,10 +27,8 @@ public class MersenneNumberDivisorGpuTesterTests
{
var tester = new MersenneNumberDivisorGpuTester();
UInt128 divisor = (UInt128.One << 65) - UInt128.One;
ReadOnlyGpuUInt128 divisorValue = new ReadOnlyGpuUInt128(divisor);
tester.IsDivisible(65UL, in divisorValue).Should().BeTrue();
divisorValue = new ReadOnlyGpuUInt128(divisor + 2); // Reusing divisorValue for the shifted candidate.
tester.IsDivisible(65UL, in divisorValue).Should().BeFalse();
tester.IsDivisible(65UL, divisor).Should().BeTrue();
tester.IsDivisible(65UL, divisor + 2).Should().BeFalse();
}
[Fact]
@@ -47,7 +40,7 @@ public class MersenneNumberDivisorGpuTesterTests
.GetField("_divisorCandidates", BindingFlags.NonPublic | BindingFlags.Static)!
.SetValue(null, Array.Empty<(ulong, uint)>());
tester.IsPrime(31UL, UInt128.Zero, 0UL, out bool divisorsExhausted).Should().BeTrue();
tester.IsPrime(11UL, UInt128.Zero, 0UL, out bool divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeFalse();
}
@@ -56,7 +49,7 @@ public class MersenneNumberDivisorGpuTesterTests
public void IsPrime_sets_divisorsExhausted_true_when_divisible_by_specified_divisor()
{
var tester = new MersenneNumberDivisorGpuTester();
tester.IsPrime(35UL, 31UL, 0UL, out bool divisorsExhausted).Should().BeFalse();
tester.IsPrime(3UL, 7UL, 0UL, out bool divisorsExhausted).Should().BeFalse();
divisorsExhausted.Should().BeTrue();
}
@@ -65,7 +58,7 @@ public class MersenneNumberDivisorGpuTesterTests
public void IsPrime_accepts_large_search_limits()
{
var tester = new MersenneNumberDivisorGpuTester();
tester.IsPrime(35UL, 31UL, ulong.MaxValue, out bool exhausted).Should().BeFalse();
tester.IsPrime(3UL, 7UL, ulong.MaxValue, out bool exhausted).Should().BeFalse();
exhausted.Should().BeTrue();
}
@@ -74,15 +67,15 @@ public class MersenneNumberDivisorGpuTesterTests
public void ByDivisor_tester_tracks_divisors_across_primes()
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
tester.ConfigureFromMaxPrime(43UL);
tester.ConfigureFromMaxPrime(11UL);
tester.IsPrime(31UL, out bool exhausted).Should().BeTrue();
tester.IsPrime(5UL, out bool exhausted).Should().BeTrue();
exhausted.Should().BeTrue();
tester.IsPrime(37UL, out exhausted).Should().BeFalse();
tester.IsPrime(7UL, out exhausted).Should().BeTrue();
exhausted.Should().BeTrue();
tester.IsPrime(43UL, out exhausted).Should().BeFalse();
tester.IsPrime(11UL, out exhausted).Should().BeFalse();
exhausted.Should().BeTrue();
}
@@ -91,7 +84,7 @@ public class MersenneNumberDivisorGpuTesterTests
public void ByDivisor_tester_requires_configuration()
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
Action act = () => tester.IsPrime(31UL, out _);
Action act = () => tester.IsPrime(5UL, out _);
act.Should().Throw<InvalidOperationException>();
}
@@ -100,42 +93,13 @@ public class MersenneNumberDivisorGpuTesterTests
public void ByDivisor_tester_respects_filter_based_limit()
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
tester.ConfigureFromMaxPrime(41UL);
tester.ConfigureFromMaxPrime(5UL);
tester.IsPrime(31UL, out _).Should().BeTrue();
tester.IsPrime(37UL, out bool exhausted).Should().BeFalse();
tester.IsPrime(5UL, out _).Should().BeTrue();
tester.IsPrime(7UL, out bool exhausted).Should().BeTrue();
exhausted.Should().BeTrue();
tester.IsPrime(41UL, out bool divisorsExhausted).Should().BeFalse();
divisorsExhausted.Should().BeTrue();
}
[Theory]
[InlineData(71UL, false)]
[InlineData(89UL, true)]
[InlineData(127UL, true)]
[Trait("Category", "Fast")]
public void ByDivisor_tester_matches_incremental_expectations(ulong exponent, bool expectedPrime)
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
tester.ConfigureFromMaxPrime(exponent);
tester.IsPrime(exponent, out bool exhausted).Should().Be(expectedPrime);
exhausted.Should().BeTrue();
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_gpu_tester_uses_cycle_remainders_per_divisor()
{
var tester = new MersenneNumberDivisorByDivisorGpuTester
{
GpuBatchSize = 8,
};
tester.ConfigureFromMaxPrime(43UL);
tester.IsPrime(41UL, out bool divisorsExhausted).Should().BeFalse();
tester.IsPrime(11UL, out bool divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
}
@@ -144,168 +108,18 @@ public class MersenneNumberDivisorGpuTesterTests
public void ByDivisor_session_checks_divisors_across_primes()
{
var tester = new MersenneNumberDivisorByDivisorGpuTester();
tester.ConfigureFromMaxPrime(43UL);
tester.ConfigureFromMaxPrime(13UL);
using var session = tester.CreateDivisorSession();
ulong[] primes = { 31UL, 37UL, 41UL, 43UL };
ulong[] primes = { 5UL, 7UL, 11UL, 13UL };
byte[] hits = new byte[primes.Length];
ulong cycle223 = MersenneDivisorCycles.CalculateCycleLength(223UL, MontgomeryDivisorData.FromModulus(223UL));
session.CheckDivisor(223UL, MontgomeryDivisorData.FromModulus(223UL), cycle223, primes, hits);
hits.Should().ContainInOrder(new byte[] { 0, 1, 0, 0 });
session.CheckDivisor(23UL, primes, hits);
hits.Should().ContainInOrder(new byte[] { 0, 0, 1, 0 });
Array.Fill(hits, (byte)0);
ulong cycle13367 = MersenneDivisorCycles.CalculateCycleLength(13367UL, MontgomeryDivisorData.FromModulus(13367UL));
session.CheckDivisor(13367UL, MontgomeryDivisorData.FromModulus(13367UL), cycle13367, primes, hits);
hits.Should().ContainInOrder(new byte[] { 0, 0, 1, 0 });
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_gpu_session_reuses_cycle_remainders_across_batches()
{
var tester = new MersenneNumberDivisorByDivisorGpuTester
{
GpuBatchSize = 2,
};
tester.ConfigureFromMaxPrime(47UL);
using var session = tester.CreateDivisorSession();
ulong[] primes = { 31UL, 37UL, 41UL, 43UL, 47UL };
byte[] hits = new byte[primes.Length];
ulong cycle223 = MersenneDivisorCycles.CalculateCycleLength(223UL, MontgomeryDivisorData.FromModulus(223UL));
session.CheckDivisor(223UL, MontgomeryDivisorData.FromModulus(223UL), cycle223, primes, hits);
hits.Should().ContainInOrder(new byte[] { 0, 1, 0, 0, 0 });
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_seven_as_composite()
{
var tester = new MersenneNumberDivisorByDivisorGpuTester
{
GpuBatchSize = 5,
};
tester.ConfigureFromMaxPrime(43UL);
using var session = tester.CreateDivisorSession();
ulong[] exponents = { 6UL, 7UL, 9UL, 10UL, 12UL };
byte[] hits = new byte[exponents.Length];
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(7UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(7UL, divisorData);
session.CheckDivisor(7UL, divisorData, cycle, exponents, hits);
var hostBufferField = typeof(MersenneNumberDivisorByDivisorGpuTester.DivisorScanSession)
.GetField("_hostBuffer", BindingFlags.NonPublic | BindingFlags.Instance)!;
ulong[] hostBuffer = (ulong[])hostBufferField.GetValue(session)!;
Span<ulong> residues = hostBuffer.AsSpan(0, exponents.Length);
byte[] computedHits = new byte[exponents.Length];
for (int i = 0; i < residues.Length; i++)
{
computedHits[i] = residues[i] == divisorData.MontgomeryOne ? (byte)1 : (byte)0;
}
computedHits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 });
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_session_marks_mersenne_numbers_divisible_by_eleven_as_composite()
{
var tester = new MersenneNumberDivisorByDivisorGpuTester
{
GpuBatchSize = 5,
};
tester.ConfigureFromMaxPrime(61UL);
using var session = tester.CreateDivisorSession();
ulong[] exponents = { 10UL, 11UL, 20UL, 21UL, 30UL };
byte[] hits = new byte[exponents.Length];
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(11UL);
ulong cycle = MersenneDivisorCycles.CalculateCycleLength(11UL, divisorData);
session.CheckDivisor(11UL, divisorData, cycle, exponents, hits);
var hostBufferField = typeof(MersenneNumberDivisorByDivisorGpuTester.DivisorScanSession)
.GetField("_hostBuffer", BindingFlags.NonPublic | BindingFlags.Instance)!;
ulong[] hostBuffer = (ulong[])hostBufferField.GetValue(session)!;
Span<ulong> residues = hostBuffer.AsSpan(0, exponents.Length);
byte[] computedHits = new byte[exponents.Length];
for (int i = 0; i < residues.Length; i++)
{
computedHits[i] = residues[i] == divisorData.MontgomeryOne ? (byte)1 : (byte)0;
}
computedHits.Should().Equal(new byte[] { 1, 0, 1, 0, 1 });
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_gpu_tester_skips_divisors_excluded_by_small_cycle_generation()
{
var cycles = MersenneDivisorCycles.Shared;
var tableField = typeof(MersenneDivisorCycles).GetField("_table", BindingFlags.NonPublic | BindingFlags.Instance)!;
var smallCyclesField = typeof(MersenneDivisorCycles).GetField("_smallCycles", BindingFlags.NonPublic | BindingFlags.Instance)!;
var originalTable = (List<(ulong Divisor, ulong Cycle)>)tableField.GetValue(cycles)!;
var originalSmall = (ulong[]?)smallCyclesField.GetValue(cycles);
try
{
var patchedTable = new List<(ulong Divisor, ulong Cycle)>();
ulong[] patchedSmall = new ulong[PerfectNumberConstants.MaxQForDivisorCycles + 1];
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(191UL);
patchedSmall[191] = MersenneDivisorCycles.CalculateCycleLength(191UL, divisorData);
tableField.SetValue(cycles, patchedTable);
smallCyclesField.SetValue(cycles, patchedSmall);
DivisorCycleCache.Shared.RefreshSnapshot();
var tester = new MersenneNumberDivisorByDivisorGpuTester();
tester.ConfigureFromMaxPrime(19UL);
typeof(MersenneNumberDivisorByDivisorGpuTester)
.GetField("_divisorLimit", BindingFlags.NonPublic | BindingFlags.Instance)!
.SetValue(tester, 200UL);
tester.IsPrime(19UL, out bool divisorsExhausted).Should().BeTrue();
divisorsExhausted.Should().BeTrue();
}
finally
{
tableField.SetValue(cycles, originalTable);
smallCyclesField.SetValue(cycles, originalSmall);
DivisorCycleCache.Shared.RefreshSnapshot();
GpuContextPool.DisposeAll();
}
}
[Fact]
[Trait("Category", "Fast")]
public void ByDivisor_cpu_session_reuses_cycle_remainders_across_primes()
{
var tester = new MersenneNumberDivisorByDivisorCpuTester
{
BatchSize = 2,
};
tester.ConfigureFromMaxPrime(47UL);
using var session = tester.CreateDivisorSession();
ulong[] primes = { 31UL, 37UL, 41UL, 43UL, 47UL };
byte[] hits = new byte[primes.Length];
ulong cycle223 = MersenneDivisorCycles.CalculateCycleLength(223UL, MontgomeryDivisorData.FromModulus(223UL));
session.CheckDivisor(223UL, MontgomeryDivisorData.FromModulus(223UL), cycle223, primes, hits);
hits.Should().ContainInOrder(new byte[] { 0, 1, 0, 0, 0 });
session.CheckDivisor(31UL, primes, hits);
hits.Should().ContainInOrder(new byte[] { 1, 0, 0, 0 });
}
}
@@ -9,7 +9,7 @@ public class MersenneNumberIncrementalGpuTesterTests
{
private static bool LastDigitIsSeven(ulong exponent) => (exponent & 3UL) == 3UL;
[Theory(Skip = "Disabled until the GPU incremental cycle heuristics are ported.")]
[Theory]
[InlineData(GpuKernelType.Pow2Mod, false)]
[InlineData(GpuKernelType.Incremental, false)]
[InlineData(GpuKernelType.Pow2Mod, true)]
@@ -20,8 +20,7 @@ public class MersenneNumberIncrementalGpuTesterTests
if (type == GpuKernelType.Pow2Mod)
{
// Mirrored by the divisor GPU coverage to ensure the failing cases stay validated.
RunCase(tester, 37UL, 1UL, expectedPrime: false);
RunCase(tester, 11UL, 1UL, expectedPrime: false);
RunCase(tester, 71UL, 341_860UL, expectedPrime: false);
}
@@ -1,5 +1,9 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using FluentAssertions;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
using Xunit;
using UInt128 = System.UInt128;
@@ -50,8 +54,8 @@ public class MersenneNumberOrderGpuTesterTests
var smallCyclesField = typeof(MersenneDivisorCycles).GetField("_smallCycles", BindingFlags.NonPublic | BindingFlags.Instance)!;
var originalTable = ((List<(ulong Divisor, ulong Cycle)>)tableField.GetValue(cycles)!).Select(x => x).ToList();
var originalSmall = (ulong[]?)smallCyclesField.GetValue(cycles);
ulong[]? backupSmall = originalSmall is null ? null : (ulong[])originalSmall.Clone();
var originalSmall = (uint[]?)smallCyclesField.GetValue(cycles);
uint[]? backupSmall = originalSmall is null ? null : (uint[])originalSmall.Clone();
cycles.LoadFrom(tempPath);
GpuContextPool.DisposeAll();
@@ -1,200 +0,0 @@
using System.Collections.Generic;
using System.Numerics;
using FluentAssertions;
using Xunit;
namespace PerfectNumbers.Core.Tests;
public class PrimeOrderCalculatorTests
{
public static IEnumerable<object[]> SmallPrimes()
{
yield return new object[] { 3UL };
yield return new object[] { 5UL };
yield return new object[] { 7UL };
yield return new object[] { 11UL };
yield return new object[] { 13UL };
yield return new object[] { 17UL };
yield return new object[] { 19UL };
yield return new object[] { 31UL };
yield return new object[] { 61UL };
yield return new object[] { 89UL };
yield return new object[] { 127UL };
yield return new object[] { 521UL };
}
public static IEnumerable<object[]> PrimesWithPrevious()
{
yield return new object[] { 5UL, 3UL };
yield return new object[] { 7UL, 5UL };
yield return new object[] { 11UL, 7UL };
yield return new object[] { 13UL, 11UL };
yield return new object[] { 17UL, 13UL };
yield return new object[] { 19UL, 17UL };
yield return new object[] { 31UL, 29UL };
yield return new object[] { 61UL, 59UL };
yield return new object[] { 89UL, 83UL };
yield return new object[] { 127UL, 113UL };
yield return new object[] { 521UL, 509UL };
}
[Theory]
[Trait("Category", "Fast")]
[MemberData(nameof(SmallPrimes))]
public void Calculate_heuristic_mode_matches_naive_cycle_length(ulong prime)
{
ulong expected = ComputeOrderByDoubling(prime);
PrimeOrderCalculator.PrimeOrderResult result = PrimeOrderCalculator.Calculate(
prime,
previousOrder: null,
MontgomeryDivisorData.FromModulus(prime),
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault);
result.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found);
result.Order.Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[MemberData(nameof(PrimesWithPrevious))]
public void Calculate_heuristic_mode_matches_strict_mode_even_with_previous_order(ulong prime, ulong previousPrime)
{
ulong previousOrder = ComputeOrderByDoubling(previousPrime);
PrimeOrderCalculator.PrimeOrderResult heuristic = PrimeOrderCalculator.Calculate(
prime,
previousOrder,
MontgomeryDivisorData.FromModulus(prime),
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault);
PrimeOrderCalculator.PrimeOrderResult strict = PrimeOrderCalculator.Calculate(
prime,
previousOrder: null,
MontgomeryDivisorData.FromModulus(prime),
PrimeOrderCalculator.PrimeOrderSearchConfig.StrictDefault);
heuristic.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found);
strict.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found);
heuristic.Order.Should().Be(strict.Order);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(2UL, 1UL)]
[InlineData(3UL, 2UL)]
public void Calculate_handles_trivial_primes(ulong prime, ulong expectedOrder)
{
PrimeOrderCalculator.PrimeOrderResult result = PrimeOrderCalculator.Calculate(
prime,
previousOrder: null,
MontgomeryDivisorData.FromModulus(prime),
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault);
result.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found);
result.Order.Should().Be(expectedOrder);
}
[Fact]
[Trait("Category", "Fast")]
public void Calculate_heuristic_mode_returns_unresolved_when_phi_cannot_be_factored()
{
var config = new PrimeOrderCalculator.PrimeOrderSearchConfig(
smallFactorLimit: 1,
pollardRhoMilliseconds: 0,
maxPowChecks: 8,
mode: PrimeOrderCalculator.PrimeOrderMode.Heuristic);
PrimeOrderCalculator.PrimeOrderResult result = PrimeOrderCalculator.Calculate(
prime: 13UL,
previousOrder: null,
MontgomeryDivisorData.FromModulus(13UL),
config);
result.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.HeuristicUnresolved);
result.Order.Should().Be(ComputeOrderByDoubling(13UL));
}
[Fact]
[Trait("Category", "Fast")]
public void Calculate_heuristic_mode_attempts_candidates_before_falling_back_to_strict_mode()
{
var heuristicConfig = new PrimeOrderCalculator.PrimeOrderSearchConfig(
smallFactorLimit: 2,
pollardRhoMilliseconds: 0,
maxPowChecks: 1,
mode: PrimeOrderCalculator.PrimeOrderMode.Heuristic);
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(239UL);
PrimeOrderCalculator.PrimeOrderResult heuristic = PrimeOrderCalculator.Calculate(
prime: 239UL,
previousOrder: null,
divisorData,
heuristicConfig);
heuristic.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.HeuristicUnresolved);
heuristic.Order.Should().Be(ComputeOrderByDoubling(239UL));
var strictConfig = new PrimeOrderCalculator.PrimeOrderSearchConfig(
smallFactorLimit: 2,
pollardRhoMilliseconds: 0,
maxPowChecks: 1,
mode: PrimeOrderCalculator.PrimeOrderMode.Strict);
PrimeOrderCalculator.PrimeOrderResult strict = PrimeOrderCalculator.Calculate(
prime: 239UL,
previousOrder: null,
divisorData,
strictConfig);
strict.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found);
strict.Order.Should().Be(heuristic.Order);
}
[Fact]
[Trait("Category", "Fast")]
public void Calculate_handles_128_bit_prime()
{
UInt128 prime = UInt128.Parse("18446744073709641691");
PrimeOrderCalculator.PrimeOrderResultWide result = PrimeOrderCalculator.Calculate(
prime,
previousOrder: null,
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault);
result.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found);
result.Order.Should().Be((UInt128)1229782938247309446UL);
}
[Fact]
[Trait("Category", "Fast")]
public void GetCycle_uses_wide_prime_heuristic()
{
UInt128 prime = UInt128.Parse("18446744073709641691");
MersenneDivisorCycles.GetCycle(prime).Should().Be((UInt128)1229782938247309446UL);
}
private static ulong ComputeOrderByDoubling(ulong prime)
{
if (prime == 2UL)
{
return 1UL;
}
if (prime == 3UL)
{
return 2UL;
}
ulong order = 1UL;
ulong value = 2UL % prime;
while (value != 1UL)
{
value = (value * 2UL) % prime;
order++;
}
return order;
}
}
@@ -1,4 +1,5 @@
using FluentAssertions;
using PerfectNumbers.Core;
using Xunit;
namespace PerfectNumbers.Core.Tests;
@@ -5,6 +5,7 @@ namespace PerfectNumbers.Core.Tests;
public class RleBlacklistTests
{
[Fact(Skip = "TODO: implement test")]
[Trait("Category", "Fast")]
public void Placeholder()
{
}
@@ -5,6 +5,7 @@ namespace PerfectNumbers.Core.Tests;
public class TextFileWriterTests
{
[Fact(Skip = "TODO: implement test")]
[Trait("Category", "Fast")]
public void Placeholder()
{
}
@@ -13,7 +13,7 @@ public class UIntExtensionsTests
[InlineData(uint.MaxValue)]
public void Mod3_matches_operator(uint value)
{
UIntExtensions.Mod3(value).Should().Be(value % 3U);
value.Mod3().Should().Be(value % 3U);
}
[Theory]
@@ -27,6 +27,28 @@ public class UIntExtensionsTests
value.Mod5().Should().Be(value % 5U);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0U)]
[InlineData(1U)]
[InlineData(1234567890U)]
[InlineData(uint.MaxValue)]
public void Mod6_matches_operator(uint value)
{
value.Mod6().Should().Be(value % 6U);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0U)]
[InlineData(1U)]
[InlineData(1234567890U)]
[InlineData(uint.MaxValue)]
public void Mod7_matches_operator(uint value)
{
value.Mod7().Should().Be(value % 7U);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0U)]
@@ -37,5 +59,38 @@ public class UIntExtensionsTests
{
value.Mod8().Should().Be(value % 8U);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0U)]
[InlineData(1U)]
[InlineData(1234567890U)]
[InlineData(uint.MaxValue)]
public void Mod10_matches_operator(uint value)
{
value.Mod10().Should().Be(value % 10U);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0U)]
[InlineData(1U)]
[InlineData(1234567890U)]
[InlineData(uint.MaxValue)]
public void Mod11_matches_operator(uint value)
{
value.Mod11().Should().Be(value % 11U);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0U)]
[InlineData(1U)]
[InlineData(1234567890U)]
[InlineData(uint.MaxValue)]
public void Mod128_matches_operator(uint value)
{
value.Mod128().Should().Be(value % 128U);
}
}
@@ -1,44 +0,0 @@
using System.Numerics;
using FluentAssertions;
using PerfectNumbers.Core;
using Xunit;
namespace PerfectNumbers.Core.Tests;
[Trait("Category", "Fast")]
public class ULongExtensionsMultiplyShiftTests
{
[Theory]
[InlineData(0UL, 0UL, 1)]
[InlineData(1UL, 3UL, 1)]
[InlineData(ulong.MaxValue, 3UL, 3)]
[InlineData(0xFFFF_FFFF_FFFF_FFFFUL, 5UL, 4)]
[InlineData(0x8000_0000_0000_0000UL, 7UL, 5)]
public void MultiplyShiftRight_matches_big_integer(ulong value, ulong multiplier, int shift)
{
BigInteger expected = ((BigInteger)value * multiplier) >> shift;
ulong result = ULongExtensions.MultiplyShiftRight(value, multiplier, shift);
result.Should().Be((ulong)expected);
}
[Theory]
[InlineData(0UL, 0UL, 1)]
[InlineData(1UL, 3UL, 1)]
[InlineData(ulong.MaxValue, 3UL, 3)]
[InlineData(0xFFFF_FFFF_FFFF_FFFFUL, 5UL, 4)]
[InlineData(0x8000_0000_0000_0000UL, 7UL, 5)]
[InlineData(0xDEAD_BEEF_DEAD_BEEFUL, 11UL, 63)]
public void MultiplyShiftRightShiftFirst_matches_big_integer(ulong value, ulong multiplier, int shift)
{
BigInteger expected = ((BigInteger)value * multiplier) >> shift;
ulong result = ULongExtensions.MultiplyShiftRightShiftFirst(value, multiplier, shift);
result.Should().Be((ulong)expected);
}
}
@@ -21,6 +21,66 @@ public class ULongExtensionsTests
ULongExtensions.Mod10(value).Should().Be(expected);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0UL)]
[InlineData(ulong.MaxValue)]
[InlineData(1234567890123456789UL)]
public void Mod3_matches_operator(ulong value)
{
value.Mod3().Should().Be(value % 3UL);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0UL)]
[InlineData(ulong.MaxValue)]
[InlineData(1234567890123456789UL)]
public void Mod5_matches_operator(ulong value)
{
value.Mod5().Should().Be(value % 5UL);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0UL)]
[InlineData(ulong.MaxValue)]
[InlineData(1234567890123456789UL)]
public void Mod6_matches_operator(ulong value)
{
value.Mod6().Should().Be(value % 6UL);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0UL)]
[InlineData(ulong.MaxValue)]
[InlineData(1234567890123456789UL)]
public void Mod7_matches_operator(ulong value)
{
value.Mod7().Should().Be(value % 7UL);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0UL)]
[InlineData(ulong.MaxValue)]
[InlineData(1234567890123456789UL)]
public void Mod8_matches_operator(ulong value)
{
value.Mod8().Should().Be(value % 8UL);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0UL)]
[InlineData(ulong.MaxValue)]
[InlineData(1234567890123456789UL)]
public void Mod11_matches_operator(ulong value)
{
value.Mod11().Should().Be(value % 11UL);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0UL)]
@@ -51,6 +111,22 @@ public class ULongExtensionsTests
step3.Should().Be(expected3);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(0UL)]
[InlineData(1UL)]
[InlineData(1234567890123456789UL)]
[InlineData(ulong.MaxValue)]
public void Mod11_7_5_3_matches_operator(ulong value)
{
value.Mod11_7_5_3(out ulong mod11, out ulong mod7, out ulong mod5, out ulong mod3);
mod11.Should().Be(value % 11UL);
mod7.Should().Be(value % 7UL);
mod5.Should().Be(value % 5UL);
mod3.Should().Be(value % 3UL);
}
[Theory]
[Trait("Category", "Fast")]
[InlineData(2UL, true)]
-4
View File
@@ -8,8 +8,6 @@ public sealed class AlphaCache
public AlphaValues Get(EInteger p, int k)
{
// TODO: Replace this dictionary lookup with a lock-free cache that reuses pooled AlphaValues so alpha requests avoid
// redoing the expensive PowerCache.Get computations across threads.
if (!_cache.TryGetValue((p, k), out var values))
{
ERational alphaPValue = AlphaCalculations.ComputeAlphaP(p, k);
@@ -23,8 +21,6 @@ public sealed class AlphaCache
public void Clear()
{
// TODO: Return pooled AlphaValues instances to a shared cache before clearing so repeated warm-ups do not thrash the
// allocator when alpha tables refresh during large divisor scans.
_cache.Clear();
}
-9
View File
@@ -8,8 +8,6 @@ public static class AlphaCalculations
{
int exponent = checked(4 * k + 1);
EInteger y = PowerCache.Get(p, exponent);
// TODO: Reuse pooled numerator/denominator buffers here so the alphaP computation stops allocating
// intermediate EInteger instances every invocation.
return ERational.Create(y * p - 1, y * (p - 1));
}
@@ -17,9 +15,6 @@ public static class AlphaCalculations
{
EInteger num = PowerCache.Get(q, a2 + 1) - EInteger.One;
EInteger den = PowerCache.Get(q, a2) * (q - EInteger.One);
// TODO: Reuse pooled ERational builders here so repeated factor computations pull precomputed
// numerator/denominator tuples instead of re-executing PowerCache chains each time, matching the
// fastest alpha benchmarks.
return ERational.Create(num, den);
}
@@ -27,11 +22,7 @@ public static class AlphaCalculations
{
ERational result = ERational.One;
for (int i = 0; i < maxR; i++)
{
// TODO: Replace the repeated Multiply calls with a span-based aggregator that batches factors,
// avoiding transient ERational instances when maxR grows large.
result = result.Multiply(ComputeAlphaFactor(primes[i], a2));
}
return result;
}
}
@@ -2,7 +2,6 @@ namespace PerfectNumbers.Core;
public enum AlphaMScannerCandidateStatus
{
// TODO: Store these statuses as byte-coded constants so hot-path evaluations can stay branchless when scanning candidates.
UNKNOWN,
PRODUCT_ABOVE_TARGET,
PRODUCT_BELOW_TARGET,
+5 -9
View File
@@ -2,12 +2,8 @@ namespace PerfectNumbers.Core.Cpu;
public static class CpuConstants
{
// Allowed q mod 10 sets depending on last digit of M_p
public const int LastSevenMask10 = (1 << 7) | (1 << 9);
// For M_p ending with 1, restrict to {1,3,9}; 7 considered only for q == 7 (not possible for large p)
public const int LastOneMask10 = (1 << 1) | (1 << 3) | (1 << 9);
// TODO: Generate these masks at startup from the benchmarked Mod10 automaton tables so CPU filtering stays aligned with
// the optimized divisor-cycle residue steps.
// TODO: Fold in the Mod6ComparisonBenchmarks stride table here so callers can merge the mod 6 skips with these masks and
// avoid redundant residue work in the CPU hot loop.
}
// Allowed q mod 10 sets depending on last digit of M_p
public const int LastSevenMask10 = (1 << 7) | (1 << 9);
// For M_p ending with 1, restrict to {1,3,9}; 7 considered only for q == 7 (not possible for large p)
public const int LastOneMask10 = (1 << 1) | (1 << 3) | (1 << 9);
}
@@ -1,559 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using PerfectNumbers.Core;
using PerfectNumbers.Core.Gpu;
namespace PerfectNumbers.Core.Cpu;
public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDivisorByDivisorTester
{
private readonly object _sync = new();
private readonly ConcurrentBag<DivisorScanSession> _sessionPool = new();
private readonly bool _useGpuPow2Mod;
private readonly bool _useGpuCycleComputation;
private ulong _divisorLimit;
private ulong _lastStatusDivisor;
private bool _isConfigured;
private int _batchSize = 1_024;
public MersenneNumberDivisorByDivisorCpuTester(bool useGpuPow2Mod = false, bool useGpuCycleComputation = false)
{
_useGpuPow2Mod = useGpuPow2Mod;
_useGpuCycleComputation = useGpuCycleComputation;
}
public int BatchSize
{
get => _batchSize;
set => _batchSize = Math.Max(1, value);
}
public void ConfigureFromMaxPrime(ulong maxPrime)
{
lock (_sync)
{
_divisorLimit = ComputeDivisorLimitFromMaxPrime(maxPrime);
_lastStatusDivisor = 0UL;
_isConfigured = true;
}
}
public ulong DivisorLimit
{
get
{
lock (_sync)
{
if (!_isConfigured)
{
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
return _divisorLimit;
}
}
}
public ulong GetAllowedMaxDivisor(ulong prime)
{
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)
{
ulong allowedMax;
lock (_sync)
{
if (!_isConfigured)
{
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
allowedMax = ComputeAllowedMaxDivisor(prime, _divisorLimit);
}
if (allowedMax < 3UL)
{
divisorsExhausted = true;
return true;
}
ulong processedCount;
ulong lastProcessed;
bool processedAll;
bool composite = CheckDivisors(
prime,
allowedMax,
out lastProcessed,
out processedAll,
out processedCount);
if (processedCount > 0UL)
{
lock (_sync)
{
UpdateStatusUnsafe(processedCount);
}
}
if (composite)
{
divisorsExhausted = true;
return false;
}
divisorsExhausted = processedAll || composite;
return true;
}
public void PrepareCandidates(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++)
{
allowedMaxValues[index] = ComputeAllowedMaxDivisor(primes[index], divisorLimit);
}
}
public IMersenneNumberDivisorByDivisorTester.IDivisorScanSession CreateDivisorSession()
{
lock (_sync)
{
if (!_isConfigured)
{
throw new InvalidOperationException("ConfigureFromMaxPrime must be called before using the tester.");
}
if (_sessionPool.TryTake(out DivisorScanSession? session))
{
session.Reset();
return session;
}
return new DivisorScanSession(this);
}
}
private void ReturnSession(DivisorScanSession session)
{
_sessionPool.Add(session);
}
private bool CheckDivisors(
ulong prime,
ulong allowedMax,
out ulong lastProcessed,
out bool processedAll,
out ulong processedCount)
{
lastProcessed = 0UL;
processedCount = 0UL;
processedAll = false;
if (allowedMax < 3UL)
{
return false;
}
UInt128 step = (UInt128)prime << 1;
if (step == UInt128.Zero)
{
processedAll = true;
return false;
}
UInt128 limit = allowedMax;
UInt128 divisor = step + UInt128.One;
if (divisor > limit)
{
processedAll = true;
return false;
}
Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? factorCache = null;
Dictionary<ulong, ulong>? cycleRemainderCache = null;
CycleHint cycleHint = default;
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);
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);
// 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)
{
ulong candidate = (ulong)divisor;
processedCount++;
lastProcessed = candidate;
bool admissible = lastIsSeven
? (remainder10 == 3 || remainder10 == 7 || remainder10 == 9)
: (remainder10 == 1 || remainder10 == 3 || remainder10 == 9);
if (admissible && (remainder8 == 1 || remainder8 == 7) && remainder3 != 0 && remainder5 != 0 && remainder7 != 0 && remainder11 != 0)
{
MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(candidate);
if (TryResolveDivisorCycle(candidate, prime, divisorData, cycleCache, ref factorCache, ref cycleHint, out ulong divisorCycle) && divisorCycle != 0UL)
{
if (CheckDivisor(prime, candidate, divisorCycle, divisorData, ref cycleRemainderCache, ref cycleHint) != 0)
{
processedAll = true;
return true;
}
}
}
divisor += step;
remainder10 = AddMod(remainder10, step10, (byte)10);
remainder8 = AddMod(remainder8, step8, (byte)8);
remainder5 = AddMod(remainder5, step5, (byte)5);
remainder3 = AddMod(remainder3, step3, (byte)3);
remainder7 = AddMod(remainder7, step7, (byte)7);
remainder11 = AddMod(remainder11, step11, (byte)11);
}
processedAll = divisor > limit;
return false;
}
private byte CheckDivisor(
ulong prime,
ulong divisor,
ulong divisorCycle,
in MontgomeryDivisorData divisorData,
ref Dictionary<ulong, ulong>? cycleRemainders,
ref CycleHint cycleHint)
{
if (divisorCycle == 0UL)
{
throw new InvalidOperationException($"Cycle length resolution returned zero for divisor {divisor}.");
}
Dictionary<ulong, ulong>? remainderCache = cycleRemainders;
if (_useGpuPow2Mod && remainderCache is null)
{
remainderCache = new Dictionary<ulong, ulong>(8);
cycleRemainders = remainderCache;
}
ulong reducedExponent;
if (remainderCache is not null && remainderCache.TryGetValue(divisorCycle, out ulong cachedRemainder))
{
reducedExponent = cachedRemainder;
}
else if (cycleHint.HasCycle && cycleHint.LastCycleLength == divisorCycle)
{
reducedExponent = cycleHint.LastReducedExponent;
}
else
{
reducedExponent = prime % divisorCycle;
if (remainderCache is not null)
{
remainderCache[divisorCycle] = reducedExponent;
}
}
cycleHint.LastCycleLength = divisorCycle;
cycleHint.LastReducedExponent = reducedExponent;
cycleHint.HasCycle = true;
if (_useGpuPow2Mod && remainderCache is not null)
{
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(reducedExponent, divisor, out ulong remainder, divisorData);
if (status == GpuPow2ModStatus.Success)
{
return remainder == 1UL ? (byte)1 : (byte)0;
}
PrimeOrderGpuHeuristics.ReportPow2Failure(
"by-divisor residue scan",
divisor,
status);
}
ulong residue = reducedExponent.Pow2MontgomeryModFromCycleRemainder(divisorData);
return residue == 1UL ? (byte)1 : (byte)0;
}
private bool TryResolveDivisorCycle(
ulong divisor,
ulong prime,
in MontgomeryDivisorData divisorData,
DivisorCycleCache cycleCache,
ref Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? factorCache,
ref CycleHint cycleHint,
out ulong cycleLength)
{
if (divisor <= PerfectNumberConstants.MaxQForDivisorCycles)
{
cycleLength = cycleCache.GetCycleLength(divisor);
if (cycleLength == 0UL)
{
throw new InvalidOperationException($"Missing cached cycle length for divisor {divisor}.");
}
cycleHint.LastCycleLength = cycleLength;
cycleHint.HasCycle = true;
return true;
}
if (_useGpuCycleComputation && TryCalculateCycleLengthGpu(divisor, prime, divisorData, ref cycleHint, out cycleLength))
{
return cycleLength != 0UL;
}
factorCache ??= new Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>(8);
if (MersenneDivisorCycles.TryCalculateCycleLengthForExponent(
divisor,
prime,
divisorData,
factorCache,
out cycleLength,
allowGpuPow2: _useGpuPow2Mod) && cycleLength != 0UL)
{
cycleHint.LastCycleLength = cycleLength;
cycleHint.HasCycle = true;
return true;
}
cycleLength = cycleCache.GetCycleLength(divisor);
if (cycleLength == 0UL)
{
return false;
}
cycleHint.LastCycleLength = cycleLength;
cycleHint.HasCycle = true;
return true;
}
private bool TryCalculateCycleLengthGpu(
ulong divisor,
ulong prime,
in MontgomeryDivisorData divisorData,
ref CycleHint cycleHint,
out ulong cycleLength)
{
PrimeOrderCalculator.PrimeOrderSearchConfig config = PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault;
ulong? previousOrder = cycleHint.HasCycle ? cycleHint.LastCycleLength : null;
try
{
if (!PrimeOrderGpuHeuristics.TryCalculateOrder(divisor, previousOrder, config, divisorData, out PrimeOrderCalculator.PrimeOrderResult result))
{
throw new InvalidOperationException("GPU requested fallback.");
}
cycleLength = result.Order;
if (cycleLength == 0UL)
{
return false;
}
cycleHint.LastCycleLength = cycleLength;
cycleHint.HasCycle = true;
return true;
}
catch (InvalidOperationException ex)
{
Console.Error.WriteLine($"GPU cycle length computation failed for divisor {divisor}: {ex.Message}");
throw;
}
}
private static byte AddMod(byte value, byte delta, byte modulus)
{
int sum = value + delta;
if (sum >= modulus)
{
sum -= modulus;
}
return (byte)sum;
}
private void UpdateStatusUnsafe(ulong processedCount)
{
if (processedCount == 0UL)
{
return;
}
ulong interval = PerfectNumberConstants.ConsoleInterval;
if (interval == 0UL)
{
_lastStatusDivisor = 0UL;
return;
}
ulong total = _lastStatusDivisor + processedCount;
// TODO: Replace this modulo with the ring-buffer style counter (subtract loop) used in the fast CLI
// status benchmarks so we avoid `%` in this hot loop while still wrapping progress correctly.
_lastStatusDivisor = total % interval;
}
private static ulong ComputeDivisorLimitFromMaxPrime(ulong maxPrime)
{
if (maxPrime <= 1UL)
{
return 0UL;
}
if (maxPrime - 1UL >= 64UL)
{
return ulong.MaxValue;
}
return (1UL << (int)(maxPrime - 1UL)) - 1UL;
}
private struct CycleHint
{
public ulong LastCycleLength;
public ulong LastReducedExponent;
public bool HasCycle;
}
private static ulong ComputeAllowedMaxDivisor(ulong prime, ulong divisorLimit)
{
if (prime <= 1UL)
{
return 0UL;
}
if (prime - 1UL >= 64UL)
{
return divisorLimit;
}
return Math.Min((1UL << (int)(prime - 1UL)) - 1UL, divisorLimit);
}
private sealed class DivisorScanSession : IMersenneNumberDivisorByDivisorTester.IDivisorScanSession
{
private readonly MersenneNumberDivisorByDivisorCpuTester _owner;
private bool _disposed;
internal DivisorScanSession(MersenneNumberDivisorByDivisorCpuTester owner)
{
_owner = owner;
}
internal void Reset()
{
_disposed = false;
}
public void CheckDivisor(ulong divisor, in MontgomeryDivisorData divisorData, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(DivisorScanSession));
}
int length = primes.Length;
if (length == 0)
{
return;
}
MontgomeryDivisorData cachedData = divisorData;
if (cachedData.Modulus != divisor)
{
cachedData = MontgomeryDivisorDataCache.Get(divisor);
}
if (divisorCycle == 0UL)
{
divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
if (divisorCycle == 0UL)
{
hits.Clear();
return;
}
}
// Keep these remainder steppers in place so future updates continue reusing the previously computed residues.
// They are critical for avoiding repeated full Montgomery exponentiation work when scanning divisors.
var exponentStepper = new ExponentRemainderStepper(cachedData);
if (!exponentStepper.IsValidModulus)
{
hits.Clear();
return;
}
var cycleStepper = new CycleRemainderStepper(divisorCycle);
ulong remainder = cycleStepper.Initialize(primes[0]);
hits[0] = remainder == 0UL
? (exponentStepper.ComputeNextIsUnity(primes[0]) ? (byte)1 : (byte)0)
: (byte)0;
for (int i = 1; i < length; i++)
{
remainder = cycleStepper.ComputeNext(primes[i]);
if (remainder != 0UL)
{
hits[i] = 0;
continue;
}
hits[i] = exponentStepper.ComputeNextIsUnity(primes[i]) ? (byte)1 : (byte)0;
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_owner.ReturnSession(this);
}
}
}
@@ -2,101 +2,82 @@ namespace PerfectNumbers.Core.Cpu;
public class MersenneNumberIncrementalCpuTester(GpuKernelType kernelType)
{
private static readonly MersenneDivisorCycles _cycles = MersenneDivisorCycles.Shared;
private readonly GpuKernelType _kernelType = kernelType;
private static readonly MersenneDivisorCycles _cycles = MersenneDivisorCycles.Shared;
private readonly GpuKernelType _kernelType = kernelType;
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
{
UInt128 k = UInt128.One;
UInt128 one = k;
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
// Use residue automaton always on CPU
var auto = new MersenneResidueAutomaton(exponent);
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
{
UInt128 k, one = k = UInt128.One;
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
// Use residue automaton always on CPU
var auto = new MersenneResidueAutomaton(exponent);
bool reject;
bool shouldCheck;
UInt128 q = auto.CurrentQ();
UInt128 divPow;
UInt128 halfPow;
UInt128 phi;
UInt128 qCycle;
bool reject, shouldCheck;
UInt128 q = auto.CurrentQ(),
divPow, halfPow, phi, qCycle;
ulong div;
ulong mod8;
ulong phi64;
ulong div, mod8, phi64;
while (k <= maxK && Volatile.Read(ref isPrime))
{
// TODO: Replace this direct GetCycle call with DivisorCycleCache.Lookup so we reuse the single snapshot block
// and compute missing cycles on the configured device without queuing additional block generation.
qCycle = MersenneDivisorCycles.GetCycle(q);
shouldCheck = auto.Mod10R != 5UL;
if (shouldCheck)
{
mod8 = auto.Mod8R;
if (mod8 != 1UL && mod8 != 7UL)
{
shouldCheck = false;
}
else if (auto.Mod3R == 0UL || auto.Mod5R == 0UL)
{
// TODO: Swap these Mod3/Mod5 zero checks to the cached residue tables once the automaton exposes
// the benchmarked lookup-based helpers so CPU scans avoid runtime modulo instructions.
shouldCheck = false;
}
}
while (k <= maxK && Volatile.Read(ref isPrime))
{
qCycle = MersenneDivisorCycles.GetCycle(q);
shouldCheck = lastIsSeven
? (auto.Mod10R != 5UL)
: (auto.Mod10R != 5UL);
if (shouldCheck)
{
mod8 = auto.Mod8R;
if (mod8 != 1UL && mod8 != 7UL)
{
shouldCheck = false;
}
else if (auto.Mod3R == 0UL || auto.Mod5R == 0UL)
{
shouldCheck = false;
}
}
if (shouldCheck)
{
if (_kernelType == GpuKernelType.Pow2Mod)
{
// TODO: Swap this PowModWithCycle call for the ProcessEightBitWindows helper once
// the scalar implementation lands so CPU pow2mod scans match the GPU speedups
// captured in GpuPow2ModBenchmarks.
reject = exponent.PowModWithCycle(q, qCycle) == UInt128.One;
}
else
{
phi = q - one;
reject = false;
if (phi <= ulong.MaxValue)
{
phi64 = (ulong)phi;
// TODO: Route the phi-based powmods through the upcoming windowed helper to
// avoid the current square-and-multiply cost highlighted in the Pow2 bench
// suite when scanning large divisors.
if (phi64.PowModWithCycle(q, qCycle) == one)
{
// TODO: Once the windowed pow2 helper is available expose a version
// that reuses cached windows here instead of recomputing via the slow
// bit ladder for every divisor.
halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - one;
if (halfPow.BinaryGcd(q) == one)
{
div = phi64.FastDiv64(exponent, divMul);
// TODO: Replace this divisor powmod with the shared windowed
// implementation so each candidate uses the benchmarked fast
// path instead of the current per-bit multiply chain.
divPow = div.PowModWithCycle(q, qCycle) - one;
if (divPow.BinaryGcd(q) == one)
{
reject = true;
}
}
}
}
}
if (shouldCheck)
{
if (_kernelType == GpuKernelType.Pow2Mod)
{
reject = exponent.PowModWithCycle(q, qCycle) == UInt128.One;
}
else
{
phi = q - one;
reject = false;
if (phi <= ulong.MaxValue)
{
phi64 = (ulong)phi;
if (phi64.PowModWithCycle(q, qCycle) == one)
{
halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - one;
if (halfPow.BinaryGcd(q) == one)
{
div = phi64.FastDiv64(exponent, divMul);
divPow = div.PowModWithCycle(q, qCycle) - one;
if (divPow.BinaryGcd(q) == one)
{
reject = true;
}
}
}
}
}
if (reject && q.IsPrimeCandidate())
{
Volatile.Write(ref isPrime, false);
break;
}
}
if (reject && q.IsPrimeCandidate())
{
Volatile.Write(ref isPrime, false);
break;
}
}
k += 1UL;
auto.Next();
q = auto.CurrentQ();
}
}
}
k += 1UL;
auto.Next();
q = auto.CurrentQ();
}
return;
}
}
@@ -3,43 +3,39 @@ namespace PerfectNumbers.Core;
public class MersenneNumberLucasLehmerCpuTester
{
public bool IsPrime(ulong exponent)
{
{
// Early rejections aligned with incremental/order sieves, but safe for small p:
// - If 3 | p and p != 3, then 7 | M_p -> composite.
// - If p ≡ 1 (mod 4) and p shares a factor with (p-1), reject fast.
// - If p is divisible by divisors specific to numbers ending with 1 or 7, reject fast.
// TODO: Replace these `%` checks with Mod3/Mod5/Mod7/Mod11 helpers once Lucas–Lehmer CPU filtering
// shares the benchmarked bitmask implementations and avoids slow modulo instructions in the hot path.
if (
((exponent % 3UL) == 0UL && exponent != 3UL) ||
((exponent % 5UL) == 0UL && exponent != 5UL) ||
((exponent % 7UL) == 0UL && exponent != 7UL) ||
((exponent % 11UL) == 0UL && exponent != 11UL)
(exponent.Mod3() == 0UL && exponent != 3UL) ||
(exponent.Mod5() == 0UL && exponent != 5UL) ||
(exponent.Mod7() == 0UL && exponent != 7UL) ||
(exponent.Mod11() == 0UL && exponent != 11UL)
)
{
return false;
}
{
return false;
}
if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOne())
{
return false;
}
if ((exponent & 3UL) == 1UL && exponent.SharesFactorWithExponentMinusOne())
{
return false;
}
UInt128 s = UInt128Numbers.Four, two = UInt128Numbers.Two,
m = (UInt128.One << (int)exponent) - UInt128.One;
UInt128 s = UInt128Numbers.Four, two = UInt128Numbers.Two,
m = (UInt128.One << (int)exponent) - UInt128.One;
UInt128 limit = exponent - UInt128Numbers.Two;
for (UInt128 i = UInt128.Zero; i < limit; i++)
{
// TODO: Port this Lucas–Lehmer powmod to the ProcessEightBitWindows helper so the CPU
// fallback benefits from the same windowed ladder that halves runtime in the Pow2 benchmarks.
s = s.PowModWithCycle(two, MersenneDivisorCycles.GetCycle(m)) - two;
if (s < UInt128.Zero)
{
s += m;
}
}
UInt128 limit = exponent - UInt128Numbers.Two;
for (UInt128 i = UInt128.Zero; i < limit; i++)
{
s = s.PowModWithCycle(two, MersenneDivisorCycles.GetCycle(m)) - two;
if (s < UInt128.Zero)
{
s += m;
}
}
return s == UInt128.Zero;
}
return s == UInt128.Zero;
}
}
@@ -13,10 +13,7 @@ public class MersenneNumberOrderCpuTester(GpuKernelType kernelType)
while (k <= maxK && Volatile.Read(ref isPrime))
{
qCycle = MersenneDivisorCycles.GetCycle(q);
// TODO: When this lookup misses the snapshot, invoke the configured device to compute the
// single required cycle on demand without persisting it so the CPU order path retains the
// cycle-stepping speedups while honoring the no-extra-cache constraint for large divisors.
qCycle = MersenneDivisorCycles.GetCycle(q);
int allowMask2 = lastIsSeven ? CpuConstants.LastSevenMask10 : CpuConstants.LastOneMask10;
bool shouldCheck = ((allowMask2 >> (int)auto.Mod10R) & 1) != 0;
if (shouldCheck)
@@ -33,42 +30,33 @@ public class MersenneNumberOrderCpuTester(GpuKernelType kernelType)
}
if (shouldCheck)
{
if (_kernelType == GpuKernelType.Pow2Mod)
{
// TODO: Point this CPU pow2mod check at the ProcessEightBitWindows helper once it
// ships so by-divisor scans avoid the slow single-bit ladder that benchmarks flagged.
if (exponent.PowModWithCycle(q, qCycle) == 1UL)
{
Volatile.Write(ref isPrime, false);
break;
}
}
else
{
UInt128 phi = q - 1UL;
if (phi <= ulong.MaxValue)
{
ulong phi64 = (ulong)phi;
// TODO: Switch these phi-based powmods to the shared windowed helper so CPU
// fallback paths keep pace with the optimized GPU kernels on large divisors.
if (phi64.PowModWithCycle(q, qCycle) == 1UL)
{
// TODO: Reuse the windowed pow2 helper for halfPow as soon as it is
// available instead of recalculating via square-and-multiply.
UInt128 halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - 1UL;
if (halfPow.BinaryGcd(q) == 1UL)
{
ulong divMul = (ulong)((((UInt128)1 << 64) - 1UL) / exponent) + 1UL;
ulong div = phi64.FastDiv64(exponent, divMul);
// TODO: Replace this divisor powmod with the ProcessEightBitWindows
// implementation so order scans stop paying the slower bit-serial
// loop measured in the MulMod benchmarks.
UInt128 divPow = div.PowModWithCycle(q, qCycle) - 1UL;
if (divPow.BinaryGcd(q) == 1UL)
{
Volatile.Write(ref isPrime, false);
break;
}
if (_kernelType == GpuKernelType.Pow2Mod)
{
if (exponent.PowModWithCycle(q, qCycle) == 1UL)
{
Volatile.Write(ref isPrime, false);
break;
}
}
else
{
UInt128 phi = q - 1UL;
if (phi <= ulong.MaxValue)
{
ulong phi64 = (ulong)phi;
if (phi64.PowModWithCycle(q, qCycle) == 1UL)
{
UInt128 halfPow = (phi64 >> 1).PowModWithCycle(q, qCycle) - 1UL;
if (halfPow.BinaryGcd(q) == 1UL)
{
ulong divMul = (ulong)((((UInt128)1 << 64) - 1UL) / exponent) + 1UL;
ulong div = phi64.FastDiv64(exponent, divMul);
UInt128 divPow = div.PowModWithCycle(q, qCycle) - 1UL;
if (divPow.BinaryGcd(q) == 1UL)
{
Volatile.Write(ref isPrime, false);
break;
}
}
}
}
@@ -10,14 +10,9 @@ public class MersenneNumberResidueCpuTester
// Initialize/update Mersenne residue tracker and start a merge walk for ascending q divisors
_mersenneResidueTracker ??= new(ResidueModel.Mersenne);
// For small divisors q <= 4,000,000 use cached cycle length to fast-path checks
// and avoid per-q powmods when possible when scanning lanes.
// TODO: Wire this residue scan into DivisorCycleCache so the mandatory cycle acceleration from the
// by-divisor benchmarks applies here instead of relying solely on ModResidueTracker powmods.
// TODO: When a required cycle is missing from the snapshot, compute only that single cycle on the device
// selected by the current settings, skip persisting the result, and avoid scheduling additional cache blocks so
// we keep operating with the single shared block.
_mersenneResidueTracker.BeginMerge(exponent);
// For small divisors q <= 4,000,000 use cached cycle length to fast-path checks
// and avoid per-q powmods when possible when scanning lanes.
_mersenneResidueTracker.BeginMerge(exponent);
// Start at k = 1: q0 = 2*p*1 + 1
UInt128 k = UInt128.One;
@@ -85,15 +80,12 @@ public class MersenneNumberResidueCpuTester
r5_3 = r5_2 + step5; if (r5_3 >= 10UL) r5_3 -= 10UL; if (r5_3 >= 5UL) r5_3 -= 5UL;
// lane 0
if (lanes >= 1 &&
(((allowMask >> (int)r10_0) & 1) != 0 && (r8_0 == 1UL || r8_0 == 7UL) && r3_0 != 0UL && r5_0 != 0UL))
{
// TODO: Once cycle data is exposed for residue scans, consult the cached divisor cycle here
// (and below) so every qualifying q reuses precomputed lengths instead of recomputing via
// tracker.MergeOrAppend.
// cycle-based quick check for small q
if (localIsPrime && tracker.MergeOrAppend(exponent, q, out isDivider) && isDivider &&
(q <= ulong.MaxValue ? ((ulong)q).IsPrimeCandidate() : q.IsPrimeCandidate()))
if (lanes >= 1 &&
(((allowMask >> (int)r10_0) & 1) != 0 && (r8_0 == 1UL || r8_0 == 7UL) && r3_0 != 0UL && r5_0 != 0UL))
{
// cycle-based quick check for small q
if (localIsPrime && tracker.MergeOrAppend(exponent, q, out isDivider) && isDivider &&
(q <= ulong.MaxValue ? ((ulong)q).IsPrimeCandidate() : q.IsPrimeCandidate()))
{
localIsPrime = false;
}
@@ -1,102 +0,0 @@
using System;
using System.Numerics;
namespace PerfectNumbers.Core;
internal struct CycleRemainderStepper
{
private readonly ulong _cycleLength;
private ulong _previousPrime;
private ulong _currentRemainder;
private bool _hasState;
public CycleRemainderStepper(ulong cycleLength)
{
if (cycleLength == 0UL)
{
throw new ArgumentOutOfRangeException(nameof(cycleLength), "Cycle length must be non-zero.");
}
_cycleLength = cycleLength;
_previousPrime = 0UL;
_currentRemainder = 0UL;
_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;
}
public ulong Initialize(ulong prime)
{
ulong cycleLength = _cycleLength;
ulong remainder = prime;
if (remainder >= cycleLength)
{
// TODO: Swap this `%` for the shared divisor-cycle remainder helper so initialization reuses the cached
// cycle deltas benchmarked faster than on-the-fly modulo work, matching the
// MersenneDivisorCycleLengthGpuBenchmarks winner for both CPU and GPU call sites.
remainder %= cycleLength;
}
_previousPrime = prime;
_currentRemainder = remainder;
_hasState = true;
return remainder;
}
public ulong ComputeNext(ulong prime)
{
if (!_hasState)
{
throw new InvalidOperationException("CycleRemainderStepper must be initialized before computing.");
}
if (prime <= _previousPrime)
{
throw new ArgumentOutOfRangeException(nameof(prime), "Primes must be processed in strictly increasing order.");
}
ulong cycleLength = _cycleLength;
ulong delta = prime - _previousPrime;
_previousPrime = prime;
if (ulong.MaxValue - _currentRemainder < delta)
{
UInt128 extended = (UInt128)_currentRemainder + delta;
// TODO: Replace this `%` with the UInt128-aware cycle reducer so large deltas use the cached subtraction
// ladder instead of falling back to the slower modulo implementation.
_currentRemainder = (ulong)(extended % cycleLength);
return _currentRemainder;
}
_currentRemainder += delta;
if (_currentRemainder >= cycleLength)
{
_currentRemainder -= cycleLength;
if (_currentRemainder >= cycleLength)
{
// TODO: Route this `%` through the shared divisor-cycle helper so repeated wrap-arounds avoid
// modulo operations and match the benchmarked fast path highlighted in
// MersenneDivisorCycleLengthGpuBenchmarks.
_currentRemainder %= cycleLength;
}
}
return _currentRemainder;
}
public void CaptureState(out ulong previousPrime, out ulong previousRemainder, out bool hasState)
{
// TODO: Expose these fields directly once the residue scanners adopt the single-cycle helper
// so the hot path can read them without paying for an additional wrapper call per iteration.
previousPrime = _previousPrime;
previousRemainder = _currentRemainder;
hasState = _hasState;
}
}
-337
View File
@@ -1,337 +0,0 @@
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;
namespace PerfectNumbers.Core;
public sealed class DivisorCycleCache
{
private const byte ByteZero = 0;
private const byte ByteOne = 1;
private const int StackBufferThreshold = 128;
private readonly ConcurrentDictionary<Accelerator, Action<Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>>> _gpuKernelCache = new(AcceleratorReferenceComparer.Instance);
private volatile ulong[] _snapshot;
private volatile bool _useGpuGeneration = true;
private readonly int _divisorCyclesBatchSize;
private static int _sharedDivisorCyclesBatchSize = GpuConstants.GpuCycleStepsPerInvocation;
public static void SetDivisorCyclesBatchSize(int divisorCyclesBatchSize)
{
_sharedDivisorCyclesBatchSize = Math.Max(1, divisorCyclesBatchSize);
}
public static DivisorCycleCache Shared { get; } = new DivisorCycleCache(_sharedDivisorCyclesBatchSize);
public int PreferredBatchSize => _divisorCyclesBatchSize;
private DivisorCycleCache(int divisorCyclesBatchSize)
{
_divisorCyclesBatchSize = Math.Max(1, divisorCyclesBatchSize);
_snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
}
public void ConfigureGeneratorDevice(bool useGpu)
{
_useGpuGeneration = useGpu;
}
public void RefreshSnapshot()
{
_snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
}
public ulong GetCycleLength(ulong divisor)
{
Span<ulong> result = stackalloc ulong[1];
ReadOnlySpan<ulong> singleDivisor = stackalloc ulong[1] { divisor };
GetCycleLengths(singleDivisor, result);
return result[0];
}
public void GetCycleLengths(ReadOnlySpan<ulong> divisors, Span<ulong> cycles)
{
ulong[] snapshot = _snapshot;
int length = divisors.Length;
int[]? rentedMissing = null;
Span<int> missingBuffer = length <= StackBufferThreshold
? stackalloc int[length]
: new Span<int>(rentedMissing = ArrayPool<int>.Shared.Rent(length), 0, length);
int missingCount = 0;
for (int i = 0; i < length; i++)
{
ulong divisor = divisors[i];
if (divisor <= 1UL)
{
throw new InvalidDataException("Divisor must be > 1");
}
if (divisor < (ulong)snapshot.Length)
{
ulong cached = snapshot[divisor];
if (cached == 0UL)
{
throw new InvalidDataException($"Divisor cycle is missing for {divisor}");
}
cycles[i] = cached;
}
else
{
missingBuffer[missingCount++] = i;
}
}
if (missingCount != 0)
{
ReadOnlySpan<int> missingIndices = missingBuffer[..missingCount];
if (_useGpuGeneration)
{
ComputeCyclesGpu(divisors, cycles, missingIndices);
}
else
{
ComputeCyclesCpu(divisors, cycles, missingIndices);
}
}
if (rentedMissing is not null)
{
ArrayPool<int>.Shared.Return(rentedMissing, clearArray: false);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ComputeCyclesCpu(ReadOnlySpan<ulong> divisors, Span<ulong> cycles, ReadOnlySpan<int> indices)
{
for (int i = 0; i < indices.Length; i++)
{
int targetIndex = indices[i];
ulong divisor = divisors[targetIndex];
MontgomeryDivisorData divisorData = MontgomeryDivisorData.FromModulus(divisor);
cycles[targetIndex] = MersenneDivisorCycles.CalculateCycleLength(divisor, divisorData);
}
}
private void ComputeCyclesGpu(ReadOnlySpan<ulong> divisors, Span<ulong> cycles, ReadOnlySpan<int> indices)
{
int count = indices.Length;
ulong[]? rentedDivisors = null;
Span<ulong> workDivisors = count <= StackBufferThreshold
? stackalloc ulong[count]
: new Span<ulong>(rentedDivisors = ArrayPool<ulong>.Shared.Rent(count), 0, count);
ulong[]? rentedResults = null;
Span<ulong> workResults = count <= StackBufferThreshold
? stackalloc ulong[count]
: new Span<ulong>(rentedResults = ArrayPool<ulong>.Shared.Rent(count), 0, count);
for (int i = 0; i < count; i++)
{
workDivisors[i] = divisors[indices[i]];
}
ComputeCyclesGpuCore(workDivisors, workResults);
for (int i = 0; i < count; i++)
{
cycles[indices[i]] = workResults[i];
}
if (rentedDivisors is not null)
{
ArrayPool<ulong>.Shared.Return(rentedDivisors, clearArray: false);
ArrayPool<ulong>.Shared.Return(rentedResults!, clearArray: false);
}
}
private void ComputeCyclesGpuCore(ReadOnlySpan<ulong> divisors, Span<ulong> destination)
{
int length = divisors.Length;
var gpuLease = GpuKernelPool.GetKernel(useGpuOrder: true);
var execution = gpuLease.EnterExecutionScope();
Accelerator accelerator = gpuLease.Accelerator;
var kernel = _gpuKernelCache.GetOrAdd(accelerator, LoadKernel);
using MemoryBuffer1D<ulong, Stride1D.Dense> divisorBuffer = accelerator.Allocate1D<ulong>(length);
using MemoryBuffer1D<ulong, Stride1D.Dense> powBuffer = accelerator.Allocate1D<ulong>(length);
using MemoryBuffer1D<ulong, Stride1D.Dense> orderBuffer = accelerator.Allocate1D<ulong>(length);
using MemoryBuffer1D<ulong, Stride1D.Dense> resultBuffer = accelerator.Allocate1D<ulong>(length);
using MemoryBuffer1D<byte, Stride1D.Dense> statusBuffer = accelerator.Allocate1D<byte>(length);
ulong[]? rentedPow = null;
Span<ulong> powSpan = length <= StackBufferThreshold
? stackalloc ulong[length]
: new Span<ulong>(rentedPow = ArrayPool<ulong>.Shared.Rent(length), 0, length);
ulong[]? rentedOrder = null;
Span<ulong> orderSpan = length <= StackBufferThreshold
? stackalloc ulong[length]
: new Span<ulong>(rentedOrder = ArrayPool<ulong>.Shared.Rent(length), 0, length);
ulong[]? rentedResult = null;
Span<ulong> resultSpan = length <= StackBufferThreshold
? stackalloc ulong[length]
: new Span<ulong>(rentedResult = ArrayPool<ulong>.Shared.Rent(length), 0, length);
byte[]? rentedStatus = null;
Span<byte> statusSpan = length <= StackBufferThreshold
? stackalloc byte[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);
for (int i = 0; i < length; i++)
{
ulong divisor = divisors[i];
ulong initialPow = 2UL;
if (initialPow >= divisor)
{
initialPow -= divisor;
}
powSpan[i] = initialPow;
orderSpan[i] = 1UL;
resultSpan[i] = 0UL;
statusSpan[i] = ByteZero;
}
powBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(powSpan), length);
orderBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(orderSpan), length);
resultBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(resultSpan), length);
statusBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(statusSpan), length);
int pending;
do
{
kernel(length, _divisorCyclesBatchSize, divisorBuffer.View, powBuffer.View, orderBuffer.View, resultBuffer.View, statusBuffer.View);
accelerator.Synchronize();
statusBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(statusSpan), length);
pending = 0;
for (int i = 0; i < length; i++)
{
if (statusSpan[i] == ByteZero)
{
pending++;
}
}
}
while (pending > 0);
resultBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(resultSpan), length);
resultSpan.CopyTo(destination);
if (rentedPow is not null)
{
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();
gpuLease.Dispose();
}
private static Action<Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>> LoadKernel(Accelerator accelerator)
{
return accelerator.LoadAutoGroupedStreamKernel<Index1D, int, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<byte, Stride1D.Dense>>(GpuAdvanceDivisorCyclesKernel);
}
private static void GpuAdvanceDivisorCyclesKernel(
Index1D index,
int steps,
ArrayView1D<ulong, Stride1D.Dense> divisors,
ArrayView1D<ulong, Stride1D.Dense> pow,
ArrayView1D<ulong, Stride1D.Dense> order,
ArrayView1D<ulong, Stride1D.Dense> cycles,
ArrayView1D<byte, Stride1D.Dense> status)
{
if (status[index] != ByteZero)
{
return;
}
ulong divisor = divisors[index];
if ((divisor & (divisor - 1UL)) == 0UL)
{
cycles[index] = 1UL;
status[index] = ByteOne;
pow[index] = 1UL;
order[index] = 1UL;
return;
}
ulong currentPow = pow[index];
ulong currentOrder = order[index];
if (divisor <= 3UL)
{
while (currentPow != 1UL)
{
currentPow += currentPow;
if (currentPow >= divisor)
{
currentPow -= divisor;
}
currentOrder++;
}
cycles[index] = currentOrder;
status[index] = ByteOne;
pow[index] = currentPow;
order[index] = currentOrder;
return;
}
do
{
currentPow += currentPow;
if (currentPow >= divisor)
{
currentPow -= divisor;
}
currentOrder++;
if (currentPow == 1UL)
{
cycles[index] = currentOrder;
status[index] = ByteOne;
pow[index] = currentPow;
order[index] = currentOrder;
return;
}
}
while (--steps != 0);
pow[index] = currentPow;
order[index] = currentOrder;
}
private sealed class AcceleratorReferenceComparer : IEqualityComparer<Accelerator>
{
internal static AcceleratorReferenceComparer Instance { get; } = new();
public bool Equals(Accelerator? x, Accelerator? y) => ReferenceEquals(x, y);
public int GetHashCode(Accelerator obj) => RuntimeHelpers.GetHashCode(obj);
}
}
+19
View File
@@ -0,0 +1,19 @@
using System.Runtime.CompilerServices;
namespace PerfectNumbers.Core;
public sealed class EulerPrimeTester
{
private readonly PrimeTester _primeTester;
public EulerPrimeTester(PrimeTester primeTester)
{
_primeTester = primeTester;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsEulerPrime(ulong n)
{
return (n & 3UL) == 1UL && _primeTester.IsPrime(n, CancellationToken.None);
}
}
@@ -1,143 +0,0 @@
namespace PerfectNumbers.Core;
internal struct ExponentRemainderStepper
{
private readonly MontgomeryDivisorData _divisor;
private readonly ulong _modulus;
private readonly ulong _nPrime;
private readonly ulong _montgomeryOne;
private ulong _previousExponent;
private ulong _currentMontgomery;
private bool _hasState;
public ExponentRemainderStepper(in MontgomeryDivisorData divisor)
{
_divisor = divisor;
_modulus = divisor.Modulus;
_nPrime = divisor.NPrime;
_montgomeryOne = divisor.MontgomeryOne;
_previousExponent = 0UL;
_currentMontgomery = divisor.MontgomeryOne;
_hasState = false;
}
public bool IsValidModulus => _modulus > 1UL && (_modulus & 1UL) != 0UL;
public bool HasState => _hasState;
public ulong PreviousExponent => _previousExponent;
public void Reset()
{
_previousExponent = 0UL;
_currentMontgomery = _montgomeryOne;
_hasState = false;
}
public ulong ComputeNext(ulong exponent)
{
if (!IsValidModulus)
{
_hasState = false;
return 0UL;
}
if (!_hasState || exponent <= _previousExponent)
{
// TODO: Route these state resets through the ProcessEightBitWindows helper once the scalar
// Pow2MontgomeryMod implementation adopts it so delta stepping inherits the benchmarked
// 2× gains for large exponents.
_currentMontgomery = exponent.Pow2MontgomeryModWindowed(_divisor, keepMontgomery: true);
_previousExponent = exponent;
_hasState = true;
return ReduceCurrent();
}
ulong delta = exponent - _previousExponent;
// TODO: Once divisor cycle lengths are mandatory, pull the delta multiplier from the
// single-block divisor-cycle snapshot so we can skip the powmod entirely and reuse the
// cached Montgomery residue ladder highlighted in MersenneDivisorCycleLengthGpuBenchmarks.
// TODO: Replace this per-delta powmod with the upcoming windowed ladder so incremental
// updates stop paying the single-bit cost highlighted in GpuPow2ModBenchmarks.
ulong multiplier = delta.Pow2MontgomeryModWindowed(_divisor, keepMontgomery: true);
_currentMontgomery = _currentMontgomery.MontgomeryMultiply(multiplier, _modulus, _nPrime);
_previousExponent = exponent;
return ReduceCurrent();
}
public bool ComputeNextIsUnity(ulong exponent)
{
if (!IsValidModulus)
{
_hasState = false;
return false;
}
if (!_hasState || exponent <= _previousExponent)
{
// TODO: Switch this reload to the shared windowed pow2 helper once available so CPU
// residue checks align with the optimized ProcessEightBitWindows timings.
_currentMontgomery = exponent.Pow2MontgomeryModWindowed(_divisor, keepMontgomery: true);
_previousExponent = exponent;
_hasState = true;
return _currentMontgomery == _montgomeryOne;
}
ulong delta = exponent - _previousExponent;
// TODO: Reuse the divisor-cycle derived Montgomery delta once the cache exposes single-cycle
// lookups so this branch also avoids recomputing powmods when the snapshot lacks the divisor.
// TODO: Use the windowed delta pow2 helper here as well to avoid the single-bit ladder that
// currently lags behind the benchmarked implementation for huge divisor cycles.
ulong multiplier = delta.Pow2MontgomeryModWindowed(_divisor, keepMontgomery: true);
_currentMontgomery = _currentMontgomery.MontgomeryMultiply(multiplier, _modulus, _nPrime);
_previousExponent = exponent;
return _currentMontgomery == _montgomeryOne;
}
public bool TryInitializeFromMontgomeryResult(ulong exponent, ulong montgomeryResult, out bool isUnity)
{
if (!IsValidModulus)
{
_hasState = false;
isUnity = false;
return false;
}
_currentMontgomery = montgomeryResult;
_previousExponent = exponent;
_hasState = true;
isUnity = _currentMontgomery == _montgomeryOne;
return true;
}
public bool TryAdvanceWithMontgomeryDelta(ulong exponent, ulong montgomeryDelta, out bool isUnity)
{
if (!IsValidModulus)
{
_hasState = false;
isUnity = false;
return false;
}
if (!_hasState || exponent <= _previousExponent)
{
// TODO: Replace this fallback path with the upcoming ProcessEightBitWindows helper so
// fresh Montgomery states also benefit from the faster pow2 ladder measured on CPUs.
_currentMontgomery = exponent.Pow2MontgomeryModWindowed(_divisor, keepMontgomery: true);
_previousExponent = exponent;
_hasState = true;
isUnity = _currentMontgomery == _montgomeryOne;
return true;
}
// TODO: Once the divisor-cycle cache exposes a direct Montgomery delta, multiply it here instead
// of relying on the caller-provided delta so incremental scans remain in sync with the snapshot
// without computing additional cycles or mutating cache state.
_currentMontgomery = _currentMontgomery.MontgomeryMultiply(montgomeryDelta, _modulus, _nPrime);
_previousExponent = exponent;
isUnity = _currentMontgomery == _montgomeryOne;
return true;
}
private ulong ReduceCurrent() => _currentMontgomery.MontgomeryMultiply(1UL, _modulus, _nPrime);
}
+4 -8
View File
@@ -1,11 +1,7 @@
namespace PerfectNumbers.Core.Gpu
{
public static class GpuConstants
{
public const int ScanBatchSize = 2_097_152;
public const int GpuCycleStepsPerInvocation = 256;
// TODO: Auto-tune these GPU constants per accelerator so batch sizing aligns with the fastest kernel
// configuration reported by the Pow2Mod and divisor-cycle benchmarks.
public static class GpuConstants
{
public const int ScanBatchSize = 2_097_152;
}
}
}
+3 -6
View File
@@ -6,11 +6,9 @@ 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;
// Default device preference for generic GPU kernels (prime scans, NTT, etc.)
public static bool ForceCpu { get; set; } = false;
internal sealed class PooledContext
{
@@ -158,4 +156,3 @@ public static class GpuContextPool
}
}
}
+326 -355
View File
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Threading;
using ILGPU;
using ILGPU.Runtime;
using static PerfectNumbers.Core.Gpu.GpuContextPool;
@@ -25,42 +26,42 @@ public readonly struct ResidueAutomatonArgs
public sealed class KernelContainer
{
// Serializes first-time initialization of kernels/buffers per accelerator.
public Action<AcceleratorStream, Index1D, ulong, ulong, ArrayView<GpuUInt128>, ArrayView<ulong>>? Order;
// Serializes first-time initialization of kernels/buffers per accelerator.
public Action<AcceleratorStream, Index1D, ulong, ulong, ArrayView<GpuUInt128>, ArrayView<ulong>>? Order;
public Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong,
ulong, ulong, ulong, ulong, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>>? Incremental;
ulong, ulong, ulong, ulong, ArrayView<ulong>, ArrayView1D<uint, Stride1D.Dense>>? Incremental;
public Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong,
ResidueAutomatonArgs, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>,
ResidueAutomatonArgs, ArrayView<ulong>, ArrayView1D<uint, Stride1D.Dense>,
ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>,
ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>? Pow2Mod;
public Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong,
ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<uint, Stride1D.Dense>>? IncrementalOrder;
public Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong,
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;
ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<uint, Stride1D.Dense>>? Pow2ModOrder;
// Optional device buffer with small divisor cycles (<= 4M). Index = divisor, value = cycle length.
public MemoryBuffer1D<ulong, Stride1D.Dense>? SmallCycles;
public MemoryBuffer1D<uint, Stride1D.Dense>? SmallCycles;
public MemoryBuffer1D<uint, Stride1D.Dense>? SmallPrimesLastOne;
public MemoryBuffer1D<ulong, Stride1D.Dense>? SmallPrimesPow2LastOne;
public MemoryBuffer1D<uint, Stride1D.Dense>? SmallPrimesLastSeven;
public MemoryBuffer1D<ulong, Stride1D.Dense>? SmallPrimesPow2LastSeven;
public static T InitOnce<T>(ref T? slot, Func<T> factory) where T : class
{
var current = Volatile.Read(ref slot);
if (current is not null)
public static T InitOnce<T>(ref T? slot, Func<T> factory) where T : class
{
return current;
}
var current = Volatile.Read(ref slot);
if (current is not null)
{
return current;
}
current = Volatile.Read(ref slot);
if (current is null)
{
current = factory();
Volatile.Write(ref slot, current);
current = Volatile.Read(ref slot);
if (current is null)
{
current = factory();
Volatile.Write(ref slot, current);
}
return current;
}
return current;
}
}
public readonly struct ResiduePrimeViews(
@@ -77,148 +78,146 @@ public readonly struct ResiduePrimeViews(
public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, KernelContainer kernels)
{
private bool disposedValue;
private IDisposable? _limiter = limiter;
private GpuContextLease? _gpu = gpu;
private KernelContainer _kernels = kernels;
private AcceleratorStream? _stream;
private object? _executionLock = gpu.ExecutionLock;
private bool disposedValue;
private IDisposable? _limiter = limiter;
private GpuContextLease? _gpu = gpu;
private KernelContainer _kernels = kernels;
private AcceleratorStream? _stream;
private object? _executionLock = gpu.ExecutionLock;
public readonly Accelerator Accelerator => _gpu?.Accelerator ?? throw new NullReferenceException("GPU context is already released");
public readonly Accelerator Accelerator => _gpu?.Accelerator ?? throw new NullReferenceException("GPU context is already released");
public AcceleratorStream Stream
{
get
public AcceleratorStream Stream
{
if (_stream is { } stream)
{
return stream;
}
// It feels that we can combine these into 1 return statement, but that increases the max stack frame size to 3 from 2
stream = Accelerator.CreateStream();
_stream = stream;
return stream;
}
}
public readonly ExecutionScope EnterExecutionScope() => new ExecutionScope(_executionLock);
public readonly Action<AcceleratorStream, Index1D, ulong, ulong, ArrayView<GpuUInt128>, ArrayView<ulong>> OrderKernel
{
get
{
var accel = Accelerator; // avoid capturing 'this' in lambda
lock (_gpu!.Value.KernelInitLock)
{
return KernelContainer.InitOnce(ref _kernels.Order, () =>
get
{
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>>>();
});
}
}
}
if (_stream is { } stream)
{
return stream;
}
public readonly 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>> Pow2ModKernel
{
get
stream = Accelerator.CreateStream();
_stream = stream;
return stream;
}
}
public readonly ExecutionScope EnterExecutionScope()
{
var accel = Accelerator;
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>>>();
});
}
return new ExecutionScope(_executionLock);
}
}
public readonly Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ulong, ulong, ulong, ulong, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>> IncrementalKernel
{
get
public readonly Action<AcceleratorStream, Index1D, ulong, ulong, ArrayView<GpuUInt128>, ArrayView<ulong>> OrderKernel
{
var accel = Accelerator;
lock (_gpu!.Value.KernelInitLock)
{
return KernelContainer.InitOnce(ref _kernels.Incremental, () =>
get
{
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>>>();
});
}
var accel = Accelerator; // avoid capturing 'this' in lambda
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>>>();
});
}
}
}
}
public readonly Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>> IncrementalOrderKernel
{
get
public readonly Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<ulong>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>> Pow2ModKernel
{
var accel = Accelerator;
lock (_gpu!.Value.KernelInitLock)
{
return KernelContainer.InitOnce(ref _kernels.IncrementalOrder, () =>
get
{
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>>>();
});
}
var accel = Accelerator;
lock (_gpu!.Value.KernelInitLock)
{
return KernelContainer.InitOnce(ref _kernels.Pow2Mod, () =>
{
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<ulong>, ArrayView1D<uint, 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<uint, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>>();
});
}
}
}
}
public readonly Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>> Pow2ModOrderKernel
{
get
public readonly Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ulong, ulong, ulong, ulong, ArrayView<ulong>, ArrayView1D<uint, Stride1D.Dense>> IncrementalKernel
{
var accel = Accelerator;
lock (_gpu!.Value.KernelInitLock)
{
return KernelContainer.InitOnce(ref _kernels.Pow2ModOrder, () =>
get
{
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>>>();
});
}
var accel = Accelerator;
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<uint, 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<uint, Stride1D.Dense>>>();
});
}
}
}
}
// TODO: Plumb the small-cycles device buffer into all kernels that can benefit
// (some already accept it). Consider a compact type (byte/ushort) for memory footprint.
public readonly Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<uint, Stride1D.Dense>> IncrementalOrderKernel
{
get
{
var accel = Accelerator;
lock (_gpu!.Value.KernelInitLock)
{
return KernelContainer.InitOnce(ref _kernels.IncrementalOrder, () =>
{
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<uint, Stride1D.Dense>>(IncrementalOrderKernelScan);
var kernel = KernelUtil.GetKernel(loaded);
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<uint, Stride1D.Dense>>>();
});
}
}
}
public readonly Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<uint, Stride1D.Dense>> Pow2ModOrderKernel
{
get
{
var accel = Accelerator;
lock (_gpu!.Value.KernelInitLock)
{
return KernelContainer.InitOnce(ref _kernels.Pow2ModOrder, () =>
{
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<uint, Stride1D.Dense>>(Pow2ModOrderKernelScan);
var kernel = KernelUtil.GetKernel(loaded);
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<uint, Stride1D.Dense>>>();
});
}
}
}
// TODO: Plumb the small-cycles device buffer into all kernels that can benefit
// (some already accept it). Consider a compact type (byte/ushort) for memory footprint.
private static void IncrementalKernelScan(Index1D index, ulong exponent, GpuUInt128 twoP, GpuUInt128 kStart, byte lastIsSeven, ulong divMul,
ulong q0m10, ulong q0m8, ulong q0m3, ulong q0m5, ArrayView<ulong> orders,
ArrayView1D<ulong, Stride1D.Dense> smallCycles)
ArrayView1D<uint, Stride1D.Dense> smallCycles)
{
ulong idx = (ulong)index.X;
// TODO: Replace these `%` computations with the precomputed Mod3/Mod5 tables so GPU kernels reuse cached residues
// instead of performing modulo operations that the benchmarks showed slower on wide sweeps.
ulong idxMod3 = idx % 3UL;
ulong idxMod5 = idx % 5UL;
ulong idxMod3 = idx.Mod3();
ulong idxMod5 = idx.Mod5();
// residue automaton
ulong step10 = (exponent.Mod10() << 1).Mod10();
ulong r10 = q0m10 + (step10 * idx).Mod10();
r10 -= (r10 >= 10UL) ? 10UL : 0UL;
bool shouldCheck = r10 != 5UL; // skip q ≡ 5 (mod 10); other residues handled by r8/r3/r5
if (shouldCheck)
{
r10 -= (r10 >= 10UL) ? 10UL : 0UL;
bool shouldCheck = r10 != 5UL; // skip q ≡ 5 (mod 10); other residues handled by r8/r3/r5
if (shouldCheck)
{
ulong step8 = ((exponent & 7UL) << 1) & 7UL;
ulong r8 = (q0m8 + ((step8 * idx) & 7UL)) & 7UL;
if (r8 != 1UL && r8 != 7UL)
{
shouldCheck = false;
}
if (r8 != 1UL && r8 != 7UL)
{
shouldCheck = false;
}
else
{
// TODO: Swap these `%` filters to the shared Mod helpers so the residue automaton matches the benchmarked
// bitmask-based implementation for GPU workloads.
ulong step3 = ((exponent % 3UL) << 1) % 3UL;
ulong step3 = (exponent.Mod3() << 1).Mod3();
ulong r3 = q0m3 + (step3 * idxMod3); r3 -= (r3 >= 3UL) ? 3UL : 0UL;
if (r3 == 0UL)
{
@@ -226,7 +225,7 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
}
else
{
ulong step5Loc = ((exponent % 5UL) << 1) % 5UL;
ulong step5Loc = (exponent.Mod5() << 1).Mod5();
ulong r5 = q0m5 + (step5Loc * idxMod5); if (r5 >= 5UL) r5 -= 5UL;
if (r5 == 0UL)
{
@@ -234,35 +233,33 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
}
}
}
}
}
if (!shouldCheck)
{
orders[index] = 0UL;
return;
}
if (!shouldCheck)
{
orders[index] = 0UL;
return;
}
// TODO: Is this expected that the value of kStart is modified?
GpuUInt128 k = kStart + (GpuUInt128)idx;
GpuUInt128 q = twoP;
q.Mul64(k);
q.Add(GpuUInt128.One);
ReadOnlyGpuUInt128 readOnlyQ = q.AsReadOnly();
GpuUInt128 q = twoP.Mul64(k) + GpuUInt128.One;
// Small-cycles in-kernel early rejection from device table
if (q.High == 0UL && q.Low < (ulong)smallCycles.Length)
{
ulong cycle = smallCycles[(int)q.Low];
if (cycle != 0UL && cycle <= exponent && (exponent % cycle) != 0UL)
uint cyc = smallCycles[(int)q.Low];
if (cyc != 0U)
{
orders[index] = 0UL;
return;
ulong cycle = cyc;
if (cycle <= exponent && (exponent % cycle) != 0UL)
{
orders[index] = 0UL;
return;
}
}
}
// TODO: q will be lowered by GpuUInt128.One here. Is this expected? Maybe that's why we have issues with the incremental kernel? Review other places where we use GpuUInt128 operators.
GpuUInt128 phi = q - GpuUInt128.One;
// TODO: Do we have the check here because we don't support high values or this is expected result?
if (phi.High != 0UL)
{
orders[index] = 0UL;
@@ -270,23 +267,21 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
}
ulong phi64 = phi.Low;
// TODO: Replace these Pow2Mod calls with the ProcessEightBitWindows helper when the shared windowed
// scalar implementation lands; benchmarks showed the windowed kernel trimming per-divisor runtime by ~2.4×.
if (GpuUInt128.Pow2Mod(phi64, in readOnlyQ) != GpuUInt128.One)
if (GpuUInt128.Pow2Mod(phi64, q) != GpuUInt128.One)
{
orders[index] = 0UL;
return;
}
GpuUInt128 halfPow = GpuUInt128.Pow2Mod(phi64 >> 1, in readOnlyQ) - GpuUInt128.One;
GpuUInt128 halfPow = GpuUInt128.Pow2Mod(phi64 >> 1, q) - GpuUInt128.One;
if (GpuUInt128.BinaryGcd(halfPow, q) != GpuUInt128.One)
{
orders[index] = 0UL;
return;
}
ulong div = FastDiv64Gpu(phi64, exponent, divMul);
GpuUInt128 divPow = GpuUInt128.Pow2Mod(div, in readOnlyQ) - GpuUInt128.One;
ulong div = phi64.FastDiv64(exponent, divMul);
GpuUInt128 divPow = GpuUInt128.Pow2Mod(div, q) - GpuUInt128.One;
if (GpuUInt128.BinaryGcd(divPow, q) != GpuUInt128.One)
{
orders[index] = 0UL;
@@ -299,7 +294,6 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
private static void OrderKernelScan(Index1D index, ulong exponent, ulong divMul, ArrayView<GpuUInt128> qs, ArrayView<ulong> orders)
{
GpuUInt128 q = qs[index];
ReadOnlyGpuUInt128 readOnlyQ = q.AsReadOnly();
GpuUInt128 phi = q - GpuUInt128.One;
if (phi.High != 0UL)
{
@@ -308,24 +302,22 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
}
ulong phi64 = phi.Low;
// TODO: Once the ProcessEightBitWindows helper is available, switch this order kernel to that faster
// Pow2Mod variant so cycle checks inherit the same gains observed in GpuPow2ModBenchmarks.
GpuUInt128 pow = GpuUInt128.Pow2Mod(phi64, in readOnlyQ);
GpuUInt128 pow = GpuUInt128.Pow2Mod(phi64, q);
if (pow != GpuUInt128.One)
{
orders[index] = 0UL;
return;
}
GpuUInt128 halfPow = GpuUInt128.Pow2Mod(phi64 >> 1, in readOnlyQ) - GpuUInt128.One;
GpuUInt128 halfPow = GpuUInt128.Pow2Mod(phi64 >> 1, q) - GpuUInt128.One;
if (GpuUInt128.BinaryGcd(halfPow, q) != GpuUInt128.One)
{
orders[index] = 0UL;
return;
}
ulong div = FastDiv64Gpu(phi64, exponent, divMul);
GpuUInt128 divPow = GpuUInt128.Pow2Mod(div, in readOnlyQ) - GpuUInt128.One;
ulong div = phi64.FastDiv64(exponent, divMul);
GpuUInt128 divPow = GpuUInt128.Pow2Mod(div, q) - GpuUInt128.One;
if (GpuUInt128.BinaryGcd(divPow, q) != GpuUInt128.One)
{
orders[index] = 0UL;
@@ -335,41 +327,41 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
orders[index] = exponent;
}
private void Dispose(bool disposing)
{
if (!disposedValue)
private void Dispose(bool disposing)
{
if (disposing)
{
_stream?.Dispose();
_stream = null;
if (!disposedValue)
{
if (disposing)
{
_stream?.Dispose();
_stream = null;
_gpu?.Dispose();
_gpu = null;
_gpu?.Dispose();
_gpu = null;
_executionLock = null;
_executionLock = null;
_limiter?.Dispose();
_limiter = null;
}
_limiter?.Dispose();
_limiter = null;
}
// free unmanaged resources (unmanaged objects) and override finalizer
// set large fields to null
disposedValue = true;
}
}
// free unmanaged resources (unmanaged objects) and override finalizer
// set large fields to null
disposedValue = true;
}
}
private static void Pow2ModKernelScan(Index1D index, ulong exponent, GpuUInt128 twoP, GpuUInt128 kStart, byte lastIsSeven, ulong _,
ResidueAutomatonArgs ra, ArrayView<ulong> orders,
ArrayView1D<ulong, Stride1D.Dense> smallCycles,
ArrayView1D<uint, Stride1D.Dense> smallCycles,
ArrayView1D<uint, Stride1D.Dense> smallPrimesLastOne,
ArrayView1D<uint, Stride1D.Dense> smallPrimesLastSeven,
ArrayView1D<ulong, Stride1D.Dense> smallPrimesPow2LastOne,
ArrayView1D<ulong, Stride1D.Dense> smallPrimesPow2LastSeven)
{
ulong idx = (ulong)index.X;
ulong idxMod3 = idx % 3UL;
ulong idxMod5 = idx % 5UL;
ulong idxMod3 = idx.Mod3();
ulong idxMod5 = idx.Mod5();
ulong r10 = ra.Q0M10 + (ra.Step10 * idx).Mod10(); r10 -= (r10 >= 10UL) ? 10UL : 0UL;
bool shouldCheck = r10 != 5UL;
if (shouldCheck)
@@ -407,25 +399,22 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
return;
}
// TODO: kStart is modified after this. Is this expected?
kStart.Add(idx);
GpuUInt128 q = twoP;
q.Mul64(kStart);
q.Add(GpuUInt128.One);
ReadOnlyGpuUInt128 readOnlyQ = q.AsReadOnly();
GpuUInt128 k = kStart + (GpuUInt128)idx;
GpuUInt128 q = twoP.Mul64(k) + GpuUInt128.One;
if (q.High == 0UL && q.Low < (ulong)smallCycles.Length)
{
ulong cycle = smallCycles[(int)q.Low];
// cycle should always be initialized if we're within array limit in production code
if (cycle != 0UL && cycle <= exponent && (exponent % cycle) != 0UL)
uint cyc = smallCycles[(int)q.Low];
if (cyc != 0U)
{
orders[index] = 0UL;
return;
ulong cycle = cyc;
if (cycle <= exponent && (exponent % cycle) != 0UL)
{
orders[index] = 0UL;
return;
}
}
}
// TODO: Swap Pow2Minus1Mod for the eight-bit window helper once the scalar version switches;
// benchmarks show the windowed variant cuts large-divisor scans from ~51 µs to ~21 µs.
if (GpuUInt128.Pow2Minus1Mod(exponent, in readOnlyQ) != GpuUInt128.Zero)
if (GpuUInt128.Pow2Minus1Mod(exponent, q) != GpuUInt128.Zero)
{
orders[index] = 0UL;
return;
@@ -452,22 +441,6 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
orders[index] = exponent;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong FastDiv64Gpu(ulong value, ulong divisor, ulong mul)
{
ulong quotient = GpuUInt128.MulHigh(value, mul);
GpuUInt128 remainder = new(0UL, value);
GpuUInt128 product = new(GpuUInt128.MulHigh(quotient, divisor), quotient * divisor);
remainder.Sub(product);
if (remainder.High != 0UL || remainder.Low >= divisor)
{
quotient++;
}
return quotient;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong Mod128By64(GpuUInt128 value, ulong modulus)
{
@@ -517,60 +490,61 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
}
private static void IncrementalOrderKernelScan(Index1D index, ulong exponent, GpuUInt128 twoP, GpuUInt128 kStart, byte lastIsSeven, ulong divMul,
ResidueAutomatonArgs ra, ArrayView<int> found, ArrayView1D<ulong, Stride1D.Dense> smallCycles)
ResidueAutomatonArgs ra, ArrayView<int> found, ArrayView1D<uint, Stride1D.Dense> smallCycles)
{
ulong idx = (ulong)index.X;
ulong idxMod3 = idx % 3UL;
ulong idxMod5 = idx % 5UL;
ulong idxMod3 = idx.Mod3();
ulong idxMod5 = idx.Mod5();
ulong r10 = ra.Q0M10 + (ra.Step10 * idx).Mod10(); r10 -= (r10 >= 10UL) ? 10UL : 0UL;
bool shouldCheck = r10 != 5UL;
if (shouldCheck)
{
ulong r8 = (ra.Q0M8 + ((ra.Step8 * idx) & 7UL)) & 7UL;
if (r8 != 1UL && r8 != 7UL)
{
shouldCheck = false;
}
else
{
ulong r3 = ra.Q0M3 + (ra.Step3 * idxMod3);
if (r3 >= 6UL) r3 -= 6UL;
if (r3 >= 3UL) r3 -= 3UL;
if (r3 == 0UL)
{
shouldCheck = false;
}
else
{
ulong r5 = ra.Q0M5 + (ra.Step5 * idxMod5);
if (r5 >= 15UL) r5 -= 15UL;
if (r5 >= 10UL) r5 -= 10UL;
if (r5 >= 5UL) r5 -= 5UL;
if (r5 == 0UL)
{
shouldCheck = false;
}
}
}
}
if (!shouldCheck)
{
return;
}
bool shouldCheck = r10 != 5UL;
if (shouldCheck)
{
ulong r8 = (ra.Q0M8 + ((ra.Step8 * idx) & 7UL)) & 7UL;
if (r8 != 1UL && r8 != 7UL)
{
shouldCheck = false;
}
else
{
ulong r3 = ra.Q0M3 + (ra.Step3 * idxMod3);
if (r3 >= 6UL) r3 -= 6UL;
if (r3 >= 3UL) r3 -= 3UL;
if (r3 == 0UL)
{
shouldCheck = false;
}
else
{
ulong r5 = ra.Q0M5 + (ra.Step5 * idxMod5);
if (r5 >= 15UL) r5 -= 15UL;
if (r5 >= 10UL) r5 -= 10UL;
if (r5 >= 5UL) r5 -= 5UL;
if (r5 == 0UL)
{
shouldCheck = false;
}
}
}
}
if (!shouldCheck)
{
return;
}
GpuUInt128 k = kStart + (GpuUInt128)idx;
GpuUInt128 q = twoP;
q.Mul64(k);
q.Add(GpuUInt128.One);
ReadOnlyGpuUInt128 readOnlyQ = q.AsReadOnly();
GpuUInt128 q = twoP.Mul64(k) + GpuUInt128.One;
// Small-cycles in-kernel early rejection from device table
if (q.High == 0UL && q.Low < (ulong)smallCycles.Length)
{
ulong cycle = smallCycles[(int)q.Low];
if (cycle != 0UL && cycle <= exponent && (exponent % cycle) != 0UL)
uint cyc = smallCycles[(int)q.Low];
if (cyc != 0U)
{
return;
ulong cycle = cyc;
if (cycle <= exponent && (exponent % cycle) != 0UL)
{
return;
}
}
}
GpuUInt128 phi = q - GpuUInt128.One;
@@ -580,21 +554,19 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
}
ulong phi64 = phi.Low;
// TODO: Upgrade this pow2mod order kernel to the ProcessEightBitWindows helper once available so GPU residue
// scans avoid the single-bit ladder that benchmarks found to be 2.3× slower on large exponents.
if (GpuUInt128.Pow2Mod(phi64, in readOnlyQ) != GpuUInt128.One)
if (GpuUInt128.Pow2Mod(phi64, q) != GpuUInt128.One)
{
return;
}
GpuUInt128 halfPow = GpuUInt128.Pow2Mod(phi64 >> 1, in readOnlyQ) - GpuUInt128.One;
GpuUInt128 halfPow = GpuUInt128.Pow2Mod(phi64 >> 1, q) - GpuUInt128.One;
if (GpuUInt128.BinaryGcd(halfPow, q) != GpuUInt128.One)
{
return;
}
ulong div = FastDiv64Gpu(phi64, exponent, divMul);
GpuUInt128 divPow = GpuUInt128.Pow2Mod(div, in readOnlyQ) - GpuUInt128.One;
ulong div = phi64.FastDiv64(exponent, divMul);
GpuUInt128 divPow = GpuUInt128.Pow2Mod(div, q) - GpuUInt128.One;
if (GpuUInt128.BinaryGcd(divPow, q) != GpuUInt128.One)
{
return;
@@ -604,64 +576,63 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
}
private static void Pow2ModOrderKernelScan(Index1D index, ulong exponent, GpuUInt128 twoP, GpuUInt128 kStart, byte lastIsSeven, ulong _,
ResidueAutomatonArgs ra, ArrayView<int> found, ArrayView1D<ulong, Stride1D.Dense> smallCycles)
ResidueAutomatonArgs ra, ArrayView<int> found, ArrayView1D<uint, Stride1D.Dense> smallCycles)
{
ulong idx = (ulong)index.X;
ulong idxMod3 = idx % 3UL;
ulong idxMod5 = idx % 5UL;
ulong idxMod3 = idx.Mod3();
ulong idxMod5 = idx.Mod5();
ulong r10 = ra.Q0M10 + (ra.Step10 * idx).Mod10(); r10 -= (r10 >= 10UL) ? 10UL : 0UL;
bool shouldCheck = r10 != 5UL;
if (shouldCheck)
{
ulong r8 = (ra.Q0M8 + ((ra.Step8 * idx) & 7UL)) & 7UL;
if (r8 != 1UL && r8 != 7UL)
{
shouldCheck = false;
}
else
{
ulong r3 = ra.Q0M3 + (ra.Step3 * idxMod3);
if (r3 >= 6UL) r3 -= 6UL;
if (r3 >= 3UL) r3 -= 3UL;
if (r3 == 0UL)
{
shouldCheck = false;
}
else
{
ulong r5 = ra.Q0M5 + (ra.Step5 * idxMod5);
if (r5 >= 15UL) r5 -= 15UL;
if (r5 >= 10UL) r5 -= 10UL;
if (r5 >= 5UL) r5 -= 5UL;
if (r5 == 0UL)
{
shouldCheck = false;
}
}
}
}
if (!shouldCheck)
{
return;
}
bool shouldCheck = r10 != 5UL;
if (shouldCheck)
{
ulong r8 = (ra.Q0M8 + ((ra.Step8 * idx) & 7UL)) & 7UL;
if (r8 != 1UL && r8 != 7UL)
{
shouldCheck = false;
}
else
{
ulong r3 = ra.Q0M3 + (ra.Step3 * idxMod3);
if (r3 >= 6UL) r3 -= 6UL;
if (r3 >= 3UL) r3 -= 3UL;
if (r3 == 0UL)
{
shouldCheck = false;
}
else
{
ulong r5 = ra.Q0M5 + (ra.Step5 * idxMod5);
if (r5 >= 15UL) r5 -= 15UL;
if (r5 >= 10UL) r5 -= 10UL;
if (r5 >= 5UL) r5 -= 5UL;
if (r5 == 0UL)
{
shouldCheck = false;
}
}
}
}
if (!shouldCheck)
{
return;
}
GpuUInt128 k = kStart + (GpuUInt128)idx;
GpuUInt128 q = twoP;
q.Mul64(k);
q.Add(GpuUInt128.One);
ReadOnlyGpuUInt128 readOnlyQ = q.AsReadOnly();
GpuUInt128 q = twoP.Mul64(k) + GpuUInt128.One;
// Small-cycles in-kernel early rejection from device table
if (q.High == 0UL && q.Low < (ulong)smallCycles.Length)
{
ulong cycle = smallCycles[(int)q.Low];
if (cycle != 0UL && cycle <= exponent && (exponent % cycle) != 0UL)
uint cyc = smallCycles[(int)q.Low];
if (cyc != 0U)
{
return;
ulong cycle = cyc;
if (cycle <= exponent && (exponent % cycle) != 0UL)
{
return;
}
}
}
// TODO: Replace this Pow2Mod check with the ProcessEightBitWindows helper once Pow2Minus1Mod adopts it;
// residue order scans will then benefit from the same 2× speedup the benchmarked windowed kernel delivered.
if (GpuUInt128.Pow2Mod(exponent, in readOnlyQ) != GpuUInt128.One)
if (GpuUInt128.Pow2Mod(exponent, q) != GpuUInt128.One)
{
return;
}
@@ -669,40 +640,40 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
Atomic.Or(ref found[0], 1);
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
// Enabled this when the struct is converted to a class
// GC.SuppressFinalize(this);
}
public readonly struct ExecutionScope : IDisposable
{
private readonly object? _lock;
public ExecutionScope(object? sync)
{
_lock = sync;
if (_lock is not null)
{
Monitor.Enter(_lock);
}
}
public void Dispose()
{
if (_lock is not null)
{
Monitor.Exit(_lock);
}
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
// Enabled this when the struct is converted to a class
// GC.SuppressFinalize(this);
}
public readonly struct ExecutionScope : IDisposable
{
private readonly object? _lock;
public ExecutionScope(object? sync)
{
_lock = sync;
if (_lock is not null)
{
Monitor.Enter(_lock);
}
}
public void Dispose()
{
if (_lock is not null)
{
Monitor.Exit(_lock);
}
}
}
}
}
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.
private static readonly ConcurrentDictionary<Accelerator, KernelContainer> KernelCache = new();
private static KernelContainer GetKernels(Accelerator accelerator)
{
@@ -711,7 +682,7 @@ public class GpuKernelPool
// Ensures the small cycles table is uploaded to the device for the given accelerator.
// Returns the ArrayView to pass into kernels (when kernels are extended to accept it).
public static ArrayView1D<ulong, Stride1D.Dense> EnsureSmallCyclesOnDevice(Accelerator accelerator)
public static ArrayView1D<uint, Stride1D.Dense> EnsureSmallCyclesOnDevice(Accelerator accelerator)
{
var kernels = GetKernels(accelerator);
if (kernels.SmallCycles is { } buffer)
@@ -720,15 +691,15 @@ 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)
{
if (kernels.SmallCycles is { } existing)
{
return existing.View;
}
var host = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot(); // TODO: Preload this device buffer during startup and keep it immutable so we can delete the lock above in favor of the preloaded snapshot.
var device = accelerator.Allocate1D<ulong>(host.Length);
var host = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
var device = accelerator.Allocate1D<uint>(host.Length);
device.View.CopyFromCPU(host);
kernels.SmallCycles = device;
return device.View;
@@ -746,7 +717,7 @@ 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)
{
if (kernels.SmallPrimesLastOne is { } existingLastOne &&
kernels.SmallPrimesLastSeven is { } existingLastSeven &&
@@ -779,19 +750,19 @@ public class GpuKernelPool
}
}
public static GpuKernelLease GetKernel(bool useGpuOrder)
{
var limiter = GpuPrimeWorkLimiter.Acquire();
var gpu = RentPreferred(preferCpu: !useGpuOrder);
var accelerator = gpu.Accelerator;
var kernels = GetKernels(accelerator);
return new(limiter, gpu, kernels);
}
public static GpuKernelLease GetKernel(bool useGpuOrder)
{
var limiter = GpuPrimeWorkLimiter.Acquire();
var gpu = RentPreferred(preferCpu: !useGpuOrder);
var accelerator = gpu.Accelerator;
var kernels = GetKernels(accelerator);
return new(limiter, gpu, kernels);
}
/// <summary>
/// Runs a GPU action with an acquired accelerator and stream.
/// </summary>
/// <param name="action">Action to run with (Accelerator, Stream).</param>
/// <summary>
/// Runs a GPU action with an acquired accelerator and stream.
/// </summary>
/// <param name="action">Action to run with (Accelerator, Stream).</param>
public static void Run(Action<Accelerator, AcceleratorStream> action)
{
var lease = GetKernel(useGpuOrder: true);
Loaded 100 of 151 files, more files were not shown because too many files have changed in this diff. Show more