diff --git a/EvenPerfectBitScanner.Benchmarks/BTreeBenchmarks.cs b/EvenPerfectBitScanner.Benchmarks/BTreeBenchmarks.cs
new file mode 100644
index 0000000..5d4cdaa
--- /dev/null
+++ b/EvenPerfectBitScanner.Benchmarks/BTreeBenchmarks.cs
@@ -0,0 +1,321 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using ILGPU;
+using Open.Numeric.Primes;
+using PerfectNumbers.Core;
+
+namespace EvenPerfectBitScanner.Benchmarks;
+
+[ShortRunJob(RuntimeMoniker.Net80)]
+[MemoryDiagnoser]
+public class BTreeBenchmarks
+{
+ private ulong[] _candidates = [];
+ private int[] _candidatesInt = [];
+
+ [ParamsSource(nameof(GetBenchmarkCases))]
+ public PrimeBenchmarkCase BenchmarkCase { get; set; }
+
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ _candidates = BenchmarkCase.Candidates;
+ _candidatesInt = [.. _candidates.Select(x => (int)x)];
+ }
+
+ ///
+ /// Adds all candidate ints into the keyed B-tree (key = value), using tiny/inline and mid-size fast paths; count comes from the benchmark case or defaults to 4,096 when unspecified.
+ ///
+ [Benchmark]
+ public BTree BTreeAddInt()
+ {
+ int[] values = _candidatesInt;
+ int prime;
+ BTree result = new();
+ for (int i = 0; i < values.Length; i++)
+ {
+ prime = values[i];
+ result.Add(prime, prime);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Adds all candidate ints into a dictionary keyed by their hash code (baseline); count comes from the benchmark case or defaults to 4,096 when unspecified.
+ ///
+ [Benchmark(Baseline = true)]
+ public Dictionary DictionaryAddInt()
+ {
+ int[] values = _candidatesInt;
+ int prime;
+ Dictionary result = [];
+ for (int i = 0; i < values.Length; i++)
+ {
+ prime = values[i];
+ result.Add(prime.GetHashCode(), prime);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Adds all candidate ulongs into the keyed B-tree (key = value), using tiny/inline and mid-size fast paths; count comes from the benchmark case or defaults to 4,096 when unspecified.
+ ///
+ [Benchmark]
+ public BTree BTreeAddUlong()
+ {
+ ulong[] values = _candidates;
+ ulong prime;
+ BTree result = new();
+ for (int i = 0; i < values.Length; i++)
+ {
+ prime = values[i];
+ result.Add(prime, prime);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Adds all candidate ulongs into a dictionary keyed by their hash code; count comes from the benchmark case or defaults to 4,096 when unspecified.
+ ///
+ [Benchmark]
+ public Dictionary DictionaryAddUlong()
+ {
+ ulong[] values = _candidates;
+ ulong prime;
+ Dictionary result = [];
+ for (int i = 0; i < values.Length; i++)
+ {
+ prime = values[i];
+ result.Add(prime.GetHashCode(), prime);
+ }
+
+ return result;
+ }
+
+ public static IEnumerable GetBenchmarkCases()
+ {
+ yield return PrimeBenchmarkCase.FromRange(
+ "≤100",
+ "Admissible odd values ≤ 100 (k ≤ 5, not divisible by 5)",
+ 3UL,
+ 100UL,
+ int.MaxValue);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "4x ≤4_000_000",
+ "4x sample admissible odd values ≤ 4,000,000 (k ≤ 5, not divisible by 5)",
+ 3UL,
+ 4_000_000UL,
+ 4);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "12x ≤4_000_000",
+ "12x sample admissible odd values ≤ 4,000,000 (k ≤ 5, not divisible by 5)",
+ 3UL,
+ 4_000_000UL,
+ 12);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "16x ≤4_000_000",
+ "16x sample admissible odd values ≤ 4,000,000 (k ≤ 5, not divisible by 5)",
+ 3UL,
+ 4_000_000UL,
+ 16);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "32x ≤4_000_000",
+ "16x sample admissible odd values ≤ 4,000,000 (k ≤ 5, not divisible by 5)",
+ 3UL,
+ 4_000_000UL,
+ 32);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "64x ≤4_000_000",
+ "16x sample admissible odd values ≤ 4,000,000 (k ≤ 5, not divisible by 5)",
+ 3UL,
+ 4_000_000UL,
+ 64);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "128x ≤4_000_000",
+ "128x sample admissible odd values ≤ 4,000,000 (k ≤ 5, not divisible by 5)",
+ 3UL,
+ 4_000_000UL,
+ 128);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "256x ≤4_000_000",
+ "256x sample admissible odd values ≤ 4,000,000 (k ≤ 5, not divisible by 5)",
+ 3UL,
+ 4_000_000UL,
+ 256);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "512x ≤4_000_000",
+ "512x sample admissible odd values ≤ 4,000,000 (k ≤ 5, not divisible by 5)",
+ 3UL,
+ 4_000_000UL,
+ 512);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "1024x ≤4_000_000",
+ "1024x sample admissible odd values ≤ 4,000,000 (k ≤ 5, not divisible by 5)",
+ 3UL,
+ 4_000_000UL,
+ 1024);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "2048x ≤4_000_000",
+ "2048x sample admissible odd values ≤ 4,000,000 (k ≤ 5, not divisible by 5)",
+ 3UL,
+ 4_000_000UL,
+ 2048);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "≤4_000_000",
+ "Sampled admissible odd values ≤ 4,000,000 (k ≤ 5, not divisible by 5)",
+ 3UL,
+ 4_000_000UL,
+ 4_096);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "≥16_000_000",
+ "Sampled admissible odd values ≥ 16,000,000 (k ≤ 5, not divisible by 5)",
+ 16_000_001UL,
+ 16_500_000UL,
+ 4_096);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "≥32_000_000",
+ "Sampled admissible odd values ≥ 32,000,000 (k ≤ 5, not divisible by 5)",
+ 32_000_001UL,
+ 32_500_000UL,
+ 4_096);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "≥64_000_000",
+ "Sampled admissible odd values ≥ 64,000,000 (k ≤ 5, not divisible by 5)",
+ 64_000_001UL,
+ 64_500_000UL,
+ 4_096);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "≥100_000_000",
+ "Sampled admissible odd values ≥ 100,000,000 (k ≤ 5, not divisible by 5)",
+ 100_000_001UL,
+ 100_500_000UL,
+ 4_096);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "≥138_000_000",
+ "Sampled admissible odd values ≥ 138,000,000 (k ≤ 5, not divisible by 5)",
+ 138_000_001UL,
+ 138_500_000UL,
+ 4_096);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "≥276_000_000",
+ "Sampled admissible odd values ≥ 276,000,000 (k ≤ 5, not divisible by 5)",
+ 276_000_001UL,
+ 276_500_000UL,
+ 4_096);
+
+ yield return PrimeBenchmarkCase.FromRange(
+ "≥552_000_000",
+ "Sampled admissible odd values ≥ 552,000,000 (k ≤ 5, not divisible by 5)",
+ 552_000_001UL,
+ 552_500_000UL,
+ 4_096);
+ }
+
+ public readonly record struct PrimeBenchmarkCase(string Name, string Description, ulong[] Candidates)
+ {
+ public override string ToString()
+ {
+ return Name;
+ }
+
+ public static PrimeBenchmarkCase FromRange(
+ string name,
+ string description,
+ ulong startInclusive,
+ ulong endInclusive,
+ int maxCount)
+ {
+ ulong[] values = CollectRange(startInclusive, endInclusive, maxCount);
+ return new PrimeBenchmarkCase(name, description, values);
+ }
+ }
+
+ private static ulong[] CollectRange(ulong startInclusive, ulong endInclusive, int maxCount)
+ {
+ maxCount = maxCount < 0 ? 0 : maxCount;
+ if (maxCount == 0)
+ {
+ return [];
+ }
+
+ int estimatedLength = (int)Math.Min((ulong)maxCount, endInclusive - startInclusive + 1UL);
+ ulong[] buffer = new ulong[estimatedLength];
+ int count = 0;
+
+ for (ulong value = startInclusive; value <= endInclusive && count < maxCount; value++)
+ {
+ if (!IsAdmissibleMersenneDivisorCandidate(value))
+ {
+ continue;
+ }
+
+ if (count == buffer.Length)
+ {
+ Array.Resize(ref buffer, buffer.Length * 2);
+ }
+
+ buffer[count++] = value;
+ }
+
+ if (count == buffer.Length)
+ {
+ return buffer;
+ }
+
+ ulong[] result = new ulong[count];
+ Array.Copy(buffer, result, count);
+ return result;
+ }
+
+ private static bool IsAdmissibleMersenneDivisorCandidate(ulong value)
+ {
+ if ((value & 1UL) == 0UL || value % 5UL == 0UL || value <= 1UL)
+ {
+ return false;
+ }
+
+ ulong qMinusOne = value - 1UL;
+
+ for (ulong k = 1; k <= 5; k++)
+ {
+ ulong denominator = k << 1;
+ if (qMinusOne % denominator != 0UL)
+ {
+ continue;
+ }
+
+ ulong p = qMinusOne / denominator;
+ if (p < 2UL)
+ {
+ continue;
+ }
+
+ if (Prime.Numbers.IsPrime(p))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/PerfectNumbers.Core/BTree.cs b/PerfectNumbers.Core/BTree.cs
new file mode 100644
index 0000000..69c5b9b
--- /dev/null
+++ b/PerfectNumbers.Core/BTree.cs
@@ -0,0 +1,271 @@
+using System.Runtime.CompilerServices;
+
+namespace PerfectNumbers.Core;
+
+[method: MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+public sealed class BTree()
+{
+ private const int HashByteCount = sizeof(int);
+ private const int DefaultCapacity = 64;
+ private const int SmallBufferCapacity = 4;
+ private const int RootInlineThreshold = 2048;
+ private const int RootInlineInitialCapacity = 64;
+ private const int ShallowTreeThreshold = 1024;
+ private const int ShallowHashByteCount = 1;
+ private const int LeafInitialCapacity = 8;
+ private static readonly EqualityComparer _defaultComparer = EqualityComparer.Default;
+
+ private readonly TKey[] _smallKeys = new TKey[SmallBufferCapacity];
+ private readonly TValue[] _smallValues = new TValue[SmallBufferCapacity];
+ private int _smallCount;
+
+ private Node[] _nodes = [];
+ private int _nodeCount;
+
+ public int Count;
+
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
+ public void Add(TKey key, TValue value)
+ {
+ TKey[] smallKeys = _smallKeys;
+ TValue[] smallValues = _smallValues;
+ int smallCount = _smallCount;
+ if (smallCount < SmallBufferCapacity)
+ {
+ smallKeys[smallCount] = key;
+ smallValues[smallCount] = value;
+ _smallCount = smallCount + 1;
+ Count++;
+ return;
+ }
+
+ EnsureRoot();
+
+ Node[] nodes = _nodes;
+ if (smallCount > 0)
+ {
+ ref Node rootNode = ref nodes[0];
+ for (int i = 0; i < smallCount; i++)
+ {
+ rootNode.AddKeyValue(smallKeys[i], smallValues[i], RootInlineInitialCapacity);
+ }
+
+ _smallCount = 0;
+ }
+
+ if (Count < RootInlineThreshold)
+ {
+ ref Node root = ref nodes[0];
+ root.AddKeyValue(key, value, RootInlineInitialCapacity);
+ Count++;
+ return;
+ }
+
+ InsertIntoTree(GetHash(key), key, value, Count);
+ Count++;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
+ public int CopyTo(Span destination)
+ {
+ ArgumentOutOfRangeException.ThrowIfLessThan(destination.Length, Count);
+
+ var written = 0;
+
+ int smallCount = _smallCount;
+ TValue[] smallValues = _smallValues;
+ for (int i = 0; i < smallCount; i++)
+ {
+ destination[written++] = smallValues[i];
+ }
+
+ int nodeCount = _nodeCount;
+ if (nodeCount == 0)
+ {
+ return written;
+ }
+
+ FixedCapacityStack stack = new(nodeCount);
+ stack.Push(0);
+
+ Node[]? nodes = _nodes;
+ while (stack.Count > 0)
+ {
+ int index = stack.Pop();
+ ref readonly Node node = ref nodes[index];
+ written += node.CopyValues(destination[written..]);
+
+ for (int childIndex = 0; childIndex < Node.ChildCount; childIndex++)
+ {
+ index = node.GetChild(childIndex);
+ if (index != -1)
+ {
+ stack.Push(index);
+ }
+ }
+ }
+
+ return written;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ private static uint GetHash(TKey key)
+ {
+ return key is not null
+ ? unchecked((uint)_defaultComparer.GetHashCode(key))
+ : 0u;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ private void EnsureRoot()
+ {
+ if (_nodeCount != 0)
+ {
+ return;
+ }
+
+ _nodes = new Node[DefaultCapacity];
+ _nodes[0] = new Node();
+ _nodeCount = 1;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveOptimization)]
+ private void InsertIntoTree(uint hash, TKey key, TValue value, int currentCount)
+ {
+ var nodes = _nodes;
+ var currentIndex = 0;
+ int depth = currentCount < ShallowTreeThreshold ? ShallowHashByteCount : HashByteCount;
+
+ int nodeCount = _nodeCount;
+ for (int i = 0; i < depth; i++)
+ {
+ ref Node node = ref nodes[currentIndex];
+ int bucket = (byte)(hash >> (i << 3)) >> 5;
+ int nextIndex = node.GetChild(bucket);
+ if (nextIndex == -1)
+ {
+ nextIndex = nodeCount;
+ nodes = EnsureNodeCapacity(nodes, nextIndex, nodeCount);
+ node = ref nodes[currentIndex];
+ nodes[nextIndex] = new Node();
+ node.SetChild(bucket, nextIndex);
+ nodeCount++;
+ }
+
+ currentIndex = nextIndex;
+ }
+
+ _nodes = nodes;
+ _nodeCount = nodeCount;
+ ref Node leaf = ref nodes[currentIndex];
+ leaf.AddKeyValue(key, value, LeafInitialCapacity);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ private static Node[] EnsureNodeCapacity(in Node[] nodes, int requiredIndex, int liveNodeCount)
+ {
+ int currentLength = nodes.Length;
+ if (requiredIndex < currentLength)
+ {
+ return nodes;
+ }
+
+ int targetLength = currentLength == 0 ? DefaultCapacity : currentLength;
+ while (targetLength <= requiredIndex)
+ {
+ targetLength <<= 1;
+ }
+
+ Node[] newNodes = new Node[targetLength];
+ if (liveNodeCount > currentLength)
+ {
+ liveNodeCount = currentLength;
+ }
+
+ Array.Copy(nodes, newNodes, liveNodeCount);
+ return newNodes;
+ }
+
+ private struct Node
+ {
+ public const int ChildCount = 8;
+
+ private TKey[]? _keys;
+ private TValue[]? _values;
+ private int _entryCount;
+ private int[]? _children;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ public readonly int GetChild(int slot)
+ {
+ int[]? children = _children;
+ return children == null ? -1 : children[slot];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ public void SetChild(int slot, int index)
+ {
+ int[]? children = _children;
+ if (children == null)
+ {
+ children = new int[ChildCount];
+ for (int i = 0; i < ChildCount; i++)
+ {
+ children[i] = -1;
+ }
+
+ _children = children;
+ }
+
+ children[slot] = index;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ public void AddKeyValue(TKey key, TValue value, int desiredInitialCapacity)
+ {
+ TKey[]? keys = _keys;
+ TValue[]? values = _values;
+ int entryCount = _entryCount;
+
+ int newLength;
+ if (keys == null)
+ {
+ newLength = desiredInitialCapacity < 4 ? 4 : desiredInitialCapacity;
+ keys = new TKey[newLength];
+ values = new TValue[newLength];
+ _keys = keys;
+ _values = values;
+ }
+ else if (entryCount == keys.Length)
+ {
+ newLength = keys.Length << 1;
+ TKey[] newKeys = new TKey[newLength];
+ TValue[] newValues = new TValue[newLength];
+ new Span(keys, 0, entryCount).CopyTo(newKeys);
+ new Span(values, 0, entryCount).CopyTo(newValues);
+ keys = newKeys;
+ values = newValues;
+ _keys = newKeys;
+ _values = newValues;
+ }
+
+ keys[entryCount] = key;
+ values![entryCount] = value;
+ _entryCount = entryCount + 1;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ public readonly int CopyValues(Span destination)
+ {
+ TValue[]? values = _values;
+ if (values == null)
+ {
+ return 0;
+ }
+
+ int entryCount = _entryCount;
+ new Span(values, 0, entryCount).CopyTo(destination);
+ return entryCount;
+ }
+ }
+}