From 627d2e9b141fef98cd2dc1d98e9cfff2e97fe665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sun, 19 Oct 2025 22:49:38 +0200 Subject: [PATCH 01/11] Cache composite primality results in PartialFactor --- .../PrimeOrderCalculator.Cpu.cs | 32 ++++++++++++-- PerfectNumbers.Core/ThreadStaticPools.cs | 42 +++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs index 536eebd..835b80d 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs @@ -757,6 +757,27 @@ internal static partial class PrimeOrderCalculator Stack? compositeStack = null; PartialFactorResult result; + Dictionary? localPrimalityCache = null; + + bool EvaluatePrimality(ulong candidate) + { + Dictionary? cache = localPrimalityCache; + if (cache is not null && cache.TryGetValue(candidate, out bool cachedValue)) + { + return cachedValue; + } + + bool computed = GetOrComputePrimality(candidate); + if (!computed) + { + cache ??= ThreadStaticPools.RentUlongBoolDictionary(4); + cache[candidate] = false; + localPrimalityCache = cache; + } + + return computed; + } + if (!gpuFactored) { // We don't need to worry about leftovers, because we always use indexes within the calculated counts @@ -821,7 +842,7 @@ internal static partial class PrimeOrderCalculator // continue; // } - bool isPrime = GetOrComputePrimality(composite); + bool isPrime = EvaluatePrimality(composite); if (isPrime) { @@ -868,7 +889,7 @@ internal static partial class PrimeOrderCalculator continue; } - bool isPrime = GetOrComputePrimality(composite); + bool isPrime = EvaluatePrimality(composite); if (isPrime) { @@ -894,7 +915,7 @@ internal static partial class PrimeOrderCalculator } else { - cofactorIsPrime = GetOrComputePrimality(cofactor); + cofactorIsPrime = EvaluatePrimality(cofactor); } ArrayPool pool = ThreadStaticPools.FactorEntryPool; @@ -968,6 +989,11 @@ internal static partial class PrimeOrderCalculator // ThreadStaticPools.UlongPool.Return(primeSlotsArray); // ThreadStaticPools.IntPool.Return(exponentSlotsArray); + if (localPrimalityCache is not null) + { + ThreadStaticPools.ReturnUlongBoolDictionary(localPrimalityCache); + } + if (counts is not null) { ThreadStaticPools.ReturnUlongIntDictionary(counts); diff --git a/PerfectNumbers.Core/ThreadStaticPools.cs b/PerfectNumbers.Core/ThreadStaticPools.cs index 33a9de2..0a35395 100644 --- a/PerfectNumbers.Core/ThreadStaticPools.cs +++ b/PerfectNumbers.Core/ThreadStaticPools.cs @@ -278,6 +278,48 @@ namespace PerfectNumbers.Core pool.Add(dictionary); } + [ThreadStatic] + private static List>? _ulongBoolDictionaryPool; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Dictionary RentUlongBoolDictionary(int capacityHint) + { + List>? pool = _ulongBoolDictionaryPool; + if (pool is not null && pool.Count > 0) + { + int lastIndex = pool.Count - 1; + Dictionary dictionary = pool[lastIndex]; + pool.RemoveAt(lastIndex); + if (dictionary.Count > 0) + { + dictionary.Clear(); + } + + if (dictionary.Count < capacityHint) + { + dictionary.EnsureCapacity(capacityHint); + } + + return dictionary; + } + + return new Dictionary(capacityHint); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReturnUlongBoolDictionary(Dictionary dictionary) + { + List>? pool = _ulongBoolDictionaryPool; + if (pool is null) + { + pool = new List>(4); + _ulongBoolDictionaryPool = pool; + } + + dictionary.Clear(); + pool.Add(dictionary); + } + [ThreadStatic] private static Dictionary? _mersenneFactorCacheDictionary; From 4917f690fc3079b7fd6ac2f89b7e55a7523b0f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sun, 19 Oct 2025 22:59:44 +0200 Subject: [PATCH 02/11] Make partial factor primality cache thread static --- .../PrimeOrderCalculator.Cpu.cs | 88 +++++++++++++------ 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs index 835b80d..fa20429 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs @@ -720,6 +720,63 @@ internal static partial class PrimeOrderCalculator return isPrime; } + + [ThreadStatic] + private static Dictionary? _partialFactorPrimalityCache; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ResetPartialFactorPrimalityCache() + { + Dictionary? cache = _partialFactorPrimalityCache; + if (cache is not null && cache.Count > 0) + { + cache.Clear(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Dictionary GetPartialFactorPrimalityCache() + { + Dictionary? cache = _partialFactorPrimalityCache; + if (cache is null) + { + cache = ThreadStaticPools.RentUlongBoolDictionary(4); + _partialFactorPrimalityCache = cache; + } + + return cache; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool EvaluatePrimalityPartialFactor(ulong candidate) + { + if (TryGetCachedPrimality(candidate, out bool cached)) + { + return cached; + } + + ulong globalCacheLimit = (ulong)PerfectNumberConstants.MaxQForDivisorCycles; + if (candidate > globalCacheLimit) + { + Dictionary? cache = _partialFactorPrimalityCache; + if (cache is not null && cache.TryGetValue(candidate, out bool cachedValue)) + { + return cachedValue; + } + } + + bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(candidate); + CachePrimalityResult(candidate, isPrime); + + if (candidate > globalCacheLimit) + { + Dictionary cache = GetPartialFactorPrimalityCache(); + cache[candidate] = isPrime; + } + + return isPrime; + } + private static PartialFactorResult PartialFactor(ulong value, in PrimeOrderSearchConfig config) { if (value <= 1UL) @@ -757,26 +814,7 @@ internal static partial class PrimeOrderCalculator Stack? compositeStack = null; PartialFactorResult result; - Dictionary? localPrimalityCache = null; - - bool EvaluatePrimality(ulong candidate) - { - Dictionary? cache = localPrimalityCache; - if (cache is not null && cache.TryGetValue(candidate, out bool cachedValue)) - { - return cachedValue; - } - - bool computed = GetOrComputePrimality(candidate); - if (!computed) - { - cache ??= ThreadStaticPools.RentUlongBoolDictionary(4); - cache[candidate] = false; - localPrimalityCache = cache; - } - - return computed; - } + ResetPartialFactorPrimalityCache(); if (!gpuFactored) { @@ -842,7 +880,7 @@ internal static partial class PrimeOrderCalculator // continue; // } - bool isPrime = EvaluatePrimality(composite); + bool isPrime = EvaluatePrimalityPartialFactor(composite); if (isPrime) { @@ -889,7 +927,7 @@ internal static partial class PrimeOrderCalculator continue; } - bool isPrime = EvaluatePrimality(composite); + bool isPrime = EvaluatePrimalityPartialFactor(composite); if (isPrime) { @@ -915,7 +953,7 @@ internal static partial class PrimeOrderCalculator } else { - cofactorIsPrime = EvaluatePrimality(cofactor); + cofactorIsPrime = EvaluatePrimalityPartialFactor(cofactor); } ArrayPool pool = ThreadStaticPools.FactorEntryPool; @@ -989,10 +1027,6 @@ internal static partial class PrimeOrderCalculator // ThreadStaticPools.UlongPool.Return(primeSlotsArray); // ThreadStaticPools.IntPool.Return(exponentSlotsArray); - if (localPrimalityCache is not null) - { - ThreadStaticPools.ReturnUlongBoolDictionary(localPrimalityCache); - } if (counts is not null) { From b7c187bbc51cdb666a8821a027781a030a0445bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sun, 19 Oct 2025 23:16:35 +0200 Subject: [PATCH 03/11] Check Pollard Rho deadline inside partial factor loop --- PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs index fa20429..adf11e0 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs @@ -873,6 +873,13 @@ internal static partial class PrimeOrderCalculator { while (compositeStack.Count > 0) { + long currentTimestamp = Stopwatch.GetTimestamp(); + if (currentTimestamp > deadlineTimestamp) + { + pollardRhoDeadlineReached = true; + break; + } + ulong composite = compositeStack.Pop(); // composite will never be smaller than 1 on the execution path // if (composite <= 1UL) @@ -899,7 +906,10 @@ internal static partial class PrimeOrderCalculator compositeStack.Push(quotient); } - pollardRhoDeadlineReached = Stopwatch.GetTimestamp() > deadlineTimestamp; + if (!pollardRhoDeadlineReached) + { + pollardRhoDeadlineReached = Stopwatch.GetTimestamp() > deadlineTimestamp; + } } if (pollardRhoDeadlineReached) From 0fbd635828feeb345282b596374ebea6dd26d17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sun, 19 Oct 2025 23:35:08 +0200 Subject: [PATCH 04/11] Reuse pooled local cache for partial factor primality checks --- .../PrimeOrderCalculator.Cpu.cs | 51 +++++++------------ 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs index adf11e0..9f1d5c8 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs @@ -721,34 +721,8 @@ internal static partial class PrimeOrderCalculator } - [ThreadStatic] - private static Dictionary? _partialFactorPrimalityCache; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ResetPartialFactorPrimalityCache() - { - Dictionary? cache = _partialFactorPrimalityCache; - if (cache is not null && cache.Count > 0) - { - cache.Clear(); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Dictionary GetPartialFactorPrimalityCache() - { - Dictionary? cache = _partialFactorPrimalityCache; - if (cache is null) - { - cache = ThreadStaticPools.RentUlongBoolDictionary(4); - _partialFactorPrimalityCache = cache; - } - - return cache; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool EvaluatePrimalityPartialFactor(ulong candidate) + private static bool EvaluatePrimalityPartialFactor(ref Dictionary? localCache, ulong candidate) { if (TryGetCachedPrimality(candidate, out bool cached)) { @@ -758,7 +732,7 @@ internal static partial class PrimeOrderCalculator ulong globalCacheLimit = (ulong)PerfectNumberConstants.MaxQForDivisorCycles; if (candidate > globalCacheLimit) { - Dictionary? cache = _partialFactorPrimalityCache; + Dictionary? cache = localCache; if (cache is not null && cache.TryGetValue(candidate, out bool cachedValue)) { return cachedValue; @@ -770,7 +744,13 @@ internal static partial class PrimeOrderCalculator if (candidate > globalCacheLimit) { - Dictionary cache = GetPartialFactorPrimalityCache(); + Dictionary? cache = localCache; + if (cache is null) + { + cache = ThreadStaticPools.RentUlongBoolDictionary(4); + localCache = cache; + } + cache[candidate] = isPrime; } @@ -813,8 +793,8 @@ internal static partial class PrimeOrderCalculator List pending = ThreadStaticPools.RentPrimeOrderPendingEntryList(2); Stack? compositeStack = null; PartialFactorResult result; + Dictionary? primalityCache = null; - ResetPartialFactorPrimalityCache(); if (!gpuFactored) { @@ -887,7 +867,7 @@ internal static partial class PrimeOrderCalculator // continue; // } - bool isPrime = EvaluatePrimalityPartialFactor(composite); + bool isPrime = EvaluatePrimalityPartialFactor(ref primalityCache, composite); if (isPrime) { @@ -937,7 +917,7 @@ internal static partial class PrimeOrderCalculator continue; } - bool isPrime = EvaluatePrimalityPartialFactor(composite); + bool isPrime = EvaluatePrimalityPartialFactor(ref primalityCache, composite); if (isPrime) { @@ -963,7 +943,7 @@ internal static partial class PrimeOrderCalculator } else { - cofactorIsPrime = EvaluatePrimalityPartialFactor(cofactor); + cofactorIsPrime = EvaluatePrimalityPartialFactor(ref primalityCache, cofactor); } ArrayPool pool = ThreadStaticPools.FactorEntryPool; @@ -1038,6 +1018,11 @@ internal static partial class PrimeOrderCalculator // ThreadStaticPools.IntPool.Return(exponentSlotsArray); + if (primalityCache is not null) + { + ThreadStaticPools.ReturnUlongBoolDictionary(primalityCache); + } + if (counts is not null) { ThreadStaticPools.ReturnUlongIntDictionary(counts); From 43e2894661f4329c2f1f4245f2bfe6a36f0d04d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sun, 19 Oct 2025 23:48:07 +0200 Subject: [PATCH 05/11] Refine partial factor primality caching --- .../PrimeOrderCalculator.Cpu.cs | 6 ++- PerfectNumbers.Core/ThreadStaticPools.cs | 45 +++++++++++++------ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs index 9f1d5c8..ac490dc 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs @@ -730,7 +730,8 @@ internal static partial class PrimeOrderCalculator } ulong globalCacheLimit = (ulong)PerfectNumberConstants.MaxQForDivisorCycles; - if (candidate > globalCacheLimit) + bool bypassesGlobalCache = candidate > globalCacheLimit; + if (bypassesGlobalCache) { Dictionary? cache = localCache; if (cache is not null && cache.TryGetValue(candidate, out bool cachedValue)) @@ -742,12 +743,13 @@ internal static partial class PrimeOrderCalculator bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(candidate); CachePrimalityResult(candidate, isPrime); - if (candidate > globalCacheLimit) + if (bypassesGlobalCache) { Dictionary? cache = localCache; if (cache is null) { cache = ThreadStaticPools.RentUlongBoolDictionary(4); + cache.Clear(); localCache = cache; } diff --git a/PerfectNumbers.Core/ThreadStaticPools.cs b/PerfectNumbers.Core/ThreadStaticPools.cs index 0a35395..3fc47e5 100644 --- a/PerfectNumbers.Core/ThreadStaticPools.cs +++ b/PerfectNumbers.Core/ThreadStaticPools.cs @@ -279,22 +279,39 @@ namespace PerfectNumbers.Core } [ThreadStatic] - private static List>? _ulongBoolDictionaryPool; + private static Stack>? _ulongBoolDictionaryPool; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Dictionary RentUlongBoolDictionary(int capacityHint) { - List>? pool = _ulongBoolDictionaryPool; - if (pool is not null && pool.Count > 0) + Stack>? pool = _ulongBoolDictionaryPool; + if (pool is null) { - int lastIndex = pool.Count - 1; - Dictionary dictionary = pool[lastIndex]; - pool.RemoveAt(lastIndex); - if (dictionary.Count > 0) - { - dictionary.Clear(); - } + pool = new Stack>(4); + _ulongBoolDictionaryPool = pool; + } + // Keep the previous null-check implementation here for reference. + // Do not uncomment this branch; the pool is always initialized above. + // if (pool is not null && pool.Count > 0) + // { + // Dictionary dictionary = pool.Pop(); + // if (dictionary.Count < capacityHint) + // { + // dictionary.EnsureCapacity(capacityHint); + // } + // + // return dictionary; + // } + + if (pool.Count > 0) + { + Dictionary dictionary = pool.Pop(); + // Callers clear dictionaries after renting to avoid duplicate work. + // if (dictionary.Count > 0) + // { + // dictionary.Clear(); + // } if (dictionary.Count < capacityHint) { dictionary.EnsureCapacity(capacityHint); @@ -309,15 +326,15 @@ namespace PerfectNumbers.Core [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ReturnUlongBoolDictionary(Dictionary dictionary) { - List>? pool = _ulongBoolDictionaryPool; + Stack>? pool = _ulongBoolDictionaryPool; if (pool is null) { - pool = new List>(4); + pool = new Stack>(4); _ulongBoolDictionaryPool = pool; } - dictionary.Clear(); - pool.Add(dictionary); + // Do not clear pooled dictionaries here; callers must clear on rent. + pool.Push(dictionary); } [ThreadStatic] From e4f6cbbe25436126817abc4d21590e8682cf23c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Sun, 19 Oct 2025 23:48:12 +0200 Subject: [PATCH 06/11] Requeue timed-out Pollard Rho composites --- PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs index ac490dc..91078ef 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs @@ -855,13 +855,6 @@ internal static partial class PrimeOrderCalculator { while (compositeStack.Count > 0) { - long currentTimestamp = Stopwatch.GetTimestamp(); - if (currentTimestamp > deadlineTimestamp) - { - pollardRhoDeadlineReached = true; - break; - } - ulong composite = compositeStack.Pop(); // composite will never be smaller than 1 on the execution path // if (composite <= 1UL) @@ -877,6 +870,14 @@ internal static partial class PrimeOrderCalculator continue; } + long currentTimestamp = Stopwatch.GetTimestamp(); + if (currentTimestamp > deadlineTimestamp) + { + pollardRhoDeadlineReached = true; + pending.Add(new PendingEntry(composite, knownComposite: true)); + continue; + } + if (!TryPollardRho(composite, deadlineTimestamp, out ulong factor)) { pending.Add(new PendingEntry(composite, knownComposite: true)); From bad5c28fdf1bdfe8507a341482482597cef27418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 00:01:21 +0200 Subject: [PATCH 07/11] Align scheduler thread count with CLI configuration --- EvenPerfectBitScanner/Program.cs | 1 + PerfectNumbers.Core/PrimeOrderCalculator.cs | 14 +++- PerfectNumbers.Core/UnboundedTaskScheduler.cs | 66 +++++++++++++++++-- 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/EvenPerfectBitScanner/Program.cs b/EvenPerfectBitScanner/Program.cs index c5866e8..77eb824 100644 --- a/EvenPerfectBitScanner/Program.cs +++ b/EvenPerfectBitScanner/Program.cs @@ -55,6 +55,7 @@ internal static class Program ulong remainder = currentP % 6UL; bool startPrimeProvided = _cliArguments.StartPrimeProvided; int threadCount = Math.Max(1, _cliArguments.ThreadCount); + UnboundedTaskScheduler.ConfigureThreadCount(threadCount); int blockSize = Math.Max(1, _cliArguments.BlockSize); int gpuPrimeThreads = Math.Max(1, _cliArguments.GpuPrimeThreads); int gpuPrimeBatch = Math.Max(1, _cliArguments.GpuPrimeBatch); diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.cs b/PerfectNumbers.Core/PrimeOrderCalculator.cs index 34b4c1b..fd2da4b 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.cs @@ -10,9 +10,21 @@ internal static partial class PrimeOrderCalculator internal readonly struct PrimeOrderSearchConfig(uint smallFactorLimit, int pollardRhoMilliseconds, int maxPowChecks, PrimeOrderMode mode) { - public static readonly PrimeOrderSearchConfig HeuristicDefault = new(smallFactorLimit: 100_000, pollardRhoMilliseconds: 24, maxPowChecks: 24, PrimeOrderMode.Heuristic); + public static PrimeOrderSearchConfig HeuristicDefault + { + get + { + return CreateHeuristicDefault(UnboundedTaskScheduler.ConfiguredThreadCount); + } + } public static readonly PrimeOrderSearchConfig StrictDefault = new(smallFactorLimit: 1_000_000, pollardRhoMilliseconds: 0, maxPowChecks: 0, PrimeOrderMode.Strict); + public static PrimeOrderSearchConfig CreateHeuristicDefault(int threadCount) + { + int normalizedThreadCount = threadCount < 1 ? 1 : threadCount; + return new(smallFactorLimit: 100_000, pollardRhoMilliseconds: normalizedThreadCount * 24, maxPowChecks: 24, PrimeOrderMode.Heuristic); + } + public readonly uint SmallFactorLimit = smallFactorLimit; public readonly int PollardRhoMilliseconds = pollardRhoMilliseconds; public readonly int MaxPowChecks = maxPowChecks; diff --git a/PerfectNumbers.Core/UnboundedTaskScheduler.cs b/PerfectNumbers.Core/UnboundedTaskScheduler.cs index ec9209f..66912e9 100644 --- a/PerfectNumbers.Core/UnboundedTaskScheduler.cs +++ b/PerfectNumbers.Core/UnboundedTaskScheduler.cs @@ -1,18 +1,53 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace PerfectNumbers.Core; public sealed class UnboundedTaskScheduler : TaskScheduler { - public static UnboundedTaskScheduler Instance { get; } = new(); + private static readonly object InstanceLock = new(); + private static UnboundedTaskScheduler? _instance; + private static int _configuredThreadCount = Environment.ProcessorCount; - private readonly TaskThreadPool _threadPool; - - private UnboundedTaskScheduler() + public static UnboundedTaskScheduler Instance { - _threadPool = new TaskThreadPool(10_340, ExecuteTask); + get + { + lock (InstanceLock) + { + _instance ??= new UnboundedTaskScheduler(GetNormalizedThreadCount(_configuredThreadCount)); + return _instance; + } + } + } + + public static int ConfiguredThreadCount => GetNormalizedThreadCount(Volatile.Read(ref _configuredThreadCount)); + + private readonly object _poolLock = new(); + private TaskThreadPool _threadPool; + + private UnboundedTaskScheduler(int threadCount) + { + _threadPool = new TaskThreadPool(threadCount, ExecuteTask); + } + + public static void ConfigureThreadCount(int threadCount) + { + int normalizedThreadCount = GetNormalizedThreadCount(threadCount); + + lock (InstanceLock) + { + Volatile.Write(ref _configuredThreadCount, normalizedThreadCount); + + if (_instance is null) + { + return; + } + + _instance.UpdateThreadPool(normalizedThreadCount); + } } public override int MaximumConcurrencyLevel => int.MaxValue; @@ -22,6 +57,22 @@ public sealed class UnboundedTaskScheduler : TaskScheduler _threadPool.Queue(task); } + private void UpdateThreadPool(int threadCount) + { + lock (_poolLock) + { + if (_threadPool.ThreadCount == threadCount) + { + return; + } + + TaskThreadPool newPool = new(threadCount, ExecuteTask); + TaskThreadPool oldPool = _threadPool; + _threadPool = newPool; + oldPool.Dispose(); + } + } + private void ExecuteTask(Task task) { TryExecuteTask(task); @@ -36,4 +87,9 @@ public sealed class UnboundedTaskScheduler : TaskScheduler { return _threadPool.GetScheduledTasks(); } + + private static int GetNormalizedThreadCount(int threadCount) + { + return threadCount < 1 ? 1 : threadCount; + } } From 752c07c57ebc2b3b03d65389880ca13e5d04f399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 00:14:36 +0200 Subject: [PATCH 08/11] Persist partial factor primality in pending entries --- .../PrimeOrderCalculator.Cpu.cs | 89 ++++++++----------- PerfectNumbers.Core/ThreadStaticPools.cs | 59 ------------ 2 files changed, 36 insertions(+), 112 deletions(-) diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs index 91078ef..8fac79f 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs @@ -10,10 +10,34 @@ namespace PerfectNumbers.Core; internal static partial class PrimeOrderCalculator { - internal readonly struct PendingEntry(ulong value, bool knownComposite) + internal readonly struct PendingEntry { - public readonly ulong Value = value; - public readonly bool KnownComposite = knownComposite; + public readonly ulong Value; + public readonly bool KnownComposite; + public readonly bool HasKnownPrimality; + public readonly bool IsPrime; + + public PendingEntry(ulong value, bool knownComposite) + { + Value = value; + KnownComposite = knownComposite; + HasKnownPrimality = knownComposite; + IsPrime = false; + } + + private PendingEntry(ulong value, bool knownComposite, bool hasKnownPrimality, bool isPrime) + { + Value = value; + KnownComposite = knownComposite; + HasKnownPrimality = hasKnownPrimality; + IsPrime = isPrime; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public PendingEntry WithPrimality(bool isPrime) + { + return new PendingEntry(Value, KnownComposite, true, isPrime); + } } private static ulong CalculateInternal(ulong prime, ulong? previousOrder, in MontgomeryDivisorData divisorData, in PrimeOrderSearchConfig config) @@ -721,44 +745,6 @@ internal static partial class PrimeOrderCalculator } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool EvaluatePrimalityPartialFactor(ref Dictionary? localCache, ulong candidate) - { - if (TryGetCachedPrimality(candidate, out bool cached)) - { - return cached; - } - - ulong globalCacheLimit = (ulong)PerfectNumberConstants.MaxQForDivisorCycles; - bool bypassesGlobalCache = candidate > globalCacheLimit; - if (bypassesGlobalCache) - { - Dictionary? cache = localCache; - if (cache is not null && cache.TryGetValue(candidate, out bool cachedValue)) - { - return cachedValue; - } - } - - bool isPrime = Open.Numeric.Primes.Prime.Numbers.IsPrime(candidate); - CachePrimalityResult(candidate, isPrime); - - if (bypassesGlobalCache) - { - Dictionary? cache = localCache; - if (cache is null) - { - cache = ThreadStaticPools.RentUlongBoolDictionary(4); - cache.Clear(); - localCache = cache; - } - - cache[candidate] = isPrime; - } - - return isPrime; - } - private static PartialFactorResult PartialFactor(ulong value, in PrimeOrderSearchConfig config) { if (value <= 1UL) @@ -795,9 +781,6 @@ internal static partial class PrimeOrderCalculator List pending = ThreadStaticPools.RentPrimeOrderPendingEntryList(2); Stack? compositeStack = null; PartialFactorResult result; - Dictionary? primalityCache = null; - - if (!gpuFactored) { // We don't need to worry about leftovers, because we always use indexes within the calculated counts @@ -862,7 +845,7 @@ internal static partial class PrimeOrderCalculator // continue; // } - bool isPrime = EvaluatePrimalityPartialFactor(ref primalityCache, composite); + bool isPrime = GetOrComputePrimality(composite); if (isPrime) { @@ -920,9 +903,14 @@ internal static partial class PrimeOrderCalculator continue; } - bool isPrime = EvaluatePrimalityPartialFactor(ref primalityCache, composite); + if (!entry.HasKnownPrimality) + { + bool isPrime = GetOrComputePrimality(composite); + entry = entry.WithPrimality(isPrime); + pending[index] = entry; + } - if (isPrime) + if (entry.IsPrime) { AddFactorToCollector(ref useDictionary, ref counts, primeSlots, exponentSlots, ref factorCount, composite, 1); } @@ -946,7 +934,7 @@ internal static partial class PrimeOrderCalculator } else { - cofactorIsPrime = EvaluatePrimalityPartialFactor(ref primalityCache, cofactor); + cofactorIsPrime = GetOrComputePrimality(cofactor); } ArrayPool pool = ThreadStaticPools.FactorEntryPool; @@ -1021,11 +1009,6 @@ internal static partial class PrimeOrderCalculator // ThreadStaticPools.IntPool.Return(exponentSlotsArray); - if (primalityCache is not null) - { - ThreadStaticPools.ReturnUlongBoolDictionary(primalityCache); - } - if (counts is not null) { ThreadStaticPools.ReturnUlongIntDictionary(counts); diff --git a/PerfectNumbers.Core/ThreadStaticPools.cs b/PerfectNumbers.Core/ThreadStaticPools.cs index 3fc47e5..33a9de2 100644 --- a/PerfectNumbers.Core/ThreadStaticPools.cs +++ b/PerfectNumbers.Core/ThreadStaticPools.cs @@ -278,65 +278,6 @@ namespace PerfectNumbers.Core pool.Add(dictionary); } - [ThreadStatic] - private static Stack>? _ulongBoolDictionaryPool; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Dictionary RentUlongBoolDictionary(int capacityHint) - { - Stack>? pool = _ulongBoolDictionaryPool; - if (pool is null) - { - pool = new Stack>(4); - _ulongBoolDictionaryPool = pool; - } - - // Keep the previous null-check implementation here for reference. - // Do not uncomment this branch; the pool is always initialized above. - // if (pool is not null && pool.Count > 0) - // { - // Dictionary dictionary = pool.Pop(); - // if (dictionary.Count < capacityHint) - // { - // dictionary.EnsureCapacity(capacityHint); - // } - // - // return dictionary; - // } - - if (pool.Count > 0) - { - Dictionary dictionary = pool.Pop(); - // Callers clear dictionaries after renting to avoid duplicate work. - // if (dictionary.Count > 0) - // { - // dictionary.Clear(); - // } - if (dictionary.Count < capacityHint) - { - dictionary.EnsureCapacity(capacityHint); - } - - return dictionary; - } - - return new Dictionary(capacityHint); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ReturnUlongBoolDictionary(Dictionary dictionary) - { - Stack>? pool = _ulongBoolDictionaryPool; - if (pool is null) - { - pool = new Stack>(4); - _ulongBoolDictionaryPool = pool; - } - - // Do not clear pooled dictionaries here; callers must clear on rent. - pool.Push(dictionary); - } - [ThreadStatic] private static Dictionary? _mersenneFactorCacheDictionary; From d99610098a6095b3c107d83f6ebd222f20bd6ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 00:41:32 +0200 Subject: [PATCH 09/11] Store Pollard Rho primality on pending entries --- PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs index 8fac79f..8f61221 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs @@ -36,7 +36,8 @@ internal static partial class PrimeOrderCalculator [MethodImpl(MethodImplOptions.AggressiveInlining)] public PendingEntry WithPrimality(bool isPrime) { - return new PendingEntry(Value, KnownComposite, true, isPrime); + bool knownComposite = KnownComposite || !isPrime; + return new PendingEntry(Value, knownComposite, true, isPrime); } } @@ -857,13 +858,13 @@ internal static partial class PrimeOrderCalculator if (currentTimestamp > deadlineTimestamp) { pollardRhoDeadlineReached = true; - pending.Add(new PendingEntry(composite, knownComposite: true)); + pending.Add(new PendingEntry(composite, knownComposite: false).WithPrimality(false)); continue; } if (!TryPollardRho(composite, deadlineTimestamp, out ulong factor)) { - pending.Add(new PendingEntry(composite, knownComposite: true)); + pending.Add(new PendingEntry(composite, knownComposite: false).WithPrimality(false)); continue; } From e9cff7bfcd94d599efaab329d9e420ae5c107d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 00:51:10 +0200 Subject: [PATCH 10/11] Reuse heuristic prime order config across runtime --- PerfectNumbers.Core/PrimeOrderCalculator.cs | 10 +++++++++- PerfectNumbers.Core/UnboundedTaskScheduler.cs | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.cs b/PerfectNumbers.Core/PrimeOrderCalculator.cs index fd2da4b..75c9cd3 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.cs @@ -10,15 +10,23 @@ internal static partial class PrimeOrderCalculator internal readonly struct PrimeOrderSearchConfig(uint smallFactorLimit, int pollardRhoMilliseconds, int maxPowChecks, PrimeOrderMode mode) { + private static PrimeOrderSearchConfig _heuristicDefault = CreateHeuristicDefault(UnboundedTaskScheduler.ConfiguredThreadCount); + public static PrimeOrderSearchConfig HeuristicDefault { get { - return CreateHeuristicDefault(UnboundedTaskScheduler.ConfiguredThreadCount); + return _heuristicDefault; } } + public static readonly PrimeOrderSearchConfig StrictDefault = new(smallFactorLimit: 1_000_000, pollardRhoMilliseconds: 0, maxPowChecks: 0, PrimeOrderMode.Strict); + public static void ConfigureHeuristicDefault(int threadCount) + { + _heuristicDefault = CreateHeuristicDefault(threadCount); + } + public static PrimeOrderSearchConfig CreateHeuristicDefault(int threadCount) { int normalizedThreadCount = threadCount < 1 ? 1 : threadCount; diff --git a/PerfectNumbers.Core/UnboundedTaskScheduler.cs b/PerfectNumbers.Core/UnboundedTaskScheduler.cs index 66912e9..cbfe5fd 100644 --- a/PerfectNumbers.Core/UnboundedTaskScheduler.cs +++ b/PerfectNumbers.Core/UnboundedTaskScheduler.cs @@ -40,6 +40,7 @@ public sealed class UnboundedTaskScheduler : TaskScheduler lock (InstanceLock) { Volatile.Write(ref _configuredThreadCount, normalizedThreadCount); + PrimeOrderCalculator.PrimeOrderSearchConfig.ConfigureHeuristicDefault(normalizedThreadCount); if (_instance is null) { From a7ed762428e0a31ecaa4cfa9b7b6b9b476d880c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Mon, 20 Oct 2025 01:07:02 +0200 Subject: [PATCH 11/11] Simplify scheduler configuration and pending composite caching --- .../PrimeOrderCalculator.Cpu.cs | 4 +- PerfectNumbers.Core/PrimeOrderCalculator.cs | 8 +-- PerfectNumbers.Core/UnboundedTaskScheduler.cs | 59 ++++++------------- 3 files changed, 23 insertions(+), 48 deletions(-) diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs index 8f61221..5148dc0 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.Cpu.cs @@ -858,13 +858,13 @@ internal static partial class PrimeOrderCalculator if (currentTimestamp > deadlineTimestamp) { pollardRhoDeadlineReached = true; - pending.Add(new PendingEntry(composite, knownComposite: false).WithPrimality(false)); + pending.Add(new PendingEntry(composite, knownComposite: true)); continue; } if (!TryPollardRho(composite, deadlineTimestamp, out ulong factor)) { - pending.Add(new PendingEntry(composite, knownComposite: false).WithPrimality(false)); + pending.Add(new PendingEntry(composite, knownComposite: true)); continue; } diff --git a/PerfectNumbers.Core/PrimeOrderCalculator.cs b/PerfectNumbers.Core/PrimeOrderCalculator.cs index 75c9cd3..d3fd7e6 100644 --- a/PerfectNumbers.Core/PrimeOrderCalculator.cs +++ b/PerfectNumbers.Core/PrimeOrderCalculator.cs @@ -1,3 +1,5 @@ +using System.Runtime.CompilerServices; + namespace PerfectNumbers.Core; internal static partial class PrimeOrderCalculator @@ -14,10 +16,8 @@ internal static partial class PrimeOrderCalculator public static PrimeOrderSearchConfig HeuristicDefault { - get - { - return _heuristicDefault; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _heuristicDefault; } public static readonly PrimeOrderSearchConfig StrictDefault = new(smallFactorLimit: 1_000_000, pollardRhoMilliseconds: 0, maxPowChecks: 0, PrimeOrderMode.Strict); diff --git a/PerfectNumbers.Core/UnboundedTaskScheduler.cs b/PerfectNumbers.Core/UnboundedTaskScheduler.cs index cbfe5fd..b552fa9 100644 --- a/PerfectNumbers.Core/UnboundedTaskScheduler.cs +++ b/PerfectNumbers.Core/UnboundedTaskScheduler.cs @@ -1,31 +1,18 @@ using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; namespace PerfectNumbers.Core; public sealed class UnboundedTaskScheduler : TaskScheduler { - private static readonly object InstanceLock = new(); private static UnboundedTaskScheduler? _instance; private static int _configuredThreadCount = Environment.ProcessorCount; - public static UnboundedTaskScheduler Instance - { - get - { - lock (InstanceLock) - { - _instance ??= new UnboundedTaskScheduler(GetNormalizedThreadCount(_configuredThreadCount)); - return _instance; - } - } - } + public static UnboundedTaskScheduler Instance => _instance ??= new UnboundedTaskScheduler(_configuredThreadCount); - public static int ConfiguredThreadCount => GetNormalizedThreadCount(Volatile.Read(ref _configuredThreadCount)); + public static int ConfiguredThreadCount => _configuredThreadCount; - private readonly object _poolLock = new(); private TaskThreadPool _threadPool; private UnboundedTaskScheduler(int threadCount) @@ -35,20 +22,15 @@ public sealed class UnboundedTaskScheduler : TaskScheduler public static void ConfigureThreadCount(int threadCount) { - int normalizedThreadCount = GetNormalizedThreadCount(threadCount); + _configuredThreadCount = threadCount; + PrimeOrderCalculator.PrimeOrderSearchConfig.ConfigureHeuristicDefault(threadCount); - lock (InstanceLock) - { - Volatile.Write(ref _configuredThreadCount, normalizedThreadCount); - PrimeOrderCalculator.PrimeOrderSearchConfig.ConfigureHeuristicDefault(normalizedThreadCount); - - if (_instance is null) - { - return; - } - - _instance.UpdateThreadPool(normalizedThreadCount); - } + // On EvenPerfectBitScanner's execution path the scheduler instance is created after configuration, + // so updating an existing pool would never run. Leave the update path disabled for now. + // if (_instance is not null) + // { + // _instance.UpdateThreadPool(threadCount); + // } } public override int MaximumConcurrencyLevel => int.MaxValue; @@ -60,18 +42,15 @@ public sealed class UnboundedTaskScheduler : TaskScheduler private void UpdateThreadPool(int threadCount) { - lock (_poolLock) + if (_threadPool.ThreadCount == threadCount) { - if (_threadPool.ThreadCount == threadCount) - { - return; - } - - TaskThreadPool newPool = new(threadCount, ExecuteTask); - TaskThreadPool oldPool = _threadPool; - _threadPool = newPool; - oldPool.Dispose(); + return; } + + TaskThreadPool newPool = new(threadCount, ExecuteTask); + TaskThreadPool oldPool = _threadPool; + _threadPool = newPool; + oldPool.Dispose(); } private void ExecuteTask(Task task) @@ -89,8 +68,4 @@ public sealed class UnboundedTaskScheduler : TaskScheduler return _threadPool.GetScheduledTasks(); } - private static int GetNormalizedThreadCount(int threadCount) - { - return threadCount < 1 ? 1 : threadCount; - } }