Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73608e0c2f |
No files matched your search
Vendored
+1
-5
@@ -2,10 +2,6 @@
|
||||
"dotnet.defaultSolution": "FlexiCoder.sln",
|
||||
"chatgpt.openOnStartup": true,
|
||||
"cSpell.words": [
|
||||
"CCCCCCCDUL",
|
||||
"ILGPU",
|
||||
"mers",
|
||||
"primality",
|
||||
"suff"
|
||||
"ILGPU"
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
|
||||
(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,339 +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)
|
||||
{
|
||||
if (modulus <= 1UL || (modulus & 1UL) == 0UL)
|
||||
{
|
||||
return fallbackOrder != 0UL ? fallbackOrder : MersenneDivisorCycles.CalculateCycleLength(modulus);
|
||||
}
|
||||
|
||||
// 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,
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using BenchmarkDotNet.Configs;
|
||||
using System;
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
namespace EvenPerfectBitScanner.Benchmarks;
|
||||
@@ -7,23 +7,7 @@ public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var config = DefaultConfig.Instance.WithOptions(ConfigOptions.JoinSummary);
|
||||
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
|
||||
|
||||
// Pow2MontgomeryModCycleComputationBenchmarks benchmarks = new()
|
||||
// {
|
||||
// Scale = Pow2MontgomeryModCycleComputationBenchmarks.InputScale.Large
|
||||
// };
|
||||
|
||||
// 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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+1859
-1479
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,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,354 +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;
|
||||
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, prime, out ulong remainder);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Overflow);
|
||||
remainder.Should().Be(0UL);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2Mod_returns_success_for_supported_prime()
|
||||
{
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, 193UL, out ulong remainder);
|
||||
|
||||
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
|
||||
{
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, prime, out ulong remainder);
|
||||
|
||||
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
|
||||
{
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(16UL, 193UL, out ulong remainder);
|
||||
|
||||
status.Should().Be(GpuPow2ModStatus.Overflow);
|
||||
remainder.Should().Be(0UL);
|
||||
|
||||
GpuPow2ModStatus followUp = PrimeOrderGpuHeuristics.TryPow2Mod(4UL, 193UL, out ulong followUpRemainder);
|
||||
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 };
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders);
|
||||
|
||||
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 };
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, 257UL, remainders);
|
||||
|
||||
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 };
|
||||
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders);
|
||||
|
||||
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 };
|
||||
|
||||
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, 193UL, remainders);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryPow2ModBatch_throws_when_remainder_span_is_too_small()
|
||||
{
|
||||
ulong[] remainders = new ulong[1];
|
||||
ulong[] exponents = { 1UL, 2UL };
|
||||
|
||||
Action act = () => PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, 5UL, remainders);
|
||||
|
||||
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;
|
||||
|
||||
PrimeOrderCalculator.PrimeOrderResult result = PrimeOrderCalculator.Calculate(prime, null, 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;
|
||||
@@ -49,57 +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();
|
||||
pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLength(pair.divisor));
|
||||
}
|
||||
|
||||
uniqueDivisors.Should().BeEquivalentTo(expected);
|
||||
|
||||
var (_, completeCount) = MersenneDivisorCycles.FindLast(path);
|
||||
completeCount.Should().Be(expected.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +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);
|
||||
session.CheckDivisor(23UL, cycle23, primes, hits);
|
||||
hits.Should().ContainInOrder(new byte[] { 0, 0, 1, 0 });
|
||||
|
||||
Array.Fill(hits, (byte)0);
|
||||
ulong cycle31 = MersenneDivisorCycles.CalculateCycleLength(31UL);
|
||||
session.CheckDivisor(31UL, cycle31, primes, hits);
|
||||
hits.Should().ContainInOrder(new byte[] { 1, 0, 0, 0 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using System;
|
||||
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;
|
||||
@@ -104,21 +103,6 @@ public class MersenneNumberDivisorGpuTesterTests
|
||||
divisorsExhausted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Fast")]
|
||||
public void ByDivisor_gpu_tester_uses_cycle_remainders_per_divisor()
|
||||
{
|
||||
var tester = new MersenneNumberDivisorByDivisorGpuTester
|
||||
{
|
||||
GpuBatchSize = 8,
|
||||
};
|
||||
|
||||
tester.ConfigureFromMaxPrime(13UL);
|
||||
|
||||
tester.IsPrime(11UL, out bool divisorsExhausted).Should().BeFalse();
|
||||
divisorsExhausted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Fast")]
|
||||
public void ByDivisor_session_checks_divisors_across_primes()
|
||||
@@ -130,56 +114,12 @@ public class MersenneNumberDivisorGpuTesterTests
|
||||
ulong[] primes = { 5UL, 7UL, 11UL, 13UL };
|
||||
byte[] hits = new byte[primes.Length];
|
||||
|
||||
ulong cycle23 = MersenneDivisorCycles.CalculateCycleLength(23UL);
|
||||
session.CheckDivisor(23UL, cycle23, primes, hits);
|
||||
session.CheckDivisor(23UL, primes, hits);
|
||||
hits.Should().ContainInOrder(new byte[] { 0, 0, 1, 0 });
|
||||
|
||||
Array.Fill(hits, (byte)0);
|
||||
ulong cycle31 = MersenneDivisorCycles.CalculateCycleLength(31UL);
|
||||
session.CheckDivisor(31UL, cycle31, primes, hits);
|
||||
session.CheckDivisor(31UL, primes, hits);
|
||||
hits.Should().ContainInOrder(new byte[] { 1, 0, 0, 0 });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Fast")]
|
||||
public void ByDivisor_gpu_session_reuses_cycle_remainders_across_batches()
|
||||
{
|
||||
var tester = new MersenneNumberDivisorByDivisorGpuTester
|
||||
{
|
||||
GpuBatchSize = 2,
|
||||
};
|
||||
|
||||
tester.ConfigureFromMaxPrime(17UL);
|
||||
|
||||
using var session = tester.CreateDivisorSession();
|
||||
ulong[] primes = { 5UL, 7UL, 11UL, 13UL, 17UL };
|
||||
byte[] hits = new byte[primes.Length];
|
||||
|
||||
ulong cycle23 = MersenneDivisorCycles.CalculateCycleLength(23UL);
|
||||
session.CheckDivisor(23UL, cycle23, primes, hits);
|
||||
|
||||
hits.Should().ContainInOrder(new byte[] { 0, 0, 1, 0, 0 });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", "Fast")]
|
||||
public void ByDivisor_cpu_session_reuses_cycle_remainders_across_primes()
|
||||
{
|
||||
var tester = new MersenneNumberDivisorByDivisorCpuTester
|
||||
{
|
||||
BatchSize = 2,
|
||||
};
|
||||
|
||||
tester.ConfigureFromMaxPrime(17UL);
|
||||
|
||||
using var session = tester.CreateDivisorSession();
|
||||
ulong[] primes = { 5UL, 7UL, 11UL, 13UL, 17UL };
|
||||
byte[] hits = new byte[primes.Length];
|
||||
|
||||
ulong cycle23 = MersenneDivisorCycles.CalculateCycleLength(23UL);
|
||||
session.CheckDivisor(23UL, cycle23, primes, hits);
|
||||
|
||||
hits.Should().ContainInOrder(new byte[] { 0, 0, 1, 0, 0 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,192 +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,
|
||||
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,
|
||||
PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault);
|
||||
|
||||
PrimeOrderCalculator.PrimeOrderResult strict = PrimeOrderCalculator.Calculate(
|
||||
prime,
|
||||
previousOrder: null,
|
||||
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,
|
||||
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,
|
||||
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);
|
||||
|
||||
PrimeOrderCalculator.PrimeOrderResult heuristic = PrimeOrderCalculator.Calculate(
|
||||
prime: 239UL,
|
||||
previousOrder: null,
|
||||
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,
|
||||
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)]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,370 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using PerfectNumbers.Core;
|
||||
|
||||
namespace PerfectNumbers.Core.Cpu;
|
||||
|
||||
public sealed class MersenneNumberDivisorByDivisorCpuTester : IMersenneNumberDivisorByDivisorTester
|
||||
{
|
||||
private readonly object _sync = new();
|
||||
private readonly ConcurrentBag<DivisorScanSession> _sessionPool = new();
|
||||
private ulong _divisorLimit;
|
||||
private ulong _lastStatusDivisor;
|
||||
private bool _isConfigured;
|
||||
private int _batchSize = 1_024;
|
||||
|
||||
|
||||
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 = 0UL;
|
||||
ulong lastProcessed = 0UL;
|
||||
bool processedAll = false;
|
||||
|
||||
bool composite = CheckDivisors(
|
||||
prime,
|
||||
allowedMax,
|
||||
out lastProcessed,
|
||||
out processedAll,
|
||||
out processedCount);
|
||||
|
||||
if (processedCount > 0UL)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
UpdateStatusUnsafe(lastProcessed, 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;
|
||||
}
|
||||
|
||||
Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>? factorCache = null;
|
||||
ulong divisor = 3UL;
|
||||
while (divisor <= allowedMax)
|
||||
{
|
||||
processedCount++;
|
||||
lastProcessed = divisor;
|
||||
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(divisor);
|
||||
// TODO: Hoist MontgomeryDivisorData acquisition into the divisor-cycle cache so we reuse the
|
||||
// staged ProcessEightBitWindows-ready metadata instead of reloading the slower Montgomery
|
||||
// structs for every divisor.
|
||||
ulong divisorCycle;
|
||||
if (divisor <= PerfectNumberConstants.MaxQForDivisorCycles)
|
||||
{
|
||||
divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
|
||||
}
|
||||
else
|
||||
{
|
||||
factorCache ??= new Dictionary<ulong, MersenneDivisorCycles.FactorCacheEntry>(8);
|
||||
if (!MersenneDivisorCycles.TryCalculateCycleLengthForExponent(divisor, prime, factorCache, out divisorCycle) || divisorCycle == 0UL)
|
||||
{
|
||||
divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
|
||||
}
|
||||
}
|
||||
if (divisorCycle != 0UL)
|
||||
{
|
||||
byte hit = CheckDivisor(prime, divisorCycle, divisorData);
|
||||
if (hit != 0)
|
||||
{
|
||||
processedAll = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (divisor >= allowedMax - 1UL)
|
||||
{
|
||||
processedAll = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (divisor > ulong.MaxValue - 2UL)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
divisor += 2UL;
|
||||
// TODO: Replace this linear increment with the batched divisor-cycle walker validated in the
|
||||
// CPU by-divisor benchmarks so we advance directly to the next cached divisor candidate
|
||||
// instead of testing every odd integer.
|
||||
}
|
||||
|
||||
if (!processedAll)
|
||||
{
|
||||
processedAll = divisor > allowedMax;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static byte CheckDivisor(ulong prime, ulong divisorCycle, in MontgomeryDivisorData divisorData)
|
||||
{
|
||||
ulong residue = prime.Pow2MontgomeryModWithCycle(divisorCycle, divisorData);
|
||||
return residue == 1UL ? (byte)1 : (byte)0;
|
||||
}
|
||||
|
||||
private void UpdateStatusUnsafe(ulong lastProcessed, 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 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, ulong divisorCycle, ReadOnlySpan<ulong> primes, Span<byte> hits)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(DivisorScanSession));
|
||||
}
|
||||
|
||||
int length = primes.Length;
|
||||
if (length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (divisorCycle == 0UL)
|
||||
{
|
||||
divisorCycle = DivisorCycleCache.Shared.GetCycleLength(divisor);
|
||||
if (divisorCycle == 0UL)
|
||||
{
|
||||
hits.Clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
MontgomeryDivisorData divisorData = MontgomeryDivisorDataCache.Get(divisor);
|
||||
|
||||
var exponentStepper = new ExponentRemainderStepper(divisorData);
|
||||
if (!exponentStepper.IsValidModulus)
|
||||
{
|
||||
hits.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var stepper = new CycleRemainderStepper(divisorCycle);
|
||||
|
||||
ulong remainder = stepper.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 = stepper.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;
|
||||
}
|
||||
}
|
||||
@@ -1,335 +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];
|
||||
cycles[targetIndex] = MersenneDivisorCycles.CalculateCycleLength(divisors[targetIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -27,19 +28,19 @@ public sealed class KernelContainer
|
||||
{
|
||||
// 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;
|
||||
public Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong,
|
||||
ResidueAutomatonArgs, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>,
|
||||
ulong, ulong, ulong, ulong, ArrayView<ulong>, ArrayView1D<uint, Stride1D.Dense>>? Incremental;
|
||||
public 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>>? Pow2Mod;
|
||||
public Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong,
|
||||
ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>? IncrementalOrder;
|
||||
ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<uint, 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;
|
||||
@@ -123,7 +124,7 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -132,15 +133,15 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
{
|
||||
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 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<ulong, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<uint, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>, ArrayView1D<ulong, Stride1D.Dense>>>();
|
||||
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, ulong, ulong, ulong, ulong, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>> IncrementalKernel
|
||||
public readonly Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ulong, ulong, ulong, ulong, ArrayView<ulong>, ArrayView1D<uint, Stride1D.Dense>> IncrementalKernel
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -149,15 +150,15 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
{
|
||||
return KernelContainer.InitOnce(ref _kernels.Incremental, () =>
|
||||
{
|
||||
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ulong, ulong, ulong, ulong, ArrayView<ulong>, ArrayView1D<ulong, Stride1D.Dense>>(IncrementalKernelScan);
|
||||
var 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<ulong, Stride1D.Dense>>>();
|
||||
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ulong, ulong, ulong, ulong, ArrayView<ulong>, ArrayView1D<uint, Stride1D.Dense>>>();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public readonly Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>> IncrementalOrderKernel
|
||||
public readonly Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<uint, Stride1D.Dense>> IncrementalOrderKernel
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -166,15 +167,15 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
{
|
||||
return KernelContainer.InitOnce(ref _kernels.IncrementalOrder, () =>
|
||||
{
|
||||
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>(IncrementalOrderKernelScan);
|
||||
var 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<ulong, Stride1D.Dense>>>();
|
||||
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<ulong, Stride1D.Dense>> Pow2ModOrderKernel
|
||||
public readonly Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<uint, Stride1D.Dense>> Pow2ModOrderKernel
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -183,9 +184,9 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
{
|
||||
return KernelContainer.InitOnce(ref _kernels.Pow2ModOrder, () =>
|
||||
{
|
||||
var loaded = accel.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<ulong, Stride1D.Dense>>(Pow2ModOrderKernelScan);
|
||||
var 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<ulong, Stride1D.Dense>>>();
|
||||
return kernel.CreateLauncherDelegate<Action<AcceleratorStream, Index1D, ulong, GpuUInt128, GpuUInt128, byte, ulong, ResidueAutomatonArgs, ArrayView<int>, ArrayView1D<uint, Stride1D.Dense>>>();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -196,13 +197,11 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
|
||||
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();
|
||||
@@ -218,9 +217,7 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
}
|
||||
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)
|
||||
{
|
||||
@@ -228,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)
|
||||
{
|
||||
@@ -245,18 +242,20 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
}
|
||||
|
||||
GpuUInt128 k = kStart + (GpuUInt128)idx;
|
||||
GpuUInt128 q = twoP;
|
||||
q.Mul64(k);
|
||||
q.Add(GpuUInt128.One);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,8 +267,6 @@ 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, q) != GpuUInt128.One)
|
||||
{
|
||||
orders[index] = 0UL;
|
||||
@@ -283,7 +280,7 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
return;
|
||||
}
|
||||
|
||||
ulong div = FastDiv64Gpu(phi64, exponent, divMul);
|
||||
ulong div = phi64.FastDiv64(exponent, divMul);
|
||||
GpuUInt128 divPow = GpuUInt128.Pow2Mod(div, q) - GpuUInt128.One;
|
||||
if (GpuUInt128.BinaryGcd(divPow, q) != GpuUInt128.One)
|
||||
{
|
||||
@@ -305,8 +302,6 @@ 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, q);
|
||||
if (pow != GpuUInt128.One)
|
||||
{
|
||||
@@ -321,7 +316,7 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
return;
|
||||
}
|
||||
|
||||
ulong div = FastDiv64Gpu(phi64, exponent, divMul);
|
||||
ulong div = phi64.FastDiv64(exponent, divMul);
|
||||
GpuUInt128 divPow = GpuUInt128.Pow2Mod(div, q) - GpuUInt128.One;
|
||||
if (GpuUInt128.BinaryGcd(divPow, q) != GpuUInt128.One)
|
||||
{
|
||||
@@ -358,15 +353,15 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
|
||||
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)
|
||||
@@ -405,20 +400,20 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
}
|
||||
|
||||
GpuUInt128 k = kStart + (GpuUInt128)idx;
|
||||
GpuUInt128 q = twoP;
|
||||
q.Mul64(k);
|
||||
q.Add(GpuUInt128.One);
|
||||
GpuUInt128 q = twoP.Mul64(k) + GpuUInt128.One;
|
||||
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: 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, q) != GpuUInt128.Zero)
|
||||
{
|
||||
orders[index] = 0UL;
|
||||
@@ -446,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)
|
||||
{
|
||||
@@ -511,11 +490,11 @@ 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)
|
||||
@@ -553,17 +532,19 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
}
|
||||
|
||||
GpuUInt128 k = kStart + (GpuUInt128)idx;
|
||||
GpuUInt128 q = twoP;
|
||||
q.Mul64(k);
|
||||
q.Add(GpuUInt128.One);
|
||||
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;
|
||||
@@ -573,8 +554,6 @@ 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, q) != GpuUInt128.One)
|
||||
{
|
||||
return;
|
||||
@@ -586,7 +565,7 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
return;
|
||||
}
|
||||
|
||||
ulong div = FastDiv64Gpu(phi64, exponent, divMul);
|
||||
ulong div = phi64.FastDiv64(exponent, divMul);
|
||||
GpuUInt128 divPow = GpuUInt128.Pow2Mod(div, q) - GpuUInt128.One;
|
||||
if (GpuUInt128.BinaryGcd(divPow, q) != GpuUInt128.One)
|
||||
{
|
||||
@@ -597,11 +576,11 @@ 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)
|
||||
@@ -639,20 +618,20 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
}
|
||||
|
||||
GpuUInt128 k = kStart + (GpuUInt128)idx;
|
||||
GpuUInt128 q = twoP;
|
||||
q.Mul64(k);
|
||||
q.Add(GpuUInt128.One);
|
||||
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, q) != GpuUInt128.One)
|
||||
{
|
||||
return;
|
||||
@@ -694,7 +673,7 @@ public ref struct GpuKernelLease(IDisposable limiter, GpuContextLease gpu, Kerne
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -703,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)
|
||||
@@ -712,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;
|
||||
@@ -738,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 &&
|
||||
|
||||
@@ -8,8 +8,6 @@ public static class GpuPrimeWorkLimiter
|
||||
|
||||
public static IDisposable Acquire()
|
||||
{
|
||||
// TODO: Inline the pooled limiter guard from the GpuLimiterThroughputBenchmarks so prime-sieve
|
||||
// GPU jobs stop allocating Releaser instances on every acquisition.
|
||||
var sem = _semaphore;
|
||||
sem.Wait();
|
||||
return new Releaser(sem);
|
||||
@@ -29,8 +27,6 @@ public static class GpuPrimeWorkLimiter
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Switch to a shared limiter implementation with GpuWorkLimiter so we can coordinate CPU/GPU prime work using
|
||||
// the same semaphore pool without reallocating per adjustment.
|
||||
_semaphore = new SemaphoreSlim(value, value);
|
||||
_currentLimit = value;
|
||||
}
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace PerfectNumbers.Core.Gpu;
|
||||
|
||||
public struct GpuRational : IEquatable<GpuRational>, IComparable<GpuRational>
|
||||
{
|
||||
private const double Pow64 = 18446744073709551616.0;
|
||||
|
||||
public GpuUInt128 Numerator;
|
||||
|
||||
public GpuUInt128 Denominator;
|
||||
|
||||
public static readonly GpuRational Zero = new(GpuUInt128.Zero, GpuUInt128.One);
|
||||
|
||||
public static readonly GpuRational One = new(GpuUInt128.One, GpuUInt128.One);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public GpuRational(GpuUInt128 numerator)
|
||||
: this(numerator, GpuUInt128.One)
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public GpuRational(ulong numerator)
|
||||
: this(new GpuUInt128(numerator), GpuUInt128.One)
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public GpuRational(GpuUInt128 numerator, GpuUInt128 denominator)
|
||||
{
|
||||
if (denominator.IsZero)
|
||||
{
|
||||
throw new DivideByZeroException("GpuRational denominator cannot be zero.");
|
||||
}
|
||||
|
||||
Numerator = numerator;
|
||||
Denominator = denominator;
|
||||
Normalize();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public GpuRational(ulong numerator, ulong denominator)
|
||||
: this(new GpuUInt128(numerator), new GpuUInt128(denominator))
|
||||
{
|
||||
}
|
||||
|
||||
public readonly bool IsZero
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => Numerator.IsZero;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator GpuRational(GpuUInt128 value) => new(value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator GpuRational(ulong value) => new(value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(in GpuRational other)
|
||||
{
|
||||
GpuUInt128 leftDenominator = Denominator;
|
||||
GpuUInt128 rightDenominator = other.Denominator;
|
||||
|
||||
GpuUInt128 gcd = GpuUInt128.BinaryGcd(leftDenominator, rightDenominator);
|
||||
if (gcd != GpuUInt128.One)
|
||||
{
|
||||
leftDenominator = GpuUInt128.DivideExact(leftDenominator, gcd);
|
||||
rightDenominator = GpuUInt128.DivideExact(rightDenominator, gcd);
|
||||
// Reusing both denominator variables to store their reduced values because the unreduced inputs are no longer needed.
|
||||
}
|
||||
|
||||
Numerator.Mul(rightDenominator);
|
||||
GpuUInt128 scaledOtherNumerator = other.Numerator;
|
||||
scaledOtherNumerator.Mul(leftDenominator);
|
||||
Numerator.Add(scaledOtherNumerator);
|
||||
|
||||
Denominator = leftDenominator;
|
||||
// Denominator now holds the reduced left denominator; multiply by the original right denominator to form the LCM.
|
||||
Denominator.Mul(other.Denominator);
|
||||
Normalize();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Sub(in GpuRational other)
|
||||
{
|
||||
GpuUInt128 leftDenominator = Denominator;
|
||||
GpuUInt128 rightDenominator = other.Denominator;
|
||||
|
||||
GpuUInt128 gcd = GpuUInt128.BinaryGcd(leftDenominator, rightDenominator);
|
||||
if (gcd != GpuUInt128.One)
|
||||
{
|
||||
leftDenominator = GpuUInt128.DivideExact(leftDenominator, gcd);
|
||||
rightDenominator = GpuUInt128.DivideExact(rightDenominator, gcd);
|
||||
// Reusing the denominator variables to keep the reduced values since the raw denominators are no longer required.
|
||||
}
|
||||
|
||||
Numerator.Mul(rightDenominator);
|
||||
GpuUInt128 scaledOtherNumerator = other.Numerator;
|
||||
scaledOtherNumerator.Mul(leftDenominator);
|
||||
if (Numerator.CompareTo(scaledOtherNumerator) < 0)
|
||||
{
|
||||
throw new InvalidOperationException("Resulting GpuRational would be negative.");
|
||||
}
|
||||
|
||||
Numerator.Sub(scaledOtherNumerator);
|
||||
|
||||
Denominator = leftDenominator;
|
||||
Denominator.Mul(other.Denominator);
|
||||
Normalize();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Mul(in GpuRational other)
|
||||
{
|
||||
GpuUInt128 rightDenominator = other.Denominator;
|
||||
GpuUInt128 gcd = GpuUInt128.BinaryGcd(Numerator, rightDenominator);
|
||||
if (gcd != GpuUInt128.One)
|
||||
{
|
||||
Numerator = GpuUInt128.DivideExact(Numerator, gcd);
|
||||
rightDenominator = GpuUInt128.DivideExact(rightDenominator, gcd);
|
||||
// Reusing rightDenominator to keep the reduced value because the original denominator is no longer required.
|
||||
}
|
||||
|
||||
GpuUInt128 rightNumerator = other.Numerator;
|
||||
GpuUInt128 leftDenominator = Denominator;
|
||||
gcd = GpuUInt128.BinaryGcd(rightNumerator, leftDenominator);
|
||||
if (gcd != GpuUInt128.One)
|
||||
{
|
||||
rightNumerator = GpuUInt128.DivideExact(rightNumerator, gcd);
|
||||
leftDenominator = GpuUInt128.DivideExact(leftDenominator, gcd);
|
||||
// Reusing rightNumerator and leftDenominator after cross-cancellation keeps the intermediates compact.
|
||||
}
|
||||
|
||||
Numerator.Mul(rightNumerator);
|
||||
Denominator = leftDenominator;
|
||||
Denominator.Mul(rightDenominator);
|
||||
Normalize();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Div(in GpuRational other)
|
||||
{
|
||||
if (other.Numerator.IsZero)
|
||||
{
|
||||
throw new DivideByZeroException("Cannot divide by a zero GpuRational.");
|
||||
}
|
||||
|
||||
GpuUInt128 rightNumerator = other.Numerator;
|
||||
GpuUInt128 gcd = GpuUInt128.BinaryGcd(Numerator, rightNumerator);
|
||||
if (gcd != GpuUInt128.One)
|
||||
{
|
||||
Numerator = GpuUInt128.DivideExact(Numerator, gcd);
|
||||
rightNumerator = GpuUInt128.DivideExact(rightNumerator, gcd);
|
||||
// Reusing rightNumerator to store the reduced numerator keeps temporary allocations out of the hot path.
|
||||
}
|
||||
|
||||
GpuUInt128 rightDenominator = other.Denominator;
|
||||
GpuUInt128 leftDenominator = Denominator;
|
||||
gcd = GpuUInt128.BinaryGcd(rightDenominator, leftDenominator);
|
||||
if (gcd != GpuUInt128.One)
|
||||
{
|
||||
rightDenominator = GpuUInt128.DivideExact(rightDenominator, gcd);
|
||||
leftDenominator = GpuUInt128.DivideExact(leftDenominator, gcd);
|
||||
// Reusing both denominator variables here mirrors the cross-cancellation performed on the numerators.
|
||||
}
|
||||
|
||||
Numerator.Mul(rightDenominator);
|
||||
Denominator = leftDenominator;
|
||||
Denominator.Mul(rightNumerator);
|
||||
Normalize();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static GpuRational operator +(GpuRational left, GpuRational right)
|
||||
{
|
||||
left.Add(right);
|
||||
return left;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static GpuRational operator -(GpuRational left, GpuRational right)
|
||||
{
|
||||
left.Sub(right);
|
||||
return left;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static GpuRational operator *(GpuRational left, GpuRational right)
|
||||
{
|
||||
left.Mul(right);
|
||||
return left;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static GpuRational operator /(GpuRational left, GpuRational right)
|
||||
{
|
||||
left.Div(right);
|
||||
return left;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly int CompareTo(GpuRational other)
|
||||
{
|
||||
GpuUInt128 leftScaled = Numerator;
|
||||
leftScaled.Mul(other.Denominator);
|
||||
GpuUInt128 rightScaled = other.Numerator;
|
||||
rightScaled.Mul(Denominator);
|
||||
return leftScaled.CompareTo(rightScaled);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool Equals(GpuRational other) => Numerator == other.Numerator && Denominator == other.Denominator;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override readonly bool Equals(object? obj) => obj is GpuRational other && Equals(other);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override readonly int GetHashCode() => HashCode.Combine(Numerator, Denominator);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator ==(GpuRational left, GpuRational right) => left.Equals(right);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator !=(GpuRational left, GpuRational right) => !left.Equals(right);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator <(GpuRational left, GpuRational right) => left.CompareTo(right) < 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator >(GpuRational left, GpuRational right) => left.CompareTo(right) > 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator <=(GpuRational left, GpuRational right) => left.CompareTo(right) <= 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator >=(GpuRational left, GpuRational right) => left.CompareTo(right) >= 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly double ToDouble()
|
||||
{
|
||||
double numerator = Numerator.High == 0UL ? Numerator.Low : Numerator.High * Pow64 + Numerator.Low;
|
||||
double denominator = Denominator.High == 0UL ? Denominator.Low : Denominator.High * Pow64 + Denominator.Low;
|
||||
return numerator / denominator;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override readonly string ToString() => $"{Numerator}/{Denominator}";
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void Normalize()
|
||||
{
|
||||
if (Numerator.IsZero)
|
||||
{
|
||||
Denominator = GpuUInt128.One;
|
||||
return;
|
||||
}
|
||||
|
||||
GpuUInt128 gcd = GpuUInt128.BinaryGcd(Numerator, Denominator);
|
||||
if (gcd != GpuUInt128.One)
|
||||
{
|
||||
Numerator = GpuUInt128.DivideExact(Numerator, gcd);
|
||||
Denominator = GpuUInt128.DivideExact(Denominator, gcd);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -8,9 +8,6 @@ public static class GpuWorkLimiter
|
||||
|
||||
public static IDisposable Acquire()
|
||||
{
|
||||
// TODO: Replace the per-call Releaser allocation with the pooled struct-based guard from the
|
||||
// GpuLimiterThroughputBenchmarks so limiter acquisition does not allocate when we enter the
|
||||
// GPU scanning hot path.
|
||||
var sem = _semaphore;
|
||||
sem.Wait();
|
||||
return new Releaser(sem);
|
||||
@@ -33,8 +30,6 @@ public static class GpuWorkLimiter
|
||||
// Swap to a new semaphore for the new limit. We intentionally
|
||||
// do not dispose the old semaphore to avoid releasing handles
|
||||
// that may still be in use by existing Releaser instances.
|
||||
// TODO: Consolidate with GpuPrimeWorkLimiter so both limiters share a pooled SemaphoreSlim and avoid rebuilding the
|
||||
// limiter state whenever limits are adjusted from the CLI.
|
||||
_semaphore = new SemaphoreSlim(value, value);
|
||||
_currentLimit = value;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Numerics;
|
||||
using ILGPU;
|
||||
using ILGPU.Algorithms;
|
||||
using ILGPU.Runtime;
|
||||
@@ -13,25 +15,25 @@ public sealed class MersenneNumberDivisorGpuTester
|
||||
private Action<Index1D, ulong, GpuUInt128, ArrayView<byte>> GetKernel(Accelerator accelerator) =>
|
||||
_kernelCache.GetOrAdd(accelerator, acc => acc.LoadAutoGroupedStreamKernel<Index1D, ulong, GpuUInt128, ArrayView<byte>>(Kernel));
|
||||
|
||||
public static void BuildDivisorCandidates()
|
||||
{
|
||||
ulong[] snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
|
||||
(ulong divisor, uint cycle)[] list = new (ulong divisor, uint cycle)[snapshot.Length / 2];
|
||||
ulong cycle;
|
||||
int count = 0, i, snapshotLength = snapshot.Length;
|
||||
for (i = 3; i < snapshotLength; i += 2)
|
||||
{
|
||||
cycle = snapshot[i];
|
||||
if (cycle == 0U)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
public static void BuildDivisorCandidates()
|
||||
{
|
||||
uint[] snapshot = MersenneDivisorCycles.Shared.ExportSmallCyclesSnapshot();
|
||||
(ulong divisor, uint cycle)[] list = new (ulong divisor, uint cycle)[snapshot.Length / 2];
|
||||
uint cycle;
|
||||
int count = 0, i, snapshotLength = snapshot.Length;
|
||||
for (i = 3; i < snapshotLength; i += 2)
|
||||
{
|
||||
cycle = snapshot[i];
|
||||
if (cycle == 0U)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
list[count++] = ((ulong)i, (uint)cycle);
|
||||
}
|
||||
list[count++] = ((ulong)i, cycle);
|
||||
}
|
||||
|
||||
_divisorCandidates = count == 0 ? [] : list[..count];
|
||||
}
|
||||
_divisorCandidates = count == 0 ? [] : list[..count];
|
||||
}
|
||||
|
||||
public bool IsDivisible(ulong exponent, UInt128 divisor)
|
||||
{
|
||||
@@ -52,7 +54,7 @@ public sealed class MersenneNumberDivisorGpuTester
|
||||
return divisible;
|
||||
}
|
||||
|
||||
private static (ulong divisor, uint cycle)[]? _divisorCandidates = Array.Empty<(ulong divisor, uint cycle)>();
|
||||
private static (ulong divisor, uint cycle)[]? _divisorCandidates = Array.Empty<(ulong divisor, uint cycle)>();
|
||||
|
||||
public bool IsPrime(ulong p, UInt128 d, ulong divisorCyclesSearchLimit, out bool divisorsExhausted)
|
||||
{
|
||||
@@ -68,19 +70,17 @@ public sealed class MersenneNumberDivisorGpuTester
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_divisorCandidates is { Length: > 0 } candidates)
|
||||
{
|
||||
int len = candidates.Length;
|
||||
uint cycle;
|
||||
for (int k = 0; k < len; k++)
|
||||
{
|
||||
(ulong dSmall, cycle) = candidates[k];
|
||||
// TODO: Replace this `%` check with the shared divisor-cycle divisibility helper so GPU scans
|
||||
// reuse the cached cycle remainders instead of recomputing modulo for every candidate.
|
||||
if (p % cycle != 0UL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (_divisorCandidates is { Length: > 0 } candidates)
|
||||
{
|
||||
int len = candidates.Length;
|
||||
uint cycle;
|
||||
for (int k = 0; k < len; k++)
|
||||
{
|
||||
(ulong dSmall, cycle) = candidates[k];
|
||||
if (p % cycle != 0UL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsDivisible(p, dSmall))
|
||||
{
|
||||
@@ -110,16 +110,11 @@ public sealed class MersenneNumberDivisorGpuTester
|
||||
continue;
|
||||
}
|
||||
|
||||
UInt128 cycle128 = MersenneDivisorCycles.GetCycle(d);
|
||||
// TODO: When this lookup falls outside the preloaded snapshot, invoke the single-cycle GPU helper
|
||||
// instead of allowing GetCycle to allocate an entire block; the divisor-cycle benchmarks showed the
|
||||
// one-off computation keeping memory flat while still delivering the optimized stepping data.
|
||||
// TODO: Feed this check through the ProcessEightBitWindows-based residue tracker so we avoid the slow
|
||||
// UInt128 modulo on the CPU fallback highlighted by the Pow2Montgomery benchmarks.
|
||||
if ((UInt128)p % cycle128 != UInt128.Zero)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
UInt128 cycle128 = MersenneDivisorCycles.GetCycle(d);
|
||||
if ((UInt128)p % cycle128 != UInt128.Zero)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsDivisible(p, d))
|
||||
{
|
||||
@@ -145,11 +140,9 @@ public sealed class MersenneNumberDivisorGpuTester
|
||||
? 63 - XMath.LeadingZeroCount(divisor.Low)
|
||||
: 127 - XMath.LeadingZeroCount(divisor.High);
|
||||
mod = divisor;
|
||||
if (x == 0)
|
||||
{
|
||||
// TODO: Swap Pow2Mod for the ProcessEightBitWindows helper once Pow2Minus1Mod adopts it;
|
||||
// GpuPow2ModBenchmarks showed the windowed kernel cutting large divisors from ~51 µs to ~21 µs.
|
||||
baseVal = GpuUInt128.Pow2Mod(exponent, mod);
|
||||
if (x == 0)
|
||||
{
|
||||
baseVal = GpuUInt128.Pow2Mod(exponent, mod);
|
||||
result[0] = baseVal.High == 0UL && baseVal.Low == 1UL ? (byte)1 : (byte)0;
|
||||
return;
|
||||
}
|
||||
@@ -164,11 +157,9 @@ public sealed class MersenneNumberDivisorGpuTester
|
||||
baseVal = new GpuUInt128(0UL, 1UL << x);
|
||||
}
|
||||
|
||||
GpuUInt128 pow = baseVal;
|
||||
pow.ModPow(exponent / ux, mod);
|
||||
// TODO: Replace this trailing Pow2Mod call with the ProcessEightBitWindows variant once the shared
|
||||
// windowed helper lands so mixed-radix decompositions benefit from the same GPU speedup.
|
||||
var part2 = GpuUInt128.Pow2Mod(exponent % ux, mod);
|
||||
GpuUInt128 pow = baseVal;
|
||||
pow.ModPow(exponent / ux, mod);
|
||||
var part2 = GpuUInt128.Pow2Mod(exponent % ux, mod);
|
||||
GpuUInt128 product = pow;
|
||||
product.MulMod(part2, mod);
|
||||
result[0] = product.High == 0UL && product.Low == 1UL ? (byte)1 : (byte)0;
|
||||
|
||||
@@ -1,104 +1,107 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.InteropServices;
|
||||
using ILGPU;
|
||||
using ILGPU.Runtime;
|
||||
|
||||
namespace PerfectNumbers.Core.Gpu;
|
||||
|
||||
public class MersenneNumberIncrementalGpuTester(GpuKernelType kernelType, bool useGpuOrder)
|
||||
{
|
||||
private readonly GpuKernelType _kernelType = kernelType;
|
||||
private readonly bool _useGpuOrder = useGpuOrder;
|
||||
private readonly GpuKernelType _kernelType = kernelType;
|
||||
private readonly bool _useGpuOrder = useGpuOrder;
|
||||
|
||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||
{
|
||||
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
|
||||
var execution = gpuLease.EnterExecutionScope();
|
||||
var accelerator = gpuLease.Accelerator;
|
||||
var stream = gpuLease.Stream;
|
||||
int batchSize = GpuConstants.ScanBatchSize;
|
||||
UInt128 kStart = 1UL;
|
||||
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
|
||||
byte last = lastIsSeven ? (byte)1 : (byte)0;
|
||||
|
||||
var pow2Kernel = gpuLease.Pow2ModKernel; // TODO: Route this binding to the ProcessEightBitWindows kernel once the scalar helper lands so GPU incremental scans inherit the windowed speedup from GpuPow2ModBenchmarks.
|
||||
var incKernel = gpuLease.IncrementalKernel;
|
||||
exponent.Mod10_8_5_3Steps(out ulong step10, out ulong step8, out ulong step5, out ulong step3);
|
||||
GpuUInt128 twoPGpu = (GpuUInt128)twoP;
|
||||
var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
|
||||
ResiduePrimeViews primeViews = default;
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
public void Scan(ulong exponent, UInt128 twoP, bool lastIsSeven, UInt128 maxK, ref bool isPrime)
|
||||
{
|
||||
primeViews = GpuKernelPool.EnsureSmallPrimesOnDevice(accelerator);
|
||||
}
|
||||
|
||||
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
|
||||
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||
UInt128 remaining;
|
||||
int currentSize;
|
||||
int i;
|
||||
UInt128 q = UInt128.Zero;
|
||||
|
||||
try
|
||||
{
|
||||
while (kStart <= maxK && Volatile.Read(ref isPrime))
|
||||
{
|
||||
remaining = maxK - kStart + UInt128.One;
|
||||
currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining;
|
||||
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
||||
UInt128 q0 = twoP * kStart + UInt128.One;
|
||||
// TODO: Swap this multiply-and-add initialization for the divisor-cycle stepping helper so the GPU incremental path reuses cached offsets instead of recomputing twoP multiples every batch.
|
||||
var gpuLease = GpuKernelPool.GetKernel(_useGpuOrder);
|
||||
var execution = gpuLease.EnterExecutionScope();
|
||||
var accelerator = gpuLease.Accelerator;
|
||||
var stream = gpuLease.Stream;
|
||||
int batchSize = GpuConstants.ScanBatchSize; // large batch improves GPU occupancy
|
||||
UInt128 kStart = 1UL;
|
||||
ulong divMul = (ulong)((((UInt128)1 << 64) - UInt128.One) / exponent) + 1UL;
|
||||
byte last = lastIsSeven ? (byte)1 : (byte)0; // ILGPU kernels do not support bool parameters
|
||||
|
||||
var pow2Kernel = gpuLease.Pow2ModKernel;
|
||||
var incKernel = gpuLease.IncrementalKernel;
|
||||
ulong step10 = (exponent.Mod10() << 1).Mod10();
|
||||
ulong step8 = ((exponent & 7UL) << 1) & 7UL;
|
||||
ulong step3 = (exponent.Mod3() << 1).Mod3();
|
||||
ulong step5 = (exponent.Mod5() << 1).Mod5();
|
||||
GpuUInt128 twoPGpu = (GpuUInt128)twoP;
|
||||
var smallCyclesView = GpuKernelPool.EnsureSmallCyclesOnDevice(accelerator);
|
||||
ResiduePrimeViews primeViews = default;
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||
pow2Kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven,
|
||||
primeViews.LastOnePow2, primeViews.LastSevenPow2);
|
||||
}
|
||||
else
|
||||
{
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
incKernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
q0m10, q0m8, q0m3, q0m5, orderBuffer.View, smallCyclesView);
|
||||
primeViews = GpuKernelPool.EnsureSmallPrimesOnDevice(accelerator);
|
||||
}
|
||||
|
||||
stream.Synchronize();
|
||||
orderBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(orders), currentSize);
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
var orderBuffer = accelerator.Allocate1D<ulong>(batchSize);
|
||||
// Avoid giant stack allocations that can trigger StackOverflow when batchSize is large.
|
||||
// Rent a reusable array from the shared pool instead.
|
||||
ulong[] orderArray = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||
UInt128 remaining;
|
||||
int currentSize;
|
||||
int i;
|
||||
UInt128 q = UInt128.Zero;
|
||||
try
|
||||
{
|
||||
for (i = 0; i < currentSize; i++)
|
||||
{
|
||||
if (orders[i] != 0UL)
|
||||
while (kStart <= maxK && Volatile.Read(ref isPrime))
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
break;
|
||||
remaining = maxK - kStart + UInt128.One;
|
||||
currentSize = remaining > (UInt128)batchSize ? batchSize : (int)remaining;
|
||||
Span<ulong> orders = orderArray.AsSpan(0, currentSize);
|
||||
// Precompute residue automaton bases for this batch
|
||||
UInt128 q0 = twoP * kStart + UInt128.One;
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
var kernelArgs = new ResidueAutomatonArgs(q0m10, step10, q0m8, step8, q0m3, step3, q0m5, step5);
|
||||
pow2Kernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
kernelArgs, orderBuffer.View, smallCyclesView, primeViews.LastOne, primeViews.LastSeven,
|
||||
primeViews.LastOnePow2, primeViews.LastSevenPow2);
|
||||
}
|
||||
else
|
||||
{
|
||||
q0.Mod10_8_5_3(out ulong q0m10, out ulong q0m8, out ulong q0m5, out ulong q0m3);
|
||||
incKernel(stream, currentSize, exponent, twoPGpu, (GpuUInt128)kStart, last, divMul,
|
||||
q0m10, q0m8, q0m3, q0m5, orderBuffer.View, smallCyclesView);
|
||||
}
|
||||
|
||||
stream.Synchronize();
|
||||
orderBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(orders), currentSize);
|
||||
if (_kernelType == GpuKernelType.Pow2Mod)
|
||||
{
|
||||
for (i = 0; i < currentSize; i++)
|
||||
{
|
||||
if (orders[i] != 0UL)
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
q = twoP * kStart + UInt128.One;
|
||||
for (i = 0; i < currentSize && Volatile.Read(ref isPrime); i++, q += twoP)
|
||||
{
|
||||
if (orders[i] != 0UL && q.IsPrimeCandidate())
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kStart += (UInt128)currentSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
finally
|
||||
{
|
||||
q = twoP * kStart + UInt128.One;
|
||||
// TODO: Replace this per-lane multiplication and the subsequent q += twoP stride with the shared divisor-cycle ladder so each iteration advances by cached cycle increments and automatically triggers on-demand GPU computations when the snapshot lacks the requested cycle.
|
||||
for (i = 0; i < currentSize && Volatile.Read(ref isPrime); i++, q += twoP)
|
||||
{
|
||||
if (orders[i] != 0UL && q.IsPrimeCandidate())
|
||||
{
|
||||
Volatile.Write(ref isPrime, false);
|
||||
}
|
||||
}
|
||||
ArrayPool<ulong>.Shared.Return(orderArray);
|
||||
orderBuffer.Dispose();
|
||||
execution.Dispose();
|
||||
gpuLease.Dispose();
|
||||
}
|
||||
|
||||
kStart += (UInt128)currentSize;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<ulong>.Shared.Return(orderArray);
|
||||
orderBuffer.Dispose();
|
||||
execution.Dispose();
|
||||
gpuLease.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loaded 100 of 139 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user