BitContradiction performance improvements.

This commit is contained in:
mlemanczyk committed 2026-01-20 00:39:31 +01:00
1 parent 4f12375427
commit b934112074
2 files changed
+122 -49

No files matched your search

@@ -6,33 +6,51 @@ using Open.Collections;
namespace PerfectNumbers.Core.ByDivisor; namespace PerfectNumbers.Core.ByDivisor;
// internal readonly struct PrivateWorkSet internal sealed class PrivateWorkSet
// { {
// private const int MaxSupportedBitLength = 200_000_000; // Per-thread scratch buffers to avoid per-run allocations in the hot path.
// Buffers grow on demand and are never shrunk.
public ulong[] KnownOne0 = [];
public ulong[] KnownOne1 = [];
public ulong[] KnownZero0 = [];
public ulong[] KnownZero1 = [];
public ulong[] QMaskWords = [];
public ulong[] AOneWin = [];
public ulong[] AZeroWin = [];
public int[] RunStarts = [];
public int[] RunLengths = [];
// public PrivateWorkSet() [MethodImpl(MethodImplOptions.AggressiveInlining)]
// { public void EnsureCapacity(int wordCount, int qWordCount, int runCapacity)
// int windowSize = MaxSupportedBitLength + 1; {
// QBits = new int[windowSize]; if (KnownOne0.Length < wordCount)
// int wordCount = (windowSize + 63) >> 6; {
// KnownOne0 = new ulong[wordCount]; EnsureUlong(ref KnownOne0, wordCount);
// KnownOne1 = new ulong[wordCount]; EnsureUlong(ref KnownOne1, wordCount);
// KnownZero0 = new ulong[wordCount]; EnsureUlong(ref KnownZero0, wordCount);
// KnownZero1 = new ulong[wordCount]; EnsureUlong(ref KnownZero1, wordCount);
// RunStarts = new int[windowSize]; }
// RunLengths = new int[windowSize];
// StableUnknownDelta8Cache = [];
// }
// internal readonly int[] QBits; if (QMaskWords.Length < qWordCount)
// internal readonly int[] RunStarts; {
// internal readonly int[] RunLengths; EnsureUlong(ref QMaskWords, qWordCount);
// internal readonly ulong[] KnownOne0; EnsureUlong(ref AOneWin, qWordCount);
// internal readonly ulong[] KnownOne1; EnsureUlong(ref AZeroWin, qWordCount);
// internal readonly ulong[] KnownZero0; }
// internal readonly ulong[] KnownZero1;
// internal readonly Dictionary<int, int[]> StableUnknownDelta8Cache; if (RunStarts.Length < runCapacity)
// } {
EnsureInt(ref RunStarts, runCapacity);
EnsureInt(ref RunLengths, runCapacity);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void EnsureUlong(ref ulong[] arr, int needed) => Array.Resize(ref arr, needed);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void EnsureInt(ref int[] arr, int needed) => Array.Resize(ref arr, needed);
}
/// <summary> /// <summary>
@@ -66,7 +84,7 @@ internal static class BitContradictionSolverWithAMultiplier
[ThreadStatic] private static long _statDeltaHits; [ThreadStatic] private static long _statDeltaHits;
[ThreadStatic] private static long _statDeltaBuilds; [ThreadStatic] private static long _statDeltaBuilds;
[ThreadStatic] private static long _statStableAdvanceBigShift; // ile razy columnCount>=64 w TryAdvanceStableUnknown [ThreadStatic] private static long _statStableAdvanceBigShift; // how many times columnCount>=64 in TryAdvanceStableUnknown
[ThreadStatic] private static long _statBatchEntry; [ThreadStatic] private static long _statBatchEntry;
[ThreadStatic] private static long _statBatchCols; [ThreadStatic] private static long _statBatchCols;
[ThreadStatic] private static long _statCleanChecks; [ThreadStatic] private static long _statCleanChecks;
@@ -128,6 +146,9 @@ internal static class BitContradictionSolverWithAMultiplier
[ThreadStatic] [ThreadStatic]
private static Dictionary<int, int[]>? _pow2DiffModCache; private static Dictionary<int, int[]>? _pow2DiffModCache;
[ThreadStatic]
private static PrivateWorkSet? _privateWorkSet;
private readonly struct QModCacheKey(long hash, int mod) private readonly struct QModCacheKey(long hash, int mod)
{ {
public readonly long Hash = hash; public readonly long Hash = hash;
@@ -170,9 +191,9 @@ internal static class BitContradictionSolverWithAMultiplier
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ComputeForcedRemainingAndClean( private static void ComputeForcedRemainingAndClean(
ulong[] qMask, in ulong[] qMask,
ulong[] oneWin, in ulong[] oneWin,
ulong[] zeroWin, in ulong[] zeroWin,
int wordCount, int wordCount,
out int forced, out int forced,
out int remaining, out int remaining,
@@ -188,10 +209,10 @@ internal static class BitContradictionSolverWithAMultiplier
ulong one = oneWin[i]; ulong one = oneWin[i];
ulong known = one | zeroWin[i]; ulong known = one | zeroWin[i];
ulong forcedBits = q & one; one &= q;
ulong unknownBits = q & ~known; ulong unknownBits = q & ~known;
forced += BitOperations.PopCount(forcedBits); forced += BitOperations.PopCount(one);
remaining += BitOperations.PopCount(unknownBits); remaining += BitOperations.PopCount(unknownBits);
if ((q & known) != 0) if ((q & known) != 0)
@@ -1257,9 +1278,6 @@ internal static class BitContradictionSolverWithAMultiplier
[ThreadStatic] [ThreadStatic]
private static bool _initialized; private static bool _initialized;
// [ThreadStatic]
// private static PrivateWorkSet _privateWorkSet;
public static void Initialize() public static void Initialize()
{ {
if (_initialized) if (_initialized)
@@ -1291,7 +1309,7 @@ internal static class BitContradictionSolverWithAMultiplier
out bool divides) out bool divides)
#endif #endif
{ {
// PrivateWorkSet workSet = _privateWorkSet; PrivateWorkSet workSet = _privateWorkSet ??= new PrivateWorkSet();
divides = false; divides = false;
@@ -1358,14 +1376,15 @@ internal static class BitContradictionSolverWithAMultiplier
if (windowSize < ForcedALowBits) windowSize = ForcedALowBits; if (windowSize < ForcedALowBits) windowSize = ForcedALowBits;
windowSize = (windowSize + 63) & ~63; windowSize = (windowSize + 63) & ~63;
int wordCount = (windowSize + 63) >> 6; int wordCount = (windowSize + 63) >> 6;
// var knownOne0 = workSet.KnownOne0[..wordCount]; workSet.EnsureCapacity(wordCount, qWordCount: 0, runCapacity: qOneOffsetsLength);
// var knownOne1 = workSet.KnownOne1[..wordCount]; ulong[] knownOne0 = workSet.KnownOne0;
// var knownZero0 = workSet.KnownZero0[..wordCount]; ulong[] knownOne1 = workSet.KnownOne1;
// var knownZero1 = workSet.KnownZero1[..wordCount]; ulong[] knownZero0 = workSet.KnownZero0;
ulong[] knownOne0 = new ulong[wordCount]; ulong[] knownZero1 = workSet.KnownZero1;
ulong[] knownOne1 = new ulong[wordCount]; Array.Clear(knownOne0, 0, wordCount);
ulong[] knownZero0 = new ulong[wordCount]; Array.Clear(knownOne1, 0, wordCount);
ulong[] knownZero1 = new ulong[wordCount]; Array.Clear(knownZero0, 0, wordCount);
Array.Clear(knownZero1, 0, wordCount);
int segmentBase = 0; int segmentBase = 0;
int maxKnownA = -1; int maxKnownA = -1;
int pLong = (int)p; int pLong = (int)p;
@@ -1459,6 +1478,56 @@ internal static class BitContradictionSolverWithAMultiplier
// We have learned bits up to maxFixed in the current segment. // We have learned bits up to maxFixed in the current segment.
if (maxFixed > maxKnownA) maxKnownA = maxFixed; if (maxFixed > maxKnownA) maxKnownA = maxFixed;
// ------------------------------------------------------------
// Cheap low-column prefilter (prefix-style):
// For the first N columns, all a-indices that contribute to the column are within
// the forced low-bit region (aLow). When every contributing bit is known (remaining==0),
// we can propagate carry deterministically and reject immediately on contradiction.
// This is significantly cheaper than entering the full row-aware window DP.
// ------------------------------------------------------------
{
const int PrefilterColumns = 512; // 256..512 is a good trade-off; keep <= 1024 forced bits
int preMax = PrefilterColumns - 1;
if (preMax > maxFixed) preMax = maxFixed;
if (preMax > maxAllowedA) preMax = maxAllowedA;
// Do NOT clamp to maxOffset: for columns above maxOffset the full set of q=1 positions
// still contributes, and aLow still fixes the corresponding a-bits.
// Only run if we have at least a few columns to check.
if (preMax >= 8)
{
CarryRange preCarry = CarryRange.Zero;
for (int col = 0; col <= preMax; col++)
{
// Required result bit for M_p in columns [0..p-1] is 1.
const int requiredBit = 1;
// Compute forced ones exactly: sum of a_{col - t} for all q_t=1 with t<=col.
int forced = 0;
for (int i = 0; i < qOneOffsetsLength; i++)
{
int t = qOneOffsets[i];
if (t > col) break;
int aIndex = col - t;
// For this prefilter we only run where aIndex is guaranteed within [0..maxFixed].
int w = aIndex >> 6;
ulong m64 = 1UL << (aIndex & 63);
if ((knownOne0[w] & m64) != 0) forced++;
}
// remaining==0 here, so possible==forced. Propagate carry range deterministically.
if (!TryPropagateCarry(ref preCarry, forced, forced, requiredBit))
{
#if DETAILED_LOG
reason = ContradictionReason.ParityUnreachable;
#endif
divides = false;
return true;
}
}
}
}
int topIndex0 = maxAllowedA; int topIndex0 = maxAllowedA;
int topIndex1 = maxAllowedA - 1; int topIndex1 = maxAllowedA - 1;
bool top0KnownOne = topIndex0 == 0; bool top0KnownOne = topIndex0 == 0;
@@ -1476,8 +1545,8 @@ internal static class BitContradictionSolverWithAMultiplier
// return true; // return true;
// } // }
int[] runStarts = new int[qOneOffsetsLength]; int[] runStarts = workSet.RunStarts;
int[] runLengths = new int[qOneOffsetsLength]; int[] runLengths = workSet.RunLengths;
// Span<int> runStarts = qOneOffsetsLength <= 256 ? stackalloc int[qOneOffsetsLength] : new int[qOneOffsetsLength]; // Span<int> runStarts = qOneOffsetsLength <= 256 ? stackalloc int[qOneOffsetsLength] : new int[qOneOffsetsLength];
// Span<int> runLengths = qOneOffsetsLength <= 256 ? stackalloc int[qOneOffsetsLength] : new int[qOneOffsetsLength]; // Span<int> runLengths = qOneOffsetsLength <= 256 ? stackalloc int[qOneOffsetsLength] : new int[qOneOffsetsLength];
// Span<int> runStarts = workSet.RunStarts; // Span<int> runStarts = workSet.RunStarts;
@@ -1610,7 +1679,9 @@ internal static class BitContradictionSolverWithAMultiplier
// Row-aware masks for q and a-window (supports q > 64 bits) // Row-aware masks for q and a-window (supports q > 64 bits)
int qBitLen = maxOffset + 1; int qBitLen = maxOffset + 1;
int qWordCount = (qBitLen + 63) >> 6; int qWordCount = (qBitLen + 63) >> 6;
ulong[] qMaskWords = new ulong[qWordCount]; workSet.EnsureCapacity(wordCount, qWordCount, qOneOffsetsLength);
ulong[] qMaskWords = workSet.QMaskWords;
Array.Clear(qMaskWords, 0, qWordCount);
for (int i = 0; i < qOneOffsetsLength; i++) for (int i = 0; i < qOneOffsetsLength; i++)
{ {
int t = qOneOffsets[i]; int t = qOneOffsets[i];
@@ -1619,8 +1690,10 @@ internal static class BitContradictionSolverWithAMultiplier
ulong lastWordMask = (qBitLen & 63) == 0 ? ulong.MaxValue : ((1UL << (qBitLen & 63)) - 1UL); ulong lastWordMask = (qBitLen & 63) == 0 ? ulong.MaxValue : ((1UL << (qBitLen & 63)) - 1UL);
// Sliding window over a: bit t corresponds to a_{column - t}. // Sliding window over a: bit t corresponds to a_{column - t}.
ulong[] aOneWin = new ulong[qWordCount]; ulong[] aOneWin = workSet.AOneWin;
ulong[] aZeroWin = new ulong[qWordCount]; ulong[] aZeroWin = workSet.AZeroWin;
Array.Clear(aOneWin, 0, qWordCount);
Array.Clear(aZeroWin, 0, qWordCount);
int windowColumn = 0; int windowColumn = 0;
// Initialize window for column 0: mark negative a indices as zero (t>0). Clearing is only needed when the // Initialize window for column 0: mark negative a indices as zero (t>0). Clearing is only needed when the
// arrays ever come from a pool, instead of creation. // arrays ever come from a pool, instead of creation.
@@ -4631,7 +4631,7 @@ public struct MersenneNumberDivisorByDivisorCpuTesterWithTemplate() : IMersenneN
} }
else else
{ {
var printToConsole = (_consolePrintOut++) == 10_000; var printToConsole = (_consolePrintOut++) == 1;
if (printToConsole) if (printToConsole)
{ {
// sw.Stop(); // sw.Stop();