From c7c5c7b81be3423ca61ea7b3a74946203f7ec16a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Bj=C3=B6rnstam?= Date: Wed, 22 Apr 2026 19:30:46 +0200 Subject: [PATCH] added proper benchmarking against others changed StandardStrategy tonuse binary search. and ao on --- PersistentMap/KeyStrategies/IntScanner.cs | 90 +++++ .../KeyStrategies/StandardStrategy.cs | 2 +- TestProject1/FuzzTestStandardStrategy.cs | 170 ++++++++ .../AgainstImmutableDict/AgainstImmutable.cs | 96 ----- .../AgainstLanguageExt/AgainstLanguageExt.cs | 198 ---------- benchmarks/AgainstLanguageExt/Cycicmap.cs | 112 ------ .../AgainstLanguageExt/integerBenchmarks.cs | 220 ----------- benchmarks/MyBenchMarks/MyBenchMarks.csproj | 19 + benchmarks/MyBenchMarks/Program.cs | 368 ++++++++++++++++++ 9 files changed, 648 insertions(+), 627 deletions(-) create mode 100644 PersistentMap/KeyStrategies/IntScanner.cs create mode 100644 TestProject1/FuzzTestStandardStrategy.cs delete mode 100644 benchmarks/AgainstImmutableDict/AgainstImmutable.cs delete mode 100644 benchmarks/AgainstLanguageExt/AgainstLanguageExt.cs delete mode 100644 benchmarks/AgainstLanguageExt/Cycicmap.cs delete mode 100644 benchmarks/AgainstLanguageExt/integerBenchmarks.cs create mode 100644 benchmarks/MyBenchMarks/MyBenchMarks.csproj create mode 100644 benchmarks/MyBenchMarks/Program.cs diff --git a/PersistentMap/KeyStrategies/IntScanner.cs b/PersistentMap/KeyStrategies/IntScanner.cs new file mode 100644 index 0000000..de7b691 --- /dev/null +++ b/PersistentMap/KeyStrategies/IntScanner.cs @@ -0,0 +1,90 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace PersistentMap; + +public static class IntScanner +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int FindFirstGreaterOrEqual(ReadOnlySpan keys, int target) + { + // Fallback for short arrays or unsupported hardware. + // AVX2 processes 8 integers at a time. + if (!Avx2.IsSupported || keys.Length < 8) + return LinearScan(keys, target); + + return Avx512F.IsSupported + ? ScanAvx512(keys, target) + : ScanAvx2(keys, target); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int LinearScan(ReadOnlySpan keys, int target) + { + for (var i = 0; i < keys.Length; i++) + if (keys[i] >= target) + return i; + return keys.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe int ScanAvx2(ReadOnlySpan keys, int target) + { + // AVX2 lacks a native GreaterOrEqual for 32-bit integers. + // We use GreaterThan(Data, target - 1). + var vTarget = Vector256.Create(target - 1); + var i = 0; + var len = keys.Length; + + for (; i <= len - 8; i += 8) + { + fixed (int* ptr = keys) + { + var vData = Avx2.LoadVector256(ptr + i); + var vResult = Avx2.CompareGreaterThan(vData, vTarget); + + // MoveMask creates a 32-bit integer from the most significant bit of each byte. + var mask = (uint)Avx2.MoveMask(vResult.AsByte()); + + if (mask != 0) + { + // Since an int is 4 bytes, MoveMask sets 4 bits per matching element. + // Dividing the trailing zero count by 4 maps the byte offset back to the integer index. + return i + (BitOperations.TrailingZeroCount(mask) / 4); + } + } + } + + return LinearScan(keys.Slice(i), target) + i; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe int ScanAvx512(ReadOnlySpan keys, int target) + { + // AVX-512 processes 16 integers (512 bits) per instruction. + var vTarget = Vector512.Create(target); + var i = 0; + var len = keys.Length; + + for (; i <= len - 16; i += 16) + { + fixed (int* ptr = keys) + { + var vData = Avx512F.LoadVector512(ptr + i); + + // Vector512 API is used directly here to cleanly get the mask + var mask = Vector512.GreaterThanOrEqual(vData, vTarget); + + if (mask != Vector512.Zero) + { + uint m = (uint)mask.ExtractMostSignificantBits(); + return i + BitOperations.TrailingZeroCount(m); + } + } + } + + return LinearScan(keys.Slice(i), target) + i; + } +} diff --git a/PersistentMap/KeyStrategies/StandardStrategy.cs b/PersistentMap/KeyStrategies/StandardStrategy.cs index 23ebfdd..77f6701 100644 --- a/PersistentMap/KeyStrategies/StandardStrategy.cs +++ b/PersistentMap/KeyStrategies/StandardStrategy.cs @@ -23,7 +23,7 @@ public readonly struct StandardStrategy : IKeyStrategy } // Tell the B-Tree to skip SIMD routing and just use LinearSearch public bool UsesPrefixes => false; - + public bool UseBinarySearch => true; // This will never be called because UsesPrefixes is false, // but we must satisfy the interface. [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/TestProject1/FuzzTestStandardStrategy.cs b/TestProject1/FuzzTestStandardStrategy.cs new file mode 100644 index 0000000..69e598f --- /dev/null +++ b/TestProject1/FuzzTestStandardStrategy.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using Xunit.Abstractions; +using PersistentMap; + +public class BTreeFuzzTestStandardStrategy +{ + private readonly ITestOutputHelper _output; + private readonly StandardStrategy _strategy = new(); + + public BTreeFuzzTestStandardStrategy(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void Fuzz_Insert_And_Remove_consistency() + { + // CONFIGURATION + const int Iterations = 100_000; // High enough to trigger all splits/merges + const int KeyRange = 5000; // Small enough to cause frequent collisions + const bool showOps = false; + int Seed = 2135974; // Environment.TickCount; + + // ORACLES + var reference = new SortedDictionary(); + var subject = BaseOrderedMap>.CreateTransient(_strategy); + + var random = new Random(Seed); + _output.WriteLine($"Starting Fuzz Test with Seed: {Seed}"); + + try + { + for (int i = 0; i < Iterations; i++) + { + // 1. Pick an Action: 70% Insert/Update, 30% Remove + bool isInsert = random.NextDouble() < 0.7; + int key = random.Next(KeyRange); + int val = key * 100; + + if (isInsert) + { + // ACTION: INSERT + if (showOps)Console.WriteLine($"insert: {key} : {val}"); + if (key == 4436) + { + Console.WriteLine("BP"); + } + + reference[key] = val; + subject.Set(key, val); + } + else + { + // ACTION: REMOVE + if (reference.ContainsKey(key)) + { + if (showOps)Console.WriteLine($"remove ${key}"); + reference.Remove(key); + subject.Remove(key); + } + else + { + // Try removing non-existent key (should be safe) + subject.Remove(key); + } + } + + // 2. VERIFY CONSISTENCY (Expensive but necessary) + // We check consistency every 1000 ops or if the tree is small, + // to keep the test fast enough. + //if (i % 1000 == 0 || reference.Count < 100) + //{ + AssertConsistency(reference, subject); + //} + } + + // Final check + AssertConsistency(reference, subject); + } + catch (Exception) + { + _output.WriteLine($"FAILED at iteration with SEED: {Seed}"); + throw; // Re-throw to fail the test + } + } + + [Fact] + public void Fuzz_Range_Queries() + { + // Validates that your Range Enumerator matches LINQ on the reference + const int Iterations = 1000; + const int KeyRange = 2000; + int Seed = Environment.TickCount; + + var reference = new SortedDictionary(); + var subject = BaseOrderedMap>.CreateTransient(_strategy); + var random = new Random(Seed); + + // Fill Data + for(int i=0; i= min + + // 1. Reference Result (LINQ) + // Note: SortedDictionary doesn't have a direct Range query, so we filter memory. + var expected = reference + .Where(kv => kv.Key >= min && kv.Key <= max) + .Select(kv => kv.Key) + .ToList(); + + // 2. Subject Result + var actual = persistent.Range(min, max) + .Select(kv => kv.Key) + .ToList(); + + // 3. Compare + if (!expected.SequenceEqual(actual)) + { + _output.WriteLine($"Range Mismatch! Range: [{min}, {max}]"); + _output.WriteLine($"Expected: {string.Join(",", expected)}"); + _output.WriteLine($"Actual: {string.Join(",", actual)}"); + Assert.Fail("Range query results differ."); + } + } + } + + private void AssertConsistency(SortedDictionary expected, TransientMap> actual) + { + // 1. Count + if (expected.Count != actual.Count) + { + Console.WriteLine("BP"); + throw new Exception($"Count Mismatch! Expected {expected.Count}, Got {actual.Count}"); + } + + // 2. Full Scan Verification + using var enumerator = actual.GetEnumerator(); + foreach (var kvp in expected) + { + if (!enumerator.MoveNext()) + { + throw new Exception("Enumerator ended too early!"); + } + + if (enumerator.Current.Key != kvp.Key || enumerator.Current.Value != kvp.Value) + { + Console.WriteLine("BP"); + throw new Exception($"Content Mismatch! Expected [{kvp.Key}:{kvp.Value}], Got [{enumerator.Current.Key}:{enumerator.Current.Value}]"); + } + } + + if (enumerator.MoveNext()) + { + throw new Exception("Enumerator has extra items!"); + } + } +} diff --git a/benchmarks/AgainstImmutableDict/AgainstImmutable.cs b/benchmarks/AgainstImmutableDict/AgainstImmutable.cs deleted file mode 100644 index 269355f..0000000 --- a/benchmarks/AgainstImmutableDict/AgainstImmutable.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Running; -using PersistentMap; - -// Ensure your PersistentMap namespace is included here -// using MyProject.Collections; - - -[MemoryDiagnoser] -public class ImmutableBenchmark -{ - [Params(10, 100)] - public int N { get; set; } - - [Params(10000)] - public int CollectionSize { get; set; } - - private ImmutableDictionary _immutableDict; - private ImmutableSortedDictionary _immutableSortedDict; - - // 1. Add field for your map - private PersistentMap _persistentMap; - - private string[] _searchKeys; - - [GlobalSetup] - public void Setup() - { - var random = new Random(42); - var data = new Dictionary(); - - while (data.Count < CollectionSize) - { - string key = GenerateRandomString(random, N); - if (!data.ContainsKey(key)) - { - data[key] = "value"; - } - } - - _immutableDict = data.ToImmutableDictionary(); - _immutableSortedDict = data.ToImmutableSortedDictionary(); - - // 2. Initialize your map. - // ASSUMPTION: Standard immutable pattern (Add returns new instance). - // Adjust if you have a bulk loader like .ToPersistentMap() or a constructor. - _persistentMap = PersistentMap.Empty(new UnicodeStrategy()); - foreach (var kvp in data) - { - _persistentMap = _persistentMap.Set(kvp.Key, kvp.Value); - } - - _searchKeys = data.Keys.ToArray(); - } - - [Benchmark(Baseline = true)] - public string ImmutableDict_Lookup() - { - var key = _searchKeys[CollectionSize / 2]; - _immutableDict.TryGetValue(key, out var value); - return value; - } - - [Benchmark] - public string ImmutableSortedDict_Lookup() - { - var key = _searchKeys[CollectionSize / 2]; - _immutableSortedDict.TryGetValue(key, out var value); - return value; - } - - // 3. Add the benchmark case - [Benchmark] - public string PersistentMap_Lookup() - { - var key = _searchKeys[CollectionSize / 2]; - // Adjust API call if your map uses a different method (e.g. Find, Get, indexer) - _persistentMap.TryGetValue(key, out var value); - return value; - } - - private string GenerateRandomString(Random rng, int length) - { - const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - var buffer = new char[length]; - for (int i = 0; i < length; i++) - { - buffer[i] = chars[rng.Next(chars.Length)]; - } - return new string(buffer); - } -} \ No newline at end of file diff --git a/benchmarks/AgainstLanguageExt/AgainstLanguageExt.cs b/benchmarks/AgainstLanguageExt/AgainstLanguageExt.cs deleted file mode 100644 index 3a221ca..0000000 --- a/benchmarks/AgainstLanguageExt/AgainstLanguageExt.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System.Collections.Immutable; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Running; -using LanguageExt; -using PersistentMap; - -namespace AgainstLanguageExt; -// Mocking your library types for the sake of the example to ensure compilation. -// Replace these with your actual namespace imports. - - -[MemoryDiagnoser] -[HideColumns("Ratio", "RatioSD", "Alloc Ratio")] -public class MapBenchmarks -{ - [Params(10, 100, 1000)] - public int KeyLength { get; set; } // Key length - - [Params(1000, 100000, 1000000)] - public int CollectionSize { get; set; } - - // Data Source - private KeyValuePair[] _data; - private string[] _searchKeys; - - private int _index = 0; - private int _mask; - - // Comparison Targets (for Lookup) - private ImmutableDictionary _sysDict; - private ImmutableSortedDictionary _sysSorted; - private LanguageExt.HashMap _langExtHash; - private LanguageExt.Map _langExtSorted; // Map is Sorted in LangExt - private PersistentMap _persistentMap; - - [GlobalSetup] - public void Setup() - { - var random = new Random(42); - var dict = new Dictionary(); - - // 1. Generate Data - while (dict.Count < CollectionSize) - { - string key = GenerateRandomString(random, KeyLength); - if (!dict.ContainsKey(key)) - { - dict[key] = random.Next(); - } - } - _data = dict.ToArray(); - _searchKeys = dict.Keys.ToArray(); - - // 2. Pre-build maps for the Lookup benchmarks - _sysDict = dict.ToImmutableDictionary(); - _sysSorted = dict.ToImmutableSortedDictionary(); - - _langExtHash = Prelude.toHashMap(dict); - _langExtSorted = Prelude.toMap(dict); - - // Build PersistentMap for lookup test - var trans = PersistentMap.Empty(new UnicodeStrategy()).ToTransient(); - foreach (var kvp in _data) - { - trans.Set(kvp.Key, kvp.Value); - } - _persistentMap = trans.ToPersistent(); - } - - // ========================================== - // BUILD BENCHMARKS - // ========================================== - - [Benchmark(Description = "Lookup: PersistentMap (Cyclic)")] - public int Lookup_Persistent_Cyclic() - { - // Fast wrap-around - var key = _searchKeys[_index++ & _mask]; - _persistentMap.TryGetValue(key, out var value); - return value; - } - - [Benchmark(Description = "Lookup: Sys.Sorted (Cyclic)")] - public int Lookup_SysSorted_Cyclic() - { - var key = _searchKeys[_index++ & _mask]; - _sysSorted.TryGetValue(key, out var value); - return value; - } - - [Benchmark(Description = "Build: Sys.ImmutableDict")] - public ImmutableDictionary Build_SysImmutable() - { - // Using CreateRange/ToImmutable is usually the standard 'bulk' build - return _data.ToImmutableDictionary(); - } - - [Benchmark(Description = "Build: LangExt.HashMap")] - public LanguageExt.HashMap Build_LangExtHash() - { - return Prelude.toHashMap(_data); - } - - [Benchmark(Description = "Build: LangExt.SortedMap")] - public LanguageExt.Map Build_LangExtSorted() - { - return Prelude.toMap(_data); - } - - [Benchmark(Description = "Build: PersistentMap (Iterative)")] - public PersistentMap Build_Persistent_Iterative() - { - // Simulating naive immutable building (O(n log n) or worse due to copying) - var map = PersistentMap.Empty(new UnicodeStrategy()); - foreach (var item in _data) - { - map = map.Set(item.Key, item.Value); - } - return map; - } - - [Benchmark(Description = "Build: PersistentMap (Transient)")] - public PersistentMap Build_Persistent_Transient() - { - // Simulating efficient mutable build -> freeze - var trans = PersistentMap.Empty(new UnicodeStrategy()).ToTransient(); - foreach (var item in _data) - { - trans.Set(item.Key, item.Value); - } - return trans.ToPersistent(); - } - - // ========================================== - // LOOKUP BENCHMARKS - // ========================================== - - [Benchmark(Baseline = true, Description = "Lookup: Sys.ImmutableDict")] - public int Lookup_SysImmutable() - { - var key = _searchKeys[CollectionSize / 2]; - _sysDict.TryGetValue(key, out var value); - return value; - } - - [Benchmark(Description = "Lookup: Sys.SortedDict")] - public int Lookup_SysSorted() - { - var key = _searchKeys[CollectionSize / 2]; - _sysSorted.TryGetValue(key, out var value); - return value; - } - - [Benchmark(Description = "Lookup: LangExt.HashMap")] - public int Lookup_LangExtHash() - { - var key = _searchKeys[CollectionSize / 2]; - // LanguageExt often uses Find which returns Option, or [] operator - // Assuming TryGetValue-like behavior or using match for fairness - return _langExtHash.Find(key).IfNone(0); - } - - [Benchmark(Description = "Lookup: LangExt.SortedMap")] - public int Lookup_LangExtSorted() - { - var key = _searchKeys[CollectionSize / 2]; - return _langExtSorted.Find(key).IfNone(0); - } - - [Benchmark(Description = "Lookup: PersistentMap")] - public int Lookup_PersistentMap() - { - var key = _searchKeys[CollectionSize / 2]; - _persistentMap.TryGetValue(key, out var value); - return value; - } - - // Helper - private string GenerateRandomString(Random rng, int length) - { - const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - var buffer = new char[length]; - for (int i = 0; i < length; i++) - { - buffer[i] = chars[rng.Next(chars.Length)]; - } - return new string(buffer); - } -} - -public class Program -{ - public static void Main(string[] args) - { - // This scans the assembly and lets the command line (args) decide what to run - BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); - } -} \ No newline at end of file diff --git a/benchmarks/AgainstLanguageExt/Cycicmap.cs b/benchmarks/AgainstLanguageExt/Cycicmap.cs deleted file mode 100644 index 0cf32e4..0000000 --- a/benchmarks/AgainstLanguageExt/Cycicmap.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Running; -using LanguageExt; -using static LanguageExt.Prelude; -using PersistentMap; - - -[MemoryDiagnoser] -[HideColumns("Ratio", "RatioSD", "Alloc Ratio")] -public class CyclicMapBenchmarks -{ - // Powers of 2 for fast bitwise masking - [Params(1024, 131072)] - public int CollectionSize { get; set; } - - [Params(10, 100, 1000)] - public int N { get; set; } - - // Collections - private PersistentMap _persistentMap; - private ImmutableSortedDictionary _sysSorted; - private LanguageExt.HashMap _langExtHash; - private LanguageExt.Map _langExtSorted; - - // Lookup scaffolding - private string[] _searchKeys; - private int _index = 0; - private int _mask; - - [GlobalSetup] - public void Setup() - { - _mask = CollectionSize - 1; - var random = new Random(42); - var data = new Dictionary(); - - // 1. Generate Data - while (data.Count < CollectionSize) - { - string key = GenerateRandomString(random, N); - if (!data.ContainsKey(key)) - { - data[key] = random.Next(); - } - } - - // 2. Build Collections - // PersistentMap - var builder = PersistentMap.Empty(new UnicodeStrategy()).ToTransient(); - foreach (var kvp in data) builder.Set(kvp.Key, kvp.Value); - _persistentMap = builder.ToPersistent(); - - // System - _sysSorted = data.ToImmutableSortedDictionary(); - - // LanguageExt - _langExtHash = toHashMap(data); - _langExtSorted = toMap(data); - - // 3. Setup Cyclic Keys - _searchKeys = data.Keys.ToArray(); - // Shuffle to defeat branch prediction / cache pre-fetching - Random.Shared.Shuffle(_searchKeys); - } - - [Benchmark(Description = "Cyclic: PersistentMap")] - public int Lookup_Persistent() - { - var key = _searchKeys[_index++ & _mask]; - _persistentMap.TryGetValue(key, out var value); - return value; - } - - [Benchmark(Description = "Cyclic: Sys.Sorted")] - public int Lookup_SysSorted() - { - var key = _searchKeys[_index++ & _mask]; - _sysSorted.TryGetValue(key, out var value); - return value; - } - - [Benchmark(Description = "Cyclic: LangExt.HashMap")] - public int Lookup_LangExtHash() - { - var key = _searchKeys[_index++ & _mask]; - // Option struct return, overhead is minimal but present - return _langExtHash.Find(key).IfNone(0); - } - - [Benchmark(Description = "Cyclic: LangExt.Sorted")] - public int Lookup_LangExtSorted() - { - var key = _searchKeys[_index++ & _mask]; - // AVL Tree traversal - return _langExtSorted.Find(key).IfNone(0); - } - - private string GenerateRandomString(Random rng, int length) - { - const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - var buffer = new char[length]; - for (int i = 0; i < length; i++) - { - buffer[i] = chars[rng.Next(chars.Length)]; - } - return new string(buffer); - } -} \ No newline at end of file diff --git a/benchmarks/AgainstLanguageExt/integerBenchmarks.cs b/benchmarks/AgainstLanguageExt/integerBenchmarks.cs deleted file mode 100644 index 4b91b17..0000000 --- a/benchmarks/AgainstLanguageExt/integerBenchmarks.cs +++ /dev/null @@ -1,220 +0,0 @@ -using System; -using System.Linq; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Running; -using LanguageExt; // Your Namespace -using LanguageExt; -using LanguageExt; -using PersistentMap; // NuGet: LanguageExt.Core - -[MemoryDiagnoser] -public class ImmutableCollectionBenchmarks -{ - [Params(100, 1000, 100_000)] - public int N; - - private int[] _keys; - private int[] _values; - - // --- 1. Your Collections --- - private PersistentMap _niceMap; - private IntStrategy _strategy; - - // --- 2. Microsoft Collections --- - private System.Collections.Immutable.ImmutableSortedDictionary _msSortedMap; - private System.Collections.Immutable.ImmutableDictionary _msHashMap; - - // --- 3. LanguageExt Collections --- - private LanguageExt.Map _leMap; // AVL Tree (Sorted) - private LanguageExt.HashMap _leHashMap; // Hash Array Mapped Trie (Unsorted) - - [GlobalSetup] - public void Setup() - { - // Generate Data - var rand = new Random(42); - _keys = Enumerable.Range(0, N).Select(x => x * 2).ToArray(); - rand.Shuffle(_keys); - _values = _keys.Select(k => k * 100).ToArray(); - _strategy = new IntStrategy(); - - // 1. Setup NiceBTree - var transient = BaseOrderedMap.CreateTransient(_strategy); - for (int i = 0; i < N; i++) transient.Set(_keys[i], _values[i]); - _niceMap = transient.ToPersistent(); - - // 2. Setup MS Immutable - var msBuilder = System.Collections.Immutable.ImmutableSortedDictionary.CreateBuilder(); - for (int i = 0; i < N; i++) msBuilder.Add(_keys[i], _values[i]); - _msSortedMap = msBuilder.ToImmutable(); - - _msHashMap = System.Collections.Immutable.ImmutableDictionary.CreateRange( - _keys.Zip(_values, (k, v) => new System.Collections.Generic.KeyValuePair(k, v))); - - // 3. Setup LanguageExt - // Note: LanguageExt performs best when bulk-loaded from tuples - var tuples = _keys.Zip(_values, (k, v) => (k, v)); - _leMap = new LanguageExt.Map(tuples); - _leHashMap = new HashMap(tuples); - } - - // ========================================================= - // 1. BUILD (Item by Item) - // Note: LanguageExt has no "Mutable Builder", so this tests - // the cost of pure immutable inserts vs your Transient/Builder. - // ========================================================= - - [Benchmark(Description = "Build: PersistentMap (Transient)")] - public int Build_NiceBTree() - { - var t = BaseOrderedMap.CreateTransient(_strategy); - for (int i = 0; i < N; i++) t.Set(_keys[i], _values[i]); - return t.Count; - } - -[Benchmark(Description = "Build: PersistentMap (Persistent)")] - public int Build_PersistentBTree() - { - var t = PersistentMap.Empty(_strategy); - for (int i = 0; i < N; i++) t.Set(_keys[i], _values[i]); - return t.Count; - } - - [Benchmark(Description = "Build: MS Sorted (Builder)")] - public int Build_MsSorted() - { - var b = System.Collections.Immutable.ImmutableSortedDictionary.CreateBuilder(); - for (int i = 0; i < N; i++) b.Add(_keys[i], _values[i]); - return b.Count; - } - - [Benchmark(Description = "Build: LanguageExt Map (AVL)")] - public int Build_LanguageExt_Map() - { - var map = LanguageExt.Map.Empty; - for (int i = 0; i < N; i++) - { - // Pure immutable add - map = map.Add(_keys[i], _values[i]); - } - return map.Count; - } - - [Benchmark(Description = "Build: LanguageExt HashMap")] - public int Build_LanguageExt_HashMap() - { - var map = LanguageExt.HashMap.Empty; - for (int i = 0; i < N; i++) - { - map = map.Add(_keys[i], _values[i]); - } - return map.Count; - } - - // ========================================================= - // 2. READ (Lookup) - // ========================================================= - - [Benchmark(Description = "Read: NiceBTree")] - public int Read_NiceBTree() - { - var found = 1; - if (_niceMap.TryGetValue(_keys[N/2], out _)) found++; - - return found; - } - - [Benchmark(Description = "Read: MS Sorted")] - public int Read_MsSorted() - { - int found = 0; - if (_msSortedMap.ContainsKey(_keys[N/2])) found++; - - return found; - } - - [Benchmark(Description = "Read: LanguageExt Map")] - public int Read_LanguageExt_Map() - { - int found = 0; - // Find returns Option, IsSome checks if it exists - if (_leMap.Find(_keys[N/2]).IsSome) found++; - - return found; - } - - [Benchmark(Description = "Read: LanguageExt HashMap")] - public int Read_LanguageExt_HashMap() - { - int found = 0; - - if (_leHashMap.Find(_keys[N/2]).IsSome) found++; - - return found; - } - - // ========================================================= - // 3. ITERATE (Foreach) - // ========================================================= - - [Benchmark(Description = "Iterate: NiceBTree")] - public int Iterate_NiceBTree() - { - int sum = 0; - foreach (var kvp in _niceMap) sum += kvp.Key; - return sum; - } - - [Benchmark(Description = "Iterate: MS Sorted")] - public int Iterate_MsSorted() - { - int sum = 0; - foreach (var kvp in _msSortedMap) sum += kvp.Key; - return sum; - } - - [Benchmark(Description = "Iterate: LanguageExt Map")] - public int Iterate_LanguageExt_Map() - { - int sum = 0; - // LanguageExt Map is IEnumerable<(Key, Value)> - foreach (var item in _leMap) sum += item.Key; - return sum; - } - - [Benchmark(Description = "Iterate: LanguageExt HashMap")] - public int Iterate_LanguageExt_HashMap() - { - int sum = 0; - foreach (var item in _leHashMap) sum += item.Key; - return sum; - } - - // ========================================================= - // 4. SET (Persistent / Immutable Update) - // ========================================================= - - [Benchmark(Description = "Set: NiceBTree")] - public PersistentMap Set_NiceBTree() - { - return _niceMap.Set(_keys[N / 2], -1); - } - - [Benchmark(Description = "Set: MS Sorted")] - public System.Collections.Immutable.ImmutableSortedDictionary Set_MsSorted() - { - return _msSortedMap.SetItem(_keys[N / 2], -1); - } - - [Benchmark(Description = "Set: LanguageExt Map")] - public LanguageExt.Map Set_LanguageExt_Map() - { - return _leMap.SetItem(_keys[N / 2], -1); - } - - [Benchmark(Description = "Set: LanguageExt HashMap")] - public LanguageExt.HashMap Set_LanguageExt_HashMap() - { - return _leHashMap.SetItem(_keys[N / 2], -1); - } -} diff --git a/benchmarks/MyBenchMarks/MyBenchMarks.csproj b/benchmarks/MyBenchMarks/MyBenchMarks.csproj new file mode 100644 index 0000000..a55ce4e --- /dev/null +++ b/benchmarks/MyBenchMarks/MyBenchMarks.csproj @@ -0,0 +1,19 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/benchmarks/MyBenchMarks/Program.cs b/benchmarks/MyBenchMarks/Program.cs new file mode 100644 index 0000000..dcad462 --- /dev/null +++ b/benchmarks/MyBenchMarks/Program.cs @@ -0,0 +1,368 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using LanguageExt; +using PersistentMap; + +namespace MapBenchmarks; + +[MemoryDiagnoser] +public class IntMapBenchmarks +{ + [Params(100, 1000, 10000, 100000)] + public int N { get; set; } + + private int[] _allKeys; + private int[] _retrieveKeys; + private int[] _updateKeys; + private int[] _removeKeys; + private int[] _mixedKeys; // Half existing, half new + + // Pre-built collections for read/update/remove tests + private ImmutableDictionary _immDict; + private ImmutableSortedDictionary _immSortedDict; + private LanguageExt.Map _extMap; + private LanguageExt.HashMap _extHashMap; + private PersistentMap _persistentMap; + + private readonly IntStrategy _intStrategy = new IntStrategy(); + + [GlobalSetup] + public void Setup() + { + var rnd = new Random(42); + + // Build integer keys (inserted sorted) + _allKeys = Enumerable.Range(0, N).ToArray(); + + int subsetSize = Math.Max(1, N / 10); + + // Subsets for different operations + var shuffled = _allKeys.OrderBy(x => rnd.Next()).ToArray(); + _retrieveKeys = shuffled.Take(subsetSize).ToArray(); + _updateKeys = shuffled.Skip(subsetSize).Take(subsetSize).ToArray(); + _removeKeys = shuffled.Skip(subsetSize * 2).Take(subsetSize).ToArray(); + + // Mixed keys: half existing, half completely new (N + 1 to N + subsetSize/2) + var existingHalf = shuffled.Skip(subsetSize * 3).Take(subsetSize / 2).ToArray(); + var newHalf = Enumerable.Range(N + 1, subsetSize - (subsetSize / 2)).ToArray(); + _mixedKeys = existingHalf.Concat(newHalf).OrderBy(x => rnd.Next()).ToArray(); + + // Pre-build collections + _immDict = ImmutableDictionary.CreateRange(_allKeys.Select(k => new KeyValuePair(k, k))); + _immSortedDict = ImmutableSortedDictionary.CreateRange(_allKeys.Select(k => new KeyValuePair(k, k))); + + _extMap = LanguageExt.Map.empty(); + _extHashMap = LanguageExt.HashMap.empty(); + foreach (var k in _allKeys) + { + _extMap = _extMap.AddOrUpdate(k, k); + _extHashMap = _extHashMap.AddOrUpdate(k, k); + } + + var transient = BaseOrderedMap.CreateTransient(_intStrategy); + foreach (var k in _allKeys) transient.Set(k, k); + _persistentMap = transient.ToPersistent(); + } + + // --- 1. BUILD --- + + [Benchmark] + public ImmutableDictionary Build_ImmDict() + { + var map = ImmutableDictionary.Empty; + foreach (var k in _allKeys) map = map.Add(k, k); + return map; + } + + [Benchmark] + public ImmutableSortedDictionary Build_ImmSortedDict() + { + var map = ImmutableSortedDictionary.Empty; + foreach (var k in _allKeys) map = map.Add(k, k); + return map; + } + + [Benchmark] + public LanguageExt.Map Build_ExtMap() + { + var map = LanguageExt.Map.empty(); + foreach (var k in _allKeys) map = map.AddOrUpdate(k, k); + return map; + } + + [Benchmark] + public LanguageExt.HashMap Build_ExtHashMap() + { + var map = LanguageExt.HashMap.empty(); + foreach (var k in _allKeys) map = map.AddOrUpdate(k, k); + return map; + } + + [Benchmark] + public PersistentMap Build_PersistentMap() + { + var map = PersistentMap.Empty(_intStrategy); + foreach (var k in _allKeys) map = map.Set(k, k); + return map; + } + + [Benchmark] + public PersistentMap Build_TransientMap() + { + var map = BaseOrderedMap.CreateTransient(_intStrategy); + foreach (var k in _allKeys) map.Set(k, k); + return map.ToPersistent(); + } + + // --- 2. RETRIEVAL --- + + [Benchmark] + public int Retrieve_ImmDict() + { + int count = 0; + foreach (var k in _retrieveKeys) + if (_immDict.TryGetValue(k, out _)) count++; + return count; + } + + [Benchmark] + public int Retrieve_ImmSortedDict() + { + int count = 0; + foreach (var k in _retrieveKeys) + if (_immSortedDict.TryGetValue(k, out _)) count++; + return count; + } + + [Benchmark] + public int Retrieve_ExtMap() + { + int count = 0; + foreach (var k in _retrieveKeys) + if (_extMap.Find(k).IsSome) count++; + return count; + } + + [Benchmark] + public int Retrieve_ExtHashMap() + { + int count = 0; + foreach (var k in _retrieveKeys) + if (_extHashMap.Find(k).IsSome) count++; + return count; + } + + [Benchmark] + public int Retrieve_PersistentMap() + { + int count = 0; + foreach (var k in _retrieveKeys) + if (_persistentMap.TryGetValue(k, out _)) count++; + return count; + } + + // --- 3. UPDATING --- + + [Benchmark] + public ImmutableDictionary Update_ImmDict() + { + var map = _immDict; + foreach (var k in _updateKeys) map = map.SetItem(k, 999); + return map; + } + +[Benchmark] + public PersistentMap Update_PersistentMap() + { + var map = _persistentMap; + foreach (var k in _updateKeys) map = map.Set(k, 999); + return map; + } + + [Benchmark] + public PersistentMap Update_TransientMap() + { + var transient = _persistentMap.ToTransient(); + foreach (var k in _updateKeys) transient.Set(k, 999); + return transient.ToPersistent(); + } + + [Benchmark] + public ImmutableSortedDictionary Update_ImmSortedDict() + { + var map = _immSortedDict; + foreach (var k in _updateKeys) map = map.SetItem(k, 999); + return map; + } + + [Benchmark] + public LanguageExt.Map Update_ExtMap() + { + var map = _extMap; + foreach (var k in _updateKeys) map = map.SetItem(k, 999); + return map; + } + + [Benchmark] + public LanguageExt.HashMap Update_ExtHashMap() + { + var map = _extHashMap; + foreach (var k in _updateKeys) map = map.SetItem(k, 999); + return map; + } + + // --- 4. UPDATE & SET (MIXED) --- + + [Benchmark] + public ImmutableDictionary UpdateSet_ImmDict() + { + var map = _immDict; + foreach (var k in _mixedKeys) map = map.SetItem(k, 999); + return map; + } + +[Benchmark] + public PersistentMap UpdateSet_PersistentMap() + { + var map = _persistentMap; + foreach (var k in _mixedKeys) map = map.Set(k, 999); + return map; + } + + [Benchmark] + public PersistentMap UpdateSet_TransientMap() + { + var transient = _persistentMap.ToTransient(); + foreach (var k in _mixedKeys) transient.Set(k, 999); + return transient.ToPersistent(); + } + + [Benchmark] + public ImmutableSortedDictionary UpdateSet_ImmSortedDict() + { + var map = _immSortedDict; + foreach (var k in _mixedKeys) map = map.SetItem(k, 999); + return map; + } + + [Benchmark] + public LanguageExt.Map UpdateSet_ExtMap() + { + var map = _extMap; + foreach (var k in _mixedKeys) map = map.AddOrUpdate(k, 999); + return map; + } + + [Benchmark] + public LanguageExt.HashMap UpdateSet_ExtHashMap() + { + var map = _extHashMap; + foreach (var k in _mixedKeys) map = map.AddOrUpdate(k, 999); + return map; + } + + // --- 5. ITERATION --- + + [Benchmark] + public int Iterate_ImmDict() + { + int sum = 0; + foreach (var kvp in _immDict) sum += kvp.Value; + return sum; + } + + [Benchmark] + public int Iterate_PersistentMap() + { + int sum = 0; + foreach (var kvp in _persistentMap) sum += kvp.Value; + return sum; + } + + [Benchmark] + public int Iterate_ImmSortedDict() + { + int sum = 0; + foreach (var kvp in _immSortedDict) sum += kvp.Value; + return sum; + } + + [Benchmark] + public int Iterate_ExtMap() + { + int sum = 0; + foreach (var kvp in _extMap) sum += kvp.Value; + return sum; + } + + [Benchmark] + public int Iterate_ExtHashMap() + { + int sum = 0; + foreach (var kvp in _extHashMap) sum += kvp.Value; + return sum; + } + + + // --- 6. REMOVAL --- + + [Benchmark] + public ImmutableDictionary Remove_ImmDict() + { + var map = _immDict; + foreach (var k in _removeKeys) map = map.Remove(k); + return map; + } + +[Benchmark] + public PersistentMap Remove_PersistentMap() + { + var map = _persistentMap; + foreach (var k in _removeKeys) map = map.Remove(k); + return map; + } + [Benchmark] + public PersistentMap Remove_TransientMap() + { + var transient = _persistentMap.ToTransient(); + foreach (var k in _removeKeys) transient.Remove(k); + return transient.ToPersistent(); + } + + [Benchmark] + public ImmutableSortedDictionary Remove_ImmSortedDict() + { + var map = _immSortedDict; + foreach (var k in _removeKeys) map = map.Remove(k); + return map; + } + + [Benchmark] + public LanguageExt.Map Remove_ExtMap() + { + var map = _extMap; + foreach (var k in _removeKeys) map = map.Remove(k); + return map; + } + + [Benchmark] + public LanguageExt.HashMap Remove_ExtHashMap() + { + var map = _extHashMap; + foreach (var k in _removeKeys) map = map.Remove(k); + return map; + } + + + + public static void Main(string[] args) + { + BenchmarkSwitcher + .FromAssembly(typeof(IntMapBenchmarks).Assembly) + .Run(args); + } +}