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..a332f23 100644 --- a/EvenPerfectBitScanner.Benchmarks/MulHighBenchmarks.cs +++ b/EvenPerfectBitScanner.Benchmarks/MulHighBenchmarks.cs @@ -36,10 +36,8 @@ 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); + // Routes through the UInt128-based helper adopted by CPU hot paths after benchmarking. + return ULongExtensions.MulHighCpu(Input.X, Input.Y); } /// @@ -107,7 +105,7 @@ public class MulHighBenchmarks [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ulong MulHighWithGpuUInt128(ulong x, ulong y) { - return ULongExtensions.MulHighGpuCompatible(x, y); + return ULongExtensions.MulHighGpu(x, y); } [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/Gpu/Kernels/PrimeTesterKernels.cs b/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs index 1d284e6..bf1e357 100644 --- a/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs +++ b/PerfectNumbers.Core/Gpu/Kernels/PrimeTesterKernels.cs @@ -10,35 +10,32 @@ internal static class PrimeTesterKernels public static void SmallPrimeSieveKernel(Index1D index, ArrayView numbers, ArrayView smallPrimes, ArrayView results) { ulong n = numbers[index]; + byte result = 1; // EvenPerfectBitScanner filters candidates so production GPU launches only see: // - n >= 31 (the scanner starts at 136,279,841 and test mode seeds 31) // - n odd (candidate generators and ModResidueTracker eliminate even exponents) // - n not divisible by 5 (residue prefiltering and AddPrimes transform cover these composites) // Tests should honor the same constraints so the host wrapper can remain branchless. - ulong log2Estimate = 63UL - (ulong)XMath.LeadingZeroCount(n | 1UL); - ulong gcd = BinaryGcdGpu(n, log2Estimate); - bool heuristicActive = n.Mod10() == 1UL; - bool gcdReject = heuristicActive & (gcd != 1UL); - - bool composite = gcdReject; - bool continueLoop = !gcdReject; long length = smallPrimes.Length; for (int i = 0; i < length; i++) { ulong prime = smallPrimes[i]; ulong primeSquare = (ulong)prime * prime; - bool withinRange = primeSquare <= n; - ulong remainder = n % prime; - bool divides = remainder == 0UL; + if (primeSquare > n) + { + break; + } - bool markComposite = continueLoop & withinRange & divides; - composite |= markComposite; - continueLoop &= withinRange & !divides; + if (n % prime == 0UL) + { + result = 0; + break; + } } - results[index] = composite ? (byte)0 : (byte)1; + results[index] = result; } public static void HeuristicTrialDivisionKernel(Index1D index, ArrayView divisors, ulong n, ArrayView results) 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..4c057ef 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) + { + // Retain this manual decomposition for GPU-style arithmetic so callers staging accelerator work + // can avoid the UInt128 intrinsic while keeping parity with GpuUInt128.MulHigh. 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;