From 0e1212aa1c1c8c95f0acefe6d6523f03a54ab1e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sun, 26 Oct 2025 22:42:21 +0100 Subject: [PATCH] Split MulHigh extensions into CPU and GPU variants --- .../Mul64Benchmarks.cs | 2 +- .../MulHighBenchmarks.cs | 10 +++++----- .../MulMod64Benchmarks.cs | 2 +- .../ULongExtensionsTests.cs | 14 ++++++++++++-- PerfectNumbers.Core/UInt128Extensions.cs | 2 +- PerfectNumbers.Core/ULongExtensions.Gpu.cs | 1 + PerfectNumbers.Core/ULongExtensions.cs | 17 +++++++++++------ 7 files changed, 32 insertions(+), 16 deletions(-) diff --git a/EvenPerfectBitScanner.Benchmarks/Mul64Benchmarks.cs b/EvenPerfectBitScanner.Benchmarks/Mul64Benchmarks.cs index 4203709..f5c0d4e 100644 --- a/EvenPerfectBitScanner.Benchmarks/Mul64Benchmarks.cs +++ b/EvenPerfectBitScanner.Benchmarks/Mul64Benchmarks.cs @@ -89,7 +89,7 @@ public class Mul64Benchmarks { ulong leftLow = (ulong)left; ulong rightLow = (ulong)right; - return ((UInt128)(leftLow * (ulong)(right >> 64) + leftLow.MulHigh(rightLow)) << 64) | (UInt128)(leftLow * rightLow); + return ((UInt128)(leftLow * (ulong)(right >> 64) + leftLow.MulHighCpu(rightLow)) << 64) | (UInt128)(leftLow * rightLow); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/EvenPerfectBitScanner.Benchmarks/MulHighBenchmarks.cs b/EvenPerfectBitScanner.Benchmarks/MulHighBenchmarks.cs index 49e2208..852010c 100644 --- a/EvenPerfectBitScanner.Benchmarks/MulHighBenchmarks.cs +++ b/EvenPerfectBitScanner.Benchmarks/MulHighBenchmarks.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using PerfectNumbers.Core; +using PerfectNumbers.Core.Gpu; namespace EvenPerfectBitScanner.Benchmarks; @@ -36,10 +37,7 @@ public class MulHighBenchmarks [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); + return ULongExtensions.MulHighCpu(Input.X, Input.Y); } /// @@ -107,7 +105,9 @@ public class MulHighBenchmarks [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong MulHighWithGpuUInt128(ulong x, ulong y) { - return ULongExtensions.MulHighGpuCompatible(x, y); + GpuUInt128 product = new(x); + GpuUInt128.Mul64(ref product, 0UL, y); + return product.High; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/EvenPerfectBitScanner.Benchmarks/MulMod64Benchmarks.cs b/EvenPerfectBitScanner.Benchmarks/MulMod64Benchmarks.cs index d4454a9..949b10a 100644 --- a/EvenPerfectBitScanner.Benchmarks/MulMod64Benchmarks.cs +++ b/EvenPerfectBitScanner.Benchmarks/MulMod64Benchmarks.cs @@ -240,7 +240,7 @@ public class MulMod64Benchmarks ulong left = a % modulus; ulong right = b % modulus; ulong low = left * right; - ulong high = left.MulHigh(right); + ulong high = left.MulHighCpu(right); UInt128 product = ((UInt128)high << 64) | low; return (ulong)(product % modulus); } diff --git a/PerfectNumbers.Core.Tests/ULongExtensionsTests.cs b/PerfectNumbers.Core.Tests/ULongExtensionsTests.cs index b192dab..1cbee47 100644 --- a/PerfectNumbers.Core.Tests/ULongExtensionsTests.cs +++ b/PerfectNumbers.Core.Tests/ULongExtensionsTests.cs @@ -131,11 +131,21 @@ public class ULongExtensionsTests [InlineData(0UL, 0UL)] [InlineData(ulong.MaxValue, 1UL)] [InlineData(123456789UL, 987654321UL)] - public void MulHigh_matches_high_bits(ulong x, ulong y) + public void MulHighCpu_matches_high_bits(ulong x, ulong y) { ulong expected = (ulong)(((UInt128)x * y) >> 64); - x.MulHigh(y).Should().Be(expected); + x.MulHighCpu(y).Should().Be(expected); + } + + [Theory] + [Trait("Category", "Fast")] + [InlineData(0UL, 0UL)] + [InlineData(ulong.MaxValue, 1UL)] + [InlineData(123456789UL, 987654321UL)] + public void MulHighGpu_matches_cpu_baseline(ulong x, ulong y) + { + x.MulHighGpu(y).Should().Be(x.MulHighCpu(y)); } [Theory] diff --git a/PerfectNumbers.Core/UInt128Extensions.cs b/PerfectNumbers.Core/UInt128Extensions.cs index 2e72d58..973bec6 100644 --- a/PerfectNumbers.Core/UInt128Extensions.cs +++ b/PerfectNumbers.Core/UInt128Extensions.cs @@ -760,7 +760,7 @@ public static class UInt128Extensions // expression tree around the shift. Mirroring the MulHigh layout lets RyuJIT // keep the intermediate sum in registers instead of reloading the partial // product from the stack. - ulong high = aLow.MulHigh(bLow); + ulong high = aLow.MulHighCpu(bLow); high += aLow * bHigh; return ((UInt128)high << 64) | low; diff --git a/PerfectNumbers.Core/ULongExtensions.Gpu.cs b/PerfectNumbers.Core/ULongExtensions.Gpu.cs index 6b736ee..2e37a22 100644 --- a/PerfectNumbers.Core/ULongExtensions.Gpu.cs +++ b/PerfectNumbers.Core/ULongExtensions.Gpu.cs @@ -11,6 +11,7 @@ namespace PerfectNumbers.Core; public static partial class ULongExtensions { [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Obsolete("Use ULongExtensions.MulHighCpu or ULongExtensions.MulHighGpu.")] public static ulong MulHighGpuCompatible(this ulong x, ulong y) { GpuUInt128 product = new(x); diff --git a/PerfectNumbers.Core/ULongExtensions.cs b/PerfectNumbers.Core/ULongExtensions.cs index 08af224..7c3667c 100644 --- a/PerfectNumbers.Core/ULongExtensions.cs +++ b/PerfectNumbers.Core/ULongExtensions.cs @@ -430,14 +430,19 @@ public static partial class ULongExtensions } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static UInt128 Mul64(this ulong a, ulong b) => ((UInt128)a.MulHigh(b) << 64) | (UInt128)(a * b); + public static UInt128 Mul64(this ulong a, ulong b) => ((UInt128)a.MulHighCpu(b) << 64) | (UInt128)(a * b); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ulong MulHigh(this ulong x, ulong y) + public static ulong MulHighCpu(this ulong x, ulong y) { - // TODO: Investigate replacing this manual decomposition with the UInt128-based implementation - // for CPU callers; the latest benchmarks show the intrinsic path is an order of magnitude - // faster, while GPU code can keep using GpuUInt128.MulHigh. + return (ulong)(((UInt128)x * y) >> 64); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MulHighGpu(this ulong x, ulong y) + { + // The CPU helpers rely on UInt128 via MulHighCpu; retain this manual decomposition for GPU-style + // arithmetic so callers can avoid the intrinsic when staging work for accelerators. ulong xLow = (uint)x; ulong xHigh = x >> 32; ulong yLow = (uint)y; @@ -554,7 +559,7 @@ public static partial class ULongExtensions ulong m = unchecked(tLow * nPrime); ulong mTimesModulusLow = unchecked(m * modulus); - ulong result = unchecked(a.MulHigh(b) + m.MulHigh(modulus) + (unchecked(tLow + mTimesModulusLow) < tLow ? 1UL : 0UL)); + ulong result = unchecked(a.MulHighCpu(b) + m.MulHighCpu(modulus) + (unchecked(tLow + mTimesModulusLow) < tLow ? 1UL : 0UL)); if (result >= modulus) { result -= modulus;