Add shift-first multiply helper and document benchmarks
This commit is contained in:
1 parent
40dff45a81
commit
84ebbed98b
15 files changed
+1333
-100
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-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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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