From 67d9f4be55c42fc89e5e164d254d83a05f54da3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sun, 12 Oct 2025 18:54:05 +0200 Subject: [PATCH] Restore original MultiplyAddInline logic --- .../MutableUInt128MultiplyAddBenchmarks.cs | 67 +++++++++++++++++++ EvenPerfectBitScanner.Benchmarks/Program.cs | 8 ++- PerfectNumbers.Core/MutableUInt128.cs | 43 ++++++++++++ 3 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 EvenPerfectBitScanner.Benchmarks/MutableUInt128MultiplyAddBenchmarks.cs diff --git a/EvenPerfectBitScanner.Benchmarks/MutableUInt128MultiplyAddBenchmarks.cs b/EvenPerfectBitScanner.Benchmarks/MutableUInt128MultiplyAddBenchmarks.cs new file mode 100644 index 0000000..772d4c6 --- /dev/null +++ b/EvenPerfectBitScanner.Benchmarks/MutableUInt128MultiplyAddBenchmarks.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Numerics; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using PerfectNumbers.Core; + +namespace EvenPerfectBitScanner.Benchmarks; + +[ShortRunJob(RuntimeMoniker.Net80)] +[MemoryDiagnoser] +public class MutableUInt128MultiplyAddBenchmarks +{ + private static readonly MultiplyAddInput[] Inputs = + [ + new(Create(0UL, 0x0000_0000_0000_0007UL), 0x3UL, 0x5UL, "TinyOperands"), + new(Create(0x1234_5678_9ABC_DEF0UL, 0x0FED_CBA9_8765_4321UL), 0xFFFF_FFFF_0000_0001UL, 0x1357_9BDF_2468_ACEEUL, "MixedOperands"), + new(Create(0xFFFF_FFFF_FFFF_FFFFUL, 0xFFFF_FFFF_FFFF_FFFFUL), 0xFFFF_FFFF_FFFF_FFFBUL, 0xFFFF_FFFF_FFFF_FFF7UL, "NearMaxOperands"), + ]; + + [ParamsSource(nameof(GetInputs))] + public MultiplyAddInput Input { get; set; } + + public static IEnumerable GetInputs() => Inputs; + + private static UInt128 Create(ulong high, ulong low) + { + return ((UInt128)high << 64) | low; + } + + /// + /// Baseline MultiplyAdd implementation that chains Multiply and Add; measured 4.59 ns with mixed operands, + /// 4.68 ns when both limbs and inputs are near their maxima, and 4.49 ns with tiny operands. + /// + /// + /// Observed means: MixedOperands 4.593 ns, NearMaxOperands 4.681 ns, TinyOperands 4.487 ns. + /// + [Benchmark(Baseline = true)] + public UInt128 MultiplyAddChained() + { + MutableUInt128 value = new(Input.InitialValue); + value.MultiplyAdd(Input.Multiplier, Input.Addend); + return value.ToUInt128(); + } + + /// + /// Fully inlined MultiplyAdd implementation that kept all arithmetic inside the method; landed at 4.11 ns on mixed operands, + /// 4.25 ns on near-max inputs, and 4.27 ns on tiny operands. + /// + /// + /// Observed means: MixedOperands 4.109 ns, NearMaxOperands 4.253 ns, TinyOperands 4.271 ns. + /// + [Benchmark] + public UInt128 MultiplyAddFullyInlined() + { + MutableUInt128 value = new(Input.InitialValue); + value.MultiplyAddInline(Input.Multiplier, Input.Addend); + return value.ToUInt128(); + } + + public readonly record struct MultiplyAddInput(UInt128 InitialValue, ulong Multiplier, ulong Addend, string Name) + { + public override string ToString() + { + return Name; + } + } +} diff --git a/EvenPerfectBitScanner.Benchmarks/Program.cs b/EvenPerfectBitScanner.Benchmarks/Program.cs index b4e8c81..c979d41 100644 --- a/EvenPerfectBitScanner.Benchmarks/Program.cs +++ b/EvenPerfectBitScanner.Benchmarks/Program.cs @@ -8,8 +8,12 @@ public static class Program { public static void Main(string[] args) { - // var config = DefaultConfig.Instance.WithOptions(ConfigOptions.JoinSummary | ConfigOptions.DisableOptimizationsValidator); - // BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config); + if (args.Length > 0) + { + IConfig config = DefaultConfig.Instance.WithOptions(ConfigOptions.JoinSummary | ConfigOptions.DisableOptimizationsValidator); + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config); + return; + } Pow2MontgomeryModCycleComputationBenchmarks benchmarks = new() { diff --git a/PerfectNumbers.Core/MutableUInt128.cs b/PerfectNumbers.Core/MutableUInt128.cs index ddfd212..38d81b9 100644 --- a/PerfectNumbers.Core/MutableUInt128.cs +++ b/PerfectNumbers.Core/MutableUInt128.cs @@ -143,6 +143,49 @@ internal struct MutableUInt128 Add(addend); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void MultiplyAddInline(ulong multiplier, ulong addend) + { + const ulong Mask32 = 0xFFFF_FFFFUL; + + ulong multiplierLow = multiplier & Mask32; + ulong multiplierHigh = multiplier >> 32; + + ulong low = Low; + ulong lowLow = low & Mask32; + ulong lowHigh = low >> 32; + + ulong product00 = lowLow * multiplierLow; + ulong product01 = lowLow * multiplierHigh; + ulong product10 = lowHigh * multiplierLow; + ulong product11 = lowHigh * multiplierHigh; + + ulong carry = (product00 >> 32) + (product01 & Mask32) + (product10 & Mask32); + ulong lowResult = ((carry & Mask32) << 32) | (product00 & Mask32); + ulong highContribution = product11 + (product01 >> 32) + (product10 >> 32) + (carry >> 32); + + ulong high = High; + ulong highLow = high & Mask32; + ulong highHigh = high >> 32; + + ulong product20 = highLow * multiplierLow; + ulong product21 = highLow * multiplierHigh; + ulong product30 = highHigh * multiplierLow; + + ulong highCarry = (product20 >> 32) + (product21 & Mask32) + (product30 & Mask32); + ulong shiftedHighLow = ((highCarry & Mask32) << 32) | (product20 & Mask32); + + ulong newLow = lowResult + addend; + ulong addCarry = newLow < addend ? 1UL : 0UL; + + ulong newHigh = highContribution + shiftedHighLow; + // Discard overflow above 128 bits to match Multiply's truncation semantics. + newHigh += addCarry; + + Low = newLow; + High = newHigh; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ulong Mod(ulong modulus) {