Files
even-perfect-numbers-scanner/EvenPerfectBitScanner.Benchmarks/MulMod64Benchmarks.cs
T

257 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
/// Benchmarks the legacy GPU-oriented shim that previously powered <see cref="ULongExtensions.MulMod64Gpu(ulong, ulong, ulong)"/>;
/// dense inputs still land in the 24.4–61.9 ns range while sparse operands remain closer to 3.6–7.3 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()
{
return MulMod64GpuLegacy(Input.Left, Input.Right, Input.Modulus);
}
/// <summary>
/// Benchmarks the deferred native-modulo shim that now lives only in this suite; tiny operands still excel around 2 ns
/// while dense datasets take 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()
{
return MulMod64GpuDeferredLegacy(Input.Left, Input.Right, Input.Modulus);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulMod64GpuLegacy(ulong a, ulong b, ulong modulus)
{
GpuUInt128 state = new(a % modulus);
return state.MulMod(b, modulus);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ulong MulMod64GpuDeferredLegacy(ulong a, ulong b, ulong modulus)
{
GpuUInt128 state = new(a);
return state.MulModWithNativeModulo(b, 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;
}
}
}