From 84ebbed98b66c6c35e351400bdd880c0fbb286fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sat, 4 Oct 2025 20:01:10 +0200 Subject: [PATCH] Add shift-first multiply helper and document benchmarks --- AGENTS.md | 48 ++ .../HeuristicMultiplyShiftBenchmarks.cs | 53 ++ .../Gpu/PrimeOrderGpuHeuristicsTests.cs | 110 ++++ .../HeuristicArithmeticTests.cs | 67 ++ .../Gpu/PrimeOrderGpuHeuristics.cs | 67 ++ PerfectNumbers.Core/HeuristicArithmetic.cs | 48 ++ PerfectNumbers.Core/MersenneDivisorCycles.cs | 10 + .../PerfectNumbers.Core.csproj | 3 +- .../PrimeOrderCalculator.Wide.cs | 13 +- PerfectNumbers.Core/PrimeOrderCalculator.cs | 122 +++- apply_patch.cs | 623 +++++++++++++++--- todo.txt | 3 + tools/GpuNumericTypeProbe.csx | 160 +++++ .../HeuristicMultiplyShiftTiming.csproj | 11 + tools/HeuristicMultiplyShiftTiming/Program.cs | 95 +++ 15 files changed, 1333 insertions(+), 100 deletions(-) create mode 100644 EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs create mode 100644 PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs create mode 100644 PerfectNumbers.Core.Tests/HeuristicArithmeticTests.cs create mode 100644 PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs create mode 100644 PerfectNumbers.Core/HeuristicArithmetic.cs create mode 100644 todo.txt create mode 100644 tools/GpuNumericTypeProbe.csx create mode 100644 tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj create mode 100644 tools/HeuristicMultiplyShiftTiming/Program.cs diff --git a/AGENTS.md b/AGENTS.md index dd4f115..5a50a63 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,14 +31,23 @@ It supports the standard `git diff` patch files as input. You should be able to * `--ignore-eol` * `--ignore-line-endings` * `--target` - allows specifying target root directory for the patch +* `--profile` - capture read/parse/apply timings for each patch file. +* `--replace-line ` - replace a single line by absolute number (use `_` as the expected text to skip validation) * Build patches in standard `git diff` format. * It supports `---`, `+++`, and `@@`. * It doesn't support `***` or `*** Begin/End Patch` wrappers. * It requires `diff --git` at the beginning. * Patch header needs the actual line numbers. + * When hand-writing a diff, always include the numeric ranges in every `@@ -start,count +start,count @@` header; missing or zero counts will cause `--check` to fail immediately. * Always check if the patch will work by using `--check` parameter, without making changes to the files, which would force patch changes. Try applying the patch only when `--check` passed. * Build patches in small, line-precise hunks; confirm target lines with `Get-Content` or `nl -ba` before writing the diff to avoid context mismatches. +* Capture the exact start lines for each hunk while drafting the `@@ -start,length +start,length @@` header; double-check them with `nl -ba` so the first verification succeeds. +* For one-off line edits, prefer the `--replace-line ` option. Pass `_` as the expected text to skip validation when necessary. +* Before calling `--replace-line`, capture the target number with `nl -ba` (or an equivalent command) so the replacement lands exactly once without retrying. +* After every successful patch application, regenerate a fresh diff from the updated files before preparing the next patch so the hunk headers and context reflect the latest line numbers. +* If the patch script flags repeated mismatches, regenerate a fresh diff for the updated file instead of tweaking stale context blocks. +* When `--check` reveals that only whitespace or EOL normalization differs, rerun it with `--ignore-whitespace` and/or `--ignore-line-endings` so validation concentrates on the meaningful edits. * Regenerate the diff whenever the file shifts—never reuse an old hunk after other edits. * Keep patch filenames unique and delete applied files to prevent accidental re-use. * When a hunk fails, inspect the around-lines immediately and adjust rather than retrying the same diff. @@ -127,7 +136,46 @@ The following checks were executed with ILGPU 1.5.3 on CPU and OpenCL accelerato - [x] `UInt128` bit shifts: attempts failed in kernels; avoid inside device code. - [x] GPU context pooling: supported and recommended (validated in a separate program). +Additional numeric-type probe results (`tools/GpuNumericTypeProbe.csx`, ILGPU 1.5.3 on the CPU accelerator): + +- `System.Numerics.BigInteger`: unsupported. Kernels that exercise addition, subtraction, multiplication, division, modulo, and `CompareTo` all fail with `InternalCompilerException`, indicating fundamental codegen gaps for the type. +- `PeterO.Numbers.EInteger`: unsupported. Attempts to compile kernels that call addition, subtraction, multiplication, division, remainder, and `CompareTo` throw `InternalCompilerException`, so none of the arithmetic helpers are currently usable on the device. +- `PeterO.Numbers.ERational`: unsupported. Kernels that call addition, subtraction, multiplication, division, and `CompareTo` trigger `InternalCompilerException`; the type also lacks a remainder/modulo API for device usage. + +Patch-preparation checklist for reliable `--check` runs: + +- Always capture context with `nl -ba` before composing each hunk so the `@@` header line numbers match the target file on the first try. +- 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. + +### apply_patch.cs profiling snapshot (2025-10-04) + +- Latest timing check: `dotnet script apply_patch.cs -- --profile --check /tmp/agents_todo_update.diff`. +- Patch characteristics: 2 files, 4 hunks, 31 lines, 3468 bytes. +- Timings: read 0.45 ms, parse 2.96 ms, apply 2.96 ms. +- Focus next on streaming the patch reader and pooled buffer reuse to reduce the parse APPLY bottleneck highlighted above. + +Mersenne divisor size estimates: + +- Any prime divisor `q` of a Mersenne number `2^p - 1` must satisfy `q ≡ 1 (mod 2p)`. The minimal `q` therefore appears at `q = 2p + 1` when the multiplier `k = 1` yields a prime. +- For candidate exponents `p` in the `[138,000,000, 200,000,000]` range, the minimal compliant divisor lands below `2 · 200,000,000 + 1 = 400,000,001`, which fits in 29 bits (`2^29 = 536,870,912`). +- Extending the search up to `p = 600,000,000` pushes the minimal divisor to `1,200,000,001`, requiring 31 bits (`2^31 = 2,147,483,648`). +- Larger `k` values (and thus larger `q`) are possible—up to nearly `2^p - 1`—so the heuristics and storage should plan for wider integers when deep trial factoring is necessary, but the noted bit widths cover the first admissible divisors for these exponent bands. + +- Because every proper divisor of `2^p - 1` is at most `(2^p - 1) / 2`, the worst-case bit-width for any admissible `q` is `p - 1` bits (since `⌊(2^p - 1) / 2⌋ = 2^{p-1} - 1`). Consequently we need roughly `199,999,999` bits when `p ≤ 200,000,000` and `599,999,999` bits when `p ≤ 600,000,000`. In terms of the `q = 2kp + 1` form, this translates to `k_max = ⌊((2^p - 1) / 2 - 1) / (2p)⌋`, which is approximately `2^{p-2} / p` for large `p`. + Practical guidance: - Use native `%` and `/` in kernels when working with 32-bit or 64-bit integers if it is the most efficient option for the target backend. - For `UInt128` in kernels, avoid `%`/`/`; implement reductions via Montgomery/Barrett, or use Mersenne-specific folding for mod `2^p-1`. - 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. + - 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. + - `MixedBits`: `MultiplyShiftRight` ≈ 1.43 ns, shift-first ≈ 2.83 ns, naive ≈ 0.60 ns. + - `Sparse`: `MultiplyShiftRight` ≈ 1.57 ns, shift-first ≈ 2.84 ns, naive ≈ 0.59 ns. + - The shift-first rewrite roughly doubles the per-call cost on CPU, but it stays within 64-bit arithmetic and is the preferred fallback for GPU heuristics when avoiding `UInt128` overflow is mandatory. diff --git a/EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs b/EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs new file mode 100644 index 0000000..7bc796a --- /dev/null +++ b/EvenPerfectBitScanner.Benchmarks/HeuristicMultiplyShiftBenchmarks.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using PerfectNumbers.Core; + +namespace EvenPerfectBitScanner.Benchmarks; + +[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 1, iterationCount: 5)] +[MemoryDiagnoser] +public class HeuristicMultiplyShiftBenchmarks +{ + private static readonly MultiplyShiftInput[] Inputs = + [ + new(0x3FFF_FFFF_FFFF_FFFFUL, 3UL, 3, "NearOverflowShift"), + new(0x2000_0000_0000_0000UL, 7UL, 4, "HalfRange"), + new(0x1234_5678_9ABC_DEF0UL, 5UL, 5, "MixedBits"), + new(0x0000_0000_1000_0000UL, 11UL, 3, "Sparse"), + ]; + + [ParamsSource(nameof(GetInputs))] + public MultiplyShiftInput Input { get; set; } + + public static IEnumerable GetInputs() + { + return Inputs; + } + + [Benchmark(Baseline = true)] + public ulong SafeMultiplyShift() + { + return HeuristicArithmetic.MultiplyShiftRight(Input.Value, Input.Multiplier, Input.Shift); + } + + [Benchmark] + public ulong ShiftFirstMultiplyShift() + { + return HeuristicArithmetic.MultiplyShiftRightShiftFirst(Input.Value, Input.Multiplier, Input.Shift); + } + + [Benchmark] + public ulong NaiveMultiplyShift() + { + return (Input.Value * Input.Multiplier) >> Input.Shift; + } + + public readonly record struct MultiplyShiftInput(ulong Value, ulong Multiplier, int Shift, string Name) + { + public override string ToString() + { + return Name; + } + } +} diff --git a/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs b/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs new file mode 100644 index 0000000..9914ad6 --- /dev/null +++ b/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs @@ -0,0 +1,110 @@ +using System; +using FluentAssertions; +using PerfectNumbers.Core; +using PerfectNumbers.Core.Gpu; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +[Trait("Category", "Fast")] +public class PrimeOrderGpuHeuristicsTests +{ + [Fact] + public void TryPow2Mod_returns_overflow_for_marked_prime() + { + const ulong prime = 97UL; + PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); + try + { + PrimeOrderGpuHeuristics.MarkOverflow(prime); + + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, prime, out ulong remainder); + + status.Should().Be(GpuPow2ModStatus.Overflow); + remainder.Should().Be(0UL); + } + finally + { + PrimeOrderGpuHeuristics.ClearOverflow(prime); + } + } + + [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 TryPow2ModBatch_returns_overflow_for_marked_prime() + { + const ulong prime = 193UL; + PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); + try + { + PrimeOrderGpuHeuristics.MarkOverflow(prime); + + ulong[] remainders = new ulong[2]; + ulong[] exponents = { 1UL, 2UL }; + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, prime, remainders); + + status.Should().Be(GpuPow2ModStatus.Overflow); + remainders[0].Should().Be(0UL); + remainders[1].Should().Be(0UL); + } + finally + { + PrimeOrderGpuHeuristics.ClearOverflow(prime); + } + } + + [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); + + status.Should().Be(GpuPow2ModStatus.Unavailable); + remainders[0].Should().Be(0UL); + remainders[1].Should().Be(0UL); + } + + [Fact] + public void TryPow2ModBatch_throws_when_remainder_span_is_too_small() + { + ulong[] remainders = new ulong[1]; + ulong[] exponents = { 1UL, 2UL }; + + Action act = () => PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, 5UL, remainders); + + act.Should().Throw(); + } + + [Fact] + public void Calculate_falls_back_to_cpu_when_gpu_overflow_is_marked() + { + const ulong prime = 7UL; + PrimeOrderGpuHeuristics.ClearAllOverflowForTesting(); + try + { + PrimeOrderGpuHeuristics.MarkOverflow(prime); + + PrimeOrderCalculator.PrimeOrderResult result = PrimeOrderCalculator.Calculate(prime, null, PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); + + result.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found); + result.Order.Should().Be(3UL); + } + finally + { + PrimeOrderGpuHeuristics.ClearOverflow(prime); + } + } +} diff --git a/PerfectNumbers.Core.Tests/HeuristicArithmeticTests.cs b/PerfectNumbers.Core.Tests/HeuristicArithmeticTests.cs new file mode 100644 index 0000000..a3cd60a --- /dev/null +++ b/PerfectNumbers.Core.Tests/HeuristicArithmeticTests.cs @@ -0,0 +1,67 @@ +using System.Numerics; +using FluentAssertions; +using PerfectNumbers.Core; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +[Trait("Category", "Fast")] +public class HeuristicArithmeticTests +{ + [Theory] + [InlineData(0UL, 0UL, 1)] + [InlineData(1UL, 3UL, 1)] + [InlineData(ulong.MaxValue, 3UL, 3)] + [InlineData(0xFFFF_FFFF_FFFF_FFFFUL, 5UL, 4)] + [InlineData(0x8000_0000_0000_0000UL, 7UL, 5)] + public void MultiplyShiftRight_matches_big_integer(ulong value, ulong multiplier, int shift) + { + BigInteger expected = ((BigInteger)value * multiplier) >> shift; + + ulong result = HeuristicArithmetic.MultiplyShiftRight(value, multiplier, shift); + + result.Should().Be((ulong)expected); + } + + [Theory] + [InlineData(0UL, 0UL, 1)] + [InlineData(1UL, 3UL, 1)] + [InlineData(ulong.MaxValue, 3UL, 3)] + [InlineData(0xFFFF_FFFF_FFFF_FFFFUL, 5UL, 4)] + [InlineData(0x8000_0000_0000_0000UL, 7UL, 5)] + [InlineData(0xDEAD_BEEF_DEAD_BEEFUL, 11UL, 63)] + public void MultiplyShiftRightShiftFirst_matches_big_integer(ulong value, ulong multiplier, int shift) + { + BigInteger expected = ((BigInteger)value * multiplier) >> shift; + + ulong result = HeuristicArithmetic.MultiplyShiftRightShiftFirst(value, multiplier, shift); + + result.Should().Be((ulong)expected); + } + + [Fact] + public void MultiplyShiftRightShiftFirst_returns_zero_when_shift_is_large() + { + HeuristicArithmetic.MultiplyShiftRightShiftFirst(ulong.MaxValue, 123UL, 64).Should().Be(0UL); + } + + [Theory] + [InlineData(0UL, ulong.MaxValue, -1)] + [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); + + act.Should().Throw(); + } + + [Theory] + [InlineData(0UL, ulong.MaxValue, -1)] + [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); + + act.Should().Throw(); + } +} diff --git a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs new file mode 100644 index 0000000..ee07fc3 --- /dev/null +++ b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Concurrent; + +namespace PerfectNumbers.Core.Gpu; + +internal enum GpuPow2ModStatus +{ + Success, + Overflow, + Unavailable, +} + +internal static class PrimeOrderGpuHeuristics +{ + private static readonly ConcurrentDictionary OverflowedPrimes = new(); + + internal static void MarkOverflow(ulong prime) => OverflowedPrimes[prime] = 0; + + internal static void ClearOverflow(ulong prime) => OverflowedPrimes.TryRemove(prime, out _); + + internal static void ClearAllOverflowForTesting() => OverflowedPrimes.Clear(); + + public static GpuPow2ModStatus TryPow2Mod(ulong exponent, ulong prime, out ulong remainder) + { + remainder = 0UL; + if (prime <= 1UL) + { + return GpuPow2ModStatus.Unavailable; + } + + if (OverflowedPrimes.ContainsKey(prime)) + { + return GpuPow2ModStatus.Overflow; + } + + // TODO: Implement the GPU heuristic order calculator so this path mirrors PrimeOrderCalculator before enabling device execution. + return GpuPow2ModStatus.Unavailable; + } + + public static GpuPow2ModStatus TryPow2ModBatch(ReadOnlySpan exponents, ulong prime, Span remainders) + { + if (exponents.Length == 0) + { + return GpuPow2ModStatus.Success; + } + + if (remainders.Length < exponents.Length) + { + throw new ArgumentException("Remainder span is shorter than the exponent span.", nameof(remainders)); + } + + remainders.Slice(0, exponents.Length).Clear(); + + if (prime <= 1UL) + { + return GpuPow2ModStatus.Unavailable; + } + + if (OverflowedPrimes.ContainsKey(prime)) + { + return GpuPow2ModStatus.Overflow; + } + + // TODO: Implement the GPU heuristic order calculator so this path mirrors PrimeOrderCalculator before enabling device execution. + return GpuPow2ModStatus.Unavailable; + } +} diff --git a/PerfectNumbers.Core/HeuristicArithmetic.cs b/PerfectNumbers.Core/HeuristicArithmetic.cs new file mode 100644 index 0000000..7e62121 --- /dev/null +++ b/PerfectNumbers.Core/HeuristicArithmetic.cs @@ -0,0 +1,48 @@ +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/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index 4776322..1de140d 100644 --- a/PerfectNumbers.Core/MersenneDivisorCycles.cs +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -189,6 +189,16 @@ public class MersenneDivisorCycles return true; } + PrimeOrderCalculator.PrimeOrderResult orderResult = PrimeOrderCalculator.Calculate( + divisor, + previousOrder: null, + PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); + if (orderResult.Order != 0UL) + { + cycleLength = orderResult.Order; + return true; + } + ulong phi = divisor - 1UL; if (phi == 0UL) { diff --git a/PerfectNumbers.Core/PerfectNumbers.Core.csproj b/PerfectNumbers.Core/PerfectNumbers.Core.csproj index fa27778..f38ec24 100644 --- a/PerfectNumbers.Core/PerfectNumbers.Core.csproj +++ b/PerfectNumbers.Core/PerfectNumbers.Core.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -17,6 +17,7 @@ + diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Wide.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Wide.cs index ed0b084..8951b4d 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Wide.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Wide.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Numerics; using System.Threading; +using PerfectNumbers.Core.Gpu; namespace PerfectNumbers.Core; @@ -954,7 +955,17 @@ internal static partial class PrimeOrderCalculator { return UInt128.Zero; } - + if (modulus <= (UInt128)ulong.MaxValue && exponent <= (UInt128)ulong.MaxValue) + { + ulong prime64 = (ulong)modulus; + ulong exponent64 = (ulong)exponent; + // TODO: Extend PrimeOrderGpuHeuristics to handle 128-bit moduli so this fast path can remain on the device. + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(exponent64, prime64, out ulong remainder); + if (status == GpuPow2ModStatus.Success) + { + return remainder; + } + } BigInteger result = BigInteger.ModPow(2, (BigInteger)exponent, (BigInteger)modulus); return (UInt128)result; } diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.cs b/PerfectNumbers.Core/PrimeOrderCalculator.cs index 0decc77..93d91e0 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.cs @@ -4,7 +4,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Security.Cryptography; +using PerfectNumbers.Core.Gpu; using System.Threading; namespace PerfectNumbers.Core; @@ -134,7 +136,7 @@ internal static partial class PrimeOrderCalculator { ulong factor = factorSpan[i].Value; ulong reduced = phi / factor; - if (reduced.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL) + if (Pow2EqualsOne(reduced, prime, divisorData)) { return false; } @@ -149,7 +151,7 @@ internal static partial class PrimeOrderCalculator if ((prime & 7UL) == 1UL || (prime & 7UL) == 7UL) { ulong half = phi >> 1; - if (half.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL) + if (Pow2EqualsOne(half, prime, divisorData)) { order = half; } @@ -191,7 +193,7 @@ internal static partial class PrimeOrderCalculator } ulong reduced = order / primeFactor; - if (reduced.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL) + if (Pow2EqualsOne(reduced, prime, divisorData)) { order = reduced; continue; @@ -223,7 +225,7 @@ internal static partial class PrimeOrderCalculator #if DEBUG Console.WriteLine("Verifying a^order ≡ 1 (mod p)"); #endif - if (order.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) != 1UL) + if (!Pow2EqualsOne(order, prime, divisorData)) { return false; } @@ -283,7 +285,7 @@ internal static partial class PrimeOrderCalculator } reduced /= primeFactor; - if (reduced.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL) + if (Pow2EqualsOne(reduced, prime, divisorData)) { return false; } @@ -355,32 +357,108 @@ internal static partial class PrimeOrderCalculator int powBudget = config.MaxPowChecks <= 0 ? candidates.Count : config.MaxPowChecks; int powUsed = 0; int candidateCount = candidates.Count; + bool allowGpuBatch = true; + Span candidateSpan = CollectionsMarshal.AsSpan(candidates); #if DEBUG Console.WriteLine($"Checking candidates ({candidateCount} candidates, {powBudget} pow budget)"); #endif - for (int i = 0; i < candidateCount; i++) + int index = 0; + const int MaxGpuBatchSize = 256; + const int StackGpuBatchSize = 32; + Span stackGpuRemainders = stackalloc ulong[StackGpuBatchSize]; + while (index < candidateCount && powUsed < powBudget) { - if (powUsed >= powBudget) + int remaining = candidateCount - index; + int budgetRemaining = powBudget - powUsed; + int batchSize = Math.Min(remaining, Math.Min(budgetRemaining, MaxGpuBatchSize)); + if (batchSize <= 0) { break; } - ulong candidate = candidates[i]; - powUsed++; + ReadOnlySpan batch = candidateSpan.Slice(index, batchSize); + ulong[]? gpuPool = null; + Span pooledGpuRemainders = default; + bool gpuSuccess = false; + bool gpuStackRemainders = false; + GpuPow2ModStatus status = GpuPow2ModStatus.Unavailable; - if (candidate.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) != 1UL) + if (allowGpuBatch) { - continue; + if (batchSize <= StackGpuBatchSize) + { + Span localRemainders = stackGpuRemainders.Slice(0, batchSize); + status = PrimeOrderGpuHeuristics.TryPow2ModBatch(batch, prime, localRemainders); + if (status == GpuPow2ModStatus.Success) + { + gpuSuccess = true; + gpuStackRemainders = true; + } + } + else + { + gpuPool = ArrayPool.Shared.Rent(batchSize); + Span pooledRemainders = gpuPool.AsSpan(0, batchSize); + status = PrimeOrderGpuHeuristics.TryPow2ModBatch(batch, prime, pooledRemainders); + if (status == GpuPow2ModStatus.Success) + { + pooledGpuRemainders = pooledRemainders; + gpuSuccess = true; + } + else + { + ArrayPool.Shared.Return(gpuPool, clearArray: false); + gpuPool = null; + } + } + + if (!gpuSuccess && (status == GpuPow2ModStatus.Overflow || status == GpuPow2ModStatus.Unavailable)) + { + allowGpuBatch = false; + } } - if (!TryConfirmCandidate(prime, candidate, divisorData, config, ref powUsed, powBudget)) + for (int i = 0; i < batchSize && powUsed < powBudget; i++) { - continue; + ulong candidate = batch[i]; + powUsed++; + + bool equalsOne; + if (gpuSuccess) + { + ulong remainderValue = gpuStackRemainders ? stackGpuRemainders[i] : pooledGpuRemainders[i]; + equalsOne = remainderValue == 1UL; + } + else + { + equalsOne = Pow2EqualsOne(candidate, prime, divisorData); + } + if (!equalsOne) + { + continue; + } + + if (!TryConfirmCandidate(prime, candidate, divisorData, config, ref powUsed, powBudget)) + { + continue; + } + + if (gpuPool is not null) + { + ArrayPool.Shared.Return(gpuPool, clearArray: false); + } + + result = candidate; + return true; } - result = candidate; - return true; + if (gpuPool is not null) + { + ArrayPool.Shared.Return(gpuPool, clearArray: false); + } + + index += batchSize; } #if DEBUG @@ -601,7 +679,7 @@ internal static partial class PrimeOrderCalculator } powUsed++; - if (reduced.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL) + if (Pow2EqualsOne(reduced, prime, divisorData)) { return false; } @@ -611,6 +689,18 @@ internal static partial class PrimeOrderCalculator return true; } + private static bool Pow2EqualsOne(ulong exponent, ulong prime, in MontgomeryDivisorData divisorData) + { + // TODO: Implement the GPU heuristic path so TryPow2Mod can verify candidates on the device instead of immediately falling back to the CPU Montgomery reducer. + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(exponent, prime, out ulong remainder); + if (status == GpuPow2ModStatus.Success) + { + return remainder == 1UL; + } + + return exponent.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL; + } + private static PartialFactorResult PartialFactor(ulong value, PrimeOrderSearchConfig config) { if (value <= 1UL) diff --git a/apply_patch.cs b/apply_patch.cs index 479bd3a..734e6b8 100644 --- a/apply_patch.cs +++ b/apply_patch.cs @@ -1,6 +1,8 @@ #nullable enable using System; +using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -46,6 +48,29 @@ internal readonly record struct PatchApplicationOptions(bool CheckOnly, bool Ign internal sealed record PatchRunResult(string PatchFilePath, bool Success, string? ErrorMessage); +internal sealed record PatchProfileEntry( + string PatchFilePath, + int FileCount, + int HunkCount, + int LineCount, + long PatchBytes, + TimeSpan ReadTime, + TimeSpan ParseTime, + TimeSpan ApplyTime); + +internal sealed record LineReplacement(string RelativePath, int LineNumber, string? ExpectedContent, string NewContent); + +internal sealed class LineReplacementException : Exception +{ + public LineReplacementException(LineReplacement replacement, string message) + : base(message) + { + Replacement = replacement; + } + + public LineReplacement Replacement { get; } +} + var args = Args?.ToArray() ?? Array.Empty(); if (args.Length == 0) { @@ -56,8 +81,10 @@ if (args.Length == 0) bool checkOnly = false; bool ignoreWhitespace = false; bool ignoreLineEndings = false; +bool profile = false; string? explicitTargetRoot = null; var patchFileArguments = new List(); +var lineReplacements = new List(); for (int i = 0; i < args.Length; i++) { @@ -81,6 +108,12 @@ for (int i = 0; i < args.Length; i++) continue; } + if (string.Equals(argument, "--profile", StringComparison.Ordinal)) + { + profile = true; + continue; + } + if (string.Equals(argument, "--target", StringComparison.Ordinal)) { if (i + 1 >= args.Length) @@ -93,10 +126,38 @@ for (int i = 0; i < args.Length; i++) continue; } + if (string.Equals(argument, "--replace-line", StringComparison.Ordinal)) + { + if (i + 4 >= args.Length) + { + Console.Error.WriteLine("--replace-line requires arguments."); + Environment.Exit(1); + } + + var replacementPath = args[++i]; + + if (!int.TryParse(args[++i], NumberStyles.Integer, CultureInfo.InvariantCulture, out var replacementLine) || replacementLine <= 0) + { + Console.Error.WriteLine("--replace-line requires a positive integer line number."); + Environment.Exit(1); + } + + var expected = args[++i]; + + if (string.Equals(expected, "_", StringComparison.Ordinal)) + { + expected = null; + } + + var newContent = args[++i]; + lineReplacements.Add(new LineReplacement(replacementPath, replacementLine, expected, newContent)); + continue; + } + patchFileArguments.Add(argument); } -if (patchFileArguments.Count == 0) +if (patchFileArguments.Count == 0 && lineReplacements.Count == 0) { PrintUsage(); Environment.Exit(1); @@ -127,9 +188,9 @@ else targetRoot = Directory.GetCurrentDirectory(); } -if (patchFileArguments.Count == 0) +if (patchFileArguments.Count == 0 && lineReplacements.Count == 0) { - Console.Error.WriteLine("No patch files specified."); + Console.Error.WriteLine("No patch files or line replacements specified."); Environment.Exit(1); } @@ -144,6 +205,7 @@ if (!Directory.Exists(targetRoot)) var patchFilePaths = patchFileArguments.Select(Path.GetFullPath).ToList(); var applicationOptions = new PatchApplicationOptions(checkOnly, ignoreWhitespace, ignoreLineEndings); var patchResults = new List(); +List? patchProfiles = profile ? new List() : null; foreach (var patchFilePath in patchFilePaths) { @@ -154,14 +216,53 @@ foreach (var patchFilePath in patchFilePaths) throw new FileNotFoundException($"Patch file '{patchFilePath}' does not exist.", patchFilePath); } - var patchText = File.ReadAllLines(patchFilePath); - var parsedFiles = ParsePatch(patchText); + Stopwatch? stopwatch = null; + TimeSpan readDuration = default; + TimeSpan parseDuration = default; + TimeSpan applyDuration = default; + long patchBytes = 0L; + int fileCount = 0; + int hunkCount = 0; + int lineCount = 0; + + if (profile) + { + stopwatch = Stopwatch.StartNew(); + patchBytes = new FileInfo(patchFilePath).Length; + } + + var parsedFiles = ParsePatchFromFile(patchFilePath); + + if (stopwatch is not null) + { + readDuration = TimeSpan.Zero; + parseDuration = stopwatch.Elapsed; + fileCount = parsedFiles.Count; + + foreach (var parsedFile in parsedFiles) + { + hunkCount += parsedFile.Hunks.Count; + + foreach (var hunk in parsedFile.Hunks) + { + lineCount += hunk.Lines.Count; + } + } + + stopwatch.Restart(); + } foreach (var patchFile in parsedFiles) { ApplyPatch(targetRoot, patchFile, applicationOptions); } + if (stopwatch is not null && patchProfiles is not null) + { + applyDuration = stopwatch.Elapsed; + patchProfiles.Add(new PatchProfileEntry(patchFilePath, fileCount, hunkCount, lineCount, patchBytes, readDuration, parseDuration, applyDuration)); + } + patchResults.Add(new PatchRunResult(patchFilePath, true, null)); if (!applicationOptions.CheckOnly) @@ -175,13 +276,26 @@ foreach (var patchFilePath in patchFilePaths) } } +if (lineReplacements.Count > 0) +{ + var replacementResults = ApplyLineReplacements(targetRoot, lineReplacements, applicationOptions); + patchResults.AddRange(replacementResults); +} + foreach (var result in patchResults) { var displayPath = GetDisplayPath(result.PatchFilePath); if (result.Success) { - Console.WriteLine($"Patch '{displayPath}' applied successfully."); + if (applicationOptions.CheckOnly) + { + Console.WriteLine($"Patch '{displayPath}' verified successfully. Ready to apply."); + } + else + { + Console.WriteLine($"Patch '{displayPath}' applied successfully."); + } } else { @@ -189,139 +303,168 @@ foreach (var result in patchResults) } } +if (patchProfiles is { Count: > 0 }) +{ + Console.WriteLine("Patch profiling summary:"); + + foreach (var entry in patchProfiles) + { + var displayPath = GetDisplayPath(entry.PatchFilePath); + Console.WriteLine( + $"- {displayPath}: read {entry.ReadTime.TotalMilliseconds:F2} ms, parse {entry.ParseTime.TotalMilliseconds:F2} ms, apply {entry.ApplyTime.TotalMilliseconds:F2} ms " + + $"(files={entry.FileCount}, hunks={entry.HunkCount}, lines={entry.LineCount}, bytes={entry.PatchBytes})"); + } + + var totalFiles = patchProfiles.Sum(static e => e.FileCount); + var totalHunks = patchProfiles.Sum(static e => e.HunkCount); + var totalLines = patchProfiles.Sum(static e => e.LineCount); + var totalBytes = patchProfiles.Sum(static e => e.PatchBytes); + var totalRead = TimeSpan.FromTicks(patchProfiles.Sum(static e => e.ReadTime.Ticks)); + var totalParse = TimeSpan.FromTicks(patchProfiles.Sum(static e => e.ParseTime.Ticks)); + var totalApply = TimeSpan.FromTicks(patchProfiles.Sum(static e => e.ApplyTime.Ticks)); + + Console.WriteLine( + $"Totals: read {totalRead.TotalMilliseconds:F2} ms, parse {totalParse.TotalMilliseconds:F2} ms, apply {totalApply.TotalMilliseconds:F2} ms " + + $"(files={totalFiles}, hunks={totalHunks}, lines={totalLines}, bytes={totalBytes})"); +} + if (patchResults.Any(static r => !r.Success)) { Environment.Exit(1); } -static List ParsePatch(IReadOnlyList lines) +static List ParsePatchFromFile(string patchFilePath) +{ + using var stream = File.OpenRead(patchFilePath); + using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 16_384, leaveOpen: false); + using var lineReader = new PatchLineReader(reader); + var files = ParsePatch(lineReader); + return files; +} + +static List ParsePatch(PatchLineReader reader) { var files = new List(); - int index = 0; + string? diffHeader = null; + string? originalPath = null; + string? modifiedPath = null; + List? hunks = null; - while (index < lines.Count) + while (reader.TryRead(out var currentLine)) { - if (!lines[index].StartsWith("diff --git ", StringComparison.Ordinal)) + if (currentLine is null) + { + break; + } + + if (string.IsNullOrWhiteSpace(currentLine)) { - index++; continue; } - var diffHeader = lines[index]; - index++; - - string? originalPath = null; - string? modifiedPath = null; - var hunks = new List(); - PatchFileType type = PatchFileType.Modify; - - while (index < lines.Count) + if (!currentLine.StartsWith("diff --git ", StringComparison.Ordinal)) { - var line = lines[index]; - - if (line.StartsWith("diff --git ", StringComparison.Ordinal) && hunks.Count > 0) + if (diffHeader is null) { - break; - } - - if (line.StartsWith("--- ", StringComparison.Ordinal)) - { - originalPath = NormalizePath(line.Substring(4)); - index++; continue; } - if (line.StartsWith("+++ ", StringComparison.Ordinal)) + if (currentLine.StartsWith("--- ", StringComparison.Ordinal)) { - modifiedPath = NormalizePath(line.Substring(4)); - index++; + originalPath = NormalizePath(currentLine.AsSpan(4)); continue; } - if (line.StartsWith("@@", StringComparison.Ordinal)) + if (currentLine.StartsWith("+++ ", StringComparison.Ordinal)) { - var hunk = ParseHunk(lines, ref index); + modifiedPath = NormalizePath(currentLine.AsSpan(4)); + continue; + } + + if (currentLine.StartsWith("@@", StringComparison.Ordinal)) + { + hunks ??= new List(); + var hunk = ParseHunk(currentLine, reader); hunks.Add(hunk); continue; } - index++; + continue; } - if (originalPath is null || modifiedPath is null) + if (diffHeader is not null) { - throw new InvalidOperationException($"Missing file paths for patch header: {diffHeader}"); + FinalizePatchFile(files, diffHeader, originalPath, modifiedPath, hunks); } - if (IsDevNull(originalPath) && !IsDevNull(modifiedPath)) - { - type = PatchFileType.Add; - } - else if (!IsDevNull(originalPath) && IsDevNull(modifiedPath)) - { - type = PatchFileType.Delete; - } + diffHeader = currentLine; + originalPath = null; + modifiedPath = null; + hunks = new List(); + } - files.Add(new PatchFile(originalPath, modifiedPath, hunks, type)); + if (diffHeader is not null) + { + FinalizePatchFile(files, diffHeader, originalPath, modifiedPath, hunks); } return files; } -static PatchHunk ParseHunk(IReadOnlyList lines, ref int index) +static PatchHunk ParseHunk(string header, PatchLineReader reader) { - var header = lines[index]; var (originalStart, originalLength, modifiedStart, modifiedLength) = ParseHunkHeader(header); - index++; - var patchLines = new List(); - while (index < lines.Count) + while (reader.TryPeek(out var nextLine)) { - var line = lines[index]; - - if (line.StartsWith("diff --git ", StringComparison.Ordinal) || line.StartsWith("@@", StringComparison.Ordinal)) + if (nextLine is null) { break; } - if (line.StartsWith("+++ ", StringComparison.Ordinal) || line.StartsWith("--- ", StringComparison.Ordinal)) + if (nextLine.StartsWith("diff --git ", StringComparison.Ordinal) || + nextLine.StartsWith("@@", StringComparison.Ordinal) || + nextLine.StartsWith("+++ ", StringComparison.Ordinal) || + nextLine.StartsWith("--- ", StringComparison.Ordinal)) { break; } + if (!reader.TryRead(out var consumed) || consumed is null) + { + break; + } + + var line = consumed; + if (line.StartsWith("+", StringComparison.Ordinal)) { patchLines.Add(new AddedLine(line.Substring(1), true)); - index++; continue; } if (line.StartsWith("-", StringComparison.Ordinal)) { patchLines.Add(new RemovedLine(line.Substring(1), true)); - index++; continue; } if (line.StartsWith(" ", StringComparison.Ordinal)) { patchLines.Add(new ContextLine(line.Substring(1), true)); - index++; continue; } if (line.StartsWith("\\ No newline at end of file", StringComparison.Ordinal)) { MarkLastLineWithoutTrailingNewline(patchLines); - index++; continue; } if (line.Length == 0) { patchLines.Add(new ContextLine(string.Empty, true)); - index++; continue; } @@ -331,6 +474,193 @@ static PatchHunk ParseHunk(IReadOnlyList lines, ref int index) return new PatchHunk(originalStart, originalLength, modifiedStart, modifiedLength, patchLines); } +static void FinalizePatchFile( + List files, + string diffHeader, + string? originalPath, + string? modifiedPath, + List? hunks) +{ + if (originalPath is null || modifiedPath is null) + { + throw new InvalidOperationException($"Missing file paths for patch header: {diffHeader}"); + } + + PatchFileType type = PatchFileType.Modify; + + if (IsDevNull(originalPath) && !IsDevNull(modifiedPath)) + { + type = PatchFileType.Add; + } + else if (!IsDevNull(originalPath) && IsDevNull(modifiedPath)) + { + type = PatchFileType.Delete; + } + + files.Add(new PatchFile(originalPath, modifiedPath, hunks ?? new List(), type)); +} + +private sealed class PatchLineReader : IDisposable +{ + private const int ReadBufferSize = 4096; + private const int InitialLineBufferSize = 256; + + private readonly StreamReader _reader; + private readonly char[] _readBuffer; + private char[] _lineBuffer; + private int _readLength; + private int _readIndex; + private string? _bufferedLine; + private bool _hasBufferedLine; + private bool _disposed; + + public PatchLineReader(StreamReader reader) + { + _reader = reader; + _readBuffer = ArrayPool.Shared.Rent(ReadBufferSize); + _lineBuffer = ArrayPool.Shared.Rent(InitialLineBufferSize); + } + + public bool TryPeek(out string? line) + { + if (!_hasBufferedLine) + { + if (!TryReadLineInternal(out _bufferedLine)) + { + line = null; + return false; + } + + _hasBufferedLine = true; + } + + line = _bufferedLine; + return line is not null; + } + + public bool TryRead(out string? line) + { + if (_hasBufferedLine) + { + line = _bufferedLine; + _bufferedLine = null; + _hasBufferedLine = false; + return line is not null; + } + + return TryReadLineInternal(out line); + } + + private bool TryReadLineInternal(out string? line) + { + if (_disposed) + { + line = null; + return false; + } + + int length = 0; + + while (true) + { + if (!TryReadChar(out char current)) + { + if (length == 0) + { + line = null; + return false; + } + + break; + } + + if (current == '\r') + { + if ((_readIndex < _readLength) || EnsureCharBuffer()) + { + if (_readIndex < _readLength && _readBuffer[_readIndex] == '\n') + { + _readIndex++; + } + } + + break; + } + + if (current == '\n') + { + break; + } + + EnsureLineCapacity(length, length + 1); + _lineBuffer[length++] = current; + } + + line = new string(_lineBuffer, 0, length); + return true; + } + + private bool TryReadChar(out char value) + { + if (!EnsureCharBuffer()) + { + value = '\0'; + return false; + } + + value = _readBuffer[_readIndex++]; + return true; + } + + private bool EnsureCharBuffer() + { + if (_readIndex < _readLength) + { + return true; + } + + if (_disposed) + { + return false; + } + + _readLength = _reader.Read(_readBuffer, 0, _readBuffer.Length); + _readIndex = 0; + return _readLength > 0; + } + + private void EnsureLineCapacity(int currentLength, int requiredLength) + { + if (requiredLength <= _lineBuffer.Length) + { + return; + } + + int newSize = _lineBuffer.Length * 2; + while (newSize < requiredLength) + { + newSize *= 2; + } + + char[] newBuffer = ArrayPool.Shared.Rent(newSize); + Array.Copy(_lineBuffer, newBuffer, currentLength); + ArrayPool.Shared.Return(_lineBuffer); + _lineBuffer = newBuffer; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + ArrayPool.Shared.Return(_readBuffer); + ArrayPool.Shared.Return(_lineBuffer); + } +} + static void MarkLastLineWithoutTrailingNewline(List lines) { if (lines.Count == 0) @@ -351,20 +681,37 @@ static void MarkLastLineWithoutTrailingNewline(List lines) static (int originalStart, int originalLength, int modifiedStart, int modifiedLength) ParseHunkHeader(string header) { - // Format: @@ -start,length +start,length @@ optional text - int firstSpace = header.IndexOf(' '); - int secondSpace = header.IndexOf(' ', firstSpace + 1); - var originalRange = header.Substring(firstSpace + 1, secondSpace - firstSpace - 1); - var modifiedRangeEnd = header.IndexOf(' ', secondSpace + 1); - string modifiedRange; + var span = header.AsSpan(); - if (modifiedRangeEnd == -1) + int firstSpace = span.IndexOf(' '); + + if (firstSpace < 0) { - modifiedRange = header.Substring(secondSpace + 1).TrimEnd('@').Trim(); + throw new InvalidOperationException($"Invalid hunk header: '{header}'."); + } + + span = span[(firstSpace + 1)..]; + + int secondSpace = span.IndexOf(' '); + + if (secondSpace < 0) + { + throw new InvalidOperationException($"Invalid hunk header: '{header}'."); + } + + var originalRange = span[..secondSpace]; + span = span[(secondSpace + 1)..]; + + int thirdSpace = span.IndexOf(' '); + ReadOnlySpan modifiedRange; + + if (thirdSpace < 0) + { + modifiedRange = span; } else { - modifiedRange = header.Substring(secondSpace + 1, modifiedRangeEnd - secondSpace - 1); + modifiedRange = span[..thirdSpace]; } var (originalStart, originalLength) = ParseRange(originalRange); @@ -372,25 +719,35 @@ static (int originalStart, int originalLength, int modifiedStart, int modifiedLe return (originalStart, originalLength, modifiedStart, modifiedLength); } -static (int start, int length) ParseRange(string range) +static (int start, int length) ParseRange(ReadOnlySpan range) { - // Format: -start,length or +start,length - int commaIndex = range.IndexOf(','); - string startText; - string lengthText; + if (range.Length == 0) + { + throw new InvalidOperationException("Range descriptor cannot be empty."); + } + + if (range[0] != '-' && range[0] != '+') + { + throw new InvalidOperationException($"Unexpected range prefix: '{range[0]}' in '{range.ToString()}'."); + } + + var numericPortion = range[1..]; + int commaIndex = numericPortion.IndexOf(','); + ReadOnlySpan startText; + ReadOnlySpan lengthText; if (commaIndex >= 0) { - startText = range.Substring(1, commaIndex - 1); - lengthText = range[(commaIndex + 1)..]; + startText = numericPortion[..commaIndex]; + lengthText = numericPortion[(commaIndex + 1)..]; } else { - startText = range.Substring(1); + startText = numericPortion; lengthText = "1"; } - return (int.Parse(startText, CultureInfo.InvariantCulture), int.Parse(lengthText, CultureInfo.InvariantCulture)); + return (int.Parse(startText, NumberStyles.Integer, CultureInfo.InvariantCulture), int.Parse(lengthText, NumberStyles.Integer, CultureInfo.InvariantCulture)); } static void ApplyPatch(string root, PatchFile patchFile, PatchApplicationOptions options) @@ -428,18 +785,20 @@ static string NormalizeRelativePath(string path) return path; } -static string NormalizePath(string headerPath) +static string NormalizePath(ReadOnlySpan headerPath) { var trimmed = headerPath.Trim(); if (trimmed.StartsWith("a/", StringComparison.Ordinal) || trimmed.StartsWith("b/", StringComparison.Ordinal)) { - return trimmed.Substring(2); + trimmed = trimmed[2..]; } - return trimmed; + return new string(trimmed); } +static string NormalizePath(string headerPath) => NormalizePath(headerPath.AsSpan()); + static bool IsDevNull(string? path) { return path is null || path.Equals("/dev/null", StringComparison.Ordinal); @@ -746,6 +1105,103 @@ static void TryDeletePatchFile(string patchFilePath) } } +static List ApplyLineReplacements(string root, List replacements, PatchApplicationOptions options) +{ + var results = new List(); + + foreach (var group in replacements.GroupBy(static r => NormalizeRelativePath(r.RelativePath), StringComparer.Ordinal)) + { + var normalizedPath = group.Key; + var replacementsForFile = group.OrderBy(static r => r.LineNumber).ToList(); + + if (replacementsForFile.Count == 0) + { + continue; + } + + try + { + var targetPath = Path.Combine(root, normalizedPath); + + if (!File.Exists(targetPath)) + { + var displayPath = GetDisplayPath(targetPath); + throw new LineReplacementException(replacementsForFile[0], $"Cannot replace line because file '{displayPath}' does not exist."); + } + + var snapshot = ReadAllLinesPreservingNewlines(targetPath); + var lines = snapshot.Lines; + var pending = new List<(int Index, string NewContent)>(); + + foreach (var replacement in replacementsForFile) + { + if (replacement.LineNumber <= 0) + { + throw new LineReplacementException(replacement, $"Line numbers must be positive. Received {replacement.LineNumber} for '{GetDisplayPath(targetPath)}'."); + } + + int index = replacement.LineNumber - 1; + + if (index >= lines.Count) + { + throw new LineReplacementException(replacement, $"File '{GetDisplayPath(targetPath)}' contains only {lines.Count} lines. Cannot replace line {replacement.LineNumber}."); + } + + if (replacement.ExpectedContent is not null) + { + var actual = lines[index]; + + if (!LinesMatch(actual, replacement.ExpectedContent, options)) + { + try + { + ThrowLineMismatch($"Line {replacement.LineNumber} mismatch in '{GetDisplayPath(targetPath)}'.", replacement.ExpectedContent, actual, options); + } + catch (InvalidOperationException ex) + { + throw new LineReplacementException(replacement, ex.Message); + } + } + } + + pending.Add((index, replacement.NewContent)); + } + + if (!options.CheckOnly) + { + foreach (var (index, newContent) in pending) + { + lines[index] = newContent; + } + + WriteAllLinesPreservingNewlines(targetPath, snapshot); + } + + foreach (var replacement in replacementsForFile) + { + results.Add(new PatchRunResult(BuildReplacementDisplay(replacement), true, null)); + } + } + catch (LineReplacementException ex) + { + results.Add(new PatchRunResult(BuildReplacementDisplay(ex.Replacement), false, ex.Message)); + } + catch (Exception ex) + { + var fallback = replacementsForFile[0]; + results.Add(new PatchRunResult(BuildReplacementDisplay(fallback), false, ex.Message)); + } + } + + return results; +} + +static string BuildReplacementDisplay(LineReplacement replacement) +{ + var normalizedPath = NormalizeRelativePath(replacement.RelativePath); + return $"replace-line:{normalizedPath}:{replacement.LineNumber}"; +} + static void PrintUsage() { Console.Error.WriteLine("Usage: dotnet script apply_patch.cs -- [options] [ ...] [target-directory]"); @@ -754,6 +1210,9 @@ static void PrintUsage() Console.Error.WriteLine(" --check Validate patches without modifying files."); Console.Error.WriteLine(" --ignore-whitespace Ignore whitespace differences when matching context."); Console.Error.WriteLine(" --ignore-eol Ignore line-ending differences when matching context."); + Console.Error.WriteLine(" --profile Collect read/parse/apply timings for each patch file."); Console.Error.WriteLine(" --target Explicitly set the target directory."); + Console.Error.WriteLine(" --replace-line "); + Console.Error.WriteLine(" Replace a single line by absolute number; use '_' as expected-text to skip validation."); } diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..73977fb --- /dev/null +++ b/todo.txt @@ -0,0 +1,3 @@ +- 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/GpuNumericTypeProbe.csx b/tools/GpuNumericTypeProbe.csx new file mode 100644 index 0000000..0bfb2a6 --- /dev/null +++ b/tools/GpuNumericTypeProbe.csx @@ -0,0 +1,160 @@ +#r "nuget: ILGPU, 1.5.3" +#r "nuget: PeterO.Numbers, 1.8.1" + +using System; +using System.Numerics; +using ILGPU; +using ILGPU.Runtime; +using ILGPU.Runtime.CPU; +using PeterO.Numbers; + +var context = Context.CreateDefault(); +var accelerator = context.CreateCPUAccelerator(0); + +try +{ + Console.WriteLine("ILGPU numeric type support probe (CPU accelerator)"); + Console.WriteLine(); + + ProbeResult[] results = + [ + ProbeBigInteger(accelerator), + ProbeEInteger(accelerator), + ProbeERational(accelerator) + ]; + + foreach (ProbeResult result in results) + { + Console.WriteLine($"{result.Type}: {(result.Supported ? "supported" : "unsupported")}"); + Console.WriteLine($" Details: {result.Details}"); + Console.WriteLine(); + } +} +finally +{ + accelerator.Dispose(); + context.Dispose(); +} + +record ProbeResult(string Type, bool Supported, string Details) +{ + public static ProbeResult Success(string type, string details) => new(type, true, details); + + public static ProbeResult Failure(string type, Exception exception) => + new(type, false, $"{exception.GetType().Name}: {exception.Message}"); + + public static ProbeResult Failure(string type, string details) => new(type, false, details); +} + +static ProbeResult ProbeBigInteger(Accelerator accelerator) +{ + try + { + var kernel = accelerator.LoadAutoGroupedStreamKernel>(BigIntegerKernel); + using var buffer = accelerator.Allocate1D(1); + var extent = new Index1D((int)buffer.Length); + kernel(extent, buffer.View); + accelerator.Synchronize(); + return ProbeResult.Success( + "System.Numerics.BigInteger", + "Addition, subtraction, multiplication, division, and modulo compile on the CPU accelerator."); + } + catch (Exception ex) + { + return ProbeResult.Failure("System.Numerics.BigInteger", ex); + } +} + +static ProbeResult ProbeEInteger(Accelerator accelerator) +{ + try + { + var kernel = accelerator.LoadAutoGroupedStreamKernel>(EIntegerKernel); + using var buffer = accelerator.Allocate1D(1); + kernel(new Index1D((int)buffer.Length), buffer.View); + accelerator.Synchronize(); + return ProbeResult.Success( + "PeterO.Numbers.EInteger", + "Addition, subtraction, multiplication, division, and remainder compile on the CPU accelerator."); + } + catch (Exception ex) + { + return ProbeResult.Failure("PeterO.Numbers.EInteger", ex); + } +} + +static ProbeResult ProbeERational(Accelerator accelerator) +{ + bool hasRemainder = HasERationalRemainder(); + + try + { + var kernel = accelerator.LoadAutoGroupedStreamKernel>(ERationalKernel); + using var buffer = accelerator.Allocate1D(1); + kernel(new Index1D((int)buffer.Length), buffer.View); + accelerator.Synchronize(); + + string details = "Addition, subtraction, multiplication, and division compile on the CPU accelerator."; + details += hasRemainder + ? " ERational exposes a remainder API that requires further investigation." + : " ERational does not expose a remainder/modulo API."; + return ProbeResult.Success("PeterO.Numbers.ERational", details); + } + catch (Exception ex) + { + string details = $"{ex.GetType().Name}: {ex.Message}"; + if (!hasRemainder) + { + details += " ERational does not expose a remainder/modulo API."; + } + + return ProbeResult.Failure("PeterO.Numbers.ERational", details); + } +} + +static void BigIntegerKernel(Index1D index, ArrayView1D data) +{ + BigInteger value = new BigInteger(data[index]); + value += BigInteger.One; + value -= BigInteger.One; + value *= new BigInteger(2); + value /= new BigInteger(3); + value %= new BigInteger(5); + int comparison = value.CompareTo(BigInteger.Zero); + data[index] = comparison; +} + +static void EIntegerKernel(Index1D index, ArrayView1D data) +{ + EInteger value = EInteger.FromInt32(data[index]); + value = value + EInteger.One; + value = value - EInteger.One; + value = value * EInteger.FromInt32(2); + value = value.Divide(EInteger.FromInt32(3)); + value = value.Remainder(EInteger.FromInt32(5)); + int comparison = value.CompareTo(EInteger.Zero); + data[index] = comparison; +} + +static void ERationalKernel(Index1D index, ArrayView1D data) +{ + ERational value = ERational.FromInt32(data[index]); + ERational adjusted = (value + ERational.FromInt32(1)) - ERational.FromInt32(1); + ERational product = adjusted * ERational.FromInt32(2); + ERational quotient = product / ERational.FromInt32(3); + int comparison = quotient.CompareTo(ERational.FromInt32(0)); + data[index] = comparison; +} + +static bool HasERationalRemainder() +{ + foreach (var method in typeof(ERational).GetMethods()) + { + if (string.Equals(method.Name, "Remainder", StringComparison.Ordinal)) + { + return true; + } + } + + return false; +} diff --git a/tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj b/tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj new file mode 100644 index 0000000..e771e0a --- /dev/null +++ b/tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj @@ -0,0 +1,11 @@ + + + Exe + net8.0 + enable + enable + + + + + \ No newline at end of file diff --git a/tools/HeuristicMultiplyShiftTiming/Program.cs b/tools/HeuristicMultiplyShiftTiming/Program.cs new file mode 100644 index 0000000..1c35acf --- /dev/null +++ b/tools/HeuristicMultiplyShiftTiming/Program.cs @@ -0,0 +1,95 @@ +using System.Diagnostics; +using PerfectNumbers.Core; + +namespace HeuristicMultiplyShiftTiming; + +internal static class Program +{ + private static readonly MultiplyShiftInput[] Inputs = + [ + new(0x3FFF_FFFF_FFFF_FFFFUL, 3UL, 3, "NearOverflowShift"), + new(0x2000_0000_0000_0000UL, 7UL, 4, "HalfRange"), + new(0x1234_5678_9ABC_DEF0UL, 5UL, 5, "MixedBits"), + new(0x0000_0000_1000_0000UL, 11UL, 3, "Sparse"), + ]; + + private const int Iterations = 200_000_000; + + public static void Main() + { + Console.WriteLine($"Timing each method for {Iterations:N0} iterations per input...\n"); + + foreach (MultiplyShiftInput input in Inputs) + { + Console.WriteLine($"Input: {input.Name} (value=0x{input.Value:X16}, multiplier={input.Multiplier}, shift={input.Shift})"); + + Measurement safe = MeasureSafe(input); + Console.WriteLine($" MultiplyShiftRight (UInt128 product): {safe.AverageNanoseconds:F3} ns | checksum=0x{safe.Checksum:X16}"); + + Measurement shiftFirst = MeasureShiftFirst(input); + Console.WriteLine($" MultiplyShiftRightShiftFirst (shift-first): {shiftFirst.AverageNanoseconds:F3} ns | checksum=0x{shiftFirst.Checksum:X16}"); + + Measurement naive = MeasureNaive(input); + Console.WriteLine($" Naive ((value * multiplier) >> shift): {naive.AverageNanoseconds:F3} ns | checksum=0x{naive.Checksum:X16}\n"); + } + } + + private static Measurement MeasureSafe(MultiplyShiftInput input) + { + ulong checksum = 0UL; + Stopwatch sw = Stopwatch.StartNew(); + ulong value = input.Value; + + for (int i = 0; i < Iterations; i++) + { + checksum += HeuristicArithmetic.MultiplyShiftRight(value, input.Multiplier, input.Shift); + value += 1UL; + } + + sw.Stop(); + return CreateMeasurement(sw, checksum); + } + + private static Measurement MeasureShiftFirst(MultiplyShiftInput input) + { + ulong checksum = 0UL; + Stopwatch sw = Stopwatch.StartNew(); + ulong value = input.Value; + + for (int i = 0; i < Iterations; i++) + { + checksum += HeuristicArithmetic.MultiplyShiftRightShiftFirst(value, input.Multiplier, input.Shift); + value += 1UL; + } + + sw.Stop(); + return CreateMeasurement(sw, checksum); + } + + private static Measurement MeasureNaive(MultiplyShiftInput input) + { + ulong checksum = 0UL; + Stopwatch sw = Stopwatch.StartNew(); + ulong value = input.Value; + + for (int i = 0; i < Iterations; i++) + { + checksum += (value * input.Multiplier) >> input.Shift; + value += 1UL; + } + + sw.Stop(); + return CreateMeasurement(sw, checksum); + } + + private static Measurement CreateMeasurement(Stopwatch sw, ulong checksum) + { + double elapsedNanoseconds = sw.ElapsedTicks * (1_000_000_000.0 / Stopwatch.Frequency); + double averageNanoseconds = elapsedNanoseconds / Iterations; + return new Measurement(averageNanoseconds, checksum); + } + + private readonly record struct MultiplyShiftInput(ulong Value, ulong Multiplier, int Shift, string Name); + + private readonly record struct Measurement(double AverageNanoseconds, ulong Checksum); +}