From 4ba1a01b7ca5127a355dd552a20356411c4d1608 Mon Sep 17 00:00:00 2001 From: Ryan Scott White Date: Sun, 21 Dec 2025 21:25:58 -0800 Subject: [PATCH] Add division threshold sweep harness --- BigFloatLibrary/BigFloat.cs | 4 +- BigFloatLibrary/BigFloatNumerics.cs | 4 +- tools/ThresholdSweeps/Program.cs | 160 +++++++++++++++++-- tools/ThresholdSweeps/ThresholdSweeps.csproj | 3 + 4 files changed, 153 insertions(+), 18 deletions(-) diff --git a/BigFloatLibrary/BigFloat.cs b/BigFloatLibrary/BigFloat.cs index 7be25ff..a087491 100644 --- a/BigFloatLibrary/BigFloat.cs +++ b/BigFloatLibrary/BigFloat.cs @@ -720,8 +720,8 @@ private static BigFloat SelectMinMax(in BigFloat x, in BigFloat y, bool pickMin) /// public static BigFloat operator /(BigFloat numerator, BigFloat denominator) { - const int SMALL_NUMBER_THRESHOLD = 64; // Threshold for small number optimizations - + const int SMALL_NUMBER_THRESHOLD = 96; // Threshold for small number optimizations + // Early exit for zero divisor if (denominator.IsStrictZero) { diff --git a/BigFloatLibrary/BigFloatNumerics.cs b/BigFloatLibrary/BigFloatNumerics.cs index 3ef6769..799a620 100644 --- a/BigFloatLibrary/BigFloatNumerics.cs +++ b/BigFloatLibrary/BigFloatNumerics.cs @@ -16,10 +16,10 @@ internal static class BigFloatNumerics /// Burnikel–Ziegler division becomes more efficient than basic long division /// once either operand grows past this bit-length. The algorithm works in /// word-sized blocks; sweeps on .NET 8/9 (see docs/benchmarks/threshold-sweeps-*.md) - /// show the quadratic shift/subtract curve starting to climb by ~512 bits, so + /// show the quadratic shift/subtract curve starting to climb by ~768 bits, so /// we switch above that point to keep large divisions sub-quadratic for real-world inputs. /// - public const int BURNIKEL_ZIEGLER_THRESHOLD = 512; + public const int BURNIKEL_ZIEGLER_THRESHOLD = 768; /// /// Returns the bit-length of a mantissa using absolute value to keep diff --git a/tools/ThresholdSweeps/Program.cs b/tools/ThresholdSweeps/Program.cs index be0dd52..7829aa0 100644 --- a/tools/ThresholdSweeps/Program.cs +++ b/tools/ThresholdSweeps/Program.cs @@ -3,16 +3,27 @@ using System.Reflection; using System.Runtime.Versioning; using System.Text; +using BigFloatLibrary; namespace ThresholdSweeps; internal static class Program { private static readonly int[] MultiplicationSizes = new[] { 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144 }; - private static readonly int[] DivisionSizes = new[] { 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144 }; + private static readonly int[] DivisionEqualSizes = new[] { 32, 64, 128, 256, 512, 1024, 2048, 4096 }; + private static readonly (int NumeratorBits, int DenominatorBits)[] DivisionUnbalancedSizes = new[] + { + (1000, 10), + (2048, 32), + (4096, 64) + }; private const int MultiplyIterations = 300; - private const int DivisionIterations = 200; + private const int DivisionIterations = 120; + + private static readonly Func DivideSmallNumbers = CreateDivideDelegate(nameof(DivideSmallNumbers)); + private static readonly Func DivideStandard = CreateDivideDelegate(nameof(DivideStandard)); + private static readonly Func DivideLargeNumbers = CreateDivideDelegate(nameof(DivideLargeNumbers)); public static void Main() { @@ -51,18 +62,10 @@ public static void Main() } sb.AppendLine(); - sb.AppendLine("## Division (BigInteger.DivRem)"); - sb.AppendLine("Bit length | DivRem mean (ms)"); - sb.AppendLine("---|---:"); - - foreach (int bits in DivisionSizes) - { - var dividend = CreateBigInteger(bits, rng); - var divisor = CreateBigInteger(bits - 3, rng) | 1; // ensure non-zero, smaller divisor - - double divRem = Time(() => BigInteger.DivRem(dividend, divisor, out _), DivisionIterations * 3); - sb.AppendLine($"{bits} | {divRem:F3}"); - } + sb.AppendLine("## Division (BigFloat variants)"); + AppendDivisionEqualSizeSweep(sb, rng); + AppendDivisionUnbalancedSweep(sb, rng); + AppendDivisionRandomSweep(sb, rng, count: 8); } string output = sb.ToString(); @@ -86,6 +89,128 @@ private static double Time(Action action, int iterations) return (sw.Elapsed.TotalMilliseconds*1000) / iterations; } + private static void AppendDivisionEqualSizeSweep(StringBuilder sb, Random rng) + { + sb.AppendLine("### Equal-size operands"); + sb.AppendLine("Bit length | Small path mean (us) | Standard mean (us) | Large/BZ mean (us)"); + sb.AppendLine("---|---:|---:|---:"); + + var results = new List(DivisionEqualSizes.Length); + + foreach (int bits in DivisionEqualSizes) + { + DivisionResult result = BenchmarkDivision(bits, bits, rng); + results.Add(result); + sb.AppendLine($"{bits} | {result.SmallMean:F3} | {result.StandardMean:F3} | {result.LargeMean:F3}"); + } + + sb.AppendLine(); + sb.AppendLine(RenderCrossoverSummary(results, includeHeader: true)); + sb.AppendLine(); + } + + private static void AppendDivisionUnbalancedSweep(StringBuilder sb, Random rng) + { + sb.AppendLine("### Unbalanced operands"); + sb.AppendLine("Numerator bits | Denominator bits | Small path mean (us) | Standard mean (us) | Large/BZ mean (us)"); + sb.AppendLine("---|---|---:|---:|---:"); + + foreach ((int numeratorBits, int denominatorBits) in DivisionUnbalancedSizes) + { + DivisionResult result = BenchmarkDivision(numeratorBits, denominatorBits, rng); + sb.AppendLine($"{numeratorBits} | {denominatorBits} | {result.SmallMean:F3} | {result.StandardMean:F3} | {result.LargeMean:F3}"); + } + + sb.AppendLine(); + } + + private static void AppendDivisionRandomSweep(StringBuilder sb, Random rng, int count) + { + sb.AppendLine("### Random operand sizes"); + sb.AppendLine("Case | Numerator bits | Denominator bits | Small path mean (us) | Standard mean (us) | Large/BZ mean (us)"); + sb.AppendLine("---|---|---|---:|---:|---:"); + + for (int i = 0; i < count; i++) + { + int numeratorBits = rng.Next(32, 4097); + int denominatorBits = rng.Next(16, 2049); + DivisionResult result = BenchmarkDivision(numeratorBits, denominatorBits, rng); + sb.AppendLine($"{i + 1} | {numeratorBits} | {denominatorBits} | {result.SmallMean:F3} | {result.StandardMean:F3} | {result.LargeMean:F3}"); + } + + sb.AppendLine(); + } + + private static DivisionResult BenchmarkDivision(int numeratorBits, int denominatorBits, Random rng) + { + BigFloat numerator = CreateBigFloat(numeratorBits, rng); + BigFloat denominator = CreateBigFloat(denominatorBits, rng); + + _ = DivideSmallNumbers(numerator, denominator); + _ = DivideStandard(numerator, denominator); + _ = DivideLargeNumbers(numerator, denominator); + + int iterations = GetDivisionIterations(numeratorBits, denominatorBits); + + double smallMean = Time(() => DivideSmallNumbers(numerator, denominator), iterations); + double standardMean = Time(() => DivideStandard(numerator, denominator), iterations); + double largeMean = Time(() => DivideLargeNumbers(numerator, denominator), iterations); + + return new DivisionResult(numeratorBits, denominatorBits, smallMean, standardMean, largeMean); + } + + private static string RenderCrossoverSummary(IReadOnlyList results, bool includeHeader) + { + int? smallToStandard = FindCrossover(results, result => result.SmallMean, result => result.StandardMean); + int? standardToLarge = FindCrossover(results, result => result.StandardMean, result => result.LargeMean); + + var sb = new StringBuilder(); + if (includeHeader) + { + sb.AppendLine("Crossover summary:"); + } + + sb.AppendLine($"- Small vs standard: {(smallToStandard is null ? "no crossover in sweep" : $"{smallToStandard} bits")}."); + sb.AppendLine($"- Standard vs large/BZ: {(standardToLarge is null ? "no crossover in sweep" : $"{standardToLarge} bits")}."); + return sb.ToString(); + } + + private static int? FindCrossover(IReadOnlyList results, Func left, Func right) + { + foreach (DivisionResult result in results) + { + if (right(result) <= left(result)) + { + return result.NumeratorBits; + } + } + + return null; + } + + private static int GetDivisionIterations(int numeratorBits, int denominatorBits) + { + int maxBits = Math.Max(numeratorBits, denominatorBits); + return maxBits switch + { + >= 4096 => 30, + >= 2048 => 50, + >= 1024 => 80, + _ => DivisionIterations + }; + } + + private static Func CreateDivideDelegate(string name) + { + MethodInfo? method = typeof(BigFloat).GetMethod(name, BindingFlags.NonPublic | BindingFlags.Static); + if (method is null) + { + throw new InvalidOperationException($"Unable to locate BigFloat.{name} for threshold sweep."); + } + + return (Func)method.CreateDelegate(typeof(Func)); + } + private static (uint[] A, uint[] B) CreateOperands(int bits, Random rng) { return (ToLimbs(CreateBigInteger(bits, rng)), ToLimbs(CreateBigInteger(bits - 1, rng))); @@ -108,6 +233,11 @@ private static BigInteger CreateBigInteger(int bits, Random rng) return new BigInteger(bytes); } + private static BigFloat CreateBigFloat(int bits, Random rng) + { + return new BigFloat(CreateBigInteger(bits, rng)); + } + private static uint[] ToLimbs(BigInteger value) { byte[] bytes = value.ToByteArray(isUnsigned: true, isBigEndian: false); @@ -219,4 +349,6 @@ private static uint[] Add(uint[] a, uint[] b) return Trim(result); } + private sealed record DivisionResult(int NumeratorBits, int DenominatorBits, double SmallMean, double StandardMean, double LargeMean); + } diff --git a/tools/ThresholdSweeps/ThresholdSweeps.csproj b/tools/ThresholdSweeps/ThresholdSweeps.csproj index 30d6715..949b8a0 100644 --- a/tools/ThresholdSweeps/ThresholdSweeps.csproj +++ b/tools/ThresholdSweeps/ThresholdSweeps.csproj @@ -6,4 +6,7 @@ enable enable + + +