Add TODOs for divisor metadata caching

This commit is contained in:
Marek Lemańczyk committed 2025-10-01 22:52:21 +02:00
1 parent 513fc7acb0
commit 36d316d19c
53 files changed
+929 -393

No files matched your search

@@ -22,12 +22,28 @@ public class GpuPow2ModBenchmarks
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()
{
@@ -23,6 +23,13 @@ public class GpuUInt128AddBenchmarks
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()
{
@@ -31,6 +38,13 @@ public class GpuUInt128AddBenchmarks
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()
{
@@ -26,12 +26,26 @@ public class GpuUInt128BinaryGcdBenchmarks
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()
{
@@ -24,15 +24,31 @@ public class GpuUInt128Montgomery64Benchmarks
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);
}
@@ -23,6 +23,13 @@ public class GpuUInt128Mul64Benchmarks
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()
{
@@ -31,6 +38,12 @@ public class GpuUInt128Mul64Benchmarks
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()
{
@@ -39,6 +39,13 @@ public class GpuUInt128MulModBenchmarks
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()
{
@@ -48,6 +55,13 @@ public class GpuUInt128MulModBenchmarks
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()
{
@@ -33,12 +33,26 @@ public class GpuUInt128MulModByLimbBenchmarks
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()
{
@@ -22,6 +22,13 @@ public class GpuUInt128NativeModuloBenchmarks
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()
{
@@ -29,9 +36,18 @@ public class GpuUInt128NativeModuloBenchmarks
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);
}
@@ -33,6 +33,13 @@ public class GpuUInt128ScalarMulModBenchmarks
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()
{
@@ -41,6 +48,12 @@ public class GpuUInt128ScalarMulModBenchmarks
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()
{
@@ -21,6 +21,13 @@ public class GpuUInt128SubModScalarBenchmarks
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()
{
@@ -29,6 +36,13 @@ public class GpuUInt128SubModScalarBenchmarks
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()
{
@@ -30,7 +30,8 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
public IEnumerable<ulong> GetDivisors() => Divisors;
/// <summary>
/// Baseline single-step GPU loop kept as a reference; every unrolled variant we measured (pair, quad, oct, hex) outran it from divisors 17 through 8,388,607.
/// 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()
@@ -48,7 +49,8 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
}
/// <summary>
/// Do-while style loop; fastest among the legacy variants once divisors reach 131,071+, though still behind the unrolled loops.
/// 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()
@@ -66,7 +68,8 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
}
/// <summary>
/// Double-subtract variant retained for comparison; recent runs (17–8,388,607) show it trailing both the baseline and unrolled loops.
/// 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()
@@ -84,7 +87,8 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
}
/// <summary>
/// UInt128 modulo helper used for comparison; consistently the slowest due to wide arithmetic overhead.
/// 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()
@@ -102,7 +106,8 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
}
/// <summary>
/// Two-step unrolled loop that now trails the deeper octo/hex variants (e.g., 100.7 µs at 131,071 and 9.50 ms at 8,388,607) but still clears the baseline and legacy loops by a wide margin.
/// 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()
@@ -120,7 +125,8 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
}
/// <summary>
/// Four-step unrolled loop that used to lead across the board; recent measurements show it ceding the crown to the octo/hex versions, though it remains ~5% faster than the pair loop at divisor 131,071.
/// 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()
@@ -138,7 +144,8 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
}
/// <summary>
/// Eight-step unrolled loop that posted the best time at divisor 131,071 (~99.8 µs) and stayed within ~0.6% of the hex variant at 8,388,607.
/// 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()
@@ -156,7 +163,8 @@ public class MersenneDivisorCycleLengthGpuBenchmarks
}
/// <summary>
/// Sixteen-step unrolled loop that becomes the clear leader on the largest input we tested (8,388,607 → 9.25 ms) while sitting about 2% behind the octo version at divisor 131,071.
/// 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()
@@ -4,29 +4,38 @@ using BenchmarkDotNet.Jobs;
namespace EvenPerfectBitScanner.Benchmarks;
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)]
public class Mod8ULongBenchmarks
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ulong Mod8(ulong value) => value & 7UL;
[Params(8UL, 8191UL, 131071UL, 2147483647UL, ulong.MaxValue - 127UL)]
public ulong Value { get; set; }
/// <summary>
/// Fastest
/// </summary>
/// <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;
}
[Benchmark]
/// <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;
}
}
@@ -32,6 +32,12 @@ 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()
{
@@ -44,6 +50,12 @@ 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()
{
@@ -30,8 +30,13 @@ public class Mul64Benchmarks
public static IEnumerable<Mul64Input> GetInputs() => Inputs;
/// <summary>
/// Mul64 layout that keeps the high-word accumulation in locals.
/// 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()
{
@@ -39,8 +44,13 @@ public class Mul64Benchmarks
}
/// <summary>
/// Mul64 layout that folds the high-word expression into a single return value.
/// 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()
{
@@ -48,8 +58,13 @@ public class Mul64Benchmarks
}
/// <summary>
/// Mul64 layout shaped for the GPU helper to keep the cross products in registers.
/// 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()
{
@@ -57,8 +72,13 @@ public class Mul64Benchmarks
}
/// <summary>
/// Reference implementation that multiplies with BigInteger to expose the full 256-bit product.
/// 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()
{
@@ -27,25 +27,30 @@ public class MulHighBenchmarks
public static IEnumerable<MulHighInput> GetInputs() => Inputs;
/// <summary>
/// Baseline MulHigh implementation used across the scanner.
/// Mean runtimes (ns) by input: AllBitsSet 0.868, HighPowersOfTwo 1.133,
/// IncludesZero 0.820, LowHighWord 0.770, Mixed 1.325, ShiftHeavy 0.960.
/// Remains competitive on mixed or high-shift workloads but is consistently
/// slower than UInt128BuiltIn on both small (LowHighWord) and large inputs.
/// 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.
/// Mean runtimes (ns) by input: AllBitsSet 0.350, HighPowersOfTwo 0.338,
/// IncludesZero 0.333, LowHighWord 0.348, Mixed 0.340, ShiftHeavy 0.378.
/// Fastest variant across every dataset, with especially large wins on
/// mixed and shift-heavy cases while staying under 0.4 ns for all inputs.
/// 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()
{
@@ -53,13 +58,13 @@ public class MulHighBenchmarks
}
/// <summary>
/// Emulates UInt128 multiplication using the GPU-friendly GpuUInt128 type.
/// Mean runtimes (ns) by input: AllBitsSet 2.904, HighPowersOfTwo 2.537,
/// IncludesZero 2.468, LowHighWord 1.970, Mixed 1.728, ShiftHeavy 2.548.
/// Delivers functional parity with the built-in routine for GPU kernels,
/// but trails both the CPU UInt128 path and the current baseline on every
/// dataset, with its smallest gaps on mixed or low/high word patterns.
/// 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()
{
@@ -67,13 +72,13 @@ public class MulHighBenchmarks
}
/// <summary>
/// Reduces trailing zeros before multiplying smaller operands.
/// Mean runtimes (ns) by input: AllBitsSet 0.856, HighPowersOfTwo 0.862,
/// IncludesZero 1.122, LowHighWord 1.056, Mixed 1.692, ShiftHeavy 1.180.
/// Performs best when both operands share large power-of-two factors, but
/// incurs noticeable overhead on small/mixed values where reduction work
/// dominates the final multiply.
/// 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()
{
@@ -81,12 +86,13 @@ public class MulHighBenchmarks
}
/// <summary>
/// Schoolbook multiply splitting inputs into 32-bit halves.
/// Mean runtimes (ns) by input: AllBitsSet 0.940, HighPowersOfTwo 0.809,
/// IncludesZero 1.237, LowHighWord 0.858, Mixed 0.893, ShiftHeavy 0.879.
/// Strong option for high-power-of-two and shift-heavy inputs, but loses
/// ground on zero-heavy workloads where carry handling amplifies cost.
/// 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()
{
@@ -30,13 +30,12 @@ public class MulMod64Benchmarks
}
/// <summary>
/// Calls the UInt128-based <see cref="ULongExtensions.MulMod64(ulong, ulong, ulong)"/> helper, which stayed between
/// 3.66 ns and 3.80 ns for CrossWordBlend, MixedBitPattern, PrimeSizedModulus, SparseOperands, and ZeroOperands inputs,
/// and 3.73 ns for NearFullRange, making it the fastest option across every distribution.
/// 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.784 ns (1.00×), MixedBitPattern 3.790 ns, NearFullRange 3.727 ns, PrimeSizedModulus
/// 3.791 ns, SparseOperands 3.795 ns, ZeroOperands 3.659 ns.
/// 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()
@@ -45,13 +44,12 @@ public class MulMod64Benchmarks
}
/// <summary>
/// Calls the UInt128-based <see cref="ULongExtensions.MulMod(ulong, ulong, ulong)"/> helper, which stayed between
/// ?.?? ns and ?.?? ns for CrossWordBlend, MixedBitPattern, PrimeSizedModulus, SparseOperands, and ZeroOperands inputs,
/// and ?.?? ns for NearFullRange, making it the fastest option across every distribution.
/// 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 ?.??? ns (?.??×), MixedBitPattern ?.??? ns, NearFullRange ?.??? ns, PrimeSizedModulus
/// ?.??? ns, SparseOperands ?.??? ns, ZeroOperands ?.??? ns.
/// 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()
@@ -61,13 +59,12 @@ public class MulMod64Benchmarks
}
/// <summary>
/// Calls the UInt128-based <see cref="ULongExtensions.MulModSimplified(ulong, ulong, ulong)"/> helper, which stayed between
/// ?.?? ns and ?.?? ns for CrossWordBlend, MixedBitPattern, PrimeSizedModulus, SparseOperands, and ZeroOperands inputs,
/// and ?.?? ns for NearFullRange, making it the fastest option across every distribution.
/// 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 ?.??? ns (?.??×), MixedBitPattern ?.??? ns, NearFullRange ?.??? ns, PrimeSizedModulus
/// ?.??? ns, SparseOperands ?.??? ns, ZeroOperands ?.??? ns.
/// 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()
@@ -77,12 +74,12 @@ public class MulMod64Benchmarks
}
/// <summary>
/// Uses manual UInt128 multiplication in the benchmark method; dense operands cost 23.6–33.6 ns (6.2–8.9× slower than
/// baseline), while SparseOperands and ZeroOperands remain inexpensive at 3.58–3.87 ns.
/// 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 23.611 ns, MixedBitPattern 32.894 ns, NearFullRange 28.243 ns, PrimeSizedModulus
/// 33.576 ns, SparseOperands 3.870 ns, ZeroOperands 3.583 ns.
/// 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()
@@ -91,12 +88,12 @@ public class MulMod64Benchmarks
}
/// <summary>
/// Reduces both operands before multiplying with inline UInt128; reductions pay off for NearFullRange (4.52 ns) and
/// SparseOperands (4.39 ns) but dense patterns still cost 24.0–34.0 ns.
/// 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 24.012 ns, MixedBitPattern 33.051 ns, NearFullRange 4.522 ns, PrimeSizedModulus
/// 33.993 ns, SparseOperands 4.394 ns, ZeroOperands 4.211 ns.
/// 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()
@@ -104,6 +101,14 @@ public class MulMod64Benchmarks
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()
{
@@ -111,12 +116,12 @@ public class MulMod64Benchmarks
}
/// <summary>
/// Splits the UInt128 product into locals; performance mirrors the operand-inlining variant at 23.8–32.8 ns for dense
/// inputs while SparseOperands and ZeroOperands stay near 3.60–3.91 ns.
/// 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 23.789 ns, MixedBitPattern 32.817 ns, NearFullRange 28.666 ns, PrimeSizedModulus
/// 32.747 ns, SparseOperands 3.599 ns, ZeroOperands 3.906 ns.
/// 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()
@@ -125,12 +130,12 @@ public class MulMod64Benchmarks
}
/// <summary>
/// Combines locals with operand reduction; NearFullRange, SparseOperands, and ZeroOperands improve to 11.9–12.7 ns but
/// dense cases degrade further to 32.1–42.5 ns (8.5–11.3× slower than baseline).
/// 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 32.109 ns, MixedBitPattern 41.136 ns, NearFullRange 12.691 ns, PrimeSizedModulus
/// 42.527 ns, SparseOperands 12.288 ns, ZeroOperands 11.917 ns.
/// 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()
@@ -139,12 +144,12 @@ public class MulMod64Benchmarks
}
/// <summary>
/// Reconstructs the 128-bit product from MulHigh/MulLow pieces; dense patterns run in 24.5–34.8 ns, while
/// NearFullRange, SparseOperands, and ZeroOperands finish in 5.21–5.55 ns after the extra reduction.
/// 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 24.502 ns, MixedBitPattern 33.045 ns, NearFullRange 5.552 ns, PrimeSizedModulus
/// 34.793 ns, SparseOperands 5.214 ns, ZeroOperands 5.231 ns.
/// 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()
@@ -153,32 +158,35 @@ public class MulMod64Benchmarks
}
/// <summary>
/// Uses the GPU-friendly <see cref="ULongExtensions.MulMod64GpuCompatible(ulong, ulong, ulong)"/> helper; the shift-add
/// reduction drives dense patterns to 99.7–186.4 ns, while NearFullRange and SparseOperands finish around 40.1–40.7 ns
/// and ZeroOperands drop to 24.2 ns.
/// 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 (single-method run): CrossWordBlend 99.677 ns, MixedBitPattern 186.449 ns, NearFullRange 40.682 ns,
/// PrimeSizedModulus 112.461 ns, SparseOperands 40.067 ns, ZeroOperands 24.223 ns.
/// Observed means: CrossWordBlend 24.422 ns (6.86×), MixedBitPattern 61.850 ns, NearFullRange 7.267 ns,
/// PrimeSizedModulus 55.114 ns, SparseOperands 7.261 ns, ZeroOperands 3.587 ns.
/// </remarks>
[Benchmark]
public ulong 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 native-modulo GPU helper <see cref="ULongExtensions.MulMod64GpuCompatibleDeferred(ulong, ulong, ulong)"/>;
/// final reduction via `%` trims dense cases to 353–689 ns while NearFullRange, SparseOperands, and ZeroOperands drop to
/// 47.98 ns, 21.79 ns, and 5.24 ns respectively (single-method run).
/// 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 (single-method run): CrossWordBlend 353.778 ns, MixedBitPattern 564.491 ns, NearFullRange 47.981 ns,
/// PrimeSizedModulus 689.126 ns, SparseOperands 21.786 ns, ZeroOperands 5.236 ns.
/// 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);
}
@@ -49,7 +49,7 @@ public class Pow2MontgomeryModBenchmarks
}
/// <summary>
/// Baseline right-to-left Montgomery ladder that averages ~60 μs on the small set and ~205 μs on the large one.
/// 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()
@@ -66,7 +66,7 @@ public class Pow2MontgomeryModBenchmarks
}
/// <summary>
/// Uses a known divisor cycle to fold the exponent first, cutting runtime to ~0.69× on small inputs (~41.7 μs) and ~0.08× on large ones (~15.9 μs).
/// 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()
@@ -83,7 +83,7 @@ public class Pow2MontgomeryModBenchmarks
}
/// <summary>
/// Left-to-right Montgomery scan that trailed the baseline by ~15% on small inputs (~69.7 μs) and ~13% on large ones (~231 μs).
/// 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()
@@ -100,7 +100,7 @@ public class Pow2MontgomeryModBenchmarks
}
/// <summary>
/// Processes four exponent bits per loop, ending up ~8% slower on small cases (~65.2 μs) and ~10% slower on large ones (~225 μs).
/// 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()
@@ -117,7 +117,7 @@ public class Pow2MontgomeryModBenchmarks
}
/// <summary>
/// Windowed Montgomery ladder (width 5) that lagged by ~72% on small samples (~104 μs) and ~16% on large samples (~238 μs).
/// 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()
@@ -134,7 +134,7 @@ public class Pow2MontgomeryModBenchmarks
}
/// <summary>
/// Classic binary ladder without Montgomery reduction; it was ~3.6× slower on both small (~217 μs) and large (~733 μs) datasets.
/// 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()
@@ -151,7 +151,7 @@ public class Pow2MontgomeryModBenchmarks
}
/// <summary>
/// Precomputes all squarings once, landing ~44% slower on small inputs (~87.0 μs) and ~4.9× slower on large ones (~998 μs).
/// 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()
@@ -168,7 +168,7 @@ public class Pow2MontgomeryModBenchmarks
}
/// <summary>
/// Relies on <see cref="BigInteger.ModPow"/>, outperforming Montgomery on small inputs (~39.6 μs, 0.66×) but ~4.7× slower on large ones (~954 μs).
/// 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()
@@ -6,16 +6,15 @@ using PerfectNumbers.Core;
namespace EvenPerfectBitScanner.Benchmarks;
/// <remarks>
/// Benchmark results collected on Intel Xeon Platinum 8370C (Ubuntu 24.04, .NET 8.0, launchCount: 1, warmupCount: 1,
/// iterationCount: 5):
/// Small sample (~1e6 exponents and random odd moduli)
/// - MontgomeryWithoutCycle: 43.17 µs (baseline)
/// - MontgomeryWithPrecomputedCycle: 29.88 µs (0.69× baseline)
/// - MontgomeryWithGpuCycleComputation: 75.09 ms (1740× baseline, includes GPU cycle calculation)
/// 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: 145.32 µs (baseline)
/// - MontgomeryWithPrecomputedCycle: 10.93 µs (0.08× baseline)
/// - MontgomeryWithGpuCycleComputation: 16.55 µs (0.11× baseline, includes GPU cycle calculation)
/// - 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)]
@@ -59,7 +58,8 @@ public class Pow2MontgomeryModCycleComputationBenchmarks
}
/// <summary>
/// Baseline Montgomery reduction without precomputed cycle data.
/// 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()
@@ -76,7 +76,8 @@ public class Pow2MontgomeryModCycleComputationBenchmarks
}
/// <summary>
/// Montgomery reduction with a known cycle length, excluding the cost of calculating the cycle.
/// 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()
@@ -93,7 +94,8 @@ public class Pow2MontgomeryModCycleComputationBenchmarks
}
/// <summary>
/// Montgomery reduction with a cycle length computed on the fly using the GPU-friendly calculator.
/// 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()
@@ -9,6 +9,12 @@ 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()
{
@@ -20,6 +26,12 @@ 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()
{