Add shift-first multiply helper and document benchmarks

This commit is contained in:
Marek Lemańczyk committed 2025-10-04 20:01:10 +02:00
1 parent 40dff45a81
commit 84ebbed98b
15 files changed
+1333 -100

No files matched your search

+48
View File
@@ -31,14 +31,23 @@ It supports the standard `git diff` patch files as input. You should be able to
* `--ignore-eol`
* `--ignore-line-endings`
* `--target` - allows specifying target root directory for the patch
* `--profile` - capture read/parse/apply timings for each patch file.
* `--replace-line <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.
* 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 <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.
* Keep patch filenames unique and delete applied files to prevent accidental re-use.
* When a hunk fails, inspect the around-lines immediately and adjust rather than retrying the same diff.
@@ -127,7 +136,46 @@ The following checks were executed with ILGPU 1.5.3 on CPU and OpenCL accelerato
- [x] `UInt128` bit shifts: attempts failed in kernels; avoid inside device code.
- [x] GPU context pooling: supported and recommended (validated in a separate program).
Additional numeric-type probe results (`tools/GpuNumericTypeProbe.csx`, ILGPU 1.5.3 on the CPU accelerator):
- `System.Numerics.BigInteger`: unsupported. Kernels that exercise addition, subtraction, multiplication, division, modulo, and `CompareTo` all fail with `InternalCompilerException`, indicating fundamental codegen gaps for the type.
- `PeterO.Numbers.EInteger`: unsupported. Attempts to compile kernels that call addition, subtraction, multiplication, division, remainder, and `CompareTo` throw `InternalCompilerException`, so none of the arithmetic helpers are currently usable on the device.
- `PeterO.Numbers.ERational`: unsupported. Kernels that call addition, subtraction, multiplication, division, and `CompareTo` trigger `InternalCompilerException`; the type also lacks a remainder/modulo API for device usage.
Patch-preparation checklist for reliable `--check` runs:
- Always capture context with `nl -ba` before composing each hunk so the `@@` header line numbers match the target file on the first try.
- Trim or normalize trailing whitespace while drafting the patch, or rely on `--ignore-whitespace` during verification when formatting noise is unavoidable, then re-run without ignores to confirm the final diff matches exactly.
- Prefer regenerating the patch from scratch whenever the working tree changes instead of editing previous diffs; stale context is the most common source of mismatches.
- The patch parser now ignores blank metadata lines between `diff --git`, `---`, and `+++` headers, so you no longer need to insert dummy empty separators to satisfy the checker. Keep the header compact unless your diff tool emits those blank lines naturally.
### apply_patch.cs profiling snapshot (2025-10-04)
- Latest timing check: `dotnet script apply_patch.cs -- --profile --check /tmp/agents_todo_update.diff`.
- Patch characteristics: 2 files, 4 hunks, 31 lines, 3468 bytes.
- Timings: read 0.45 ms, parse 2.96 ms, apply 2.96 ms.
- Focus next on streaming the patch reader and pooled buffer reuse to reduce the parse APPLY bottleneck highlighted above.
Mersenne divisor size estimates:
- Any prime divisor `q` of a Mersenne number `2^p - 1` must satisfy `q ≡ 1 (mod 2p)`. The minimal `q` therefore appears at `q = 2p + 1` when the multiplier `k = 1` yields a prime.
- For candidate exponents `p` in the `[138,000,000, 200,000,000]` range, the minimal compliant divisor lands below `2 · 200,000,000 + 1 = 400,000,001`, which fits in 29 bits (`2^29 = 536,870,912`).
- Extending the search up to `p = 600,000,000` pushes the minimal divisor to `1,200,000,001`, requiring 31 bits (`2^31 = 2,147,483,648`).
- Larger `k` values (and thus larger `q`) are possible—up to nearly `2^p - 1`—so the heuristics and storage should plan for wider integers when deep trial factoring is necessary, but the noted bit widths cover the first admissible divisors for these exponent bands.
- Because every proper divisor of `2^p - 1` is at most `(2^p - 1) / 2`, the worst-case bit-width for any admissible `q` is `p - 1` bits (since `⌊(2^p - 1) / 2⌋ = 2^{p-1} - 1`). Consequently we need roughly `199,999,999` bits when `p ≤ 200,000,000` and `599,999,999` bits when `p ≤ 600,000,000`. In terms of the `q = 2kp + 1` form, this translates to `k_max = ⌊((2^p - 1) / 2 - 1) / (2p)⌋`, which is approximately `2^{p-2} / p` for large `p`.
Practical guidance:
- Use native `%` and `/` in kernels when working with 32-bit or 64-bit integers if it is the most efficient option for the target backend.
- For `UInt128` in kernels, avoid `%`/`/`; implement reductions via Montgomery/Barrett, or use Mersenne-specific folding for mod `2^p-1`.
- Prefer Mersenne folding over general modulo for `2^p-1` moduli to remove branches in hot loops.
- Heuristic multiply-shift benchmarks (2025-10-04):
- Run `dotnet run -c Release --project tools/HeuristicMultiplyShiftTiming/HeuristicMultiplyShiftTiming.csproj` to compare the `HeuristicArithmetic` helpers without the BenchmarkDotNet harness.
- Timings below report the average nanoseconds per call over 200,000,000 iterations; all variants produced identical checksums, confirming consistent arithmetic.
- `NearOverflowShift`: `MultiplyShiftRight` ≈ 2.37 ns, shift-first ≈ 4.71 ns, naive `(value * multiplier) >> shift` ≈ 0.85 ns.
- `HalfRange`: `MultiplyShiftRight` ≈ 2.11 ns, shift-first ≈ 4.15 ns, naive ≈ 0.70 ns.
- `MixedBits`: `MultiplyShiftRight` ≈ 1.43 ns, shift-first ≈ 2.83 ns, naive ≈ 0.60 ns.
- `Sparse`: `MultiplyShiftRight` ≈ 1.57 ns, shift-first ≈ 2.84 ns, naive ≈ 0.59 ns.
- The shift-first rewrite roughly doubles the per-call cost on CPU, but it stays within 64-bit arithmetic and is the preferred fallback for GPU heuristics when avoiding `UInt128` overflow is mandatory.
@@ -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;
}
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)
{
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
@@ -17,6 +17,7 @@
<ItemGroup>
<InternalsVisibleTo Include="EvenPerfectBitScanner.Tests" />
<InternalsVisibleTo Include="PerfectNumbers.Core.Tests" />
<InternalsVisibleTo Include="HeuristicMultiplyShiftTiming" />
<InternalsVisibleTo Include="EvenPerfectBitScanner.Benchmarks" />
</ItemGroup>
@@ -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;
}
+106 -16
View File
@@ -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<ulong> 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<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;
}
ulong candidate = candidates[i];
powUsed++;
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 (candidate.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) != 1UL)
if (allowGpuBatch)
{
continue;
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;
}
}
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<ulong>.Shared.Return(gpuPool, clearArray: false);
}
result = candidate;
return true;
}
result = candidate;
return true;
if (gpuPool is not null)
{
ArrayPool<ulong>.Shared.Return(gpuPool, clearArray: false);
}
index += batchSize;
}
#if DEBUG
@@ -601,7 +679,7 @@ internal static partial class PrimeOrderCalculator
}
powUsed++;
if (reduced.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL)
if (Pow2EqualsOne(reduced, prime, divisorData))
{
return false;
}
@@ -611,6 +689,18 @@ internal static partial class PrimeOrderCalculator
return true;
}
private static bool Pow2EqualsOne(ulong exponent, ulong prime, in MontgomeryDivisorData divisorData)
{
// TODO: Implement the GPU heuristic path so TryPow2Mod can verify candidates on the device instead of immediately falling back to the CPU Montgomery reducer.
GpuPow2ModStatus status = PrimeOrderGpuHeuristics.TryPow2Mod(exponent, prime, out ulong remainder);
if (status == GpuPow2ModStatus.Success)
{
return remainder == 1UL;
}
return exponent.Pow2MontgomeryModWindowed(divisorData, keepMontgomery: false) == 1UL;
}
private static PartialFactorResult PartialFactor(ulong value, PrimeOrderSearchConfig config)
{
if (value <= 1UL)
+541 -82
View File
@@ -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<string>();
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<string>();
var lineReplacements = new List<LineReplacement>();
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 <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);
}
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<PatchRunResult>();
List<PatchProfileEntry>? patchProfiles = profile ? new List<PatchProfileEntry>() : 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<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>();
int index = 0;
string? diffHeader = null;
string? originalPath = null;
string? modifiedPath = null;
List<PatchHunk>? 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<PatchHunk>();
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<PatchHunk>();
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<PatchHunk>();
}
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<string> 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<PatchLine>();
while (index < lines.Count)
while (reader.TryPeek(out var nextLine))
{
var line = lines[index];
if (line.StartsWith("diff --git ", StringComparison.Ordinal) || line.StartsWith("@@", StringComparison.Ordinal))
if (nextLine is null)
{
break;
}
if (line.StartsWith("+++ ", StringComparison.Ordinal) || line.StartsWith("--- ", StringComparison.Ordinal))
if (nextLine.StartsWith("diff --git ", StringComparison.Ordinal) ||
nextLine.StartsWith("@@", StringComparison.Ordinal) ||
nextLine.StartsWith("+++ ", StringComparison.Ordinal) ||
nextLine.StartsWith("--- ", StringComparison.Ordinal))
{
break;
}
if (!reader.TryRead(out var consumed) || consumed is null)
{
break;
}
var line = consumed;
if (line.StartsWith("+", StringComparison.Ordinal))
{
patchLines.Add(new AddedLine(line.Substring(1), true));
index++;
continue;
}
if (line.StartsWith("-", StringComparison.Ordinal))
{
patchLines.Add(new RemovedLine(line.Substring(1), true));
index++;
continue;
}
if (line.StartsWith(" ", StringComparison.Ordinal))
{
patchLines.Add(new ContextLine(line.Substring(1), true));
index++;
continue;
}
if (line.StartsWith("\\ No newline at end of file", StringComparison.Ordinal))
{
MarkLastLineWithoutTrailingNewline(patchLines);
index++;
continue;
}
if (line.Length == 0)
{
patchLines.Add(new ContextLine(string.Empty, true));
index++;
continue;
}
@@ -331,6 +474,193 @@ static PatchHunk ParseHunk(IReadOnlyList<string> lines, ref int index)
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)
{
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<PatchHunk>(), 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<char>.Shared.Rent(ReadBufferSize);
_lineBuffer = ArrayPool<char>.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<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)
{
if (lines.Count == 0)
@@ -351,20 +681,37 @@ static void MarkLastLineWithoutTrailingNewline(List<PatchLine> 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<char> 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<char> 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<char> startText;
ReadOnlySpan<char> 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<char> 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<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()
{
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(" --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 <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.");
}
+3
View File
@@ -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.
+160
View File
@@ -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);
}