using System.Runtime.CompilerServices; namespace PerfectNumbers.Core; public static class RleBlacklist { private static readonly object Sync = new(); private static volatile bool _loaded; private static HashSet? _patterns; public static void Load(string path) { if (string.IsNullOrWhiteSpace(path)) { return; } if (_loaded) { return; } lock (Sync) { if (_loaded) { return; } var set = new HashSet(StringComparer.Ordinal); // TODO: Replace this HashSet with the pooled ValueHashSet from the blacklist benchmarks so repeated // reloads stop allocating new buckets when refreshing the filtered pattern list. foreach (var line in File.ReadLines(path)) { if (string.IsNullOrWhiteSpace(line)) { continue; } var span = line.AsSpan().Trim(); if (span.Length == 0 || span[0] == '#') { continue; } // CSV support: Results/rle_blacklist_composite_only_end7_base100k_validated_primes1M.csv // Header: rle_pattern,label,count_in_composites_end7_le_100k,appears_in_primes_le_1M // Row example: "[1, 1, 3]",composite,12,False // We only include rows with label==composite and appears_in_primes_le_1M==False. bool handledCsv = false; int commaIdx = span.IndexOf(','); if (commaIdx > 0) { ReadOnlySpan firstField; int startNext; if (span[0] == '"') { int endQuote = span.Slice(1).IndexOf('"'); if (endQuote > 0) { endQuote += 1; // adjust to original span firstField = span.Slice(1, endQuote - 1); // move to char after closing quote, expect comma int after = endQuote + 1; if (after < span.Length && span[after] == ',') { startNext = after + 1; } else { startNext = commaIdx + 1; } } else { firstField = span.Slice(0, commaIdx); startNext = commaIdx + 1; } } else { firstField = span.Slice(0, commaIdx); startNext = commaIdx + 1; } // Parse remaining CSV fields (label, count, appears_in_primes) // label int comma2 = span.Slice(startNext).IndexOf(','); if (comma2 > 0) { ReadOnlySpan label = span.Slice(startNext, comma2).Trim(); int startCount = startNext + comma2 + 1; int comma3 = span.Slice(startCount).IndexOf(','); if (comma3 > 0) { // Read appears_in_primes flag int startFlag = startCount + comma3 + 1; ReadOnlySpan appears = span.Slice(startFlag).Trim().Trim('"'); bool composite = label.Equals("composite", StringComparison.OrdinalIgnoreCase); bool appearsInPrimes = appears.Equals("True", StringComparison.OrdinalIgnoreCase); if (composite && !appearsInPrimes) { var key = NormalizeBracketList(firstField); if (key != null) { set.Add(key); } } handledCsv = true; } } } if (handledCsv) { continue; } // Accept patterns as space/comma/dash-separated integers; optionally prefixed with firstbit|. var normalized = NormalizePattern(span); if (normalized != null) { set.Add(normalized); } } _patterns = set; _loaded = true; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsLoaded() => _loaded && _patterns != null && _patterns.Count != 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Matches(ulong p) { if (!IsLoaded()) { return false; } // TODO: Cache BuildRleKey results per divisor cycle bucket so residue scans reuse the // normalized strings; recomputing the key for every candidate shows up heavily in the RLE // blacklist profile once we cross the 138M threshold. string key = BuildRleKey(p); return _patterns!.Contains(key); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string BuildRleKey(ulong value) { // TODO: Replace this per-bit loop with the lookup-table driven builder validated in the // ModResidueTracker benchmarks; it amortizes the run detection across 8-bit blocks and cuts // the blacklist check latency roughly in half on large batches. // Build RLE from MSB to LSB runs. int bitLen = 64 - int.CreateChecked(ulong.LeadingZeroCount(value)); if (bitLen <= 0) { return ""; } Span runs = stackalloc int[64]; int runCount = 0; int currentBit = (int)((value >> (bitLen - 1)) & 1UL); int count = 1; for (int i = bitLen - 2; i >= 0; i--) { int b = (int)((value >> i) & 1UL); if (b == currentBit) { count++; } else { runs[runCount++] = count; currentBit = b; count = 1; } } runs[runCount++] = count; // Key format: first bit value; then run lengths joined by '-' // Example: for binary 101100 -> first=1, runs: 1,1,2,2 -> "1|1-1-2-2" // This preserves starting bit which some datasets may rely on. var sb = StringBuilderPool.Rent(); try { _ = sb.Append(((value >> (bitLen - 1)) & 1UL) == 0 ? '0' : '1'); _ = sb.Append('|'); for (int i = 0; i < runCount; i++) { if (i != 0) { _ = sb.Append('-'); } _ = sb.Append(runs[i]); } return sb.ToString(); } finally { StringBuilderPool.Return(sb); } } private static string? NormalizePattern(ReadOnlySpan span) { // Accept optional "firstbit|a-b-c" or just "a b c" / "a,b,c". int pipe = span.IndexOf('|'); int start = 0; char first = '1'; if (pipe >= 0) { if (pipe > 0 && (span[0] == '0' || span[0] == '1')) { first = span[0]; } start = pipe + 1; } Span tmp = stackalloc int[128]; int count = 0; int i = start; while (i <= span.Length) { // Read token until separator: space/comma/dash or end int j = i; while (j < span.Length) { char c = span[j]; if (c == ' ' || c == '\t' || c == ',' || c == ';' || c == '-') { break; } j++; } if (j > i) { // TODO: Replace int.TryParse with the Utf8Parser-based span helper so blacklist normalization avoids // repeated culture-aware integer parsing in hot loading paths. if (int.TryParse(span.Slice(i, j - i), out int v) && v > 0) { tmp[count++] = v; } else { return null; } } if (j >= span.Length) { break; } i = j + 1; } if (count == 0) { return null; } // Build normalized key var sb = StringBuilderPool.Rent(); try { _ = sb.Append(first); _ = sb.Append('|'); for (int k = 0; k < count; k++) { if (k != 0) { _ = sb.Append('-'); } _ = sb.Append(tmp[k]); } return sb.ToString(); } finally { StringBuilderPool.Return(sb); } } private static string? NormalizeBracketList(ReadOnlySpan span) { // Expect a JSON-like list: [1, 2, 3] int l = span.IndexOf('['); int r = span.LastIndexOf(']'); if (l < 0 || r <= l) { return null; } ReadOnlySpan inner = span.Slice(l + 1, r - l - 1); Span tmp = stackalloc int[128]; int count = 0; int i = 0; while (i <= inner.Length) { while (i < inner.Length && (inner[i] == ' ' || inner[i] == '\t' || inner[i] == ',')) { i++; } if (i >= inner.Length) { break; } int j = i; while (j < inner.Length && inner[j] != ',' && inner[j] != ' ' && inner[j] != '\t') { j++; } if (j > i) { // TODO: Swap this int.TryParse for the upcoming Utf8Parser fast path so nested patterns reuse the // zero-allocation integer parser during blacklist ingestion. if (int.TryParse(inner.Slice(i, j - i), out int v) && v > 0) { tmp[count++] = v; } else { return null; } } i = j + 1; } if (count == 0) { return null; } // Assume first bit is 1 (MSB is 1 for any positive number), so starting run is ones. var sb = StringBuilderPool.Rent(); try { _ = sb.Append('1'); _ = sb.Append('|'); for (int k = 0; k < count; k++) { if (k != 0) { _ = sb.Append('-'); } _ = sb.Append(tmp[k]); } return sb.ToString(); } finally { StringBuilderPool.Return(sb); } } }