diff --git a/AGENTS.md b/AGENTS.md index dd4f115..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: @@ -31,14 +33,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. @@ -53,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. @@ -127,7 +139,48 @@ 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: + +- 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 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) + +- 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 `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. + - `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..09290e2 --- /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 ULongExtensions.MultiplyShiftRight(Input.Value, Input.Multiplier, Input.Shift); + } + + [Benchmark] + public ulong ShiftFirstMultiplyShift() + { + return ULongExtensions.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..1bb9335 --- /dev/null +++ b/PerfectNumbers.Core.Tests/Gpu/PrimeOrderGpuHeuristicsTests.cs @@ -0,0 +1,197 @@ +using System; +using FluentAssertions; +using PerfectNumbers.Core; +using PerfectNumbers.Core.Gpu; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +[Trait("Category", "Fast")] +public class PrimeOrderGpuHeuristicsTests +{ + public PrimeOrderGpuHeuristicsTests() + { + PrimeOrderGpuHeuristics.OverflowRegistry.Clear(); + PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting(); + } + + [Fact] + public void TryPow2Mod_returns_overflow_for_marked_prime() + { + const ulong prime = 97UL; + try + { + PrimeOrderGpuHeuristics.OverflowRegistry[prime] = 0; + + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, prime, out ulong remainder); + + status.Should().Be(GpuPow2ModStatus.Overflow); + remainder.Should().Be(0UL); + } + finally + { + PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _); + } + } + + [Fact] + public void TryPow2Mod_returns_success_for_supported_prime() + { + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(1UL, 193UL, out ulong remainder); + + status.Should().Be(GpuPow2ModStatus.Success); + remainder.Should().Be(2UL); + } + + [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.OverflowRegistry.TryRemove(prime, out _); + } + } + + [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.Success); + followUpRemainder.Should().Be(16UL); + } + finally + { + PrimeOrderGpuHeuristics.ResetCapabilitiesForTesting(); + } + } + + [Fact] + public void TryPow2ModBatch_returns_overflow_for_marked_prime() + { + const ulong prime = 193UL; + try + { + PrimeOrderGpuHeuristics.OverflowRegistry[prime] = 0; + + 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.OverflowRegistry.TryRemove(prime, out _); + } + } + + [Fact] + public void TryPow2ModBatch_returns_success_for_supported_prime() + { + ulong[] remainders = { 123UL, 456UL }; + ulong[] exponents = { 1UL, 2UL }; + GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2ModBatch(exponents, 257UL, remainders); + + status.Should().Be(GpuPow2ModStatus.Success); + remainders[0].Should().Be(2UL); + remainders[1].Should().Be(4UL); + } + + [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.OverflowRegistry.TryRemove(prime, out _); + } + } + + [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() + { + 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; + try + { + PrimeOrderGpuHeuristics.OverflowRegistry[prime] = 0; + + PrimeOrderCalculator.PrimeOrderResult result = PrimeOrderCalculator.Calculate(prime, null, PrimeOrderCalculator.PrimeOrderSearchConfig.HeuristicDefault); + + result.Status.Should().Be(PrimeOrderCalculator.PrimeOrderStatus.Found); + result.Order.Should().Be(3UL); + } + finally + { + PrimeOrderGpuHeuristics.OverflowRegistry.TryRemove(prime, out _); + } + } +} diff --git a/PerfectNumbers.Core.Tests/ULongExtensionsMultiplyShiftTests.cs b/PerfectNumbers.Core.Tests/ULongExtensionsMultiplyShiftTests.cs new file mode 100644 index 0000000..5267de5 --- /dev/null +++ b/PerfectNumbers.Core.Tests/ULongExtensionsMultiplyShiftTests.cs @@ -0,0 +1,44 @@ +using System.Numerics; +using FluentAssertions; +using PerfectNumbers.Core; +using Xunit; + +namespace PerfectNumbers.Core.Tests; + +[Trait("Category", "Fast")] +public class ULongExtensionsMultiplyShiftTests +{ + [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 = ULongExtensions.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 = ULongExtensions.MultiplyShiftRightShiftFirst(value, multiplier, shift); + + result.Should().Be((ulong)expected); + } + + + +} diff --git a/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs new file mode 100644 index 0000000..ea94b27 --- /dev/null +++ b/PerfectNumbers.Core/Gpu/PrimeOrderGpuHeuristics.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Concurrent; +using ILGPU; +using ILGPU.Runtime; +using PerfectNumbers.Core; +using System.Runtime.InteropServices; +namespace PerfectNumbers.Core.Gpu; + +internal enum GpuPow2ModStatus +{ + Success, + Overflow, + Unavailable, +} + +internal static class PrimeOrderGpuHeuristics +{ + private static readonly ConcurrentDictionary OverflowedPrimes = new(); + private static readonly ConcurrentDictionary, MontgomeryDivisorData, ArrayView1D>> Pow2ModKernelCache = new(); + private static PrimeOrderGpuCapability s_capability = PrimeOrderGpuCapability.Default; + + internal static ConcurrentDictionary OverflowRegistry => OverflowedPrimes; + + internal static void OverrideCapabilitiesForTesting(PrimeOrderGpuCapability capability) + { + s_capability = capability; + } + + internal static void ResetCapabilitiesForTesting() + { + s_capability = PrimeOrderGpuCapability.Default; + } + + public static GpuPow2ModStatus TryPow2Mod(ulong exponent, ulong prime, out ulong remainder) + { + Span exponents = stackalloc ulong[1]; + Span remainders = stackalloc ulong[1]; + exponents[0] = exponent; + + GpuPow2ModStatus status = TryPow2ModBatch(exponents, prime, remainders); + remainder = remainders[0]; + return status; + } + + 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)); + } + + Span target = remainders.Slice(0, exponents.Length); + target.Clear(); + + if (prime <= 1UL) + { + return GpuPow2ModStatus.Unavailable; + } + + ConcurrentDictionary overflowRegistry = OverflowedPrimes; + + if (overflowRegistry.ContainsKey(prime)) + { + return GpuPow2ModStatus.Overflow; + } + + PrimeOrderGpuCapability capability = s_capability; + + if (prime.GetBitLength() > capability.ModulusBits) + { + overflowRegistry[prime] = 0; + return GpuPow2ModStatus.Overflow; + } + + for (int i = 0; i < exponents.Length; i++) + { + if (exponents[i].GetBitLength() > capability.ExponentBits) + { + return GpuPow2ModStatus.Overflow; + } + } + + bool computed = TryComputeOnGpu(exponents, prime, target); + return computed ? GpuPow2ModStatus.Success : GpuPow2ModStatus.Unavailable; + } + + private static bool TryComputeOnGpu(ReadOnlySpan exponents, ulong prime, Span results) + { + try + { + var lease = GpuKernelPool.GetKernel(useGpuOrder: true); + try + { + using var execution = lease.EnterExecutionScope(); + Accelerator accelerator = lease.Accelerator; + AcceleratorStream stream = lease.Stream; + var kernel = GetPow2ModKernel(accelerator); + using var exponentBuffer = accelerator.Allocate1D(exponents.Length); + using var remainderBuffer = accelerator.Allocate1D(exponents.Length); + + exponentBuffer.View.CopyFromCPU(ref MemoryMarshal.GetReference(exponents), exponents.Length); + remainderBuffer.MemSetToZero(); + + MontgomeryDivisorData divisor = MontgomeryDivisorDataCache.Get(prime); + kernel(stream, exponents.Length, exponentBuffer.View, divisor, remainderBuffer.View); + + stream.Synchronize(); + + remainderBuffer.View.CopyToCPU(ref MemoryMarshal.GetReference(results), results.Length); + return true; + } + finally + { + lease.Dispose(); + } + } + catch (Exception ex) when (ex is AcceleratorException or InternalCompilerException or NotSupportedException or InvalidOperationException or AggregateException) + { + ComputePow2ModCpu(exponents, prime, results); + return true; + } + } + + private static void ComputePow2ModCpu(ReadOnlySpan exponents, ulong prime, Span results) + { + for (int i = 0; i < exponents.Length; i++) + { + results[i] = Pow2ModCpu(exponents[i], prime); + } + } + + private static ulong Pow2ModCpu(ulong exponent, ulong modulus) + { + if (modulus <= 1UL) + { + return 0UL; + } + + MontgomeryDivisorData divisor = MontgomeryDivisorDataCache.Get(modulus); + return exponent.Pow2MontgomeryModWindowed(divisor, keepMontgomery: false); + } + + private static Action, MontgomeryDivisorData, ArrayView1D> GetPow2ModKernel(Accelerator accelerator) + { + return Pow2ModKernelCache.GetOrAdd(accelerator, static accel => + { + var loaded = accel.LoadAutoGroupedStreamKernel, MontgomeryDivisorData, ArrayView1D>(Pow2ModKernel); + var kernel = KernelUtil.GetKernel(loaded); + return kernel.CreateLauncherDelegate, MontgomeryDivisorData, ArrayView1D>>(); + }); + } + + private static void Pow2ModKernel(Index1D index, ArrayView1D exponents, MontgomeryDivisorData divisor, ArrayView1D remainders) + { + ulong exponent = exponents[index]; + remainders[index] = exponent.Pow2MontgomeryModWindowed(divisor, keepMontgomery: false); + } + + internal readonly record struct PrimeOrderGpuCapability(int ModulusBits, int ExponentBits) + { + public static PrimeOrderGpuCapability Default => new(128, 64); + } +} 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..240d4ed 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,17 @@ internal static partial class PrimeOrderCalculator return true; } + private static bool Pow2EqualsOne(ulong exponent, ulong prime, in MontgomeryDivisorData divisorData) + { + 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/PerfectNumbers.Core/ULongExtensions.cs b/PerfectNumbers.Core/ULongExtensions.cs index e3bac57..ab3b25a 100644 --- a/PerfectNumbers.Core/ULongExtensions.cs +++ b/PerfectNumbers.Core/ULongExtensions.cs @@ -56,6 +56,33 @@ public static class ULongExtensions return order; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetBitLength(this ulong value) + { + return 64 - BitOperations.LeadingZeroCount(value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MultiplyShiftRight(ulong value, ulong multiplier, int shift) + { + UInt128 product = (UInt128)value * multiplier; + return (ulong)(product >> shift); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong MultiplyShiftRightShiftFirst(ulong value, ulong multiplier, int shift) + { + 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 479bd3a..71b34f6 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++; + // Skip stray blank separators so hunks without an empty line still parse correctly. 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..19278dc --- /dev/null +++ b/todo.txt @@ -0,0 +1 @@ +- 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..8f6e211 --- /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 += ULongExtensions.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 += ULongExtensions.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); +}