From 6adac6b7a7bb06ac3efe96102cdfaa48dd3faef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sat, 4 Oct 2025 21:20:29 +0200 Subject: [PATCH] Consolidate multiply helpers and guard GPU fallbacks --- AGENTS.md | 9 +- .../HeuristicMultiplyShiftBenchmarks.cs | 4 +- .../Gpu/PrimeOrderGpuHeuristicsTests.cs | 101 ++++++++++++++++-- ...s => ULongExtensionsMultiplyShiftTests.cs} | 12 +-- .../Gpu/PrimeOrderGpuHeuristics.cs | 75 ++++++++++++- PerfectNumbers.Core/HeuristicArithmetic.cs | 48 --------- PerfectNumbers.Core/ULongExtensions.cs | 41 +++++++ apply_patch.cs | 2 +- todo.txt | 1 - tools/HeuristicMultiplyShiftTiming/Program.cs | 4 +- 10 files changed, 227 insertions(+), 70 deletions(-) rename PerfectNumbers.Core.Tests/{HeuristicArithmeticTests.cs => ULongExtensionsMultiplyShiftTests.cs} (79%) delete mode 100644 PerfectNumbers.Core/HeuristicArithmetic.cs diff --git a/AGENTS.md b/AGENTS.md index 5a50a63..f0b90d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,8 @@ To make any changes to the files in the repository, you should always utilize a dotnet script apply_patch.cs -- [target-directory] ``` +> **Temporary workflow notice:** Until the pending apply_patch.cs performance and correctness TODOs are complete, do not invoke the script in this repository. Edit files directly with shell tools (sed, tee, cat, python, etc.) and stage the results via git. Re-enable the helper only after the TODO tracker explicitly calls out that the ban has been lifted. + It supports the standard `git diff` patch files as input. You should be able to run it both in the local and remote `Codex` environments. You should expect it to work both in `Windows` and `Linux` environments. Make sure that you include `diff --git` lines, headers and file prefixes in the patch files to avoid applying issues. `apply_patch.cs` supports several parameters, depending on your needs: @@ -62,6 +64,7 @@ Do **not** attempt to apply the changes manually, or with `Python`, or with `Pow ### Test execution time policy - Always estimate runtime before launching any tests or long-running samples. If a test or ad-hoc run is likely to exceed 2 minutes, do not run it unless the user explicitly requests a longer run and provides justification. +- Never apply the two-minute guard to benchmark or profiling runs; allow them to finish and capture the summary unless the console log shows the job is clearly stuck. - Do not hardcode timeouts inside test code. Enforce time limits from the runner/shell layer so developers can run tests locally without artificial constraints. - Prefer targeted filters over full suites to keep runs short: `--filter FullyQualifiedName~` or `--filter "FullyQualifiedName=Namespace.Class.Method"`. - Keep GPU-heavy tests minimal and scoped. If a kernel compile or warmup is expected to take >2 minutes, skip running it during regular iterations. @@ -144,11 +147,13 @@ Additional numeric-type probe results (`tools/GpuNumericTypeProbe.csx`, ILGPU 1. Patch-preparation checklist for reliable `--check` runs: +- Manually review every patch file before running `--check`; confirm the hunks reflect only the intended edits and eliminate accidental whitespace-only noise ahead of time. - Always capture context with `nl -ba` before composing each hunk so the `@@` header line numbers match the target file on the first try. +- Ensure every hunk carries at least three unchanged context lines above and below the edits whenever the surrounding file provides them; regenerate the diff if the file changed since the previous attempt so the context stays accurate. - Trim or normalize trailing whitespace while drafting the patch, or rely on `--ignore-whitespace` during verification when formatting noise is unavoidable, then re-run without ignores to confirm the final diff matches exactly. - Prefer regenerating the patch from scratch whenever the working tree changes instead of editing previous diffs; stale context is the most common source of mismatches. -- The patch parser now ignores blank metadata lines between `diff --git`, `---`, and `+++` headers, so you no longer need to insert dummy empty separators to satisfy the checker. Keep the header compact unless your diff tool emits those blank lines naturally. +- The patch parser now ignores blank metadata lines between `diff --git`, `---`, and `+++` headers and skips stray blank separators between hunks, so you no longer need to insert dummy empty lines to satisfy the checker. Keep the header compact unless your diff tool emits those blank lines naturally. ### apply_patch.cs profiling snapshot (2025-10-04) @@ -172,7 +177,7 @@ Practical guidance: - Prefer Mersenne folding over general modulo for `2^p-1` moduli to remove branches in hot loops. - Heuristic multiply-shift benchmarks (2025-10-04): - - Run `dotnet run -c Release --project tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj` to compare the `HeuristicArithmetic` helpers without the BenchmarkDotNet harness. + - Run `dotnet run -c Release --project tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj` to compare the `ULongExtensions` helpers without the BenchmarkDotNet harness. - Timings below report the average nanoseconds per call over 200,000,000 iterations; all variants produced identical checksums, confirming consistent arithmetic. - `NearOverflowShift`: `MultiplyShiftRight` ≈ 2.37 ns, shift-first ≈ 4.71 ns, naive `(value * multiplier) >> shift` ≈ 0.85 ns. - `HalfRange`: `MultiplyShiftRight` ≈ 2.11 ns, shift-first ≈ 4.15 ns, naive ≈ 0.70 ns. diff --git a/EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs b/EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs index 7bc796a..09290e2 100644 --- a/EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs +++ b/EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs @@ -28,13 +28,13 @@ public class HeuristicMultiplyShiftBenchmarks [Benchmark(Baseline = true)] public ulong SafeMultiplyShift() { - return HeuristicArithmetic.MultiplyShiftRight(Input.Value, Input.Multiplier, Input.Shift); + return ULongExtensions.MultiplyShiftRight(Input.Value, Input.Multiplier, Input.Shift); } [Benchmark] public ulong ShiftFirstMultiplyShift() { - return HeuristicArithmetic.MultiplyShiftRightShiftFirst(Input.Value, Input.Multiplier, Input.Shift); + return ULongExtensions.MultiplyShiftRightShiftFirst(Input.Value, Input.Multiplier, Input.Shift); } [Benchmark] diff --git a/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs b/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs index 9914ad6..0d0ac41 100644 --- a/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs +++ b/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs @@ -9,11 +9,16 @@ namespace PerfectNumbers.Core.Tests; [Trait("Category", "Fast")] public class PrimeOrderGpuHeuristicsTests { + public PrimeOrderGpuHeuristicsTests() + { + PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); + PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting(); + } + [Fact] public void TryPow2Mod_returns_overflow_for_marked_prime() { const ulong prime = 97UL; - PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); try { PrimeOrderGpuHeuristics.MarkOverflow(prime); @@ -32,19 +37,58 @@ public class PrimeOrderGpuHeuristicsTests [Fact] public void TryPow2Mod_returns_unavailable_for_unmarked_prime() { - PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); - GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, 193UL, out ulong remainder); status.Should().Be(GpuPow2ModStatus.Unavailable); remainder.Should().Be(0UL); } + [Fact] + public void TryPow2Mod_returns_overflow_when_modulus_exceeds_capability() + { + const ulong prime = 97UL; + var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(6, 64); + PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability); + try + { + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, prime, out ulong remainder); + + status.Should().Be(GpuPow2ModStatus.Overflow); + remainder.Should().Be(0UL); + } + finally + { + PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting(); + PrimeOrderGpuHeuristics.ClearOverflow(prime); + } + } + + [Fact] + public void TryPow2Mod_returns_overflow_when_exponent_exceeds_capability() + { + var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(64, 3); + PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability); + try + { + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(16UL, 193UL, out ulong remainder); + + status.Should().Be(GpuPow2ModStatus.Overflow); + remainder.Should().Be(0UL); + + GpuPow2ModStatus followUp = PrimeOrderGpuHeuristics.TryPow2Mod(4UL, 193UL, out ulong followUpRemainder); + followUp.Should().Be(GpuPow2ModStatus.Unavailable); + followUpRemainder.Should().Be(0UL); + } + finally + { + PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting(); + } + } + [Fact] public void TryPow2ModBatch_returns_overflow_for_marked_prime() { const ulong prime = 193UL; - PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); try { PrimeOrderGpuHeuristics.MarkOverflow(prime); @@ -66,8 +110,6 @@ public class PrimeOrderGpuHeuristicsTests [Fact] public void TryPow2ModBatch_returns_unavailable_and_clears_remainders_for_unmarked_prime() { - PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); - ulong[] remainders = { 123UL, 456UL }; ulong[] exponents = { 1UL, 2UL }; GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, 257UL, remainders); @@ -77,6 +119,52 @@ public class PrimeOrderGpuHeuristicsTests remainders[1].Should().Be(0UL); } + [Fact] + public void TryPow2ModBatch_returns_overflow_when_modulus_exceeds_capability() + { + const ulong prime = 97UL; + var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(6, 64); + PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability); + try + { + ulong[] exponents = { 1UL, 2UL }; + ulong[] remainders = { 5UL, 6UL }; + + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders); + + status.Should().Be(GpuPow2ModStatus.Overflow); + remainders[0].Should().Be(0UL); + remainders[1].Should().Be(0UL); + } + finally + { + PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting(); + PrimeOrderGpuHeuristics.ClearOverflow(prime); + } + } + + [Fact] + public void TryPow2ModBatch_returns_overflow_when_any_exponent_exceeds_capability() + { + var capability = new PrimeOrderGpuHeuristics.PrimeOrderGpuCapability(64, 3); + PrimeOrderGpuHeuristics.OverrideCapabilitiesForTesting(capability); + try + { + ulong[] exponents = { 2UL, 16UL }; + ulong[] remainders = { 9UL, 11UL }; + + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, 193UL, remainders); + + status.Should().Be(GpuPow2ModStatus.Overflow); + remainders[0].Should().Be(0UL); + remainders[1].Should().Be(0UL); + } + finally + { + PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting(); + } + } + [Fact] public void TryPow2ModBatch_throws_when_remainder_span_is_too_small() { @@ -92,7 +180,6 @@ public class PrimeOrderGpuHeuristicsTests public void Calculate_falls_back_to_cpu_when_gpu_overflow_is_marked() { const ulong prime = 7UL; - PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); try { PrimeOrderGpuHeuristics.MarkOverflow(prime); diff --git a/PerfectNumbers.Core.Tests/HeuristicArithmeticTests.cs b/PerfectNumbers.Core.Tests/ULongExtensionsMultiplyShiftTests.cs similarity index 79% rename from PerfectNumbers.Core.Tests/HeuristicArithmeticTests.cs rename to PerfectNumbers.Core.Tests/ULongExtensionsMultiplyShiftTests.cs index a3cd60a..d755e1b 100644 --- a/PerfectNumbers.Core.Tests/HeuristicArithmeticTests.cs +++ b/PerfectNumbers.Core.Tests/ULongExtensionsMultiplyShiftTests.cs @@ -6,7 +6,7 @@ using Xunit; namespace PerfectNumbers.Core.Tests; [Trait("Category", "Fast")] -public class HeuristicArithmeticTests +public class ULongExtensionsMultiplyShiftTests { [Theory] [InlineData(0UL, 0UL, 1)] @@ -18,7 +18,7 @@ public class HeuristicArithmeticTests { BigInteger expected = ((BigInteger)value * multiplier) >> shift; - ulong result = HeuristicArithmetic.MultiplyShiftRight(value, multiplier, shift); + ulong result = ULongExtensions.MultiplyShiftRight(value, multiplier, shift); result.Should().Be((ulong)expected); } @@ -34,7 +34,7 @@ public class HeuristicArithmeticTests { BigInteger expected = ((BigInteger)value * multiplier) >> shift; - ulong result = HeuristicArithmetic.MultiplyShiftRightShiftFirst(value, multiplier, shift); + ulong result = ULongExtensions.MultiplyShiftRightShiftFirst(value, multiplier, shift); result.Should().Be((ulong)expected); } @@ -42,7 +42,7 @@ public class HeuristicArithmeticTests [Fact] public void MultiplyShiftRightShiftFirst_returns_zero_when_shift_is_large() { - HeuristicArithmetic.MultiplyShiftRightShiftFirst(ulong.MaxValue, 123UL, 64).Should().Be(0UL); + ULongExtensions.MultiplyShiftRightShiftFirst(ulong.MaxValue, 123UL, 64).Should().Be(0UL); } [Theory] @@ -50,7 +50,7 @@ public class HeuristicArithmeticTests [InlineData(1UL, 1UL, -3)] public void MultiplyShiftRight_throws_when_shift_is_negative(ulong value, ulong multiplier, int shift) { - Action act = () => HeuristicArithmetic.MultiplyShiftRight(value, multiplier, shift); + Action act = () => ULongExtensions.MultiplyShiftRight(value, multiplier, shift); act.Should().Throw(); } @@ -60,7 +60,7 @@ public class HeuristicArithmeticTests [InlineData(1UL, 1UL, -3)] public void MultiplyShiftRightShiftFirst_throws_when_shift_is_negative(ulong value, ulong multiplier, int shift) { - Action act = () => HeuristicArithmetic.MultiplyShiftRightShiftFirst(value, multiplier, shift); + Action act = () => ULongExtensions.MultiplyShiftRightShiftFirst(value, multiplier, shift); act.Should().Throw(); } diff --git a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs index ee07fc3..21583db 100644 --- a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs +++ b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Concurrent; +using System.Numerics; +using System.Threading; namespace PerfectNumbers.Core.Gpu; @@ -13,6 +15,22 @@ internal enum GpuPow2ModStatus internal static class PrimeOrderGpuHeuristics { private static readonly ConcurrentDictionary OverflowedPrimes = new(); + private static int s_modulusBitLimit = PrimeOrderGpuCapability.Default.ModulusBits; + private static int s_exponentBitLimit = PrimeOrderGpuCapability.Default.ExponentBits; + + internal static void OverrideCapabilitiesForTesting(PrimeOrderGpuCapability capability) + { + Volatile.Write(ref s_modulusBitLimit, capability.ModulusBits); + Volatile.Write(ref s_exponentBitLimit, capability.ExponentBits); + } + + internal static void ResetCapabilitiesForTesting() + { + OverrideCapabilitiesForTesting(PrimeOrderGpuCapability.Default); + } + + private static PrimeOrderGpuCapability Capabilities + => new(Volatile.Read(ref s_modulusBitLimit), Volatile.Read(ref s_exponentBitLimit)); internal static void MarkOverflow(ulong prime) => OverflowedPrimes[prime] = 0; @@ -33,6 +51,19 @@ internal static class PrimeOrderGpuHeuristics return GpuPow2ModStatus.Overflow; } + PrimeOrderGpuCapability capability = Capabilities; + + if (!SupportsPrime(prime, capability)) + { + MarkOverflow(prime); + return GpuPow2ModStatus.Overflow; + } + + if (!SupportsExponent(exponent, capability)) + { + return GpuPow2ModStatus.Overflow; + } + // TODO: Implement the GPU heuristic order calculator so this path mirrors PrimeOrderCalculator before enabling device execution. return GpuPow2ModStatus.Unavailable; } @@ -61,7 +92,49 @@ internal static class PrimeOrderGpuHeuristics return GpuPow2ModStatus.Overflow; } + PrimeOrderGpuCapability capability = Capabilities; + + if (!SupportsPrime(prime, capability)) + { + MarkOverflow(prime); + return GpuPow2ModStatus.Overflow; + } + + for (int i = 0; i < exponents.Length; i++) + { + if (!SupportsExponent(exponents[i], capability)) + { + return GpuPow2ModStatus.Overflow; + } + } + // TODO: Implement the GPU heuristic order calculator so this path mirrors PrimeOrderCalculator before enabling device execution. return GpuPow2ModStatus.Unavailable; } -} + + private static bool SupportsPrime(ulong prime, PrimeOrderGpuCapability capability) + { + return GetBitLength(prime) <= capability.ModulusBits; + } + + private static bool SupportsExponent(ulong exponent, PrimeOrderGpuCapability capability) + { + return GetBitLength(exponent) <= capability.ExponentBits; + } + + private static int GetBitLength(ulong value) + { + if (value == 0UL) + { + return 0; + } + + return 64 - BitOperations.LeadingZeroCount(value); + } + + internal readonly record struct PrimeOrderGpuCapability(int ModulusBits, int ExponentBits) + { + public static PrimeOrderGpuCapability Default => new(128, 64); + } + + } diff --git a/PerfectNumbers.Core/HeuristicArithmetic.cs b/PerfectNumbers.Core/HeuristicArithmetic.cs deleted file mode 100644 index 7e62121..0000000 --- a/PerfectNumbers.Core/HeuristicArithmetic.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -namespace PerfectNumbers.Core; - -internal static class HeuristicArithmetic -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ulong MultiplyShiftRight(ulong value, ulong multiplier, int shift) - { - if (shift < 0) - { - throw new ArgumentOutOfRangeException(nameof(shift), "Shift must be non-negative."); - } - - UInt128 product = (UInt128)value * multiplier; - return (ulong)(product >> shift); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ulong MultiplyShiftRightShiftFirst(ulong value, ulong multiplier, int shift) - { - if (shift < 0) - { - throw new ArgumentOutOfRangeException(nameof(shift), "Shift must be non-negative."); - } - - if (shift == 0) - { - return MultiplyShiftRight(value, multiplier, 0); - } - - if (shift >= 64) - { - return 0UL; - } - - ulong high = value >> shift; - ulong mask = (1UL << shift) - 1UL; - ulong low = value & mask; - - UInt128 highContribution = (UInt128)high * multiplier; - UInt128 lowContribution = (UInt128)low * multiplier; - - UInt128 combined = highContribution + (lowContribution >> shift); - return (ulong)combined; - } -} diff --git a/PerfectNumbers.Core/ULongExtensions.cs b/PerfectNumbers.Core/ULongExtensions.cs index e3bac57..f94b2eb 100644 --- a/PerfectNumbers.Core/ULongExtensions.cs +++ b/PerfectNumbers.Core/ULongExtensions.cs @@ -56,6 +56,47 @@ public static class ULongExtensions return order; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MultiplyShiftRight(ulong value, ulong multiplier, int shift) + { + if (shift < 0) + { + throw new ArgumentOutOfRangeException(nameof(shift), "Shift must be non-negative."); + } + + UInt128 product = (UInt128)value * multiplier; + return (ulong)(product >> shift); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MultiplyShiftRightShiftFirst(ulong value, ulong multiplier, int shift) + { + if (shift < 0) + { + throw new ArgumentOutOfRangeException(nameof(shift), "Shift must be non-negative."); + } + + if (shift == 0) + { + return MultiplyShiftRight(value, multiplier, 0); + } + + if (shift >= 64) + { + return 0UL; + } + + ulong high = value >> shift; + ulong mask = (1UL << shift) - 1UL; + ulong low = value & mask; + + UInt128 highContribution = (UInt128)high * multiplier; + UInt128 lowContribution = (UInt128)low * multiplier; + + UInt128 combined = highContribution + (lowContribution >> shift); + return (ulong)combined; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong FastDiv64(this ulong value, ulong divisor, ulong mul) { diff --git a/apply_patch.cs b/apply_patch.cs index 734e6b8..71b34f6 100644 --- a/apply_patch.cs +++ b/apply_patch.cs @@ -464,7 +464,7 @@ static PatchHunk ParseHunk(string header, PatchLineReader reader) if (line.Length == 0) { - patchLines.Add(new ContextLine(string.Empty, true)); + // Skip stray blank separators so hunks without an empty line still parse correctly. continue; } diff --git a/todo.txt b/todo.txt index 73977fb..5f09bfc 100644 --- a/todo.txt +++ b/todo.txt @@ -1,3 +1,2 @@ -- Ensure the GPU order path reroutes to the CPU heuristics whenever exponents or moduli exceed the currently implemented device integer widths (GpuUInt128/GpuUInt256/GpuUInt512), so overflow never propagates into incorrect results. - Implement the GPU heuristics for PrimeOrderCalculator using ILGPU. Reuse the numeric type compatibility probe results to select between EInteger, ERational, GpuUInt128, or any new GPU-specific big-integer representations, guarantee parity with the CPU heuristic algorithm (including 128-bit primes), prefer the shift-first multiply/shift variant to stay within device ranges, and wire the calculator through the accelerator path. - Extend the GPU numeric type probe to cover proposed custom types such as GpuUInt256 and GpuUInt512 once their definitions are available so we can plan the wide-order heuristics on the device. diff --git a/tools/HeuristicMultiplyShiftTiming/Program.cs b/tools/HeuristicMultiplyShiftTiming/Program.cs index 1c35acf..8f6e211 100644 --- a/tools/HeuristicMultiplyShiftTiming/Program.cs +++ b/tools/HeuristicMultiplyShiftTiming/Program.cs @@ -42,7 +42,7 @@ internal static class Program for (int i = 0; i < Iterations; i++) { - checksum += HeuristicArithmetic.MultiplyShiftRight(value, input.Multiplier, input.Shift); + checksum += ULongExtensions.MultiplyShiftRight(value, input.Multiplier, input.Shift); value += 1UL; } @@ -58,7 +58,7 @@ internal static class Program for (int i = 0; i < Iterations; i++) { - checksum += HeuristicArithmetic.MultiplyShiftRightShiftFirst(value, input.Multiplier, input.Shift); + checksum += ULongExtensions.MultiplyShiftRightShiftFirst(value, input.Multiplier, input.Shift); value += 1UL; }