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; + } + } +}