diff --git a/PerfectNumbers.Core.Tests/MersenneDivisorCyclesGenerateGpuTests.cs b/PerfectNumbers.Core.Tests/MersenneDivisorCyclesGenerateGpuTests.cs index 1e2020e..008f7b0 100644 --- a/PerfectNumbers.Core.Tests/MersenneDivisorCyclesGenerateGpuTests.cs +++ b/PerfectNumbers.Core.Tests/MersenneDivisorCyclesGenerateGpuTests.cs @@ -49,4 +49,57 @@ public class MersenneDivisorCyclesGenerateGpuTests } } } + + [Fact] + [Trait("Category", "Fast")] + public void Generate_produces_unique_cycles_for_expected_divisors() + { + string path = Path.GetTempFileName(); + try + { + MersenneDivisorCycles.Generate(path, maxDivisor: 50UL, threads: 4); + + var pairs = new List<(ulong divisor, ulong cycle)>(); + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read); + using var reader = new BinaryReader(stream); + while (stream.Position < stream.Length) + { + ulong divisor = reader.ReadUInt64(); + ulong cycle = reader.ReadUInt64(); + pairs.Add((divisor, cycle)); + } + + var expected = new List(); + for (ulong divisor = 3; divisor <= 50; divisor++) + { + if ((divisor & 1UL) == 0UL || divisor % 3UL == 0UL || divisor % 5UL == 0UL || divisor % 7UL == 0UL || divisor % 11UL == 0UL) + { + continue; + } + + expected.Add(divisor); + } + + pairs.Should().HaveCount(expected.Count); + + HashSet uniqueDivisors = new(); + foreach (var pair in pairs) + { + uniqueDivisors.Add(pair.divisor).Should().BeTrue(); + pair.cycle.Should().Be(MersenneDivisorCycles.CalculateCycleLength(pair.divisor)); + } + + uniqueDivisors.Should().BeEquivalentTo(expected); + + var (_, completeCount) = MersenneDivisorCycles.FindLast(path); + completeCount.Should().Be(expected.Count); + } + finally + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + } } diff --git a/PerfectNumbers.Core/MersenneDivisorCycles.cs b/PerfectNumbers.Core/MersenneDivisorCycles.cs index e9cabdc..2ab2821 100644 --- a/PerfectNumbers.Core/MersenneDivisorCycles.cs +++ b/PerfectNumbers.Core/MersenneDivisorCycles.cs @@ -150,65 +150,85 @@ public class MersenneDivisorCycles private const int BufferSize10M = 10 * 1024 * 1024; - public static void Generate(string path, ulong maxDivisor, int threads = 16) - { - // Binary-only generation of (divisor, cycle) pairs. CSV scaffolding removed. - ulong start, d = start = 2; - ulong blockSize = maxDivisor / (ulong)threads + 1; - Task[] tasks = new Task[threads]; - using Stream outputStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, BufferSize10M, useAsync: false); - // Start fresh to avoid mixing formats - outputStream.SetLength(0L); - outputStream.Position = 0L; - using var writer = new BinaryWriter(outputStream, Encoding.UTF8, leaveOpen: false); - for (var taskIndex = 0; taskIndex < threads; taskIndex++) - { - Task t = Task.Factory.StartNew((taskIndex) => - { - ulong cycle, d = start + (ulong)(int)taskIndex! * blockSize; - (ulong d, ulong cycle)[] localCycles = ArrayPool<(ulong, ulong)>.Shared.Rent((int)(maxDivisor - d + 1UL)); - int localCycleIndex = 0; - for (; d <= maxDivisor; d++) - { - if ((d & 1UL) == 0UL) - { - // skip even divisors - continue; - } + public static void Generate(string path, ulong maxDivisor, int threads = 16) + { + // Binary-only generation of (divisor, cycle) pairs. CSV scaffolding removed. + ulong start = 2; + ulong blockSize = maxDivisor / (ulong)threads + 1UL; + Task[] tasks = new Task[threads]; + using Stream outputStream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read, BufferSize10M, useAsync: false); + // Start fresh to avoid mixing formats + outputStream.SetLength(0L); + outputStream.Position = 0L; + using var writer = new BinaryWriter(outputStream, Encoding.UTF8, leaveOpen: false); + for (var taskIndex = 0; taskIndex < threads; taskIndex++) + { + ulong threadStart = start + (ulong)taskIndex * blockSize; + if (threadStart > maxDivisor) + { + tasks[taskIndex] = Task.CompletedTask; + continue; + } - cycle = CalculateCycleLength(d); + ulong threadEnd = threadStart + blockSize - 1UL; + if (threadEnd > maxDivisor) + { + threadEnd = maxDivisor; + } - localCycles[localCycleIndex++] = (d, cycle); - } + ulong localStart = threadStart; + ulong localEnd = threadEnd; + tasks[taskIndex] = Task.Run(() => + { + ulong rangeLength = localEnd - localStart + 1UL; + (ulong divisor, ulong cycle)[] localCycles = ArrayPool<(ulong, ulong)>.Shared.Rent(checked((int)rangeLength)); + int localCycleIndex = 0; - if (localCycleIndex > 0) - { - lock (writer) - { - writer.BaseStream.Position = writer.BaseStream.Length; - for (int i = 0; i < localCycleIndex; i++) - { - (d, cycle) = localCycles[i]; - if ((d % 3UL) == 0UL || (d % 5UL) == 0UL || (d % 7UL) == 0UL || (d % 11UL) == 0UL) - continue; + try + { + for (ulong divisor = localStart; divisor <= localEnd; divisor++) + { + if ((divisor & 1UL) == 0UL) + { + continue; + } - writer.Write(d); - writer.Write(cycle); - } + if ((divisor % 3UL) == 0UL || (divisor % 5UL) == 0UL || (divisor % 7UL) == 0UL || (divisor % 11UL) == 0UL) + { + continue; + } - writer.Flush(); - } - } + ulong cycle = CalculateCycleLength(divisor); + localCycles[localCycleIndex++] = (divisor, cycle); + } - ArrayPool<(ulong, ulong)>.Shared.Return(localCycles, clearArray: false); + if (localCycleIndex == 0) + { + return; + } - }, taskIndex); + lock (writer) + { + writer.BaseStream.Position = writer.BaseStream.Length; + for (int i = 0; i < localCycleIndex; i++) + { + (ulong divisor, ulong cycle) = localCycles[i]; + writer.Write(divisor); + writer.Write(cycle); + } - tasks[taskIndex] = t; - } + writer.Flush(); + } + } + finally + { + ArrayPool<(ulong, ulong)>.Shared.Return(localCycles, clearArray: false); + } + }); + } - Task.WaitAll(tasks); - } + Task.WaitAll(tasks); + } public static void GenerateGpu(string path, ulong maxDivisor, int batchSize = 1_000_000, long skipCount = 0L, long nextPosition = 0L) {