Add shift-first multiply helper and document benchmarks

This commit is contained in:
Marek Lemańczyk committed 2025-10-04 20:01:10 +02:00
1 parent 40dff45a81
commit 84ebbed98b
15 files changed
+1333 -100

No files matched your search

+160
View File
@@ -0,0 +1,160 @@
#r "nuget: ILGPU, 1.5.3"
#r "nuget: PeterO.Numbers, 1.8.1"
using System;
using System.Numerics;
using ILGPU;
using ILGPU.Runtime;
using ILGPU.Runtime.CPU;
using PeterO.Numbers;
var context = Context.CreateDefault();
var accelerator = context.CreateCPUAccelerator(0);
try
{
Console.WriteLine("ILGPU numeric type support probe (CPU accelerator)");
Console.WriteLine();
ProbeResult[] results =
[
ProbeBigInteger(accelerator),
ProbeEInteger(accelerator),
ProbeERational(accelerator)
];
foreach (ProbeResult result in results)
{
Console.WriteLine($"{result.Type}: {(result.Supported ? "supported" : "unsupported")}");
Console.WriteLine($" Details: {result.Details}");
Console.WriteLine();
}
}
finally
{
accelerator.Dispose();
context.Dispose();
}
record ProbeResult(string Type, bool Supported, string Details)
{
public static ProbeResult Success(string type, string details) => new(type, true, details);
public static ProbeResult Failure(string type, Exception exception) =>
new(type, false, $"{exception.GetType().Name}: {exception.Message}");
public static ProbeResult Failure(string type, string details) => new(type, false, details);
}
static ProbeResult ProbeBigInteger(Accelerator accelerator)
{
try
{
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<int, Stride1D.Dense>>(BigIntegerKernel);
using var buffer = accelerator.Allocate1D<int>(1);
var extent = new Index1D((int)buffer.Length);
kernel(extent, buffer.View);
accelerator.Synchronize();
return ProbeResult.Success(
"System.Numerics.BigInteger",
"Addition, subtraction, multiplication, division, and modulo compile on the CPU accelerator.");
}
catch (Exception ex)
{
return ProbeResult.Failure("System.Numerics.BigInteger", ex);
}
}
static ProbeResult ProbeEInteger(Accelerator accelerator)
{
try
{
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<int, Stride1D.Dense>>(EIntegerKernel);
using var buffer = accelerator.Allocate1D<int>(1);
kernel(new Index1D((int)buffer.Length), buffer.View);
accelerator.Synchronize();
return ProbeResult.Success(
"PeterO.Numbers.EInteger",
"Addition, subtraction, multiplication, division, and remainder compile on the CPU accelerator.");
}
catch (Exception ex)
{
return ProbeResult.Failure("PeterO.Numbers.EInteger", ex);
}
}
static ProbeResult ProbeERational(Accelerator accelerator)
{
bool hasRemainder = HasERationalRemainder();
try
{
var kernel = accelerator.LoadAutoGroupedStreamKernel<Index1D, ArrayView1D<int, Stride1D.Dense>>(ERationalKernel);
using var buffer = accelerator.Allocate1D<int>(1);
kernel(new Index1D((int)buffer.Length), buffer.View);
accelerator.Synchronize();
string details = "Addition, subtraction, multiplication, and division compile on the CPU accelerator.";
details += hasRemainder
? " ERational exposes a remainder API that requires further investigation."
: " ERational does not expose a remainder/modulo API.";
return ProbeResult.Success("PeterO.Numbers.ERational", details);
}
catch (Exception ex)
{
string details = $"{ex.GetType().Name}: {ex.Message}";
if (!hasRemainder)
{
details += " ERational does not expose a remainder/modulo API.";
}
return ProbeResult.Failure("PeterO.Numbers.ERational", details);
}
}
static void BigIntegerKernel(Index1D index, ArrayView1D<int, Stride1D.Dense> data)
{
BigInteger value = new BigInteger(data[index]);
value += BigInteger.One;
value -= BigInteger.One;
value *= new BigInteger(2);
value /= new BigInteger(3);
value %= new BigInteger(5);
int comparison = value.CompareTo(BigInteger.Zero);
data[index] = comparison;
}
static void EIntegerKernel(Index1D index, ArrayView1D<int, Stride1D.Dense> data)
{
EInteger value = EInteger.FromInt32(data[index]);
value = value + EInteger.One;
value = value - EInteger.One;
value = value * EInteger.FromInt32(2);
value = value.Divide(EInteger.FromInt32(3));
value = value.Remainder(EInteger.FromInt32(5));
int comparison = value.CompareTo(EInteger.Zero);
data[index] = comparison;
}
static void ERationalKernel(Index1D index, ArrayView1D<int, Stride1D.Dense> data)
{
ERational value = ERational.FromInt32(data[index]);
ERational adjusted = (value + ERational.FromInt32(1)) - ERational.FromInt32(1);
ERational product = adjusted * ERational.FromInt32(2);
ERational quotient = product / ERational.FromInt32(3);
int comparison = quotient.CompareTo(ERational.FromInt32(0));
data[index] = comparison;
}
static bool HasERationalRemainder()
{
foreach (var method in typeof(ERational).GetMethods())
{
if (string.Equals(method.Name, "Remainder", StringComparison.Ordinal))
{
return true;
}
}
return false;
}
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../PerfectNumbers.Core/PerfectNumbers.Core.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,95 @@
using System.Diagnostics;
using PerfectNumbers.Core;
namespace HeuristicMultiplyShiftTiming;
internal static class Program
{
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"),
];
private const int Iterations = 200_000_000;
public static void Main()
{
Console.WriteLine($"Timing each method for {Iterations:N0} iterations per input...\n");
foreach (MultiplyShiftInput input in Inputs)
{
Console.WriteLine($"Input: {input.Name} (value=0x{input.Value:X16}, multiplier={input.Multiplier}, shift={input.Shift})");
Measurement safe = MeasureSafe(input);
Console.WriteLine($" MultiplyShiftRight (UInt128 product): {safe.AverageNanoseconds:F3} ns | checksum=0x{safe.Checksum:X16}");
Measurement shiftFirst = MeasureShiftFirst(input);
Console.WriteLine($" MultiplyShiftRightShiftFirst (shift-first): {shiftFirst.AverageNanoseconds:F3} ns | checksum=0x{shiftFirst.Checksum:X16}");
Measurement naive = MeasureNaive(input);
Console.WriteLine($" Naive ((value * multiplier) >> shift): {naive.AverageNanoseconds:F3} ns | checksum=0x{naive.Checksum:X16}\n");
}
}
private static Measurement MeasureSafe(MultiplyShiftInput input)
{
ulong checksum = 0UL;
Stopwatch sw = Stopwatch.StartNew();
ulong value = input.Value;
for (int i = 0; i < Iterations; i++)
{
checksum += HeuristicArithmetic.MultiplyShiftRight(value, input.Multiplier, input.Shift);
value += 1UL;
}
sw.Stop();
return CreateMeasurement(sw, checksum);
}
private static Measurement MeasureShiftFirst(MultiplyShiftInput input)
{
ulong checksum = 0UL;
Stopwatch sw = Stopwatch.StartNew();
ulong value = input.Value;
for (int i = 0; i < Iterations; i++)
{
checksum += HeuristicArithmetic.MultiplyShiftRightShiftFirst(value, input.Multiplier, input.Shift);
value += 1UL;
}
sw.Stop();
return CreateMeasurement(sw, checksum);
}
private static Measurement MeasureNaive(MultiplyShiftInput input)
{
ulong checksum = 0UL;
Stopwatch sw = Stopwatch.StartNew();
ulong value = input.Value;
for (int i = 0; i < Iterations; i++)
{
checksum += (value * input.Multiplier) >> input.Shift;
value += 1UL;
}
sw.Stop();
return CreateMeasurement(sw, checksum);
}
private static Measurement CreateMeasurement(Stopwatch sw, ulong checksum)
{
double elapsedNanoseconds = sw.ElapsedTicks * (1_000_000_000.0 / Stopwatch.Frequency);
double averageNanoseconds = elapsedNanoseconds / Iterations;
return new Measurement(averageNanoseconds, checksum);
}
private readonly record struct MultiplyShiftInput(ulong Value, ulong Multiplier, int Shift, string Name);
private readonly record struct Measurement(double AverageNanoseconds, ulong Checksum);
}