Add shift-first multiply helper and document benchmarks
This commit is contained in:
1 parent
40dff45a81
commit
84ebbed98b
15 files changed
+1340
-107
No files matched your search
@@ -31,14 +31,23 @@ It supports the standard `git diff` patch files as input. You should be able to
|
|||||||
* `--ignore-eol`
|
* `--ignore-eol`
|
||||||
* `--ignore-line-endings`
|
* `--ignore-line-endings`
|
||||||
* `--target` - allows specifying target root directory for the patch
|
* `--target` - allows specifying target root directory for the patch
|
||||||
|
* `--profile` - capture read/parse/apply timings for each patch file.
|
||||||
|
* `--replace-line <path> <line-number> <expected-text> <new-text>` - replace a single line by absolute number (use `_` as the expected text to skip validation)
|
||||||
|
|
||||||
* Build patches in standard `git diff` format.
|
* Build patches in standard `git diff` format.
|
||||||
* It supports `---`, `+++`, and `@@`.
|
* It supports `---`, `+++`, and `@@`.
|
||||||
* It doesn't support `***` or `*** Begin/End Patch` wrappers.
|
* It doesn't support `***` or `*** Begin/End Patch` wrappers.
|
||||||
* It requires `diff --git` at the beginning.
|
* It requires `diff --git` at the beginning.
|
||||||
* Patch header needs the actual line numbers.
|
* 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.
|
* 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.
|
* 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 <path> <line-number> <expected-text> <new-text>` 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.
|
* 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.
|
* 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.
|
* 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] `UInt128` bit shifts: attempts failed in kernels; avoid inside device code.
|
||||||
- [x] GPU context pooling: supported and recommended (validated in a separate program).
|
- [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:
|
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.
|
- 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`.
|
- 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.
|
- 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.
|
||||||
@@ -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<MultiplyShiftInput> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ArgumentException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ArgumentOutOfRangeException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[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<ArgumentOutOfRangeException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ulong, byte> 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<ulong> exponents, ulong prime, Span<ulong> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -189,6 +189,16 @@ public class MersenneDivisorCycles
|
|||||||
return true;
|
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;
|
ulong phi = divisor - 1UL;
|
||||||
if (phi == 0UL)
|
if (phi == 0UL)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<InternalsVisibleTo Include="EvenPerfectBitScanner.Tests" />
|
<InternalsVisibleTo Include="EvenPerfectBitScanner.Tests" />
|
||||||
<InternalsVisibleTo Include="PerfectNumbers.Core.Tests" />
|
<InternalsVisibleTo Include="PerfectNumbers.Core.Tests" />
|
||||||
|
<InternalsVisibleTo Include="HeuristicMultiplyShiftTiming" />
|
||||||
<InternalsVisibleTo Include="EvenPerfectBitScanner.Benchmarks" />
|
<InternalsVisibleTo Include="EvenPerfectBitScanner.Benchmarks" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using PerfectNumbers.Core.Gpu;
|
||||||
|
|
||||||
namespace PerfectNumbers.Core;
|
namespace PerfectNumbers.Core;
|
||||||
|
|
||||||
@@ -954,7 +955,17 @@ internal static partial class PrimeOrderCalculator
|
|||||||
{
|
{
|
||||||
return UInt128.Zero;
|
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);
|
BigInteger result = BigInteger.ModPow(2, (BigInteger)exponent, (BigInteger)modulus);
|
||||||
return (UInt128)result;
|
return (UInt128)result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using PerfectNumbers.Core.Gpu;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace PerfectNumbers.Core;
|
namespace PerfectNumbers.Core;
|
||||||
@@ -134,7 +136,7 @@ internal static partial class PrimeOrderCalculator
|
|||||||
{
|
{
|
||||||
ulong factor = factorSpan[i].Value;
|
ulong factor = factorSpan[i].Value;
|
||||||
ulong reduced = phi / factor;
|
ulong reduced = phi / factor;
|
||||||
if (reduced.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL)
|
if (Pow2EqualsOne(reduced, prime, divisorData))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -149,7 +151,7 @@ internal static partial class PrimeOrderCalculator
|
|||||||
if ((prime & 7UL) == 1UL || (prime & 7UL) == 7UL)
|
if ((prime & 7UL) == 1UL || (prime & 7UL) == 7UL)
|
||||||
{
|
{
|
||||||
ulong half = phi >> 1;
|
ulong half = phi >> 1;
|
||||||
if (half.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL)
|
if (Pow2EqualsOne(half, prime, divisorData))
|
||||||
{
|
{
|
||||||
order = half;
|
order = half;
|
||||||
}
|
}
|
||||||
@@ -191,7 +193,7 @@ internal static partial class PrimeOrderCalculator
|
|||||||
}
|
}
|
||||||
|
|
||||||
ulong reduced = order / primeFactor;
|
ulong reduced = order / primeFactor;
|
||||||
if (reduced.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL)
|
if (Pow2EqualsOne(reduced, prime, divisorData))
|
||||||
{
|
{
|
||||||
order = reduced;
|
order = reduced;
|
||||||
continue;
|
continue;
|
||||||
@@ -223,7 +225,7 @@ internal static partial class PrimeOrderCalculator
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
Console.WriteLine("Verifying a^order ≡ 1 (mod p)");
|
Console.WriteLine("Verifying a^order ≡ 1 (mod p)");
|
||||||
#endif
|
#endif
|
||||||
if (order.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) != 1UL)
|
if (!Pow2EqualsOne(order, prime, divisorData))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -283,7 +285,7 @@ internal static partial class PrimeOrderCalculator
|
|||||||
}
|
}
|
||||||
|
|
||||||
reduced /= primeFactor;
|
reduced /= primeFactor;
|
||||||
if (reduced.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL)
|
if (Pow2EqualsOne(reduced, prime, divisorData))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -355,21 +357,84 @@ internal static partial class PrimeOrderCalculator
|
|||||||
int powBudget = config.MaxPowChecks <= 0 ? candidates.Count : config.MaxPowChecks;
|
int powBudget = config.MaxPowChecks <= 0 ? candidates.Count : config.MaxPowChecks;
|
||||||
int powUsed = 0;
|
int powUsed = 0;
|
||||||
int candidateCount = candidates.Count;
|
int candidateCount = candidates.Count;
|
||||||
|
bool allowGpuBatch = true;
|
||||||
|
Span<ulong> candidateSpan = CollectionsMarshal.AsSpan(candidates);
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Console.WriteLine($"Checking candidates ({candidateCount} candidates, {powBudget} pow budget)");
|
Console.WriteLine($"Checking candidates ({candidateCount} candidates, {powBudget} pow budget)");
|
||||||
#endif
|
#endif
|
||||||
for (int i = 0; i < candidateCount; i++)
|
int index = 0;
|
||||||
|
const int MaxGpuBatchSize = 256;
|
||||||
|
const int StackGpuBatchSize = 32;
|
||||||
|
Span<ulong> 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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong candidate = candidates[i];
|
ReadOnlySpan<ulong> batch = candidateSpan.Slice(index, batchSize);
|
||||||
|
ulong[]? gpuPool = null;
|
||||||
|
Span<ulong> pooledGpuRemainders = default;
|
||||||
|
bool gpuSuccess = false;
|
||||||
|
bool gpuStackRemainders = false;
|
||||||
|
GpuPow2ModStatus status = GpuPow2ModStatus.Unavailable;
|
||||||
|
|
||||||
|
if (allowGpuBatch)
|
||||||
|
{
|
||||||
|
if (batchSize <= StackGpuBatchSize)
|
||||||
|
{
|
||||||
|
Span<ulong> localRemainders = stackGpuRemainders.Slice(0, batchSize);
|
||||||
|
status = PrimeOrderGpuHeuristics.TryPow2ModBatch(batch, prime, localRemainders);
|
||||||
|
if (status == GpuPow2ModStatus.Success)
|
||||||
|
{
|
||||||
|
gpuSuccess = true;
|
||||||
|
gpuStackRemainders = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
gpuPool = ArrayPool<ulong>.Shared.Rent(batchSize);
|
||||||
|
Span<ulong> pooledRemainders = gpuPool.AsSpan(0, batchSize);
|
||||||
|
status = PrimeOrderGpuHeuristics.TryPow2ModBatch(batch, prime, pooledRemainders);
|
||||||
|
if (status == GpuPow2ModStatus.Success)
|
||||||
|
{
|
||||||
|
pooledGpuRemainders = pooledRemainders;
|
||||||
|
gpuSuccess = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ArrayPool<ulong>.Shared.Return(gpuPool, clearArray: false);
|
||||||
|
gpuPool = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gpuSuccess && (status == GpuPow2ModStatus.Overflow || status == GpuPow2ModStatus.Unavailable))
|
||||||
|
{
|
||||||
|
allowGpuBatch = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < batchSize && powUsed < powBudget; i++)
|
||||||
|
{
|
||||||
|
ulong candidate = batch[i];
|
||||||
powUsed++;
|
powUsed++;
|
||||||
|
|
||||||
if (candidate.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) != 1UL)
|
bool equalsOne;
|
||||||
|
if (gpuSuccess)
|
||||||
|
{
|
||||||
|
ulong remainderValue = gpuStackRemainders ? stackGpuRemainders[i] : pooledGpuRemainders[i];
|
||||||
|
equalsOne = remainderValue == 1UL;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
equalsOne = Pow2EqualsOne(candidate, prime, divisorData);
|
||||||
|
}
|
||||||
|
if (!equalsOne)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -379,10 +444,23 @@ internal static partial class PrimeOrderCalculator
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (gpuPool is not null)
|
||||||
|
{
|
||||||
|
ArrayPool<ulong>.Shared.Return(gpuPool, clearArray: false);
|
||||||
|
}
|
||||||
|
|
||||||
result = candidate;
|
result = candidate;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (gpuPool is not null)
|
||||||
|
{
|
||||||
|
ArrayPool<ulong>.Shared.Return(gpuPool, clearArray: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
index += batchSize;
|
||||||
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Console.WriteLine("No candidate confirmed");
|
Console.WriteLine("No candidate confirmed");
|
||||||
#endif
|
#endif
|
||||||
@@ -601,7 +679,7 @@ internal static partial class PrimeOrderCalculator
|
|||||||
}
|
}
|
||||||
|
|
||||||
powUsed++;
|
powUsed++;
|
||||||
if (reduced.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL)
|
if (Pow2EqualsOne(reduced, prime, divisorData))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -611,6 +689,18 @@ internal static partial class PrimeOrderCalculator
|
|||||||
return true;
|
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)
|
private static PartialFactorResult PartialFactor(ulong value, PrimeOrderSearchConfig config)
|
||||||
{
|
{
|
||||||
if (value <= 1UL)
|
if (value <= 1UL)
|
||||||
|
|||||||
+554
-95
@@ -1,6 +1,8 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
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 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<string>();
|
var args = Args?.ToArray() ?? Array.Empty<string>();
|
||||||
if (args.Length == 0)
|
if (args.Length == 0)
|
||||||
{
|
{
|
||||||
@@ -56,8 +81,10 @@ if (args.Length == 0)
|
|||||||
bool checkOnly = false;
|
bool checkOnly = false;
|
||||||
bool ignoreWhitespace = false;
|
bool ignoreWhitespace = false;
|
||||||
bool ignoreLineEndings = false;
|
bool ignoreLineEndings = false;
|
||||||
|
bool profile = false;
|
||||||
string? explicitTargetRoot = null;
|
string? explicitTargetRoot = null;
|
||||||
var patchFileArguments = new List<string>();
|
var patchFileArguments = new List<string>();
|
||||||
|
var lineReplacements = new List<LineReplacement>();
|
||||||
|
|
||||||
for (int i = 0; i < args.Length; i++)
|
for (int i = 0; i < args.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -81,6 +108,12 @@ for (int i = 0; i < args.Length; i++)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(argument, "--profile", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
profile = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(argument, "--target", StringComparison.Ordinal))
|
if (string.Equals(argument, "--target", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
if (i + 1 >= args.Length)
|
if (i + 1 >= args.Length)
|
||||||
@@ -93,10 +126,38 @@ for (int i = 0; i < args.Length; i++)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(argument, "--replace-line", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (i + 4 >= args.Length)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("--replace-line requires <path> <line-number> <expected-text> <new-text> 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);
|
patchFileArguments.Add(argument);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patchFileArguments.Count == 0)
|
if (patchFileArguments.Count == 0 && lineReplacements.Count == 0)
|
||||||
{
|
{
|
||||||
PrintUsage();
|
PrintUsage();
|
||||||
Environment.Exit(1);
|
Environment.Exit(1);
|
||||||
@@ -127,9 +188,9 @@ else
|
|||||||
targetRoot = Directory.GetCurrentDirectory();
|
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);
|
Environment.Exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +205,7 @@ if (!Directory.Exists(targetRoot))
|
|||||||
var patchFilePaths = patchFileArguments.Select(Path.GetFullPath).ToList();
|
var patchFilePaths = patchFileArguments.Select(Path.GetFullPath).ToList();
|
||||||
var applicationOptions = new PatchApplicationOptions(checkOnly, ignoreWhitespace, ignoreLineEndings);
|
var applicationOptions = new PatchApplicationOptions(checkOnly, ignoreWhitespace, ignoreLineEndings);
|
||||||
var patchResults = new List<PatchRunResult>();
|
var patchResults = new List<PatchRunResult>();
|
||||||
|
List<PatchProfileEntry>? patchProfiles = profile ? new List<PatchProfileEntry>() : null;
|
||||||
|
|
||||||
foreach (var patchFilePath in patchFilePaths)
|
foreach (var patchFilePath in patchFilePaths)
|
||||||
{
|
{
|
||||||
@@ -154,14 +216,53 @@ foreach (var patchFilePath in patchFilePaths)
|
|||||||
throw new FileNotFoundException($"Patch file '{patchFilePath}' does not exist.", patchFilePath);
|
throw new FileNotFoundException($"Patch file '{patchFilePath}' does not exist.", patchFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
var patchText = File.ReadAllLines(patchFilePath);
|
Stopwatch? stopwatch = null;
|
||||||
var parsedFiles = ParsePatch(patchText);
|
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)
|
foreach (var patchFile in parsedFiles)
|
||||||
{
|
{
|
||||||
ApplyPatch(targetRoot, patchFile, applicationOptions);
|
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));
|
patchResults.Add(new PatchRunResult(patchFilePath, true, null));
|
||||||
|
|
||||||
if (!applicationOptions.CheckOnly)
|
if (!applicationOptions.CheckOnly)
|
||||||
@@ -175,84 +276,218 @@ foreach (var patchFilePath in patchFilePaths)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lineReplacements.Count > 0)
|
||||||
|
{
|
||||||
|
var replacementResults = ApplyLineReplacements(targetRoot, lineReplacements, applicationOptions);
|
||||||
|
patchResults.AddRange(replacementResults);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var result in patchResults)
|
foreach (var result in patchResults)
|
||||||
{
|
{
|
||||||
var displayPath = GetDisplayPath(result.PatchFilePath);
|
var displayPath = GetDisplayPath(result.PatchFilePath);
|
||||||
|
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
|
{
|
||||||
|
if (applicationOptions.CheckOnly)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Patch '{displayPath}' verified successfully. Ready to apply.");
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Patch '{displayPath}' applied successfully.");
|
Console.WriteLine($"Patch '{displayPath}' applied successfully.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"Patch '{displayPath}' failed: {result.ErrorMessage}");
|
Console.Error.WriteLine($"Patch '{displayPath}' failed: {result.ErrorMessage}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
if (patchResults.Any(static r => !r.Success))
|
||||||
{
|
{
|
||||||
Environment.Exit(1);
|
Environment.Exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<PatchFile> ParsePatch(IReadOnlyList<string> lines)
|
static List<PatchFile> 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<PatchFile> ParsePatch(PatchLineReader reader)
|
||||||
{
|
{
|
||||||
var files = new List<PatchFile>();
|
var files = new List<PatchFile>();
|
||||||
int index = 0;
|
string? diffHeader = null;
|
||||||
|
|
||||||
while (index < lines.Count)
|
|
||||||
{
|
|
||||||
if (!lines[index].StartsWith("diff --git ", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var diffHeader = lines[index];
|
|
||||||
index++;
|
|
||||||
|
|
||||||
string? originalPath = null;
|
string? originalPath = null;
|
||||||
string? modifiedPath = null;
|
string? modifiedPath = null;
|
||||||
var hunks = new List<PatchHunk>();
|
List<PatchHunk>? hunks = null;
|
||||||
PatchFileType type = PatchFileType.Modify;
|
|
||||||
|
|
||||||
while (index < lines.Count)
|
while (reader.TryRead(out var currentLine))
|
||||||
{
|
{
|
||||||
var line = lines[index];
|
if (currentLine is null)
|
||||||
|
|
||||||
if (line.StartsWith("diff --git ", StringComparison.Ordinal) && hunks.Count > 0)
|
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.StartsWith("--- ", StringComparison.Ordinal))
|
if (string.IsNullOrWhiteSpace(currentLine))
|
||||||
{
|
{
|
||||||
originalPath = NormalizePath(line.Substring(4));
|
|
||||||
index++;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.StartsWith("+++ ", StringComparison.Ordinal))
|
if (!currentLine.StartsWith("diff --git ", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (diffHeader is null)
|
||||||
{
|
{
|
||||||
modifiedPath = NormalizePath(line.Substring(4));
|
|
||||||
index++;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.StartsWith("@@", StringComparison.Ordinal))
|
if (currentLine.StartsWith("--- ", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
var hunk = ParseHunk(lines, ref index);
|
originalPath = NormalizePath(currentLine.AsSpan(4));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLine.StartsWith("+++ ", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
modifiedPath = NormalizePath(currentLine.AsSpan(4));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLine.StartsWith("@@", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
hunks ??= new List<PatchHunk>();
|
||||||
|
var hunk = ParseHunk(currentLine, reader);
|
||||||
hunks.Add(hunk);
|
hunks.Add(hunk);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
index++;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (diffHeader is not null)
|
||||||
|
{
|
||||||
|
FinalizePatchFile(files, diffHeader, originalPath, modifiedPath, hunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
diffHeader = currentLine;
|
||||||
|
originalPath = null;
|
||||||
|
modifiedPath = null;
|
||||||
|
hunks = new List<PatchHunk>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diffHeader is not null)
|
||||||
|
{
|
||||||
|
FinalizePatchFile(files, diffHeader, originalPath, modifiedPath, hunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PatchHunk ParseHunk(string header, PatchLineReader reader)
|
||||||
|
{
|
||||||
|
var (originalStart, originalLength, modifiedStart, modifiedLength) = ParseHunkHeader(header);
|
||||||
|
var patchLines = new List<PatchLine>();
|
||||||
|
|
||||||
|
while (reader.TryPeek(out var nextLine))
|
||||||
|
{
|
||||||
|
if (nextLine is null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith("-", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
patchLines.Add(new RemovedLine(line.Substring(1), true));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith(" ", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
patchLines.Add(new ContextLine(line.Substring(1), true));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith("\\ No newline at end of file", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
MarkLastLineWithoutTrailingNewline(patchLines);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.Length == 0)
|
||||||
|
{
|
||||||
|
patchLines.Add(new ContextLine(string.Empty, true));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException($"Unexpected line in hunk: '{line}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PatchHunk(originalStart, originalLength, modifiedStart, modifiedLength, patchLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void FinalizePatchFile(
|
||||||
|
List<PatchFile> files,
|
||||||
|
string diffHeader,
|
||||||
|
string? originalPath,
|
||||||
|
string? modifiedPath,
|
||||||
|
List<PatchHunk>? hunks)
|
||||||
|
{
|
||||||
if (originalPath is null || modifiedPath is null)
|
if (originalPath is null || modifiedPath is null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Missing file paths for patch header: {diffHeader}");
|
throw new InvalidOperationException($"Missing file paths for patch header: {diffHeader}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PatchFileType type = PatchFileType.Modify;
|
||||||
|
|
||||||
if (IsDevNull(originalPath) && !IsDevNull(modifiedPath))
|
if (IsDevNull(originalPath) && !IsDevNull(modifiedPath))
|
||||||
{
|
{
|
||||||
type = PatchFileType.Add;
|
type = PatchFileType.Add;
|
||||||
@@ -262,73 +497,168 @@ static List<PatchFile> ParsePatch(IReadOnlyList<string> lines)
|
|||||||
type = PatchFileType.Delete;
|
type = PatchFileType.Delete;
|
||||||
}
|
}
|
||||||
|
|
||||||
files.Add(new PatchFile(originalPath, modifiedPath, hunks, type));
|
files.Add(new PatchFile(originalPath, modifiedPath, hunks ?? new List<PatchHunk>(), type));
|
||||||
}
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static PatchHunk ParseHunk(IReadOnlyList<string> lines, ref int index)
|
private sealed class PatchLineReader : IDisposable
|
||||||
{
|
{
|
||||||
var header = lines[index];
|
private const int ReadBufferSize = 4096;
|
||||||
var (originalStart, originalLength, modifiedStart, modifiedLength) = ParseHunkHeader(header);
|
private const int InitialLineBufferSize = 256;
|
||||||
index++;
|
|
||||||
|
|
||||||
var patchLines = new List<PatchLine>();
|
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;
|
||||||
|
|
||||||
while (index < lines.Count)
|
public PatchLineReader(StreamReader reader)
|
||||||
{
|
{
|
||||||
var line = lines[index];
|
_reader = reader;
|
||||||
|
_readBuffer = ArrayPool<char>.Shared.Rent(ReadBufferSize);
|
||||||
|
_lineBuffer = ArrayPool<char>.Shared.Rent(InitialLineBufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
if (line.StartsWith("diff --git ", StringComparison.Ordinal) || line.StartsWith("@@", StringComparison.Ordinal))
|
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.StartsWith("+++ ", StringComparison.Ordinal) || line.StartsWith("--- ", StringComparison.Ordinal))
|
EnsureLineCapacity(length, length + 1);
|
||||||
|
_lineBuffer[length++] = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
line = new string(_lineBuffer, 0, length);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryReadChar(out char value)
|
||||||
{
|
{
|
||||||
break;
|
if (!EnsureCharBuffer())
|
||||||
}
|
|
||||||
|
|
||||||
if (line.StartsWith("+", StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
patchLines.Add(new AddedLine(line.Substring(1), true));
|
value = '\0';
|
||||||
index++;
|
return false;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.StartsWith("-", StringComparison.Ordinal))
|
value = _readBuffer[_readIndex++];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool EnsureCharBuffer()
|
||||||
{
|
{
|
||||||
patchLines.Add(new RemovedLine(line.Substring(1), true));
|
if (_readIndex < _readLength)
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (line.StartsWith(" ", StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
patchLines.Add(new ContextLine(line.Substring(1), true));
|
return true;
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.StartsWith("\\ No newline at end of file", StringComparison.Ordinal))
|
if (_disposed)
|
||||||
{
|
{
|
||||||
MarkLastLineWithoutTrailingNewline(patchLines);
|
return false;
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line.Length == 0)
|
_readLength = _reader.Read(_readBuffer, 0, _readBuffer.Length);
|
||||||
|
_readIndex = 0;
|
||||||
|
return _readLength > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureLineCapacity(int currentLength, int requiredLength)
|
||||||
{
|
{
|
||||||
patchLines.Add(new ContextLine(string.Empty, true));
|
if (requiredLength <= _lineBuffer.Length)
|
||||||
index++;
|
{
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InvalidOperationException($"Unexpected line in hunk: '{line}'");
|
int newSize = _lineBuffer.Length * 2;
|
||||||
|
while (newSize < requiredLength)
|
||||||
|
{
|
||||||
|
newSize *= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PatchHunk(originalStart, originalLength, modifiedStart, modifiedLength, patchLines);
|
char[] newBuffer = ArrayPool<char>.Shared.Rent(newSize);
|
||||||
|
Array.Copy(_lineBuffer, newBuffer, currentLength);
|
||||||
|
ArrayPool<char>.Shared.Return(_lineBuffer);
|
||||||
|
_lineBuffer = newBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
ArrayPool<char>.Shared.Return(_readBuffer);
|
||||||
|
ArrayPool<char>.Shared.Return(_lineBuffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void MarkLastLineWithoutTrailingNewline(List<PatchLine> lines)
|
static void MarkLastLineWithoutTrailingNewline(List<PatchLine> lines)
|
||||||
@@ -351,20 +681,37 @@ static void MarkLastLineWithoutTrailingNewline(List<PatchLine> lines)
|
|||||||
|
|
||||||
static (int originalStart, int originalLength, int modifiedStart, int modifiedLength) ParseHunkHeader(string header)
|
static (int originalStart, int originalLength, int modifiedStart, int modifiedLength) ParseHunkHeader(string header)
|
||||||
{
|
{
|
||||||
// Format: @@ -start,length +start,length @@ optional text
|
var span = header.AsSpan();
|
||||||
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;
|
|
||||||
|
|
||||||
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<char> modifiedRange;
|
||||||
|
|
||||||
|
if (thirdSpace < 0)
|
||||||
|
{
|
||||||
|
modifiedRange = span;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
modifiedRange = header.Substring(secondSpace + 1, modifiedRangeEnd - secondSpace - 1);
|
modifiedRange = span[..thirdSpace];
|
||||||
}
|
}
|
||||||
|
|
||||||
var (originalStart, originalLength) = ParseRange(originalRange);
|
var (originalStart, originalLength) = ParseRange(originalRange);
|
||||||
@@ -372,25 +719,35 @@ static (int originalStart, int originalLength, int modifiedStart, int modifiedLe
|
|||||||
return (originalStart, originalLength, modifiedStart, modifiedLength);
|
return (originalStart, originalLength, modifiedStart, modifiedLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
static (int start, int length) ParseRange(string range)
|
static (int start, int length) ParseRange(ReadOnlySpan<char> range)
|
||||||
{
|
{
|
||||||
// Format: -start,length or +start,length
|
if (range.Length == 0)
|
||||||
int commaIndex = range.IndexOf(',');
|
{
|
||||||
string startText;
|
throw new InvalidOperationException("Range descriptor cannot be empty.");
|
||||||
string lengthText;
|
}
|
||||||
|
|
||||||
|
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<char> startText;
|
||||||
|
ReadOnlySpan<char> lengthText;
|
||||||
|
|
||||||
if (commaIndex >= 0)
|
if (commaIndex >= 0)
|
||||||
{
|
{
|
||||||
startText = range.Substring(1, commaIndex - 1);
|
startText = numericPortion[..commaIndex];
|
||||||
lengthText = range[(commaIndex + 1)..];
|
lengthText = numericPortion[(commaIndex + 1)..];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
startText = range.Substring(1);
|
startText = numericPortion;
|
||||||
lengthText = "1";
|
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)
|
static void ApplyPatch(string root, PatchFile patchFile, PatchApplicationOptions options)
|
||||||
@@ -428,18 +785,20 @@ static string NormalizeRelativePath(string path)
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
static string NormalizePath(string headerPath)
|
static string NormalizePath(ReadOnlySpan<char> headerPath)
|
||||||
{
|
{
|
||||||
var trimmed = headerPath.Trim();
|
var trimmed = headerPath.Trim();
|
||||||
|
|
||||||
if (trimmed.StartsWith("a/", StringComparison.Ordinal) || trimmed.StartsWith("b/", StringComparison.Ordinal))
|
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)
|
static bool IsDevNull(string? path)
|
||||||
{
|
{
|
||||||
return path is null || path.Equals("/dev/null", StringComparison.Ordinal);
|
return path is null || path.Equals("/dev/null", StringComparison.Ordinal);
|
||||||
@@ -746,6 +1105,103 @@ static void TryDeletePatchFile(string patchFilePath)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static List<PatchRunResult> ApplyLineReplacements(string root, List<LineReplacement> replacements, PatchApplicationOptions options)
|
||||||
|
{
|
||||||
|
var results = new List<PatchRunResult>();
|
||||||
|
|
||||||
|
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()
|
static void PrintUsage()
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("Usage: dotnet script apply_patch.cs -- [options] <patch-file> [<patch-file> ...] [target-directory]");
|
Console.Error.WriteLine("Usage: dotnet script apply_patch.cs -- [options] <patch-file> [<patch-file> ...] [target-directory]");
|
||||||
@@ -754,6 +1210,9 @@ static void PrintUsage()
|
|||||||
Console.Error.WriteLine(" --check Validate patches without modifying files.");
|
Console.Error.WriteLine(" --check Validate patches without modifying files.");
|
||||||
Console.Error.WriteLine(" --ignore-whitespace Ignore whitespace differences when matching context.");
|
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(" --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 <directory> Explicitly set the target directory.");
|
Console.Error.WriteLine(" --target <directory> Explicitly set the target directory.");
|
||||||
|
Console.Error.WriteLine(" --replace-line <path> <line-number> <expected-text> <new-text>");
|
||||||
|
Console.Error.WriteLine(" Replace a single line by absolute number; use '_' as expected-text to skip validation.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -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<Index1D, ArrayView1D<int, Stride1D.Dense>>(BigIntegerKernel);
|
||||||
|
using var buffer = accelerator.Allocate1D<int>(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<Index1D, ArrayView1D<int, Stride1D.Dense>>(EIntegerKernel);
|
||||||
|
using var buffer = accelerator.Allocate1D<int>(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<Index1D, ArrayView1D<int, Stride1D.Dense>>(ERationalKernel);
|
||||||
|
using var buffer = accelerator.Allocate1D<int>(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<int, Stride1D.Dense> 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<int, Stride1D.Dense> 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<int, Stride1D.Dense> 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../../PerfectNumbers.Core/PerfectNumbers.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user