diff --git a/PerfectNumbers.Core.Tests/PrimesGeneratorTests.cs b/PerfectNumbers.Core.Tests/PrimesGeneratorTests.cs index 95df8a2..8ccebe4 100644 --- a/PerfectNumbers.Core.Tests/PrimesGeneratorTests.cs +++ b/PerfectNumbers.Core.Tests/PrimesGeneratorTests.cs @@ -36,15 +36,30 @@ public class PrimesGeneratorTests var lastThreePow2 = DivisorGenerator.SmallPrimesPow2LastThree; var lastNine = DivisorGenerator.SmallPrimesLastNine; var lastNinePow2 = DivisorGenerator.SmallPrimesPow2LastNine; + var lastOneWithoutThree = DivisorGenerator.SmallPrimesLastOneWithoutLastThree; + var lastOneWithoutThreePow2 = DivisorGenerator.SmallPrimesPow2LastOneWithoutLastThree; + var lastSevenWithoutThree = DivisorGenerator.SmallPrimesLastSevenWithoutLastThree; + var lastSevenWithoutThreePow2 = DivisorGenerator.SmallPrimesPow2LastSevenWithoutLastThree; + var lastNineWithoutThree = DivisorGenerator.SmallPrimesLastNineWithoutLastThree; + var lastNineWithoutThreePow2 = DivisorGenerator.SmallPrimesPow2LastNineWithoutLastThree; lastOne.Length.Should().Be((int)PerfectNumberConstants.PrimesLimit); lastSeven.Length.Should().Be((int)PerfectNumberConstants.PrimesLimit); lastThree.Length.Should().Be((int)PerfectNumberConstants.PrimesLimit); lastNine.Length.Should().Be((int)PerfectNumberConstants.PrimesLimit); + lastOneWithoutThree.Length.Should().BeGreaterThan(0); + lastSevenWithoutThree.Length.Should().BeGreaterThan(0); + lastNineWithoutThree.Length.Should().BeGreaterThan(0); + lastOneWithoutThree.Length.Should().BeLessThan(lastOne.Length); + lastSevenWithoutThree.Length.Should().BeLessThan(lastSeven.Length); + lastNineWithoutThree.Length.Should().BeLessThan(lastNine.Length); lastOnePow2.Length.Should().Be(lastOne.Length); lastSevenPow2.Length.Should().Be(lastSeven.Length); lastThreePow2.Length.Should().Be(lastThree.Length); lastNinePow2.Length.Should().Be(lastNine.Length); + lastOneWithoutThreePow2.Length.Should().Be(lastOneWithoutThree.Length); + lastSevenWithoutThreePow2.Length.Should().Be(lastSevenWithoutThree.Length); + lastNineWithoutThreePow2.Length.Should().Be(lastNineWithoutThree.Length); lastOne[..5].Should().Equal([3U, 7U, 11U, 13U, 19U]); lastSeven[..5].Should().Equal([3U, 7U, 11U, 13U, 17U]); @@ -78,6 +93,47 @@ public class PrimesGeneratorTests lastThreePow2[i].Should().Be(prime * (ulong)prime); } + + lastOneWithoutThree.Should().NotBeEmpty(); + lastSevenWithoutThree.Should().NotBeEmpty(); + lastNineWithoutThree.Should().NotBeEmpty(); + + for (int i = 0; i < lastOneWithoutThree.Length; i++) + { + uint prime = lastOneWithoutThree[i]; + (prime % 10U != 3U).Should().BeTrue(); + if (i > 0) + { + prime.Should().BeGreaterThan(lastOneWithoutThree[i - 1]); + } + + lastOneWithoutThreePow2[i].Should().Be(prime * (ulong)prime); + } + + for (int i = 0; i < lastSevenWithoutThree.Length; i++) + { + uint prime = lastSevenWithoutThree[i]; + (prime % 10U != 3U).Should().BeTrue(); + if (i > 0) + { + prime.Should().BeGreaterThan(lastSevenWithoutThree[i - 1]); + } + + lastSevenWithoutThreePow2[i].Should().Be(prime * (ulong)prime); + } + + for (int i = 0; i < lastNineWithoutThree.Length; i++) + { + uint prime = lastNineWithoutThree[i]; + (prime % 10U != 3U).Should().BeTrue(); + if (i > 0) + { + prime.Should().BeGreaterThan(lastNineWithoutThree[i - 1]); + } + + lastNineWithoutThreePow2[i].Should().Be(prime * (ulong)prime); + } + for (int i = 0; i < lastNine.Length; i++) { uint prime = lastNine[i]; diff --git a/PerfectNumbers.Core/DivisorGenerator.cs b/PerfectNumbers.Core/DivisorGenerator.cs index 20b8c81..4cce836 100644 --- a/PerfectNumbers.Core/DivisorGenerator.cs +++ b/PerfectNumbers.Core/DivisorGenerator.cs @@ -16,6 +16,12 @@ internal static class DivisorGenerator public static readonly ulong[] SmallPrimesPow2LastThree; public static readonly uint[] SmallPrimesLastNine; public static readonly ulong[] SmallPrimesPow2LastNine; + public static readonly uint[] SmallPrimesLastOneWithoutLastThree; + public static readonly ulong[] SmallPrimesPow2LastOneWithoutLastThree; + public static readonly uint[] SmallPrimesLastSevenWithoutLastThree; + public static readonly ulong[] SmallPrimesPow2LastSevenWithoutLastThree; + public static readonly uint[] SmallPrimesLastNineWithoutLastThree; + public static readonly ulong[] SmallPrimesPow2LastNineWithoutLastThree; private const ushort DecimalMaskWhenLastIsSeven = (1 << 3) | (1 << 7) | (1 << 9); private const ushort DecimalMaskWhenLastIsThree = (1 << 1) | (1 << 3) | (1 << 7) | (1 << 9); @@ -34,7 +40,13 @@ internal static class DivisorGenerator out SmallPrimesLastThree, out SmallPrimesPow2LastThree, out SmallPrimesLastNine, - out SmallPrimesPow2LastNine); + out SmallPrimesPow2LastNine, + out SmallPrimesLastOneWithoutLastThree, + out SmallPrimesPow2LastOneWithoutLastThree, + out SmallPrimesLastSevenWithoutLastThree, + out SmallPrimesPow2LastSevenWithoutLastThree, + out SmallPrimesLastNineWithoutLastThree, + out SmallPrimesPow2LastNineWithoutLastThree); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -59,7 +71,13 @@ internal static class DivisorGenerator out uint[] lastThree, out ulong[] lastThreePow2, out uint[] lastNine, - out ulong[] lastNinePow2) + out ulong[] lastNinePow2, + out uint[] lastOneWithoutThree, + out ulong[] lastOneWithoutThreePow2, + out uint[] lastSevenWithoutThree, + out ulong[] lastSevenWithoutThreePow2, + out uint[] lastNineWithoutThree, + out ulong[] lastNineWithoutThreePow2) { uint target = PerfectNumberConstants.PrimesLimit; int targetInt = checked((int)target); @@ -75,6 +93,13 @@ internal static class DivisorGenerator lastNinePow2 = new ulong[targetInt]; List primes = new(targetInt * 4 / 3 + 16); + List lastOneWithoutThreeList = new(targetInt); + List lastOneWithoutThreePow2List = new(targetInt); + List lastSevenWithoutThreeList = new(targetInt); + List lastSevenWithoutThreePow2List = new(targetInt); + List lastNineWithoutThreeList = new(targetInt); + List lastNineWithoutThreePow2List = new(targetInt); + uint candidate = 2U; int allCount = 0; int lastOneCount = 0; @@ -82,24 +107,22 @@ internal static class DivisorGenerator int lastThreeCount = 0; int lastNineCount = 0; - while (allCount < targetInt || lastOneCount < targetInt || lastSevenCount < targetInt || lastThreeCount < targetInt || lastNineCount < targetInt) + while (allCount < targetInt || + lastOneCount < targetInt || + lastSevenCount < targetInt || + lastThreeCount < targetInt || + lastNineCount < targetInt) { bool isPrime = true; int primesCount = primes.Count; for (int i = 0; i < primesCount; i++) { uint p = primes[i]; - // TODO: Reuse a single squared local for `p` within this loop so we avoid recalculating - // `p * p` repeatedly without storing every squared prime globally (the cache must remain - // limited to the small-divisor tables to honor the memory constraints). if (p * p > candidate) { break; } - // TODO: Replace this trial-division `%` with the sieve-based generator that avoids - // per-candidate modulo work so building the small-prime tables stops dominating - // startup time for large scans. if (candidate % p == 0U) { isPrime = false; @@ -110,54 +133,80 @@ internal static class DivisorGenerator if (isPrime) { primes.Add(candidate); + ulong candidateSquared = checked((ulong)candidate * candidate); if (allCount < targetInt) { - // TODO: Reuse a single cached `candidateSquared` per iteration so we avoid issuing - // three separate 64-bit multiplications when populating the pow2 arrays here and - // in the last-one/last-seven branches below. all[allCount] = candidate; - allPow2[allCount] = checked((ulong)candidate * candidate); + allPow2[allCount] = candidateSquared; allCount++; } if (candidate != 2U) { LastDigit lastDigit = GetLastDigit(candidate); + bool allowedForLastOne = IsAllowedForLastOne(candidate, lastDigit); + bool allowedForLastSeven = IsAllowedForLastSeven(candidate, lastDigit); + bool allowedForLastThree = IsAllowedForLastThree(candidate, lastDigit); + bool allowedForLastNine = IsAllowedForLastNine(candidate, lastDigit); + bool lastDigitIsThree = lastDigit == LastDigit.Three; - if (lastOneCount < targetInt && IsAllowedForLastOne(candidate, lastDigit)) + if (allowedForLastOne && lastOneCount < targetInt) { lastOne[lastOneCount] = candidate; - lastOnePow2[lastOneCount] = checked((ulong)candidate * candidate); + lastOnePow2[lastOneCount] = candidateSquared; lastOneCount++; } - if (lastSevenCount < targetInt && IsAllowedForLastSeven(candidate, lastDigit)) + if (allowedForLastSeven && lastSevenCount < targetInt) { lastSeven[lastSevenCount] = candidate; - lastSevenPow2[lastSevenCount] = checked((ulong)candidate * candidate); + lastSevenPow2[lastSevenCount] = candidateSquared; lastSevenCount++; } - if (lastThreeCount < targetInt && IsAllowedForLastThree(candidate, lastDigit)) + if (allowedForLastThree && lastThreeCount < targetInt) { lastThree[lastThreeCount] = candidate; - lastThreePow2[lastThreeCount] = checked((ulong)candidate * candidate); + lastThreePow2[lastThreeCount] = candidateSquared; lastThreeCount++; } - if (lastNineCount < targetInt && IsAllowedForLastNine(candidate, lastDigit)) + if (allowedForLastNine && lastNineCount < targetInt) { lastNine[lastNineCount] = candidate; - lastNinePow2[lastNineCount] = checked((ulong)candidate * candidate); + lastNinePow2[lastNineCount] = candidateSquared; lastNineCount++; } + + if (!lastDigitIsThree && allowedForLastOne) + { + lastOneWithoutThreeList.Add(candidate); + lastOneWithoutThreePow2List.Add(candidateSquared); + } + + if (!lastDigitIsThree && allowedForLastSeven) + { + lastSevenWithoutThreeList.Add(candidate); + lastSevenWithoutThreePow2List.Add(candidateSquared); + } + + if (!lastDigitIsThree && allowedForLastNine) + { + lastNineWithoutThreeList.Add(candidate); + lastNineWithoutThreePow2List.Add(candidateSquared); + } } } - // TODO: Switch this increment to the Mod6 stepping table identified in the Mod6ComparisonBenchmarks - // so we skip the composite residues without relying on per-iteration `%` checks. candidate = candidate == 2U ? 3U : candidate + 2U; } + + lastOneWithoutThree = lastOneWithoutThreeList.ToArray(); + lastOneWithoutThreePow2 = lastOneWithoutThreePow2List.ToArray(); + lastSevenWithoutThree = lastSevenWithoutThreeList.ToArray(); + lastSevenWithoutThreePow2 = lastSevenWithoutThreePow2List.ToArray(); + lastNineWithoutThree = lastNineWithoutThreeList.ToArray(); + lastNineWithoutThreePow2 = lastNineWithoutThreePow2List.ToArray(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/PerfectNumbers.Core/HeuristicPrimeTester.cs b/PerfectNumbers.Core/HeuristicPrimeTester.cs index 2cbf881..c3983a1 100644 --- a/PerfectNumbers.Core/HeuristicPrimeTester.cs +++ b/PerfectNumbers.Core/HeuristicPrimeTester.cs @@ -470,7 +470,7 @@ Cleanup: for (int i = 0; i < endingOrder.Length; i++) { - ReadOnlySpan divisors = GetGroupBDivisors(endingOrder[i]); + ReadOnlySpan divisors = GetGroupBDivisors(endingOrder[i]); int index = indices[i]; if ((uint)index >= (uint)divisors.Length) { @@ -507,7 +507,7 @@ Cleanup: { for (int i = 0; i < endingOrder.Length; i++) { - ReadOnlySpan divisors = GetGroupBDivisors(endingOrder[i]); + ReadOnlySpan divisors = GetGroupBDivisors(endingOrder[i]); int index = indices[i]; if ((uint)index >= (uint)divisors.Length) { @@ -527,13 +527,13 @@ Cleanup: return false; } - private static ReadOnlySpan GetGroupBDivisors(byte ending) => ending switch + private static ReadOnlySpan GetGroupBDivisors(byte ending) => ending switch { - 1 => HeuristicPrimeSieves.GroupBDivisorsEnding1, - 3 => HeuristicPrimeSieves.GroupBDivisorsEnding3, - 7 => HeuristicPrimeSieves.GroupBDivisorsEnding7, - 9 => HeuristicPrimeSieves.GroupBDivisorsEnding9, - _ => ReadOnlySpan.Empty, + 1 => DivisorGenerator.SmallPrimesLastOneWithoutLastThree, + 3 => ReadOnlySpan.Empty, + 7 => DivisorGenerator.SmallPrimesLastSevenWithoutLastThree, + 9 => DivisorGenerator.SmallPrimesLastNineWithoutLastThree, + _ => ReadOnlySpan.Empty, }; private static ReadOnlySpan GetWheel210ResiduesForEnding(byte ending) => ending switch