From d9f3565622946bfa3385737d3bd2cebbcda30546 Mon Sep 17 00:00:00 2001 From: dadhi Date: Wed, 4 Jun 2025 11:58:13 +0200 Subject: [PATCH 01/54] rename old SmallMap to HSmallMap for bm with new SmallMap from FEC --- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 2 +- .../MemoryOwnerVsHashMap.cs | 4 +- playground/ImTools.Benchmarks/Program.cs | 4 +- src/ImTools/ImTools.cs | 122 +++++++++--------- .../Playground/MapSlimTests.cs | 2 +- test/ImTools.UnitTests/SmallMapTests.cs | 60 ++++----- 6 files changed, 97 insertions(+), 97 deletions(-) diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 48dcf60d..20396520 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -13,7 +13,7 @@ using System.Runtime.CompilerServices; using FHashMap91TypeString = ImTools.Experiments.FHashMap91, ImTools.Experiments.FHashMap91.SingleArrayEntries>>; -using SmallMapTypeString = ImTools.SmallMap, ImTools.SmallMap.SingleArrayEntries>>; +using SmallMapTypeString = ImTools.HSmallMap, ImTools.HSmallMap.SingleArrayEntries>>; using FHashMapTypeString = FastExpressionCompiler.ImTools.FHashMap, FastExpressionCompiler.ImTools.FHashMap.SingleArrayEntries>>; #nullable disable diff --git a/playground/ImTools.Benchmarks/MemoryOwnerVsHashMap.cs b/playground/ImTools.Benchmarks/MemoryOwnerVsHashMap.cs index b3909963..fa3a7b78 100644 --- a/playground/ImTools.Benchmarks/MemoryOwnerVsHashMap.cs +++ b/playground/ImTools.Benchmarks/MemoryOwnerVsHashMap.cs @@ -44,7 +44,7 @@ public class MemoryOwnerVsHashMap MemoryOwner MemoryOwner; ImHashMap ImHashMap; - SmallMap> SmallMap; + HSmallMap> SmallMap; [GlobalSetup] public void Setup() @@ -84,7 +84,7 @@ public object ImTools_populate() [Benchmark] public object SmallMap_populate() { - var imt = ImTools.SmallMap.New(); + var imt = ImTools.HSmallMap.New(); for (int i = 0; i < Count; i++) { // todo: @wip add AddSureNotPresent (use FHashMap for the reference) diff --git a/playground/ImTools.Benchmarks/Program.cs b/playground/ImTools.Benchmarks/Program.cs index 89a6ea8d..1101271a 100644 --- a/playground/ImTools.Benchmarks/Program.cs +++ b/playground/ImTools.Benchmarks/Program.cs @@ -6,7 +6,7 @@ class Program { static void Main() { - BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); // new SIO.Program().Run(); @@ -37,7 +37,7 @@ static void Main() //BenchmarkRunner.Run(); //BenchmarkRunner.Run(); - + //BenchmarkRunner.Run(); //BenchmarkRunner.Run(); // BenchmarkRunner.Run(); diff --git a/src/ImTools/ImTools.cs b/src/ImTools/ImTools.cs index 2d3400c5..730fdfd4 100644 --- a/src/ImTools/ImTools.cs +++ b/src/ImTools/ImTools.cs @@ -40,7 +40,7 @@ namespace ImTools; using System.Runtime.InteropServices; #endif -using static SmallMap; +// using static SmallMap; #nullable disable @@ -6830,7 +6830,7 @@ public static S ForEach(this ImHashMap[] parts, S state, ActionConfiguration and the tools for the SmallMap and friends -public static class SmallMap +public static class HSmallMap { internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) internal const byte MinCapacityBits = 3; // 1 << 3 == 8 @@ -6843,15 +6843,15 @@ public static class SmallMap /// Creates the map with the storage [MethodImpl((MethodImplOptions)256)] - public static SmallMap> New(byte capacityBitShift = 0) + public static HSmallMap> New(byte capacityBitShift = 0) where TEq : struct, IEq => - new SmallMap>(capacityBitShift); + new HSmallMap>(capacityBitShift); /// Creates the map with the storage [MethodImpl((MethodImplOptions)256)] - public static SmallMap> NewChunked(byte capacityBitShift = 0) + public static HSmallMap> NewChunked(byte capacityBitShift = 0) where TEq : struct, IEq => - new SmallMap>(capacityBitShift); + new HSmallMap>(capacityBitShift); /// Holds a single entry consisting of key and value. /// Value may be set or changed but the key is set in stone (by construction). @@ -6873,7 +6873,7 @@ public Entry(K key, V value) } /// Converts the packed hashes and entries into the human readable info for debugging visualization - public static DebugHashItem[] Explain(this SmallMap map) + public static DebugHashItem[] Explain(this HSmallMap map) where TEq : struct, IEq where TEntries : struct, IEntries { @@ -7214,8 +7214,8 @@ public class DebugProxy where TEq : struct, IEq where TEntries : struct, IEntries { - private readonly SmallMap _map; - internal DebugProxy(SmallMap map) => _map = map; + private readonly HSmallMap _map; + internal DebugProxy(HSmallMap map) => _map = map; public DebugHashItem[] PackedHashes => _map.Explain(); public TEntries Entries => _map.Entries; } @@ -7241,14 +7241,14 @@ public class DebugProxy /// For instance, for the `RefEq` the tombstone is . You may redefine it in the `IEq{K}.GetTombstone()` implementation. /// /// -[DebuggerTypeProxy(typeof(DebugProxy<,,,>))] +[DebuggerTypeProxy(typeof(HSmallMap.DebugProxy<,,,>))] [DebuggerDisplay("Count={Count}")] -public struct SmallMap : IReadOnlyCollection> +public struct HSmallMap : IReadOnlyCollection> where TEq : struct, IEq - where TEntries : struct, IEntries + where TEntries : struct, HSmallMap.IEntries { #if DEBUG - ProbesTracker _dbg; + HSmallMap.ProbesTracker _dbg; #endif private int _indexMask; @@ -7277,7 +7277,7 @@ public struct SmallMap : IReadOnlyCollection> public TEntries Entries => _entries; /// Capacity calculates as `1 leftShift capacityBitShift` - public SmallMap(byte capacityBitShift) + public HSmallMap(byte capacityBitShift) { _indexMask = (1 << capacityBitShift) - 1; @@ -7297,7 +7297,7 @@ public bool TryGetValue(K key, out V value) var hash = default(TEq).GetHashCode(key); var indexMask = _indexMask; - var hashMiddleMask = HashAndIndexMask & ~indexMask; + var hashMiddleMask = HSmallMap.HashAndIndexMask & ~indexMask; var hashMiddle = hash & hashMiddleMask; var hashIndex = hash & indexMask; @@ -7307,14 +7307,14 @@ public bool TryGetValue(K key, out V value) var hashesAndIndexes = _packedHashesAndIndexes; #endif - var h = GetHash(ref hashesAndIndexes, hashIndex); + var h = HSmallMap.GetHash(ref hashesAndIndexes, hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var probes = 1; - while ((h >>> ProbeCountShift) >= probes) + while ((h >>> HSmallMap.ProbeCountShift) >= probes) { // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) + if (((h >>> HSmallMap.ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) { ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); if (default(TEq).Equals(e.Key, key)) @@ -7324,7 +7324,7 @@ public bool TryGetValue(K key, out V value) } } - h = GetHash(ref hashesAndIndexes, ++hashIndex & indexMask); + h = HSmallMap.GetHash(ref hashesAndIndexes, ++hashIndex & indexMask); ++probes; } } @@ -7349,7 +7349,7 @@ public int GetEntryIndex(K key) var hash = default(TEq).GetHashCode(key); var indexMask = _indexMask; - var hashMiddleMask = HashAndIndexMask & ~indexMask; + var hashMiddleMask = HSmallMap.HashAndIndexMask & ~indexMask; var hashMiddle = hash & hashMiddleMask; var hashIndex = hash & indexMask; @@ -7359,21 +7359,21 @@ public int GetEntryIndex(K key) var hashesAndIndexes = _packedHashesAndIndexes; #endif - var h = GetHash(ref hashesAndIndexes, hashIndex); + var h = HSmallMap.GetHash(ref hashesAndIndexes, hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var probes = 1; - while ((h >>> ProbeCountShift) >= probes) + while ((h >>> HSmallMap.ProbeCountShift) >= probes) { // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) + if (((h >>> HSmallMap.ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) { ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); if (default(TEq).Equals(e.Key, key)) return h & indexMask; } - h = GetHash(ref hashesAndIndexes, ++hashIndex & indexMask); + h = HSmallMap.GetHash(ref hashesAndIndexes, ++hashIndex & indexMask); ++probes; } } @@ -7399,10 +7399,10 @@ public ref V GetOrAddValueRef(K key) var entryCount = _entries.GetCount(); // if the free space is less than 1/8 of capacity (12.5%) then Resize - if (indexMask - entryCount <= (indexMask >>> MinFreeCapacityShift)) + if (indexMask - entryCount <= (indexMask >>> HSmallMap.MinFreeCapacityShift)) indexMask = ResizeHashes(indexMask); - var hashMiddleMask = HashAndIndexMask & ~indexMask; // todo: @perf put in its own field to avoid the redundant calculation + var hashMiddleMask = HSmallMap.HashAndIndexMask & ~indexMask; // todo: @perf put in its own field to avoid the redundant calculation var hashMiddle = hash & hashMiddleMask; var hashIndex = hash & indexMask; @@ -7411,14 +7411,14 @@ public ref V GetOrAddValueRef(K key) #else var hashesAndIndexes = _packedHashesAndIndexes; #endif - ref var h = ref GetHashRef(ref hashesAndIndexes, hashIndex); + ref var h = ref HSmallMap.GetHashRef(ref hashesAndIndexes, hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var probes = 1; - while ((h >>> ProbeCountShift) >= probes) + while ((h >>> HSmallMap.ProbeCountShift) >= probes) { // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) + if (((h >>> HSmallMap.ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) { ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); #if DEBUG @@ -7427,33 +7427,33 @@ public ref V GetOrAddValueRef(K key) if (default(TEq).Equals(e.Key, key)) return ref e.Value; } - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); ++probes; } // 3. We did not find the hash and therefore the key, so insert the new entry var hRobinHooded = h; - h = (probes << ProbeCountShift) | hashMiddle | entryCount; + h = (probes << HSmallMap.ProbeCountShift) | hashMiddle | entryCount; #if DEBUG _dbg.DebugCollectAndOutputProbes(probes, "Add"); #endif // 4. If the robin hooded hash is empty then we stop // 5. Otherwise we steal the slot with the smaller probes - probes = hRobinHooded >>> ProbeCountShift; + probes = hRobinHooded >>> HSmallMap.ProbeCountShift; while (hRobinHooded != 0) { - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); - if ((h >>> ProbeCountShift) < ++probes) + h = ref HSmallMap.GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + if ((h >>> HSmallMap.ProbeCountShift) < ++probes) { #if DEBUG if (h != 0) - _dbg.RemoveProbes(h >>> ProbeCountShift); + _dbg.RemoveProbes(h >>> HSmallMap.ProbeCountShift); _dbg.DebugCollectAndOutputProbes(probes, "Add-RH"); #endif var tmp = h; - h = (probes << ProbeCountShift) | (hRobinHooded & HashAndIndexMask); + h = (probes << HSmallMap.ProbeCountShift) | (hRobinHooded & HSmallMap.HashAndIndexMask); hRobinHooded = tmp; - probes = hRobinHooded >>> ProbeCountShift; + probes = hRobinHooded >>> HSmallMap.ProbeCountShift; } } return ref _entries.AppendEntryAndGetValueRef(key); @@ -7471,7 +7471,7 @@ public bool TryRemove(K key) var hash = default(TEq).GetHashCode(key); var indexMask = _indexMask; - var hashMiddleMask = ~indexMask & HashAndIndexMask; + var hashMiddleMask = ~indexMask & HSmallMap.HashAndIndexMask; var hashMiddle = hash & hashMiddleMask; var hashIndex = hash & indexMask; @@ -7480,16 +7480,16 @@ public bool TryRemove(K key) #else var hashesAndIndexes = _packedHashesAndIndexes; #endif - ref var h = ref GetHashRef(ref hashesAndIndexes, hashIndex); + ref var h = ref HSmallMap.GetHashRef(ref hashesAndIndexes, hashIndex); var removed = false; // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var probes = 1; - while ((h >>> ProbeCountShift) >= probes) + while ((h >>> HSmallMap.ProbeCountShift) >= probes) { // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) + if (((h >>> HSmallMap.ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) { ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); if (default(TEq).Equals(e.Key, key)) @@ -7503,7 +7503,7 @@ public bool TryRemove(K key) break; } } - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); ++probes; } @@ -7511,16 +7511,16 @@ public bool TryRemove(K key) return false; ref var emptied = ref h; - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); // move the next hash into the emptied slot until the next hash is empty or ideally positioned (hash is 0 or probe is 1) - while ((h >>> ProbeCountShift) > 1) + while ((h >>> HSmallMap.ProbeCountShift) > 1) { - emptied = (((h >>> ProbeCountShift) - 1) << ProbeCountShift) | (h & HashAndIndexMask); // decrease the probe count by one cause we moving the hash closer to the ideal index + emptied = (((h >>> HSmallMap.ProbeCountShift) - 1) << HSmallMap.ProbeCountShift) | (h & HSmallMap.HashAndIndexMask); // decrease the probe count by one cause we moving the hash closer to the ideal index h = 0; emptied = ref h; - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); } return true; } @@ -7529,16 +7529,16 @@ internal int ResizeHashes(int indexMask) { if (indexMask == 0) { - _indexMask = (1 << MinCapacityBits) - 1; - _packedHashesAndIndexes = new int[1 << MinCapacityBits]; + _indexMask = (1 << HSmallMap.MinCapacityBits) - 1; + _packedHashesAndIndexes = new int[1 << HSmallMap.MinCapacityBits]; #if DEBUG Debug.WriteLine($"[ResizeHashes] new empty hashes {1} -> {_packedHashesAndIndexes.Length}"); #endif - return (1 << MinCapacityBits) - 1; + return (1 << HSmallMap.MinCapacityBits) - 1; } var oldCapacity = indexMask + 1; - var newHashAndIndexMask = HashAndIndexMask & ~oldCapacity; + var newHashAndIndexMask = HSmallMap.HashAndIndexMask & ~oldCapacity; var newIndexMask = (indexMask << 1) | 1; var newHashesAndIndexes = new int[oldCapacity << 1]; @@ -7555,8 +7555,8 @@ internal int ResizeHashes(int indexMask) // Overflow segment is wrapped-around hashes // and! the hashes at the beginning robin hooded by the wrapped-around hashes var i = 0; - while ((oldHash >>> ProbeCountShift) > 1) - oldHash = GetHash(ref oldHashes, ++i); + while ((oldHash >>> HSmallMap.ProbeCountShift) > 1) + oldHash = HSmallMap.GetHash(ref oldHashes, ++i); var oldCapacityWithOverflowSegment = i + oldCapacity; while (true) @@ -7564,24 +7564,24 @@ internal int ResizeHashes(int indexMask) if (oldHash != 0) { // get the new hash index from the old one with the next bit equal to the `oldCapacity` - var indexWithNextBit = (oldHash & oldCapacity) | (((i + 1) - (oldHash >>> ProbeCountShift)) & indexMask); + var indexWithNextBit = (oldHash & oldCapacity) | (((i + 1) - (oldHash >>> HSmallMap.ProbeCountShift)) & indexMask); // no need for robin-hooding because we already did it for the old hashes and // now just sparcing the hashes into the new array which are already in order var probes = 1; - ref var newHash = ref GetHashRef(ref newHashes, indexWithNextBit); + ref var newHash = ref HSmallMap.GetHashRef(ref newHashes, indexWithNextBit); while (newHash != 0) { - newHash = ref GetHashRef(ref newHashes, ++indexWithNextBit & newIndexMask); + newHash = ref HSmallMap.GetHashRef(ref newHashes, ++indexWithNextBit & newIndexMask); ++probes; } - newHash = (probes << ProbeCountShift) | (oldHash & newHashAndIndexMask); + newHash = (probes << HSmallMap.ProbeCountShift) | (oldHash & newHashAndIndexMask); } if (++i >= oldCapacityWithOverflowSegment) break; - oldHash = GetHash(ref oldHashes, i & indexMask); + oldHash = HSmallMap.GetHash(ref oldHashes, i & indexMask); } #if DEBUG Debug.WriteLine($"[ResizeHashes] {oldCapacity} -> {newHashesAndIndexes.Length}"); @@ -7597,16 +7597,16 @@ internal int ResizeHashes(int indexMask) public Enumerator GetEnumerator() => new(_entries); // prevents the boxing of the enumerator struct /// - IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// Enumerator of the entries in the order of their addition to the map - public struct Enumerator : IEnumerator> + public struct Enumerator : IEnumerator> { private int _index; - private Entry _current; + private HSmallMap.Entry _current; private readonly TEntries _entries; private int _countIncludingRemoved; internal Enumerator(TEntries entries) @@ -7638,7 +7638,7 @@ public bool MoveNext() } /// - public Entry Current => _current; + public HSmallMap.Entry Current => _current; object IEnumerator.Current => _current; void IEnumerator.Reset() diff --git a/test/ImTools.UnitTests/Playground/MapSlimTests.cs b/test/ImTools.UnitTests/Playground/MapSlimTests.cs index 415931bb..ff3d700d 100644 --- a/test/ImTools.UnitTests/Playground/MapSlimTests.cs +++ b/test/ImTools.UnitTests/Playground/MapSlimTests.cs @@ -126,7 +126,7 @@ public void MapSlim_Performance_IndexOf() public void MapSlim_Performance_Increment() { Gen.Int[0, 255].Array - .Select(a => (a, SmallMap.New(), new Dictionary())) + .Select(a => (a, HSmallMap.New(), new Dictionary())) .Faster( (items, map, _) => { diff --git a/test/ImTools.UnitTests/SmallMapTests.cs b/test/ImTools.UnitTests/SmallMapTests.cs index 0486c8a4..78d3d6f1 100644 --- a/test/ImTools.UnitTests/SmallMapTests.cs +++ b/test/ImTools.UnitTests/SmallMapTests.cs @@ -10,7 +10,7 @@ namespace ImTools.Experiments.UnitTests; -using static SmallMap; +using static HSmallMap; [TestFixture] public class SmallMapTests @@ -34,7 +34,7 @@ public void Real_world_test_AddOrUpdate() { var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(100).ToArray(); - var map = SmallMap.New>(); + var map = HSmallMap.New>(); foreach (var key in types) map.AddOrUpdate(key, "a"); @@ -52,7 +52,7 @@ public void Real_world_test_AddOrUpdate_NO_Resize() { var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(100).ToArray(); - var map = SmallMap.New>(8); + var map = HSmallMap.New>(8); foreach (var key in types) map.AddOrUpdate(key, "a"); @@ -69,7 +69,7 @@ public void Real_world_test_with_TryRemove_from_1000_items() { var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(1000).ToArray(); - var map = SmallMap.New>(); + var map = HSmallMap.New>(); foreach (var key in types) map.AddOrUpdate(key, "a"); @@ -88,7 +88,7 @@ public void Real_world_test_with_TryRemove_from_1000_items_TypeEq() { var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(1000).ToArray(); - var map = SmallMap.NewChunked>(); + var map = HSmallMap.NewChunked>(); foreach (var key in types) map.AddOrUpdate(key, "a"); @@ -109,7 +109,7 @@ public void Real_world_test_with_Enumerator_and_TryRemove_the_entries() var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(count).ToList(); Assert.AreEqual(count, types.Count); - var map = SmallMap.New>(); + var map = HSmallMap.New>(); foreach (var key in types) map.AddOrUpdate(key, "a"); @@ -144,7 +144,7 @@ public void Real_world_test_with_Enumerator_and_TryRemove_the_entries_ChunkedEnt var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(count).ToList(); Assert.AreEqual(count, types.Count); - var map = SmallMap.NewChunked>(); + var map = HSmallMap.NewChunked>(); foreach (var key in types) map.AddOrUpdate(key, "a"); @@ -175,7 +175,7 @@ public void Real_world_test_with_Enumerator_and_TryRemove_the_entries_ChunkedEnt [Test] public void Simplified_test_with_equal_hashes_RefEq() { - var map = SmallMap.New>(); + var map = HSmallMap.New>(); var keys = new[] { typeof(Tuple<>), typeof(Tuple<,>), typeof(Tuple<,,>) }; var i = 1; @@ -193,7 +193,7 @@ public void Simplified_test_with_equal_hashes_RefEq() [Test] public void Can_store_and_retrieve_value_from_map() { - var map = SmallMap.New(2); + var map = HSmallMap.New(2); map.AddOrUpdate(42, "1"); map.AddOrUpdate(42 + 32, "2"); @@ -280,7 +280,7 @@ [ResizeHashes] 8 -> 16 [Test] public void Can_store_and_retrieve_value_from_map_Golden() { - var map = SmallMap.New(2); + var map = HSmallMap.New(2); // var map = SmallMap.New(2); map.AddOrUpdate(42, "1"); @@ -332,7 +332,7 @@ public void Can_store_and_retrieve_value_from_map_Golden() [Test] public void Can_lookup_the_default_map_without_error() { - SmallMap> map = default; + HSmallMap> map = default; Assert.IsFalse(map.TryGetValue(42, out _)); } @@ -340,7 +340,7 @@ public void Can_lookup_the_default_map_without_error() [Test] public void Can_store_and_retrieve_value_from_map_with_Expand_in_the_middle() { - var map = SmallMap.New(1); + var map = HSmallMap.New(1); Assert.IsFalse(map.TryGetValue(42, out _)); @@ -378,7 +378,7 @@ public void Can_store_and_retrieve_value_from_map_with_Expand_in_the_middle() [Test] public void Can_resize_without_moving() { - var map = SmallMap.New(2); + var map = HSmallMap.New(2); map.AddOrUpdate(0, "0"); map.AddOrUpdate(1, "1"); @@ -395,7 +395,7 @@ public void Can_resize_without_moving() [Test] public void Can_store_and_get_stored_item_count() { - var map = SmallMap.New(); + var map = HSmallMap.New(); map.AddOrUpdate(42, "1"); map.AddOrUpdate(42 + 32 + 32, "3"); @@ -407,7 +407,7 @@ public void Can_store_and_get_stored_item_count() [Test] public void Can_update_a_stored_item_with_new_value() { - var map = SmallMap.New(); + var map = HSmallMap.New(); map.AddOrUpdate(42, "1"); map.AddOrUpdate(42, "3"); @@ -420,7 +420,7 @@ public void Can_update_a_stored_item_with_new_value() [Test] public void Can_add_key_with_0_hash_code() { - var map = SmallMap.New(); + var map = HSmallMap.New(); map.AddOrUpdate(0, "aaa"); map.AddOrUpdate(0 + 32, "2"); @@ -436,7 +436,7 @@ public void Can_add_key_with_0_hash_code() [Test] public void Can_quickly_find_the_scattered_items_with_the_same_cache() { - var map = SmallMap.New(); + var map = HSmallMap.New(); map.AddOrUpdate(42, "1"); map.AddOrUpdate(43, "a"); @@ -457,7 +457,7 @@ public void Can_quickly_find_the_scattered_items_with_the_same_cache() [Test] public void Can_remove_the_stored_item() { - var map = SmallMap.New(2); + var map = HSmallMap.New(2); map.AddOrUpdate(42, "1"); map.AddOrUpdate(42 + 32, "2"); @@ -480,7 +480,7 @@ public void Check_AddOrUpdate_random_items_and_verify_all_added() const int upperBound = 100000; Gen.Int[0, upperBound].Array.Sample(items => { - var m = SmallMap.New>(); + var m = HSmallMap.New>(); foreach (var n in items) { var k = "" + n; @@ -505,7 +505,7 @@ public void Check_AddOrUpdate_random_items_and_verify_all_added() // } static Gen<( - SmallMap, SmallMap.SingleArrayEntries>>, + HSmallMap, HSmallMap.SingleArrayEntries>>, string[])> GenImMap(int upperBound) => Gen.Int[0, upperBound].ArrayUnique.SelectMany(keys => @@ -514,7 +514,7 @@ public void Check_AddOrUpdate_random_items_and_verify_all_added() var keyArray = keys.Select(x => x.ToString()).ToArray(); var valArray = values.Select(x => x.ToString()).ToArray(); - var m = SmallMap.New>(); + var m = HSmallMap.New>(); for (var i = 0; i < keyArray.Length; i++) m.AddOrUpdate(keyArray[i], valArray[i]); return (map: m, keys: keyArray); @@ -533,8 +533,8 @@ public void Check_AddOrUpdate_metamorphic() // todo: @wip add the Copy method // copy things to the new maps - var m1 = SmallMap.New>(); - var m2 = SmallMap.New>(); + var m1 = HSmallMap.New>(); + var m2 = HSmallMap.New>(); foreach (var (k, v) in m.Select(x => (x.Key, x.Value))) { m1.AddOrUpdate(k, v); @@ -588,8 +588,8 @@ public void Check_Remove_metamorphic() var ((m, _), k1, v1, k2, v2) = t; var (sk1, sv1, sk2, sv2) = ("" + k1, "" + v1, "" + k2, "" + v2); - var m1 = SmallMap.New>(); - var m2 = SmallMap.New>(); + var m1 = HSmallMap.New>(); + var m2 = HSmallMap.New>(); foreach (var (k, v) in m.Select(x => (x.Key, x.Value))) { m1.AddOrUpdate(k, v); @@ -646,7 +646,7 @@ public void Check_Remove_ModelBased() public static class SmallMapTestTools { - internal static void Verify(this SmallMap map, IEnumerable expectedKeys = null) + internal static void Verify(this HSmallMap map, IEnumerable expectedKeys = null) where TEq : struct, IEq where TEntries : struct, IEntries { @@ -658,7 +658,7 @@ internal static void Verify(this SmallMapVerifies that the hashes correspond to the keys stored in the entries. May be called from the tests. - public static void VerifyHashesAndKeysEq(this SmallMap map) + public static void VerifyHashesAndKeysEq(this HSmallMap map) where TEq : struct, IEq where TEntries : struct, IEntries { @@ -669,7 +669,7 @@ public static void VerifyHashesAndKeysEq(this SmallMapVerifies that there is no duplicate keys stored in hashes -> entries. May be called from the tests. - public static void VerifyNoDuplicateKeys(this SmallMap map) + public static void VerifyNoDuplicateKeys(this HSmallMap map) where TEq : struct, IEq where TEntries : struct, IEntries { @@ -692,7 +692,7 @@ public static void VerifyNoDuplicateKeys(this SmallMapVerifies that the probes are consistently increasing - public static void VerifyProbesAreFitRobinHood(this SmallMap map) + public static void VerifyProbesAreFitRobinHood(this HSmallMap map) where TEq : struct, IEq where TEntries : struct, IEntries { @@ -712,7 +712,7 @@ public static void VerifyProbesAreFitRobinHood(this SmallMa } /// Verifies that the map contains all passed keys. May be called from the tests. - public static void VerifyContainAllKeys(this SmallMap map, IEnumerable expectedKeys) + public static void VerifyContainAllKeys(this HSmallMap map, IEnumerable expectedKeys) where TEq : struct, IEq where TEntries : struct, IEntries { From 174197b1ac3e0bed1a351963c53093a6c8a5a696 Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 8 Jun 2025 18:31:28 +0200 Subject: [PATCH 02/54] updating for the net9.0 --- playground/Experiments/Experiments.csproj | 4 +- .../ImTools.Benchmarks.csproj | 10 ++--- src/ImTools/ImTools.cs | 42 ++++++++++--------- src/ImTools/ImTools.csproj | 4 +- .../ImTools.UnitTests.csproj | 4 +- 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/playground/Experiments/Experiments.csproj b/playground/Experiments/Experiments.csproj index f78b5eb9..25e087ab 100644 --- a/playground/Experiments/Experiments.csproj +++ b/playground/Experiments/Experiments.csproj @@ -1,8 +1,8 @@ - net8.0 - net8.0;net6.0;net472 + net9.0 + net9.0;net8.0;net6.0;net472 net7.0 1701;1702;AD0001;NU1608 diff --git a/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj b/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj index 68a3269a..971340c0 100644 --- a/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj +++ b/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 x64 @@ -18,11 +18,11 @@ - - + + - - + + diff --git a/src/ImTools/ImTools.cs b/src/ImTools/ImTools.cs index 730fdfd4..87b4074f 100644 --- a/src/ImTools/ImTools.cs +++ b/src/ImTools/ImTools.cs @@ -6907,16 +6907,16 @@ public static DebugHashItem[] Explain(this HSmallMap< [MethodImpl((MethodImplOptions)256)] #if NET7_0_OR_GREATER - internal static ref int GetHashRef(ref int start, int distance) => ref Unsafe.Add(ref start, distance); + internal static ref int NextHashRef(ref int start, int distance) => ref Unsafe.Add(ref start, distance); #else - internal static ref int GetHashRef(ref int[] start, int distance) => ref start[distance]; + internal static ref int NextHashRef(ref int[] start, int distance) => ref start[distance]; #endif [MethodImpl((MethodImplOptions)256)] #if NET7_0_OR_GREATER - internal static int GetHash(ref int start, int distance) => Unsafe.Add(ref start, distance); + internal static int NextHash(ref int start, int distance) => Unsafe.Add(ref start, distance); #else - internal static int GetHash(ref int[] start, int distance) => start[distance]; + internal static int NextHash(ref int[] start, int distance) => start[distance]; #endif /// Uses Fibonacci hashing by multiplying the integer on the factor derived from the GoldenRatio @@ -7261,7 +7261,9 @@ public struct HSmallMap : IReadOnlyCollectionThe capacity @@ -7279,12 +7281,12 @@ public struct HSmallMap : IReadOnlyCollectionCapacity calculates as `1 leftShift capacityBitShift` public HSmallMap(byte capacityBitShift) { + capacityBitShift = capacityBitShift < HSmallMap.MinCapacityBits ? HSmallMap.MinCapacityBits : capacityBitShift; _indexMask = (1 << capacityBitShift) - 1; // the overflow tail to the hashes is the size of log2N where N==capacityBitShift, // it is probably fine to have the check for the overflow of capacity because it will be mis-predicted only once at the end of loop (it even rarely for the lookup) _packedHashesAndIndexes = new int[1 << capacityBitShift]; - _entries = default; _entries.Init(capacityBitShift); } @@ -7307,7 +7309,7 @@ public bool TryGetValue(K key, out V value) var hashesAndIndexes = _packedHashesAndIndexes; #endif - var h = HSmallMap.GetHash(ref hashesAndIndexes, hashIndex); + var h = HSmallMap.NextHash(ref hashesAndIndexes, hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var probes = 1; @@ -7324,7 +7326,7 @@ public bool TryGetValue(K key, out V value) } } - h = HSmallMap.GetHash(ref hashesAndIndexes, ++hashIndex & indexMask); + h = HSmallMap.NextHash(ref hashesAndIndexes, ++hashIndex & indexMask); ++probes; } } @@ -7359,7 +7361,7 @@ public int GetEntryIndex(K key) var hashesAndIndexes = _packedHashesAndIndexes; #endif - var h = HSmallMap.GetHash(ref hashesAndIndexes, hashIndex); + var h = HSmallMap.NextHash(ref hashesAndIndexes, hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var probes = 1; @@ -7373,7 +7375,7 @@ public int GetEntryIndex(K key) return h & indexMask; } - h = HSmallMap.GetHash(ref hashesAndIndexes, ++hashIndex & indexMask); + h = HSmallMap.NextHash(ref hashesAndIndexes, ++hashIndex & indexMask); ++probes; } } @@ -7411,7 +7413,7 @@ public ref V GetOrAddValueRef(K key) #else var hashesAndIndexes = _packedHashesAndIndexes; #endif - ref var h = ref HSmallMap.GetHashRef(ref hashesAndIndexes, hashIndex); + ref var h = ref HSmallMap.NextHashRef(ref hashesAndIndexes, hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var probes = 1; @@ -7427,7 +7429,7 @@ public ref V GetOrAddValueRef(K key) if (default(TEq).Equals(e.Key, key)) return ref e.Value; } - h = ref HSmallMap.GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.NextHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); ++probes; } @@ -7442,7 +7444,7 @@ public ref V GetOrAddValueRef(K key) probes = hRobinHooded >>> HSmallMap.ProbeCountShift; while (hRobinHooded != 0) { - h = ref HSmallMap.GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.NextHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); if ((h >>> HSmallMap.ProbeCountShift) < ++probes) { #if DEBUG @@ -7480,7 +7482,7 @@ public bool TryRemove(K key) #else var hashesAndIndexes = _packedHashesAndIndexes; #endif - ref var h = ref HSmallMap.GetHashRef(ref hashesAndIndexes, hashIndex); + ref var h = ref HSmallMap.NextHashRef(ref hashesAndIndexes, hashIndex); var removed = false; @@ -7503,7 +7505,7 @@ public bool TryRemove(K key) break; } } - h = ref HSmallMap.GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.NextHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); ++probes; } @@ -7511,7 +7513,7 @@ public bool TryRemove(K key) return false; ref var emptied = ref h; - h = ref HSmallMap.GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.NextHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); // move the next hash into the emptied slot until the next hash is empty or ideally positioned (hash is 0 or probe is 1) while ((h >>> HSmallMap.ProbeCountShift) > 1) @@ -7520,7 +7522,7 @@ public bool TryRemove(K key) h = 0; emptied = ref h; - h = ref HSmallMap.GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.NextHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); } return true; } @@ -7556,7 +7558,7 @@ internal int ResizeHashes(int indexMask) // and! the hashes at the beginning robin hooded by the wrapped-around hashes var i = 0; while ((oldHash >>> HSmallMap.ProbeCountShift) > 1) - oldHash = HSmallMap.GetHash(ref oldHashes, ++i); + oldHash = HSmallMap.NextHash(ref oldHashes, ++i); var oldCapacityWithOverflowSegment = i + oldCapacity; while (true) @@ -7569,10 +7571,10 @@ internal int ResizeHashes(int indexMask) // no need for robin-hooding because we already did it for the old hashes and // now just sparcing the hashes into the new array which are already in order var probes = 1; - ref var newHash = ref HSmallMap.GetHashRef(ref newHashes, indexWithNextBit); + ref var newHash = ref HSmallMap.NextHashRef(ref newHashes, indexWithNextBit); while (newHash != 0) { - newHash = ref HSmallMap.GetHashRef(ref newHashes, ++indexWithNextBit & newIndexMask); + newHash = ref HSmallMap.NextHashRef(ref newHashes, ++indexWithNextBit & newIndexMask); ++probes; } newHash = (probes << HSmallMap.ProbeCountShift) | (oldHash & newHashAndIndexMask); @@ -7581,7 +7583,7 @@ internal int ResizeHashes(int indexMask) if (++i >= oldCapacityWithOverflowSegment) break; - oldHash = HSmallMap.GetHash(ref oldHashes, i & indexMask); + oldHash = HSmallMap.NextHash(ref oldHashes, i & indexMask); } #if DEBUG Debug.WriteLine($"[ResizeHashes] {oldCapacity} -> {newHashesAndIndexes.Length}"); diff --git a/src/ImTools/ImTools.csproj b/src/ImTools/ImTools.csproj index 4f0baaf4..e7916e94 100644 --- a/src/ImTools/ImTools.csproj +++ b/src/ImTools/ImTools.csproj @@ -1,7 +1,7 @@ - net8.0;net6.0 - net8.0;net6.0;net45;netstandard2.0 + net9.0;net8.0;net6.0 + net9.0;net8.0;net6.0;net45;netstandard2.0 net7.0 ImTools diff --git a/test/ImTools.UnitTests/ImTools.UnitTests.csproj b/test/ImTools.UnitTests/ImTools.UnitTests.csproj index 3110412e..501fae37 100644 --- a/test/ImTools.UnitTests/ImTools.UnitTests.csproj +++ b/test/ImTools.UnitTests/ImTools.UnitTests.csproj @@ -1,8 +1,8 @@ - net8.0 - net8.0;net6.0;net472 + net9.0 + net9.0;net8.0;net6.0;net472 net7.0 From 9ef1bc15b3eb1e2581f410248193160ebffc99d9 Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 8 Jun 2025 22:15:43 +0200 Subject: [PATCH 03/54] adding padding for dia and resize --- src/ImTools/ImTools.cs | 15 ++++++++++----- test/ImTools.UnitTests/SmallMapTests.cs | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/ImTools/ImTools.cs b/src/ImTools/ImTools.cs index 87b4074f..f62837ea 100644 --- a/src/ImTools/ImTools.cs +++ b/src/ImTools/ImTools.cs @@ -6840,6 +6840,8 @@ public static class HSmallMap internal const byte MaxProbeCount = (1 << MaxProbeBits) - 1; internal const byte ProbeCountShift = 32 - MaxProbeBits; internal const int HashAndIndexMask = ~(MaxProbeCount << ProbeCountShift); + internal const int HashAndIndexMaskWithIndex = HashAndIndexMask | MaxProbeCount; + internal const int PaddingHashCount = 7; /// Creates the map with the storage [MethodImpl((MethodImplOptions)256)] @@ -6882,7 +6884,7 @@ public static DebugHashItem[] Explain(this HSmallMap< var indexMask = capacity - 1; var items = new DebugHashItem[hashes.Length]; - for (var i = 0; i < hashes.Length; i++) + for (var i = 0; i < hashes.Length - HSmallMap.PaddingHashCount; i++) { var h = hashes[i]; if (h == 0) @@ -7532,9 +7534,9 @@ internal int ResizeHashes(int indexMask) if (indexMask == 0) { _indexMask = (1 << HSmallMap.MinCapacityBits) - 1; - _packedHashesAndIndexes = new int[1 << HSmallMap.MinCapacityBits]; + _packedHashesAndIndexes = new int[1 << HSmallMap.MinCapacityBits + HSmallMap.PaddingHashCount]; #if DEBUG - Debug.WriteLine($"[ResizeHashes] new empty hashes {1} -> {_packedHashesAndIndexes.Length}"); + Debug.WriteLine($"[ResizeHashes] new empty hashes {1} -> {_packedHashesAndIndexes.Length - HSmallMap.PaddingHashCount} + {HSmallMap.PaddingHashCount} padding"); #endif return (1 << HSmallMap.MinCapacityBits) - 1; } @@ -7543,7 +7545,7 @@ internal int ResizeHashes(int indexMask) var newHashAndIndexMask = HSmallMap.HashAndIndexMask & ~oldCapacity; var newIndexMask = (indexMask << 1) | 1; - var newHashesAndIndexes = new int[oldCapacity << 1]; + var newHashesAndIndexes = new int[oldCapacity << 1 + HSmallMap.PaddingHashCount]; #if NET7_0_OR_GREATER ref var newHashes = ref MemoryMarshal.GetArrayDataReference(newHashesAndIndexes); @@ -7586,9 +7588,12 @@ internal int ResizeHashes(int indexMask) oldHash = HSmallMap.NextHash(ref oldHashes, i & indexMask); } #if DEBUG - Debug.WriteLine($"[ResizeHashes] {oldCapacity} -> {newHashesAndIndexes.Length}"); + Debug.WriteLine($"[ResizeHashes] {oldCapacity} -> {newHashesAndIndexes.Length - HSmallMap.PaddingHashCount} + {HSmallMap.PaddingHashCount} padding"); _dbg.DebugReCollectAndOutputProbes(newHashesAndIndexes); #endif + // Copy the padding hashes to the end of the new hashes array + Array.Copy(newHashesAndIndexes, 0, newHashesAndIndexes, newHashesAndIndexes.Length - HSmallMap.PaddingHashCount, HSmallMap.PaddingHashCount); + _indexMask = _indexMask << 1 | 1; _packedHashesAndIndexes = newHashesAndIndexes; return newIndexMask; diff --git a/test/ImTools.UnitTests/SmallMapTests.cs b/test/ImTools.UnitTests/SmallMapTests.cs index 78d3d6f1..3fae984b 100644 --- a/test/ImTools.UnitTests/SmallMapTests.cs +++ b/test/ImTools.UnitTests/SmallMapTests.cs @@ -678,7 +678,7 @@ public static void VerifyNoDuplicateKeys(this HSmallMap(this HSmallM var indexMask = capacity - 1; var prevProbes = -1; const int ProbeCountShift = 32 - MaxProbeBits; - for (var i = 0; i < hashes.Length; i++) + for (var i = 0; i < hashes.Length - 8; i++) { var h = hashes[i]; var probes = h >>> ProbeCountShift; From 6f87d3314379fda431018158dfe4840c26385250 Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 8 Jun 2025 22:55:37 +0200 Subject: [PATCH 04/54] copying the hashes to the padding in the insert --- src/ImTools/ImTools.cs | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/ImTools/ImTools.cs b/src/ImTools/ImTools.cs index f62837ea..09628450 100644 --- a/src/ImTools/ImTools.cs +++ b/src/ImTools/ImTools.cs @@ -6909,9 +6909,9 @@ public static DebugHashItem[] Explain(this HSmallMap< [MethodImpl((MethodImplOptions)256)] #if NET7_0_OR_GREATER - internal static ref int NextHashRef(ref int start, int distance) => ref Unsafe.Add(ref start, distance); + internal static ref int GetItemRef(ref int start, int distance) => ref Unsafe.Add(ref start, distance); #else - internal static ref int NextHashRef(ref int[] start, int distance) => ref start[distance]; + internal static ref int GetItemRef(ref int[] start, int distance) => ref start[distance]; #endif [MethodImpl((MethodImplOptions)256)] @@ -7288,7 +7288,7 @@ public HSmallMap(byte capacityBitShift) // the overflow tail to the hashes is the size of log2N where N==capacityBitShift, // it is probably fine to have the check for the overflow of capacity because it will be mis-predicted only once at the end of loop (it even rarely for the lookup) - _packedHashesAndIndexes = new int[1 << capacityBitShift]; + _packedHashesAndIndexes = new int[(1 << capacityBitShift) + HSmallMap.PaddingHashCount]; _entries.Init(capacityBitShift); } @@ -7415,7 +7415,7 @@ public ref V GetOrAddValueRef(K key) #else var hashesAndIndexes = _packedHashesAndIndexes; #endif - ref var h = ref HSmallMap.NextHashRef(ref hashesAndIndexes, hashIndex); + ref var h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var probes = 1; @@ -7431,13 +7431,17 @@ public ref V GetOrAddValueRef(K key) if (default(TEq).Equals(e.Key, key)) return ref e.Value; } - h = ref HSmallMap.NextHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, ++hashIndex & indexMask); ++probes; } // 3. We did not find the hash and therefore the key, so insert the new entry var hRobinHooded = h; - h = (probes << HSmallMap.ProbeCountShift) | hashMiddle | entryCount; + h = (probes << HSmallMap.ProbeCountShift) | hashMiddle | entryCount; // old entryCount is the new index of the added hash + + if ((hashIndex & indexMask) < HSmallMap.PaddingHashCount) + HSmallMap.GetItemRef(ref hashesAndIndexes, indexMask + 1 + (hashIndex & indexMask)) = h; // Copy the hash to the padding + #if DEBUG _dbg.DebugCollectAndOutputProbes(probes, "Add"); #endif @@ -7446,7 +7450,7 @@ public ref V GetOrAddValueRef(K key) probes = hRobinHooded >>> HSmallMap.ProbeCountShift; while (hRobinHooded != 0) { - h = ref HSmallMap.NextHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, ++hashIndex & indexMask); if ((h >>> HSmallMap.ProbeCountShift) < ++probes) { #if DEBUG @@ -7456,6 +7460,9 @@ public ref V GetOrAddValueRef(K key) #endif var tmp = h; h = (probes << HSmallMap.ProbeCountShift) | (hRobinHooded & HSmallMap.HashAndIndexMask); + if ((hashIndex & indexMask) < HSmallMap.PaddingHashCount) + HSmallMap.GetItemRef(ref hashesAndIndexes, indexMask + 1 + (hashIndex & indexMask)) = h; // Copy the hash to the padding + hRobinHooded = tmp; probes = hRobinHooded >>> HSmallMap.ProbeCountShift; } @@ -7484,7 +7491,7 @@ public bool TryRemove(K key) #else var hashesAndIndexes = _packedHashesAndIndexes; #endif - ref var h = ref HSmallMap.NextHashRef(ref hashesAndIndexes, hashIndex); + ref var h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, hashIndex); var removed = false; @@ -7507,7 +7514,7 @@ public bool TryRemove(K key) break; } } - h = ref HSmallMap.NextHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, ++hashIndex & indexMask); ++probes; } @@ -7515,7 +7522,7 @@ public bool TryRemove(K key) return false; ref var emptied = ref h; - h = ref HSmallMap.NextHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, ++hashIndex & indexMask); // move the next hash into the emptied slot until the next hash is empty or ideally positioned (hash is 0 or probe is 1) while ((h >>> HSmallMap.ProbeCountShift) > 1) @@ -7524,7 +7531,7 @@ public bool TryRemove(K key) h = 0; emptied = ref h; - h = ref HSmallMap.NextHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); + h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, ++hashIndex & indexMask); } return true; } @@ -7534,7 +7541,7 @@ internal int ResizeHashes(int indexMask) if (indexMask == 0) { _indexMask = (1 << HSmallMap.MinCapacityBits) - 1; - _packedHashesAndIndexes = new int[1 << HSmallMap.MinCapacityBits + HSmallMap.PaddingHashCount]; + _packedHashesAndIndexes = new int[(1 << HSmallMap.MinCapacityBits) + HSmallMap.PaddingHashCount]; #if DEBUG Debug.WriteLine($"[ResizeHashes] new empty hashes {1} -> {_packedHashesAndIndexes.Length - HSmallMap.PaddingHashCount} + {HSmallMap.PaddingHashCount} padding"); #endif @@ -7545,7 +7552,7 @@ internal int ResizeHashes(int indexMask) var newHashAndIndexMask = HSmallMap.HashAndIndexMask & ~oldCapacity; var newIndexMask = (indexMask << 1) | 1; - var newHashesAndIndexes = new int[oldCapacity << 1 + HSmallMap.PaddingHashCount]; + var newHashesAndIndexes = new int[(oldCapacity << 1) + HSmallMap.PaddingHashCount]; #if NET7_0_OR_GREATER ref var newHashes = ref MemoryMarshal.GetArrayDataReference(newHashesAndIndexes); @@ -7573,10 +7580,10 @@ internal int ResizeHashes(int indexMask) // no need for robin-hooding because we already did it for the old hashes and // now just sparcing the hashes into the new array which are already in order var probes = 1; - ref var newHash = ref HSmallMap.NextHashRef(ref newHashes, indexWithNextBit); + ref var newHash = ref HSmallMap.GetItemRef(ref newHashes, indexWithNextBit); while (newHash != 0) { - newHash = ref HSmallMap.NextHashRef(ref newHashes, ++indexWithNextBit & newIndexMask); + newHash = ref HSmallMap.GetItemRef(ref newHashes, ++indexWithNextBit & newIndexMask); ++probes; } newHash = (probes << HSmallMap.ProbeCountShift) | (oldHash & newHashAndIndexMask); From d0b17a86c74792d3697ed09926e776209539226f Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 8 Jun 2025 23:14:37 +0200 Subject: [PATCH 05/54] adding padding in resize --- src/ImTools/ImTools.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ImTools/ImTools.cs b/src/ImTools/ImTools.cs index 09628450..5befaead 100644 --- a/src/ImTools/ImTools.cs +++ b/src/ImTools/ImTools.cs @@ -7521,14 +7521,22 @@ public bool TryRemove(K key) if (!removed) return false; + var emptiedIndex = hashIndex & indexMask; ref var emptied = ref h; h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, ++hashIndex & indexMask); // move the next hash into the emptied slot until the next hash is empty or ideally positioned (hash is 0 or probe is 1) while ((h >>> HSmallMap.ProbeCountShift) > 1) { - emptied = (((h >>> HSmallMap.ProbeCountShift) - 1) << HSmallMap.ProbeCountShift) | (h & HSmallMap.HashAndIndexMask); // decrease the probe count by one cause we moving the hash closer to the ideal index + // Decrease the probe count by one cause we moving the hash closer to the ideal index + emptied = (((h >>> HSmallMap.ProbeCountShift) - 1) << HSmallMap.ProbeCountShift) | (h & HSmallMap.HashAndIndexMask); + if (emptiedIndex < HSmallMap.PaddingHashCount) + HSmallMap.GetItemRef(ref hashesAndIndexes, indexMask + 1 + emptiedIndex) = emptied; // Copy the hash to the padding + h = 0; + emptiedIndex = hashIndex & indexMask; + if (emptiedIndex < HSmallMap.PaddingHashCount) + HSmallMap.GetItemRef(ref hashesAndIndexes, indexMask + 1 + emptiedIndex) = 0; // Copy the hash to the padding emptied = ref h; h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, ++hashIndex & indexMask); From 3b33a1132c31ec6232ecc3f8b6942959dbaf461d Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 9 Jun 2025 16:50:40 +0200 Subject: [PATCH 06/54] experiments --- bm.bat | 2 +- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 662 ++++++++++-------- .../ImTools.Benchmarks.csproj | 4 +- playground/ImTools.Benchmarks/Program.cs | 2 +- src/ImTools/ImTools.cs | 260 ++++++- 5 files changed, 597 insertions(+), 333 deletions(-) diff --git a/bm.bat b/bm.bat index f3da08a7..a6a1a547 100644 --- a/bm.bat +++ b/bm.bat @@ -1 +1 @@ -dotnet run -f net8.0 -c Release --project playground\ImTools.Benchmarks \ No newline at end of file +dotnet run -f net9.0 -c Release --project playground\ImTools.Benchmarks \ No newline at end of file diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 20396520..8127a7e1 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -15,6 +15,7 @@ using FHashMap91TypeString = ImTools.Experiments.FHashMap91, ImTools.Experiments.FHashMap91.SingleArrayEntries>>; using SmallMapTypeString = ImTools.HSmallMap, ImTools.HSmallMap.SingleArrayEntries>>; using FHashMapTypeString = FastExpressionCompiler.ImTools.FHashMap, FastExpressionCompiler.ImTools.FHashMap.SingleArrayEntries>>; +using BenchmarkDotNet.Order; #nullable disable @@ -26,6 +27,10 @@ public class ImHashMapBenchmarks { private static readonly Type[] _keys = typeof(Dictionary<,>).Assembly.GetTypes().Take(1000).ToArray(); + // get half of the random _keys for the lookup, by selecting half of the random indexes from keys + private static readonly Random _seed = new Random(42); + private static readonly Type[] _randomLookupKeys = _keys.OrderBy(_ => _seed.Next()).Take(_keys.Length / 2).ToArray(); + public struct TypeVal : IEquatable { public static implicit operator TypeVal(Type t) => new TypeVal(t); @@ -1319,8 +1324,8 @@ private static class TypeKey } } + [MemoryDiagnoser, RankColumn, Orderer(SummaryOrderPolicy.FastestToSlowest)] [HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchMispredictions, HardwareCounter.BranchInstructions)] - [MemoryDiagnoser] public class Lookup { /* @@ -2262,9 +2267,32 @@ .NET SDK 8.0.100-rc.2.23502.2 | SmallMap_TryGetValue | 100 | 3.549 ns | 0.1293 ns | 0.2490 ns | 3.486 ns | 0.66 | 0.07 | 9 | 0 | 0 | - | NA | | FHashMap_TryGetValue | 100 | 4.265 ns | 0.1406 ns | 0.1098 ns | 4.268 ns | 0.80 | 0.08 | 10 | 0 | -0 | - | NA | + ## SmallMap SIMD vs... + + BenchmarkDotNet v0.15.0, Windows 11 (10.0.26100.4202/24H2/2024Update/HudsonValley) + Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET SDK 9.0.203 + [Host] : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2 + DefaultJob : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2 + + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|-----:|----------:|------------:| + | FHashMap_TryGetValue | 1 | 1.580 ns | 0.0212 ns | 0.0199 ns | 1.587 ns | 0.35 | 0.03 | 1 | - | NA | + | DictionarySlim_TryGetValue | 1 | 4.592 ns | 0.1535 ns | 0.4202 ns | 4.452 ns | 1.01 | 0.13 | 2 | - | NA | + | SmallMap_TryGetValue | 1 | 5.367 ns | 0.0884 ns | 0.0738 ns | 5.378 ns | 1.18 | 0.10 | 3 | - | NA | + | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 4.165 ns | 0.0467 ns | 0.0414 ns | 4.156 ns | 1.00 | 0.01 | 1 | - | NA | + | FHashMap_TryGetValue | 10 | 4.455 ns | 0.0285 ns | 0.0253 ns | 4.457 ns | 1.07 | 0.01 | 2 | - | NA | + | SmallMap_TryGetValue | 10 | 5.302 ns | 0.1556 ns | 0.1852 ns | 5.369 ns | 1.27 | 0.05 | 3 | - | NA | + | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 4.777 ns | 0.1559 ns | 0.4472 ns | 4.576 ns | 1.01 | 0.13 | 1 | - | NA | + | FHashMap_TryGetValue | 100 | 6.020 ns | 0.1795 ns | 0.2335 ns | 6.017 ns | 1.27 | 0.13 | 2 | - | NA | + | SmallMap_TryGetValue | 100 | 6.021 ns | 0.1808 ns | 0.4827 ns | 6.293 ns | 1.27 | 0.15 | 2 | - | NA | */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] - [Params(1, 10, 100)] + // [Params(1, 10, 100)] + [Params(1000)] public int Count; [GlobalSetup] @@ -2680,22 +2708,56 @@ public string Dictionary_TryGetValue() // [Benchmark] [Benchmark(Baseline = true)] - public string DictionarySlim_TryGetValue() + public int DictionarySlim_TryGetValue() { - _dictSlim.TryGetValue(LookupKey, out var result); - return result; + var count = 0; + foreach (var k in _randomLookupKeys) + { + _dictSlim.TryGetValue(k, out var result); + count += result.Length; + } + + return count; } [Benchmark] - public string SmallMap_TryGetValue() + public int SmallMap_TryGetValue() { - _smallMap.TryGetValue(LookupKey, out var result); - return result; + var count = 0; + foreach (var k in _randomLookupKeys) + { + _smallMap.TryGetValue(k, out var result); + count += result.Length; + } + + return count; } [Benchmark] - public string FHashMap_TryGetValue() => - _fHashMap.TryGetValueRef(LookupKey, out _); + public int SmallMap_TryGetValue_Permute() + { + var count = 0; + foreach (var k in _randomLookupKeys) + { + _smallMap.TryGetValue_Permute(k, out var result); + count += result.Length; + } + + return count; + } + + // [Benchmark] + public int FHashMap_TryGetValue() + { + var count = 0; + foreach (var k in _randomLookupKeys) + { + var result = _fHashMap.TryGetValueRef(k, out _); + count += result.Length; + } + + return count; + } // [Benchmark(Baseline = true)] // [Benchmark] @@ -2746,296 +2808,296 @@ public class Enumerate [Host] : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT DefaultJob : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT -| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | -|------------------------------ |------ |--------------:|-----------:|-----------:|------:|--------:|--------:|-------:|------:|----------:| -| ImHashMap_EnumerateToArray | 1 | 147.31 ns | 0.567 ns | 0.531 ns | 1.00 | 0.00 | 0.0441 | - | - | 208 B | -| ImHashMap_V1_EnumerateToArray | 1 | 160.45 ns | 0.856 ns | 0.801 ns | 1.09 | 0.01 | 0.0560 | - | - | 264 B | -| ImHashMap_FoldToArray | 1 | 55.05 ns | 0.436 ns | 0.387 ns | 0.37 | 0.00 | 0.0356 | - | - | 168 B | -| ImHashMapSlots_FoldToArray | 1 | 89.21 ns | 1.473 ns | 1.378 ns | 0.61 | 0.01 | 0.0271 | - | - | 128 B | -| DictionarySlim_ToArray | 1 | 150.88 ns | 1.424 ns | 1.189 ns | 1.02 | 0.01 | 0.0408 | - | - | 192 B | -| Dictionary_ToArray | 1 | 40.47 ns | 0.864 ns | 1.093 ns | 0.28 | 0.01 | 0.0119 | - | - | 56 B | -| ConcurrentDictionary_ToArray | 1 | 232.31 ns | 1.981 ns | 1.654 ns | 1.58 | 0.01 | 0.0114 | - | - | 56 B | -| ImmutableDict_ToArray | 1 | 618.68 ns | 11.308 ns | 10.578 ns | 4.20 | 0.07 | 0.0114 | - | - | 56 B | -| | | | | | | | | | | | -| ImHashMap_EnumerateToArray | 10 | 423.85 ns | 8.425 ns | 9.364 ns | 1.00 | 0.00 | 0.1001 | - | - | 472 B | -| ImHashMap_V1_EnumerateToArray | 10 | 492.81 ns | 4.461 ns | 4.173 ns | 1.16 | 0.03 | 0.1726 | - | - | 816 B | -| ImHashMap_FoldToArray | 10 | 213.14 ns | 4.054 ns | 3.981 ns | 0.50 | 0.02 | 0.1054 | - | - | 496 B | -| ImHashMapSlots_FoldToArray | 10 | 255.16 ns | 1.863 ns | 1.743 ns | 0.60 | 0.01 | 0.1016 | - | - | 480 B | -| DictionarySlim_ToArray | 10 | 450.09 ns | 8.918 ns | 10.616 ns | 1.06 | 0.04 | 0.1354 | - | - | 640 B | -| Dictionary_ToArray | 10 | 87.40 ns | 1.696 ns | 1.586 ns | 0.21 | 0.01 | 0.0424 | - | - | 200 B | -| ConcurrentDictionary_ToArray | 10 | 499.22 ns | 4.804 ns | 4.494 ns | 1.17 | 0.03 | 0.0420 | - | - | 200 B | -| ImmutableDict_ToArray | 10 | 1,954.98 ns | 10.732 ns | 10.038 ns | 4.60 | 0.11 | 0.0381 | - | - | 200 B | -| | | | | | | | | | | | -| ImHashMap_EnumerateToArray | 100 | 2,735.61 ns | 34.407 ns | 32.185 ns | 1.00 | 0.00 | 0.4768 | - | - | 2248 B | -| ImHashMap_V1_EnumerateToArray | 100 | 3,368.76 ns | 6.822 ns | 6.048 ns | 1.23 | 0.02 | 1.1597 | 0.0267 | - | 5472 B | -| ImHashMap_FoldToArray | 100 | 1,433.97 ns | 14.981 ns | 14.013 ns | 0.52 | 0.01 | 0.6599 | 0.0038 | - | 3112 B | -| ImHashMapSlots_FoldToArray | 100 | 1,541.62 ns | 8.594 ns | 8.039 ns | 0.56 | 0.01 | 0.6714 | 0.0038 | - | 3168 B | -| DictionarySlim_ToArray | 100 | 2,505.97 ns | 44.927 ns | 37.516 ns | 0.92 | 0.02 | 0.8469 | 0.0076 | - | 4000 B | -| Dictionary_ToArray | 100 | 549.34 ns | 6.559 ns | 6.136 ns | 0.20 | 0.00 | 0.3481 | 0.0019 | - | 1640 B | -| ConcurrentDictionary_ToArray | 100 | 2,236.03 ns | 10.044 ns | 9.395 ns | 0.82 | 0.01 | 0.3471 | - | - | 1640 B | -| ImmutableDict_ToArray | 100 | 15,683.87 ns | 68.105 ns | 60.373 ns | 5.74 | 0.08 | 0.3357 | - | - | 1640 B | -| | | | | | | | | | | | -| ImHashMap_EnumerateToArray | 1000 | 25,723.37 ns | 504.158 ns | 560.370 ns | 1.00 | 0.00 | 3.5706 | 0.1526 | - | 16808 B | -| ImHashMap_V1_EnumerateToArray | 1000 | 34,316.66 ns | 583.573 ns | 545.874 ns | 1.34 | 0.04 | 10.3149 | 1.8921 | - | 48833 B | -| ImHashMap_FoldToArray | 1000 | 16,277.05 ns | 38.330 ns | 33.979 ns | 0.64 | 0.02 | 5.2490 | 0.3052 | - | 24752 B | -| ImHashMapSlots_FoldToArray | 1000 | 15,167.14 ns | 261.927 ns | 218.721 ns | 0.59 | 0.02 | 5.2490 | 0.2899 | - | 24784 B | -| DictionarySlim_ToArray | 1000 | 22,273.30 ns | 384.716 ns | 359.864 ns | 0.87 | 0.01 | 6.9885 | 0.6714 | - | 32896 B | -| Dictionary_ToArray | 1000 | 4,997.89 ns | 20.869 ns | 18.500 ns | 0.20 | 0.00 | 3.3951 | 0.1831 | - | 16040 B | -| ConcurrentDictionary_ToArray | 1000 | 36,859.40 ns | 193.536 ns | 181.034 ns | 1.44 | 0.04 | 3.3569 | 0.1831 | - | 16040 B | -| ImmutableDict_ToArray | 1000 | 155,798.41 ns | 484.101 ns | 452.828 ns | 6.08 | 0.14 | 3.1738 | - | - | 16040 B | - -| Method | Count | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated | -|----------------------------------- |------ |-------------:|-----------:|-----------:|------:|-------:|-------:|------:|----------:| -| ImHashMap_EnumerateToArray | 1 | 153.03 ns | 0.774 ns | 0.686 ns | 1.00 | 0.0441 | - | - | 208 B | -| Experimental_ImHashMap_FoldToArray | 1 | 62.26 ns | 0.180 ns | 0.159 ns | 0.41 | 0.0271 | - | - | 128 B | -| | | | | | | | | | | -| ImHashMap_EnumerateToArray | 10 | 417.19 ns | 4.075 ns | 3.403 ns | 1.00 | 0.1001 | - | - | 472 B | -| Experimental_ImHashMap_FoldToArray | 10 | 237.62 ns | 1.079 ns | 1.009 ns | 0.57 | 0.1016 | - | - | 480 B | -| | | | | | | | | | | -| ImHashMap_EnumerateToArray | 100 | 2,693.86 ns | 7.426 ns | 6.946 ns | 1.00 | 0.4768 | - | - | 2248 B | -| Experimental_ImHashMap_FoldToArray | 100 | 1,517.72 ns | 6.061 ns | 5.669 ns | 0.56 | 0.6561 | 0.0038 | - | 3096 B | -| | | | | | | | | | | -| ImHashMap_EnumerateToArray | 1000 | 26,018.52 ns | 175.262 ns | 146.352 ns | 1.00 | 3.5706 | 0.1831 | - | 16808 B | -| Experimental_ImHashMap_FoldToArray | 1000 | 15,321.95 ns | 69.421 ns | 64.937 ns | 0.59 | 5.2490 | 0.2747 | - | 24736 B | - -## V3 baseline - -| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | -|----------------------------------------- |------ |-------------:|----------:|----------:|------:|--------:|-------:|------:|------:|----------:| -| V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 198.4 ns | 3.64 ns | 3.04 ns | 1.00 | 0.00 | 0.0496 | - | - | 208 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 158.0 ns | 3.23 ns | 3.17 ns | 0.80 | 0.02 | 0.0362 | - | - | 152 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 535.6 ns | 7.97 ns | 8.86 ns | 1.00 | 0.00 | 0.1125 | - | - | 472 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 558.9 ns | 5.35 ns | 5.25 ns | 1.04 | 0.02 | 0.1354 | - | - | 568 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 3,601.3 ns | 54.44 ns | 50.92 ns | 1.00 | 0.00 | 0.5341 | - | - | 2248 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 9,800.0 ns | 110.20 ns | 97.69 ns | 2.72 | 0.04 | 1.0529 | - | - | 4424 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 34,261.7 ns | 371.47 ns | 310.20 ns | 1.00 | 0.00 | 3.9673 | - | - | 16808 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 128,626.8 ns | 988.46 ns | 924.61 ns | 3.76 | 0.04 | 9.2773 | - | - | 38912 B | - -### Static Enumerate - incomplete - -| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | -|----------------------------------------- |------ |------------:|----------:|----------:|------:|--------:|-------:|------:|------:|----------:| -| V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 164.7 ns | 2.23 ns | 1.98 ns | 1.00 | 0.00 | 0.0496 | - | - | 208 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 245.0 ns | 4.92 ns | 5.66 ns | 1.49 | 0.04 | 0.0567 | - | - | 240 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 295.6 ns | 5.87 ns | 5.77 ns | 1.00 | 0.00 | 0.0801 | - | - | 336 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 497.5 ns | 5.77 ns | 5.12 ns | 1.68 | 0.04 | 0.0849 | - | - | 360 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 459.0 ns | 4.86 ns | 4.55 ns | 1.00 | 0.00 | 0.1116 | - | - | 472 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 683.6 ns | 9.36 ns | 8.30 ns | 1.49 | 0.02 | 0.1335 | - | - | 560 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 3,219.3 ns | 36.20 ns | 30.23 ns | 1.00 | 0.00 | 0.5341 | - | - | 2248 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 5,009.4 ns | 57.55 ns | 51.02 ns | 1.56 | 0.02 | 0.5264 | - | - | 2208 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 29,919.7 ns | 363.29 ns | 322.05 ns | 1.00 | 0.00 | 3.9673 | - | - | 16808 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 21,530.8 ns | 292.37 ns | 259.18 ns | 0.72 | 0.01 | 1.8921 | - | - | 8016 B | - -### Struct enumerator for leafs - -BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.630 (2004/?/20H1) -Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores -.NET Core SDK=5.0.100 - [Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT - DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT - - -| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | -|----------------------------------------- |------ |-------------:|------------:|------------:|------:|--------:|-------:|------:|------:|----------:| -| V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 131.8 ns | 2.43 ns | 2.27 ns | 1.00 | 0.00 | 0.0458 | - | - | 192 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 107.7 ns | 0.56 ns | 0.44 ns | 0.82 | 0.01 | 0.0305 | - | - | 128 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 251.5 ns | 3.12 ns | 2.77 ns | 1.00 | 0.00 | 0.0782 | - | - | 328 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 214.5 ns | 4.37 ns | 4.86 ns | 0.86 | 0.02 | 0.0591 | - | - | 248 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 402.3 ns | 2.40 ns | 2.00 ns | 1.00 | 0.00 | 0.1106 | - | - | 464 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 469.7 ns | 8.19 ns | 14.13 ns | 1.19 | 0.04 | 0.1144 | - | - | 480 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 2,987.6 ns | 34.44 ns | 28.76 ns | 1.00 | 0.00 | 0.5341 | - | - | 2240 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 8,085.2 ns | 54.39 ns | 50.88 ns | 2.71 | 0.03 | 0.9308 | - | - | 3904 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 29,107.9 ns | 372.54 ns | 330.25 ns | 1.00 | 0.00 | 3.9978 | - | - | 16800 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 117,098.5 ns | 1,388.31 ns | 1,298.62 ns | 4.02 | 0.07 | 8.0566 | - | - | 33992 B | - - -### Static iterative enumerator with List as a stack - -| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | -|----------------------------------------- |------ |------------:|----------:|----------:|------:|--------:|-------:|------:|------:|----------:| -| V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 130.0 ns | 1.58 ns | 1.76 ns | 1.00 | 0.00 | 0.0458 | - | - | 192 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 129.1 ns | 1.55 ns | 1.45 ns | 0.99 | 0.02 | 0.0610 | - | - | 256 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 251.6 ns | 3.58 ns | 3.35 ns | 1.00 | 0.00 | 0.0782 | - | - | 328 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 241.4 ns | 2.97 ns | 2.78 ns | 0.96 | 0.01 | 0.0896 | - | - | 376 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 414.8 ns | 8.12 ns | 10.84 ns | 1.00 | 0.00 | 0.1106 | - | - | 464 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 403.7 ns | 6.25 ns | 5.84 ns | 0.98 | 0.03 | 0.1373 | - | - | 576 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 2,947.2 ns | 23.48 ns | 20.81 ns | 1.00 | 0.00 | 0.5341 | - | - | 2240 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 2,822.2 ns | 16.96 ns | 15.87 ns | 0.96 | 0.01 | 0.5646 | - | - | 2376 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 28,594.2 ns | 307.61 ns | 287.73 ns | 1.00 | 0.00 | 3.9673 | - | - | 16800 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 27,555.7 ns | 318.08 ns | 281.97 ns | 0.96 | 0.01 | 4.0588 | - | - | 16992 B | - -### Branch3 is a Branch2 - -| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | -|----------------------------------------- |------ |-----------:|---------:|---------:|------:|--------:|-------:|------:|------:|----------:| -| V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 329.7 ns | 6.40 ns | 5.99 ns | 1.00 | 0.00 | 0.0782 | - | - | 328 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 313.1 ns | 5.69 ns | 5.59 ns | 0.95 | 0.02 | 0.0877 | - | - | 368 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 513.4 ns | 4.55 ns | 4.26 ns | 1.00 | 0.00 | 0.1106 | - | - | 464 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 525.6 ns | 7.40 ns | 6.92 ns | 1.02 | 0.02 | 0.1354 | - | - | 568 B | -| | | | | | | | | | | | -| V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 3,658.7 ns | 53.67 ns | 47.58 ns | 1.00 | 0.00 | 0.5341 | - | - | 2240 B | -| V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 3,388.2 ns | 67.71 ns | 63.34 ns | 0.93 | 0.02 | 0.5646 | - | - | 2368 B | - -### V3 RTM - -| Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | -|------------------------------ |------ |-------------:|-------------:|-------------:|-------------:|------:|--------:|-------:|------:|------:|----------:| -| V2_ImHashMap_foreach | 1 | 53.16 ns | 1.107 ns | 1.317 ns | 53.11 ns | 1.00 | 0.00 | 0.0166 | - | - | 104 B | -| V3_ImHashMap_foreach | 1 | 62.12 ns | 1.327 ns | 1.986 ns | 61.59 ns | 1.16 | 0.05 | 0.0267 | - | - | 168 B | -| V3_PartitionedHashMap_foreach | 1 | 238.62 ns | 4.811 ns | 6.084 ns | 235.92 ns | 4.50 | 0.13 | 0.0534 | - | - | 336 B | -| DictionarySlim_foreach | 1 | 12.90 ns | 0.167 ns | 0.156 ns | 12.90 ns | 0.24 | 0.01 | - | - | - | - | -| Dictionary_foreach | 1 | 14.19 ns | 0.217 ns | 0.181 ns | 14.14 ns | 0.27 | 0.01 | - | - | - | - | -| ConcurrentDictionary_foreach | 1 | 153.40 ns | 2.768 ns | 4.142 ns | 151.56 ns | 2.89 | 0.11 | 0.0100 | - | - | 64 B | -| ImmutableDict_foreach | 1 | 268.98 ns | 5.361 ns | 9.528 ns | 268.86 ns | 5.01 | 0.26 | - | - | - | - | -| | | | | | | | | | | | | -| V2_ImHashMap_foreach | 10 | 233.42 ns | 4.541 ns | 4.859 ns | 232.71 ns | 1.00 | 0.00 | 0.0200 | - | - | 128 B | -| V3_ImHashMap_foreach | 10 | 249.12 ns | 4.915 ns | 5.852 ns | 246.87 ns | 1.07 | 0.03 | 0.0391 | - | - | 248 B | -| V3_PartitionedHashMap_foreach | 10 | 746.26 ns | 14.990 ns | 18.409 ns | 748.53 ns | 3.20 | 0.13 | 0.1602 | - | - | 1008 B | -| DictionarySlim_foreach | 10 | 72.54 ns | 0.970 ns | 0.907 ns | 72.42 ns | 0.31 | 0.01 | - | - | - | - | -| Dictionary_foreach | 10 | 58.52 ns | 0.938 ns | 0.733 ns | 58.58 ns | 0.25 | 0.01 | - | - | - | - | -| ConcurrentDictionary_foreach | 10 | 468.65 ns | 9.252 ns | 12.351 ns | 464.12 ns | 2.01 | 0.06 | 0.0095 | - | - | 64 B | -| ImmutableDict_foreach | 10 | 1,127.30 ns | 15.601 ns | 14.593 ns | 1,123.20 ns | 4.82 | 0.10 | - | - | - | - | -| | | | | | | | | | | | | -| V2_ImHashMap_foreach | 100 | 2,355.54 ns | 46.224 ns | 63.271 ns | 2,337.40 ns | 1.00 | 0.00 | 0.0229 | - | - | 160 B | -| V3_ImHashMap_foreach | 100 | 2,423.13 ns | 31.652 ns | 46.395 ns | 2,412.92 ns | 1.03 | 0.04 | 0.0496 | - | - | 320 B | -| V3_PartitionedHashMap_foreach | 100 | 4,268.51 ns | 25.429 ns | 22.542 ns | 4,266.90 ns | 1.81 | 0.05 | 0.4501 | - | - | 2856 B | -| DictionarySlim_foreach | 100 | 570.39 ns | 5.827 ns | 4.866 ns | 570.93 ns | 0.24 | 0.01 | - | - | - | - | -| Dictionary_foreach | 100 | 548.46 ns | 8.579 ns | 7.605 ns | 547.90 ns | 0.23 | 0.01 | - | - | - | - | -| ConcurrentDictionary_foreach | 100 | 2,967.70 ns | 45.435 ns | 44.623 ns | 2,958.11 ns | 1.26 | 0.04 | 0.0076 | - | - | 64 B | -| ImmutableDict_foreach | 100 | 9,988.48 ns | 198.973 ns | 297.813 ns | 9,821.03 ns | 4.24 | 0.14 | - | - | - | - | -| | | | | | | | | | | | | -| V2_ImHashMap_foreach | 1000 | 23,828.30 ns | 433.708 ns | 362.166 ns | 23,743.77 ns | 1.00 | 0.00 | 0.0305 | - | - | 192 B | -| V3_ImHashMap_foreach | 1000 | 26,014.69 ns | 294.125 ns | 245.608 ns | 25,965.82 ns | 1.09 | 0.02 | 0.0610 | - | - | 552 B | -| V3_PartitionedHashMap_foreach | 1000 | 36,582.53 ns | 709.641 ns | 897.469 ns | 36,594.84 ns | 1.54 | 0.04 | 0.4883 | - | - | 3240 B | -| DictionarySlim_foreach | 1000 | 5,591.13 ns | 43.627 ns | 40.809 ns | 5,602.25 ns | 0.23 | 0.00 | - | - | - | - | -| Dictionary_foreach | 1000 | 5,319.86 ns | 51.684 ns | 45.817 ns | 5,308.36 ns | 0.22 | 0.00 | - | - | - | - | -| ConcurrentDictionary_foreach | 1000 | 38,718.40 ns | 466.979 ns | 389.949 ns | 38,728.64 ns | 1.63 | 0.03 | - | - | - | 64 B | -| ImmutableDict_foreach | 1000 | 99,156.35 ns | 1,962.968 ns | 2,181.834 ns | 98,166.03 ns | 4.17 | 0.11 | - | - | - | - | - -## V3.2 - -| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | -|--------------------- |------ |-------------:|-----------:|-----------:|------:|--------:|-------:|------:|------:|----------:| -| V3_ImHashMap_foreach | 1 | 49.59 ns | 0.758 ns | 0.633 ns | 1.00 | 0.00 | 0.0255 | - | - | 160 B | -| V2_ImHashMap_foreach | 1 | 45.96 ns | 0.273 ns | 0.242 ns | 0.93 | 0.01 | 0.0166 | - | - | 104 B | -| | | | | | | | | | | | -| V3_ImHashMap_foreach | 10 | 214.40 ns | 3.915 ns | 3.662 ns | 1.00 | 0.00 | 0.0381 | - | - | 240 B | -| V2_ImHashMap_foreach | 10 | 204.50 ns | 0.963 ns | 0.854 ns | 0.95 | 0.02 | 0.0203 | - | - | 128 B | -| | | | | | | | | | | | -| V3_ImHashMap_foreach | 100 | 2,072.90 ns | 40.826 ns | 41.926 ns | 1.00 | 0.00 | 0.0496 | - | - | 328 B | -| V2_ImHashMap_foreach | 100 | 2,206.31 ns | 29.262 ns | 25.940 ns | 1.07 | 0.03 | 0.0229 | - | - | 160 B | -| | | | | | | | | | | | -| V3_ImHashMap_foreach | 1000 | 21,043.39 ns | 298.240 ns | 232.846 ns | 1.00 | 0.00 | 0.0305 | - | - | 328 B | -| V2_ImHashMap_foreach | 1000 | 21,484.35 ns | 168.389 ns | 149.272 ns | 1.02 | 0.01 | 0.0305 | - | - | 192 B | - -## V4.0 - baseline - -BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 -Intel Core i5-8350U CPU 1.70GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores -.NET Core SDK=6.0.102 - [Host] : .NET Core 6.0.2 (CoreCLR 6.0.222.6406, CoreFX 6.0.222.6406), X64 RyuJIT - DefaultJob : .NET Core 6.0.2 (CoreCLR 6.0.222.6406, CoreFX 6.0.222.6406), X64 RyuJIT - - -| Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | -|----------------------- |------ |-------------:|-------------:|-------------:|-------------:|------:|--------:|-------:|------:|------:|----------:| -| V3_ImHashMap_Enumerate | 1 | 78.29 ns | 3.211 ns | 9.417 ns | 74.97 ns | 1.00 | 0.00 | 0.0509 | - | - | 160 B | -| Dictionary_Enumerate | 1 | 23.15 ns | 0.712 ns | 2.008 ns | 22.54 ns | 0.30 | 0.04 | - | - | - | - | -| | | | | | | | | | | | | -| V3_ImHashMap_Enumerate | 10 | 351.19 ns | 7.127 ns | 20.791 ns | 344.93 ns | 1.00 | 0.00 | 0.0763 | - | - | 240 B | -| Dictionary_Enumerate | 10 | 92.85 ns | 1.957 ns | 4.128 ns | 91.81 ns | 0.27 | 0.02 | - | - | - | - | -| | | | | | | | | | | | | -| V3_ImHashMap_Enumerate | 100 | 3,049.03 ns | 64.798 ns | 186.958 ns | 2,987.63 ns | 1.00 | 0.00 | 0.0763 | - | - | 240 B | -| Dictionary_Enumerate | 100 | 844.11 ns | 16.992 ns | 38.698 ns | 837.12 ns | 0.28 | 0.02 | - | - | - | - | -| | | | | | | | | | | | | -| V3_ImHashMap_Enumerate | 1000 | 34,113.03 ns | 1,105.023 ns | 3,116.738 ns | 33,509.27 ns | 1.00 | 0.00 | 0.1221 | - | - | 480 B | -| Dictionary_Enumerate | 1000 | 14,166.55 ns | 237.173 ns | 221.852 ns | 14,124.19 ns | 0.43 | 0.03 | - | - | - | - | - -## V4 baseline - -BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 -Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores -.NET Core SDK=6.0.202 - [Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT - DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT - -| Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | -|-------------------------------- |------ |-------------:|-----------:|-----------:|-------------:|------:|--------:|-------:|------:|------:|----------:| -| V4_ImHashMap_Enumerate | 1 | 39.51 ns | 0.865 ns | 0.996 ns | 39.43 ns | 1.00 | 0.00 | - | - | - | - | -| V3_ImHashMap_Enumerate | 1 | 44.11 ns | 0.879 ns | 0.822 ns | 44.28 ns | 1.12 | 0.04 | 0.0255 | - | - | 160 B | -| V4_PartitionedHashMap_Enumerate | 1 | 118.94 ns | 1.644 ns | 1.538 ns | 118.91 ns | 3.01 | 0.09 | - | - | - | - | -| V3_PartitionedHashMap_Enumerate | 1 | 180.82 ns | 3.462 ns | 3.238 ns | 181.09 ns | 4.57 | 0.14 | 0.0522 | - | - | 328 B | -| DictionarySlim_Enumerate | 1 | 12.77 ns | 0.254 ns | 0.238 ns | 12.76 ns | 0.32 | 0.01 | - | - | - | - | -| Dictionary_Enumerate | 1 | 15.24 ns | 0.652 ns | 1.817 ns | 14.36 ns | 0.46 | 0.05 | - | - | - | - | -| ConcurrentDictionary_foreach | 1 | 177.04 ns | 2.073 ns | 1.939 ns | 176.68 ns | 4.47 | 0.12 | 0.0100 | - | - | 64 B | -| ImmutableDict_Enumerate | 1 | 161.96 ns | 0.760 ns | 0.635 ns | 161.98 ns | 4.08 | 0.12 | - | - | - | - | -| | | | | | | | | | | | | -| V4_ImHashMap_Enumerate | 10 | 191.91 ns | 3.010 ns | 2.668 ns | 192.57 ns | 1.00 | 0.00 | - | - | - | - | -| V3_ImHashMap_Enumerate | 10 | 223.10 ns | 1.885 ns | 1.574 ns | 223.12 ns | 1.16 | 0.02 | 0.0381 | - | - | 240 B | -| V4_PartitionedHashMap_Enumerate | 10 | 379.75 ns | 4.543 ns | 4.027 ns | 379.74 ns | 1.98 | 0.03 | - | - | - | - | -| V3_PartitionedHashMap_Enumerate | 10 | 604.47 ns | 4.257 ns | 3.774 ns | 603.87 ns | 3.15 | 0.05 | 0.1793 | - | - | 1128 B | -| DictionarySlim_Enumerate | 10 | 73.15 ns | 1.438 ns | 1.345 ns | 72.82 ns | 0.38 | 0.01 | - | - | - | - | -| Dictionary_Enumerate | 10 | 57.95 ns | 0.749 ns | 0.701 ns | 57.86 ns | 0.30 | 0.01 | - | - | - | - | -| ConcurrentDictionary_foreach | 10 | 505.36 ns | 7.959 ns | 7.445 ns | 504.00 ns | 2.64 | 0.05 | 0.0095 | - | - | 64 B | -| ImmutableDict_Enumerate | 10 | 556.35 ns | 10.730 ns | 9.512 ns | 558.53 ns | 2.90 | 0.06 | - | - | - | - | -| | | | | | | | | | | | | -| V4_ImHashMap_Enumerate | 100 | 2,023.65 ns | 33.506 ns | 29.702 ns | 2,031.78 ns | 1.00 | 0.00 | - | - | - | - | -| V3_ImHashMap_Enumerate | 100 | 1,992.46 ns | 23.400 ns | 20.744 ns | 1,992.05 ns | 0.98 | 0.02 | 0.0381 | - | - | 240 B | -| V4_PartitionedHashMap_Enumerate | 100 | 2,626.85 ns | 42.089 ns | 37.311 ns | 2,616.34 ns | 1.30 | 0.02 | - | - | - | - | -| V3_PartitionedHashMap_Enumerate | 100 | 3,469.16 ns | 30.415 ns | 26.962 ns | 3,469.32 ns | 1.71 | 0.03 | 0.4349 | - | - | 2728 B | -| DictionarySlim_Enumerate | 100 | 615.29 ns | 10.217 ns | 9.057 ns | 617.89 ns | 0.30 | 0.01 | - | - | - | - | -| Dictionary_Enumerate | 100 | 578.30 ns | 3.310 ns | 2.764 ns | 578.14 ns | 0.29 | 0.00 | - | - | - | - | -| ConcurrentDictionary_foreach | 100 | 3,444.60 ns | 55.779 ns | 52.176 ns | 3,425.75 ns | 1.70 | 0.03 | 0.0076 | - | - | 64 B | -| ImmutableDict_Enumerate | 100 | 4,557.81 ns | 71.187 ns | 63.105 ns | 4,552.69 ns | 2.25 | 0.03 | - | - | - | - | -| | | | | | | | | | | | | -| V4_ImHashMap_Enumerate | 1000 | 22,259.46 ns | 204.173 ns | 180.994 ns | 22,241.67 ns | 1.00 | 0.00 | - | - | - | - | -| V3_ImHashMap_Enumerate | 1000 | 21,710.99 ns | 288.512 ns | 240.921 ns | 21,770.78 ns | 0.98 | 0.02 | 0.0610 | - | - | 480 B | -| V4_PartitionedHashMap_Enumerate | 1000 | 28,097.70 ns | 392.513 ns | 306.449 ns | 28,134.91 ns | 1.26 | 0.02 | - | - | - | - | -| V3_PartitionedHashMap_Enumerate | 1000 | 31,132.68 ns | 473.705 ns | 443.104 ns | 31,208.71 ns | 1.40 | 0.01 | 0.4272 | - | - | 2728 B | -| DictionarySlim_Enumerate | 1000 | 6,472.19 ns | 53.546 ns | 47.467 ns | 6,482.73 ns | 0.29 | 0.00 | - | - | - | - | -| Dictionary_Enumerate | 1000 | 5,700.68 ns | 65.696 ns | 61.452 ns | 5,698.15 ns | 0.26 | 0.00 | - | - | - | - | -| ConcurrentDictionary_foreach | 1000 | 43,550.74 ns | 848.746 ns | 752.391 ns | 43,765.79 ns | 1.96 | 0.04 | - | - | - | 64 B | -| ImmutableDict_Enumerate | 1000 | 46,089.57 ns | 524.157 ns | 464.651 ns | 46,183.41 ns | 2.07 | 0.03 | - | - | - | - | - -## Interesting first result - -BenchmarkDotNet v0.13.6, Windows 11 (10.0.22621.1992/22H2/2022Update/SunValley2) -11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores -.NET SDK 7.0.306 - [Host] : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 - -| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | -|------------------------- |------ |---------:|---------:|---------:|------:|--------:|----------:|------------:| -| DictionarySlim_Enumerate | 100 | 513.2 ns | 10.32 ns | 17.23 ns | 1.00 | 0.00 | - | NA | -| Dictionary_Enumerate | 100 | 331.4 ns | 6.69 ns | 13.21 ns | 0.65 | 0.04 | - | NA | -| FHashMap91_Enumerate | 100 | 979.7 ns | 18.99 ns | 22.61 ns | 1.91 | 0.09 | - | NA | - -## ... now inlining :) - -| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | -|------------------------- |------ |---------:|--------:|--------:|------:|--------:|----------:|------------:| -| DictionarySlim_Enumerate | 100 | 454.0 ns | 7.96 ns | 7.45 ns | 1.00 | 0.00 | - | NA | -| Dictionary_Enumerate | 100 | 308.2 ns | 6.17 ns | 6.60 ns | 0.68 | 0.02 | - | NA | -| FHashMap91_Enumerate | 100 | 110.0 ns | 2.09 ns | 1.96 ns | 0.24 | 0.01 | - | NA | - -*/ + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |------------------------------ |------ |--------------:|-----------:|-----------:|------:|--------:|--------:|-------:|------:|----------:| + | ImHashMap_EnumerateToArray | 1 | 147.31 ns | 0.567 ns | 0.531 ns | 1.00 | 0.00 | 0.0441 | - | - | 208 B | + | ImHashMap_V1_EnumerateToArray | 1 | 160.45 ns | 0.856 ns | 0.801 ns | 1.09 | 0.01 | 0.0560 | - | - | 264 B | + | ImHashMap_FoldToArray | 1 | 55.05 ns | 0.436 ns | 0.387 ns | 0.37 | 0.00 | 0.0356 | - | - | 168 B | + | ImHashMapSlots_FoldToArray | 1 | 89.21 ns | 1.473 ns | 1.378 ns | 0.61 | 0.01 | 0.0271 | - | - | 128 B | + | DictionarySlim_ToArray | 1 | 150.88 ns | 1.424 ns | 1.189 ns | 1.02 | 0.01 | 0.0408 | - | - | 192 B | + | Dictionary_ToArray | 1 | 40.47 ns | 0.864 ns | 1.093 ns | 0.28 | 0.01 | 0.0119 | - | - | 56 B | + | ConcurrentDictionary_ToArray | 1 | 232.31 ns | 1.981 ns | 1.654 ns | 1.58 | 0.01 | 0.0114 | - | - | 56 B | + | ImmutableDict_ToArray | 1 | 618.68 ns | 11.308 ns | 10.578 ns | 4.20 | 0.07 | 0.0114 | - | - | 56 B | + | | | | | | | | | | | | + | ImHashMap_EnumerateToArray | 10 | 423.85 ns | 8.425 ns | 9.364 ns | 1.00 | 0.00 | 0.1001 | - | - | 472 B | + | ImHashMap_V1_EnumerateToArray | 10 | 492.81 ns | 4.461 ns | 4.173 ns | 1.16 | 0.03 | 0.1726 | - | - | 816 B | + | ImHashMap_FoldToArray | 10 | 213.14 ns | 4.054 ns | 3.981 ns | 0.50 | 0.02 | 0.1054 | - | - | 496 B | + | ImHashMapSlots_FoldToArray | 10 | 255.16 ns | 1.863 ns | 1.743 ns | 0.60 | 0.01 | 0.1016 | - | - | 480 B | + | DictionarySlim_ToArray | 10 | 450.09 ns | 8.918 ns | 10.616 ns | 1.06 | 0.04 | 0.1354 | - | - | 640 B | + | Dictionary_ToArray | 10 | 87.40 ns | 1.696 ns | 1.586 ns | 0.21 | 0.01 | 0.0424 | - | - | 200 B | + | ConcurrentDictionary_ToArray | 10 | 499.22 ns | 4.804 ns | 4.494 ns | 1.17 | 0.03 | 0.0420 | - | - | 200 B | + | ImmutableDict_ToArray | 10 | 1,954.98 ns | 10.732 ns | 10.038 ns | 4.60 | 0.11 | 0.0381 | - | - | 200 B | + | | | | | | | | | | | | + | ImHashMap_EnumerateToArray | 100 | 2,735.61 ns | 34.407 ns | 32.185 ns | 1.00 | 0.00 | 0.4768 | - | - | 2248 B | + | ImHashMap_V1_EnumerateToArray | 100 | 3,368.76 ns | 6.822 ns | 6.048 ns | 1.23 | 0.02 | 1.1597 | 0.0267 | - | 5472 B | + | ImHashMap_FoldToArray | 100 | 1,433.97 ns | 14.981 ns | 14.013 ns | 0.52 | 0.01 | 0.6599 | 0.0038 | - | 3112 B | + | ImHashMapSlots_FoldToArray | 100 | 1,541.62 ns | 8.594 ns | 8.039 ns | 0.56 | 0.01 | 0.6714 | 0.0038 | - | 3168 B | + | DictionarySlim_ToArray | 100 | 2,505.97 ns | 44.927 ns | 37.516 ns | 0.92 | 0.02 | 0.8469 | 0.0076 | - | 4000 B | + | Dictionary_ToArray | 100 | 549.34 ns | 6.559 ns | 6.136 ns | 0.20 | 0.00 | 0.3481 | 0.0019 | - | 1640 B | + | ConcurrentDictionary_ToArray | 100 | 2,236.03 ns | 10.044 ns | 9.395 ns | 0.82 | 0.01 | 0.3471 | - | - | 1640 B | + | ImmutableDict_ToArray | 100 | 15,683.87 ns | 68.105 ns | 60.373 ns | 5.74 | 0.08 | 0.3357 | - | - | 1640 B | + | | | | | | | | | | | | + | ImHashMap_EnumerateToArray | 1000 | 25,723.37 ns | 504.158 ns | 560.370 ns | 1.00 | 0.00 | 3.5706 | 0.1526 | - | 16808 B | + | ImHashMap_V1_EnumerateToArray | 1000 | 34,316.66 ns | 583.573 ns | 545.874 ns | 1.34 | 0.04 | 10.3149 | 1.8921 | - | 48833 B | + | ImHashMap_FoldToArray | 1000 | 16,277.05 ns | 38.330 ns | 33.979 ns | 0.64 | 0.02 | 5.2490 | 0.3052 | - | 24752 B | + | ImHashMapSlots_FoldToArray | 1000 | 15,167.14 ns | 261.927 ns | 218.721 ns | 0.59 | 0.02 | 5.2490 | 0.2899 | - | 24784 B | + | DictionarySlim_ToArray | 1000 | 22,273.30 ns | 384.716 ns | 359.864 ns | 0.87 | 0.01 | 6.9885 | 0.6714 | - | 32896 B | + | Dictionary_ToArray | 1000 | 4,997.89 ns | 20.869 ns | 18.500 ns | 0.20 | 0.00 | 3.3951 | 0.1831 | - | 16040 B | + | ConcurrentDictionary_ToArray | 1000 | 36,859.40 ns | 193.536 ns | 181.034 ns | 1.44 | 0.04 | 3.3569 | 0.1831 | - | 16040 B | + | ImmutableDict_ToArray | 1000 | 155,798.41 ns | 484.101 ns | 452.828 ns | 6.08 | 0.14 | 3.1738 | - | - | 16040 B | + + | Method | Count | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated | + |----------------------------------- |------ |-------------:|-----------:|-----------:|------:|-------:|-------:|------:|----------:| + | ImHashMap_EnumerateToArray | 1 | 153.03 ns | 0.774 ns | 0.686 ns | 1.00 | 0.0441 | - | - | 208 B | + | Experimental_ImHashMap_FoldToArray | 1 | 62.26 ns | 0.180 ns | 0.159 ns | 0.41 | 0.0271 | - | - | 128 B | + | | | | | | | | | | | + | ImHashMap_EnumerateToArray | 10 | 417.19 ns | 4.075 ns | 3.403 ns | 1.00 | 0.1001 | - | - | 472 B | + | Experimental_ImHashMap_FoldToArray | 10 | 237.62 ns | 1.079 ns | 1.009 ns | 0.57 | 0.1016 | - | - | 480 B | + | | | | | | | | | | | + | ImHashMap_EnumerateToArray | 100 | 2,693.86 ns | 7.426 ns | 6.946 ns | 1.00 | 0.4768 | - | - | 2248 B | + | Experimental_ImHashMap_FoldToArray | 100 | 1,517.72 ns | 6.061 ns | 5.669 ns | 0.56 | 0.6561 | 0.0038 | - | 3096 B | + | | | | | | | | | | | + | ImHashMap_EnumerateToArray | 1000 | 26,018.52 ns | 175.262 ns | 146.352 ns | 1.00 | 3.5706 | 0.1831 | - | 16808 B | + | Experimental_ImHashMap_FoldToArray | 1000 | 15,321.95 ns | 69.421 ns | 64.937 ns | 0.59 | 5.2490 | 0.2747 | - | 24736 B | + + ## V3 baseline + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |----------------------------------------- |------ |-------------:|----------:|----------:|------:|--------:|-------:|------:|------:|----------:| + | V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 198.4 ns | 3.64 ns | 3.04 ns | 1.00 | 0.00 | 0.0496 | - | - | 208 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 158.0 ns | 3.23 ns | 3.17 ns | 0.80 | 0.02 | 0.0362 | - | - | 152 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 535.6 ns | 7.97 ns | 8.86 ns | 1.00 | 0.00 | 0.1125 | - | - | 472 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 558.9 ns | 5.35 ns | 5.25 ns | 1.04 | 0.02 | 0.1354 | - | - | 568 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 3,601.3 ns | 54.44 ns | 50.92 ns | 1.00 | 0.00 | 0.5341 | - | - | 2248 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 9,800.0 ns | 110.20 ns | 97.69 ns | 2.72 | 0.04 | 1.0529 | - | - | 4424 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 34,261.7 ns | 371.47 ns | 310.20 ns | 1.00 | 0.00 | 3.9673 | - | - | 16808 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 128,626.8 ns | 988.46 ns | 924.61 ns | 3.76 | 0.04 | 9.2773 | - | - | 38912 B | + + ### Static Enumerate - incomplete + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |----------------------------------------- |------ |------------:|----------:|----------:|------:|--------:|-------:|------:|------:|----------:| + | V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 164.7 ns | 2.23 ns | 1.98 ns | 1.00 | 0.00 | 0.0496 | - | - | 208 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 245.0 ns | 4.92 ns | 5.66 ns | 1.49 | 0.04 | 0.0567 | - | - | 240 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 295.6 ns | 5.87 ns | 5.77 ns | 1.00 | 0.00 | 0.0801 | - | - | 336 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 497.5 ns | 5.77 ns | 5.12 ns | 1.68 | 0.04 | 0.0849 | - | - | 360 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 459.0 ns | 4.86 ns | 4.55 ns | 1.00 | 0.00 | 0.1116 | - | - | 472 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 683.6 ns | 9.36 ns | 8.30 ns | 1.49 | 0.02 | 0.1335 | - | - | 560 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 3,219.3 ns | 36.20 ns | 30.23 ns | 1.00 | 0.00 | 0.5341 | - | - | 2248 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 5,009.4 ns | 57.55 ns | 51.02 ns | 1.56 | 0.02 | 0.5264 | - | - | 2208 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 29,919.7 ns | 363.29 ns | 322.05 ns | 1.00 | 0.00 | 3.9673 | - | - | 16808 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 21,530.8 ns | 292.37 ns | 259.18 ns | 0.72 | 0.01 | 1.8921 | - | - | 8016 B | + + ### Struct enumerator for leafs + + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.630 (2004/?/20H1) + Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores + .NET Core SDK=5.0.100 + [Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT + DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT + + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |----------------------------------------- |------ |-------------:|------------:|------------:|------:|--------:|-------:|------:|------:|----------:| + | V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 131.8 ns | 2.43 ns | 2.27 ns | 1.00 | 0.00 | 0.0458 | - | - | 192 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 107.7 ns | 0.56 ns | 0.44 ns | 0.82 | 0.01 | 0.0305 | - | - | 128 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 251.5 ns | 3.12 ns | 2.77 ns | 1.00 | 0.00 | 0.0782 | - | - | 328 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 214.5 ns | 4.37 ns | 4.86 ns | 0.86 | 0.02 | 0.0591 | - | - | 248 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 402.3 ns | 2.40 ns | 2.00 ns | 1.00 | 0.00 | 0.1106 | - | - | 464 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 469.7 ns | 8.19 ns | 14.13 ns | 1.19 | 0.04 | 0.1144 | - | - | 480 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 2,987.6 ns | 34.44 ns | 28.76 ns | 1.00 | 0.00 | 0.5341 | - | - | 2240 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 8,085.2 ns | 54.39 ns | 50.88 ns | 2.71 | 0.03 | 0.9308 | - | - | 3904 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 29,107.9 ns | 372.54 ns | 330.25 ns | 1.00 | 0.00 | 3.9978 | - | - | 16800 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 117,098.5 ns | 1,388.31 ns | 1,298.62 ns | 4.02 | 0.07 | 8.0566 | - | - | 33992 B | + + + ### Static iterative enumerator with List as a stack + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |----------------------------------------- |------ |------------:|----------:|----------:|------:|--------:|-------:|------:|------:|----------:| + | V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 130.0 ns | 1.58 ns | 1.76 ns | 1.00 | 0.00 | 0.0458 | - | - | 192 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 129.1 ns | 1.55 ns | 1.45 ns | 0.99 | 0.02 | 0.0610 | - | - | 256 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 251.6 ns | 3.58 ns | 3.35 ns | 1.00 | 0.00 | 0.0782 | - | - | 328 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 241.4 ns | 2.97 ns | 2.78 ns | 0.96 | 0.01 | 0.0896 | - | - | 376 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 414.8 ns | 8.12 ns | 10.84 ns | 1.00 | 0.00 | 0.1106 | - | - | 464 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 403.7 ns | 6.25 ns | 5.84 ns | 0.98 | 0.03 | 0.1373 | - | - | 576 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 2,947.2 ns | 23.48 ns | 20.81 ns | 1.00 | 0.00 | 0.5341 | - | - | 2240 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 2,822.2 ns | 16.96 ns | 15.87 ns | 0.96 | 0.01 | 0.5646 | - | - | 2376 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 28,594.2 ns | 307.61 ns | 287.73 ns | 1.00 | 0.00 | 3.9673 | - | - | 16800 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 27,555.7 ns | 318.08 ns | 281.97 ns | 0.96 | 0.01 | 4.0588 | - | - | 16992 B | + + ### Branch3 is a Branch2 + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |----------------------------------------- |------ |-----------:|---------:|---------:|------:|--------:|-------:|------:|------:|----------:| + | V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 329.7 ns | 6.40 ns | 5.99 ns | 1.00 | 0.00 | 0.0782 | - | - | 328 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 313.1 ns | 5.69 ns | 5.59 ns | 0.95 | 0.02 | 0.0877 | - | - | 368 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 513.4 ns | 4.55 ns | 4.26 ns | 1.00 | 0.00 | 0.1106 | - | - | 464 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 525.6 ns | 7.40 ns | 6.92 ns | 1.02 | 0.02 | 0.1354 | - | - | 568 B | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 3,658.7 ns | 53.67 ns | 47.58 ns | 1.00 | 0.00 | 0.5341 | - | - | 2240 B | + | V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 3,388.2 ns | 67.71 ns | 63.34 ns | 0.93 | 0.02 | 0.5646 | - | - | 2368 B | + + ### V3 RTM + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |------------------------------ |------ |-------------:|-------------:|-------------:|-------------:|------:|--------:|-------:|------:|------:|----------:| + | V2_ImHashMap_foreach | 1 | 53.16 ns | 1.107 ns | 1.317 ns | 53.11 ns | 1.00 | 0.00 | 0.0166 | - | - | 104 B | + | V3_ImHashMap_foreach | 1 | 62.12 ns | 1.327 ns | 1.986 ns | 61.59 ns | 1.16 | 0.05 | 0.0267 | - | - | 168 B | + | V3_PartitionedHashMap_foreach | 1 | 238.62 ns | 4.811 ns | 6.084 ns | 235.92 ns | 4.50 | 0.13 | 0.0534 | - | - | 336 B | + | DictionarySlim_foreach | 1 | 12.90 ns | 0.167 ns | 0.156 ns | 12.90 ns | 0.24 | 0.01 | - | - | - | - | + | Dictionary_foreach | 1 | 14.19 ns | 0.217 ns | 0.181 ns | 14.14 ns | 0.27 | 0.01 | - | - | - | - | + | ConcurrentDictionary_foreach | 1 | 153.40 ns | 2.768 ns | 4.142 ns | 151.56 ns | 2.89 | 0.11 | 0.0100 | - | - | 64 B | + | ImmutableDict_foreach | 1 | 268.98 ns | 5.361 ns | 9.528 ns | 268.86 ns | 5.01 | 0.26 | - | - | - | - | + | | | | | | | | | | | | | + | V2_ImHashMap_foreach | 10 | 233.42 ns | 4.541 ns | 4.859 ns | 232.71 ns | 1.00 | 0.00 | 0.0200 | - | - | 128 B | + | V3_ImHashMap_foreach | 10 | 249.12 ns | 4.915 ns | 5.852 ns | 246.87 ns | 1.07 | 0.03 | 0.0391 | - | - | 248 B | + | V3_PartitionedHashMap_foreach | 10 | 746.26 ns | 14.990 ns | 18.409 ns | 748.53 ns | 3.20 | 0.13 | 0.1602 | - | - | 1008 B | + | DictionarySlim_foreach | 10 | 72.54 ns | 0.970 ns | 0.907 ns | 72.42 ns | 0.31 | 0.01 | - | - | - | - | + | Dictionary_foreach | 10 | 58.52 ns | 0.938 ns | 0.733 ns | 58.58 ns | 0.25 | 0.01 | - | - | - | - | + | ConcurrentDictionary_foreach | 10 | 468.65 ns | 9.252 ns | 12.351 ns | 464.12 ns | 2.01 | 0.06 | 0.0095 | - | - | 64 B | + | ImmutableDict_foreach | 10 | 1,127.30 ns | 15.601 ns | 14.593 ns | 1,123.20 ns | 4.82 | 0.10 | - | - | - | - | + | | | | | | | | | | | | | + | V2_ImHashMap_foreach | 100 | 2,355.54 ns | 46.224 ns | 63.271 ns | 2,337.40 ns | 1.00 | 0.00 | 0.0229 | - | - | 160 B | + | V3_ImHashMap_foreach | 100 | 2,423.13 ns | 31.652 ns | 46.395 ns | 2,412.92 ns | 1.03 | 0.04 | 0.0496 | - | - | 320 B | + | V3_PartitionedHashMap_foreach | 100 | 4,268.51 ns | 25.429 ns | 22.542 ns | 4,266.90 ns | 1.81 | 0.05 | 0.4501 | - | - | 2856 B | + | DictionarySlim_foreach | 100 | 570.39 ns | 5.827 ns | 4.866 ns | 570.93 ns | 0.24 | 0.01 | - | - | - | - | + | Dictionary_foreach | 100 | 548.46 ns | 8.579 ns | 7.605 ns | 547.90 ns | 0.23 | 0.01 | - | - | - | - | + | ConcurrentDictionary_foreach | 100 | 2,967.70 ns | 45.435 ns | 44.623 ns | 2,958.11 ns | 1.26 | 0.04 | 0.0076 | - | - | 64 B | + | ImmutableDict_foreach | 100 | 9,988.48 ns | 198.973 ns | 297.813 ns | 9,821.03 ns | 4.24 | 0.14 | - | - | - | - | + | | | | | | | | | | | | | + | V2_ImHashMap_foreach | 1000 | 23,828.30 ns | 433.708 ns | 362.166 ns | 23,743.77 ns | 1.00 | 0.00 | 0.0305 | - | - | 192 B | + | V3_ImHashMap_foreach | 1000 | 26,014.69 ns | 294.125 ns | 245.608 ns | 25,965.82 ns | 1.09 | 0.02 | 0.0610 | - | - | 552 B | + | V3_PartitionedHashMap_foreach | 1000 | 36,582.53 ns | 709.641 ns | 897.469 ns | 36,594.84 ns | 1.54 | 0.04 | 0.4883 | - | - | 3240 B | + | DictionarySlim_foreach | 1000 | 5,591.13 ns | 43.627 ns | 40.809 ns | 5,602.25 ns | 0.23 | 0.00 | - | - | - | - | + | Dictionary_foreach | 1000 | 5,319.86 ns | 51.684 ns | 45.817 ns | 5,308.36 ns | 0.22 | 0.00 | - | - | - | - | + | ConcurrentDictionary_foreach | 1000 | 38,718.40 ns | 466.979 ns | 389.949 ns | 38,728.64 ns | 1.63 | 0.03 | - | - | - | 64 B | + | ImmutableDict_foreach | 1000 | 99,156.35 ns | 1,962.968 ns | 2,181.834 ns | 98,166.03 ns | 4.17 | 0.11 | - | - | - | - | + + ## V3.2 + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |--------------------- |------ |-------------:|-----------:|-----------:|------:|--------:|-------:|------:|------:|----------:| + | V3_ImHashMap_foreach | 1 | 49.59 ns | 0.758 ns | 0.633 ns | 1.00 | 0.00 | 0.0255 | - | - | 160 B | + | V2_ImHashMap_foreach | 1 | 45.96 ns | 0.273 ns | 0.242 ns | 0.93 | 0.01 | 0.0166 | - | - | 104 B | + | | | | | | | | | | | | + | V3_ImHashMap_foreach | 10 | 214.40 ns | 3.915 ns | 3.662 ns | 1.00 | 0.00 | 0.0381 | - | - | 240 B | + | V2_ImHashMap_foreach | 10 | 204.50 ns | 0.963 ns | 0.854 ns | 0.95 | 0.02 | 0.0203 | - | - | 128 B | + | | | | | | | | | | | | + | V3_ImHashMap_foreach | 100 | 2,072.90 ns | 40.826 ns | 41.926 ns | 1.00 | 0.00 | 0.0496 | - | - | 328 B | + | V2_ImHashMap_foreach | 100 | 2,206.31 ns | 29.262 ns | 25.940 ns | 1.07 | 0.03 | 0.0229 | - | - | 160 B | + | | | | | | | | | | | | + | V3_ImHashMap_foreach | 1000 | 21,043.39 ns | 298.240 ns | 232.846 ns | 1.00 | 0.00 | 0.0305 | - | - | 328 B | + | V2_ImHashMap_foreach | 1000 | 21,484.35 ns | 168.389 ns | 149.272 ns | 1.02 | 0.01 | 0.0305 | - | - | 192 B | + + ## V4.0 - baseline + + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 + Intel Core i5-8350U CPU 1.70GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores + .NET Core SDK=6.0.102 + [Host] : .NET Core 6.0.2 (CoreCLR 6.0.222.6406, CoreFX 6.0.222.6406), X64 RyuJIT + DefaultJob : .NET Core 6.0.2 (CoreCLR 6.0.222.6406, CoreFX 6.0.222.6406), X64 RyuJIT + + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |----------------------- |------ |-------------:|-------------:|-------------:|-------------:|------:|--------:|-------:|------:|------:|----------:| + | V3_ImHashMap_Enumerate | 1 | 78.29 ns | 3.211 ns | 9.417 ns | 74.97 ns | 1.00 | 0.00 | 0.0509 | - | - | 160 B | + | Dictionary_Enumerate | 1 | 23.15 ns | 0.712 ns | 2.008 ns | 22.54 ns | 0.30 | 0.04 | - | - | - | - | + | | | | | | | | | | | | | + | V3_ImHashMap_Enumerate | 10 | 351.19 ns | 7.127 ns | 20.791 ns | 344.93 ns | 1.00 | 0.00 | 0.0763 | - | - | 240 B | + | Dictionary_Enumerate | 10 | 92.85 ns | 1.957 ns | 4.128 ns | 91.81 ns | 0.27 | 0.02 | - | - | - | - | + | | | | | | | | | | | | | + | V3_ImHashMap_Enumerate | 100 | 3,049.03 ns | 64.798 ns | 186.958 ns | 2,987.63 ns | 1.00 | 0.00 | 0.0763 | - | - | 240 B | + | Dictionary_Enumerate | 100 | 844.11 ns | 16.992 ns | 38.698 ns | 837.12 ns | 0.28 | 0.02 | - | - | - | - | + | | | | | | | | | | | | | + | V3_ImHashMap_Enumerate | 1000 | 34,113.03 ns | 1,105.023 ns | 3,116.738 ns | 33,509.27 ns | 1.00 | 0.00 | 0.1221 | - | - | 480 B | + | Dictionary_Enumerate | 1000 | 14,166.55 ns | 237.173 ns | 221.852 ns | 14,124.19 ns | 0.43 | 0.03 | - | - | - | - | + + ## V4 baseline + + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 + Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=6.0.202 + [Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT + DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |-------------------------------- |------ |-------------:|-----------:|-----------:|-------------:|------:|--------:|-------:|------:|------:|----------:| + | V4_ImHashMap_Enumerate | 1 | 39.51 ns | 0.865 ns | 0.996 ns | 39.43 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_Enumerate | 1 | 44.11 ns | 0.879 ns | 0.822 ns | 44.28 ns | 1.12 | 0.04 | 0.0255 | - | - | 160 B | + | V4_PartitionedHashMap_Enumerate | 1 | 118.94 ns | 1.644 ns | 1.538 ns | 118.91 ns | 3.01 | 0.09 | - | - | - | - | + | V3_PartitionedHashMap_Enumerate | 1 | 180.82 ns | 3.462 ns | 3.238 ns | 181.09 ns | 4.57 | 0.14 | 0.0522 | - | - | 328 B | + | DictionarySlim_Enumerate | 1 | 12.77 ns | 0.254 ns | 0.238 ns | 12.76 ns | 0.32 | 0.01 | - | - | - | - | + | Dictionary_Enumerate | 1 | 15.24 ns | 0.652 ns | 1.817 ns | 14.36 ns | 0.46 | 0.05 | - | - | - | - | + | ConcurrentDictionary_foreach | 1 | 177.04 ns | 2.073 ns | 1.939 ns | 176.68 ns | 4.47 | 0.12 | 0.0100 | - | - | 64 B | + | ImmutableDict_Enumerate | 1 | 161.96 ns | 0.760 ns | 0.635 ns | 161.98 ns | 4.08 | 0.12 | - | - | - | - | + | | | | | | | | | | | | | + | V4_ImHashMap_Enumerate | 10 | 191.91 ns | 3.010 ns | 2.668 ns | 192.57 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_Enumerate | 10 | 223.10 ns | 1.885 ns | 1.574 ns | 223.12 ns | 1.16 | 0.02 | 0.0381 | - | - | 240 B | + | V4_PartitionedHashMap_Enumerate | 10 | 379.75 ns | 4.543 ns | 4.027 ns | 379.74 ns | 1.98 | 0.03 | - | - | - | - | + | V3_PartitionedHashMap_Enumerate | 10 | 604.47 ns | 4.257 ns | 3.774 ns | 603.87 ns | 3.15 | 0.05 | 0.1793 | - | - | 1128 B | + | DictionarySlim_Enumerate | 10 | 73.15 ns | 1.438 ns | 1.345 ns | 72.82 ns | 0.38 | 0.01 | - | - | - | - | + | Dictionary_Enumerate | 10 | 57.95 ns | 0.749 ns | 0.701 ns | 57.86 ns | 0.30 | 0.01 | - | - | - | - | + | ConcurrentDictionary_foreach | 10 | 505.36 ns | 7.959 ns | 7.445 ns | 504.00 ns | 2.64 | 0.05 | 0.0095 | - | - | 64 B | + | ImmutableDict_Enumerate | 10 | 556.35 ns | 10.730 ns | 9.512 ns | 558.53 ns | 2.90 | 0.06 | - | - | - | - | + | | | | | | | | | | | | | + | V4_ImHashMap_Enumerate | 100 | 2,023.65 ns | 33.506 ns | 29.702 ns | 2,031.78 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_Enumerate | 100 | 1,992.46 ns | 23.400 ns | 20.744 ns | 1,992.05 ns | 0.98 | 0.02 | 0.0381 | - | - | 240 B | + | V4_PartitionedHashMap_Enumerate | 100 | 2,626.85 ns | 42.089 ns | 37.311 ns | 2,616.34 ns | 1.30 | 0.02 | - | - | - | - | + | V3_PartitionedHashMap_Enumerate | 100 | 3,469.16 ns | 30.415 ns | 26.962 ns | 3,469.32 ns | 1.71 | 0.03 | 0.4349 | - | - | 2728 B | + | DictionarySlim_Enumerate | 100 | 615.29 ns | 10.217 ns | 9.057 ns | 617.89 ns | 0.30 | 0.01 | - | - | - | - | + | Dictionary_Enumerate | 100 | 578.30 ns | 3.310 ns | 2.764 ns | 578.14 ns | 0.29 | 0.00 | - | - | - | - | + | ConcurrentDictionary_foreach | 100 | 3,444.60 ns | 55.779 ns | 52.176 ns | 3,425.75 ns | 1.70 | 0.03 | 0.0076 | - | - | 64 B | + | ImmutableDict_Enumerate | 100 | 4,557.81 ns | 71.187 ns | 63.105 ns | 4,552.69 ns | 2.25 | 0.03 | - | - | - | - | + | | | | | | | | | | | | | + | V4_ImHashMap_Enumerate | 1000 | 22,259.46 ns | 204.173 ns | 180.994 ns | 22,241.67 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_Enumerate | 1000 | 21,710.99 ns | 288.512 ns | 240.921 ns | 21,770.78 ns | 0.98 | 0.02 | 0.0610 | - | - | 480 B | + | V4_PartitionedHashMap_Enumerate | 1000 | 28,097.70 ns | 392.513 ns | 306.449 ns | 28,134.91 ns | 1.26 | 0.02 | - | - | - | - | + | V3_PartitionedHashMap_Enumerate | 1000 | 31,132.68 ns | 473.705 ns | 443.104 ns | 31,208.71 ns | 1.40 | 0.01 | 0.4272 | - | - | 2728 B | + | DictionarySlim_Enumerate | 1000 | 6,472.19 ns | 53.546 ns | 47.467 ns | 6,482.73 ns | 0.29 | 0.00 | - | - | - | - | + | Dictionary_Enumerate | 1000 | 5,700.68 ns | 65.696 ns | 61.452 ns | 5,698.15 ns | 0.26 | 0.00 | - | - | - | - | + | ConcurrentDictionary_foreach | 1000 | 43,550.74 ns | 848.746 ns | 752.391 ns | 43,765.79 ns | 1.96 | 0.04 | - | - | - | 64 B | + | ImmutableDict_Enumerate | 1000 | 46,089.57 ns | 524.157 ns | 464.651 ns | 46,183.41 ns | 2.07 | 0.03 | - | - | - | - | + + ## Interesting first result + + BenchmarkDotNet v0.13.6, Windows 11 (10.0.22621.1992/22H2/2022Update/SunValley2) + 11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores + .NET SDK 7.0.306 + [Host] : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 + DefaultJob : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | + |------------------------- |------ |---------:|---------:|---------:|------:|--------:|----------:|------------:| + | DictionarySlim_Enumerate | 100 | 513.2 ns | 10.32 ns | 17.23 ns | 1.00 | 0.00 | - | NA | + | Dictionary_Enumerate | 100 | 331.4 ns | 6.69 ns | 13.21 ns | 0.65 | 0.04 | - | NA | + | FHashMap91_Enumerate | 100 | 979.7 ns | 18.99 ns | 22.61 ns | 1.91 | 0.09 | - | NA | + + ## ... now inlining :) + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | + |------------------------- |------ |---------:|--------:|--------:|------:|--------:|----------:|------------:| + | DictionarySlim_Enumerate | 100 | 454.0 ns | 7.96 ns | 7.45 ns | 1.00 | 0.00 | - | NA | + | Dictionary_Enumerate | 100 | 308.2 ns | 6.17 ns | 6.60 ns | 0.68 | 0.02 | - | NA | + | FHashMap91_Enumerate | 100 | 110.0 ns | 2.09 ns | 1.96 ns | 0.24 | 0.01 | - | NA | + + */ // [Params(1, 10, 100, 1_000)] [Params(100)] diff --git a/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj b/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj index 971340c0..ffcaa8d1 100644 --- a/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj +++ b/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj @@ -18,8 +18,8 @@ - - + + diff --git a/playground/ImTools.Benchmarks/Program.cs b/playground/ImTools.Benchmarks/Program.cs index 1101271a..ba11443b 100644 --- a/playground/ImTools.Benchmarks/Program.cs +++ b/playground/ImTools.Benchmarks/Program.cs @@ -23,7 +23,7 @@ static void Main() // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); + BenchmarkRunner.Run(); // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); diff --git a/src/ImTools/ImTools.cs b/src/ImTools/ImTools.cs index 5befaead..4540c9bc 100644 --- a/src/ImTools/ImTools.cs +++ b/src/ImTools/ImTools.cs @@ -37,7 +37,9 @@ namespace ImTools; using System.Runtime.CompilerServices; // For [MethodImpl(AggressiveInlining)] #if NET7_0_OR_GREATER +using System.Numerics; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; #endif // using static SmallMap; @@ -6841,7 +6843,6 @@ public static class HSmallMap internal const byte ProbeCountShift = 32 - MaxProbeBits; internal const int HashAndIndexMask = ~(MaxProbeCount << ProbeCountShift); internal const int HashAndIndexMaskWithIndex = HashAndIndexMask | MaxProbeCount; - internal const int PaddingHashCount = 7; /// Creates the map with the storage [MethodImpl((MethodImplOptions)256)] @@ -6884,7 +6885,7 @@ public static DebugHashItem[] Explain(this HSmallMap< var indexMask = capacity - 1; var items = new DebugHashItem[hashes.Length]; - for (var i = 0; i < hashes.Length - HSmallMap.PaddingHashCount; i++) + for (var i = 0; i < hashes.Length; i++) { var h = hashes[i]; if (h == 0) @@ -6916,9 +6917,9 @@ public static DebugHashItem[] Explain(this HSmallMap< [MethodImpl((MethodImplOptions)256)] #if NET7_0_OR_GREATER - internal static int NextHash(ref int start, int distance) => Unsafe.Add(ref start, distance); + internal static int GetItem(ref int start, int distance) => Unsafe.Add(ref start, distance); #else - internal static int NextHash(ref int[] start, int distance) => start[distance]; + internal static int GetItem(ref int[] start, int distance) => start[distance]; #endif /// Uses Fibonacci hashing by multiplying the integer on the factor derived from the GoldenRatio @@ -7288,10 +7289,117 @@ public HSmallMap(byte capacityBitShift) // the overflow tail to the hashes is the size of log2N where N==capacityBitShift, // it is probably fine to have the check for the overflow of capacity because it will be mis-predicted only once at the end of loop (it even rarely for the lookup) - _packedHashesAndIndexes = new int[(1 << capacityBitShift) + HSmallMap.PaddingHashCount]; + _packedHashesAndIndexes = new int[1 << capacityBitShift]; _entries.Init(capacityBitShift); } +#if NET7_0_OR_GREATER + internal static readonly Vector256 InitialProbesVec = Vector256.Create(1, 2, 3, 4, 5, 6, 7, 8); + internal static readonly Vector256 VectorSizeVec = Vector256.Create(8); +#endif + + /// Lookup for the key and get the associated value if the key is found + [MethodImpl((MethodImplOptions)256)] + public bool TryGetValue_SIMD(K key, out V value) + { + if (_packedHashesAndIndexes != null) + { + var hash = default(TEq).GetHashCode(key); + + var indexMask = _indexMask; + var hashMiddleMask = HSmallMap.HashAndIndexMask & ~indexMask; + var hashMiddle = hash & hashMiddleMask; + var hashIndex = hash & indexMask; + +#if NET7_0_OR_GREATER + if (Vector256.IsHardwareAccelerated) + { + var indexMaskVec = Vector256.Create(indexMask); + var hashMiddleMaskVec = Vector256.Create(hashMiddleMask); + var hashMiddleVec = Vector256.Create(hashMiddle); + + var hashesAndIndexesVec = MemoryMarshal.Cast>(_packedHashesAndIndexes.AsSpan()); + + var vIndexMask = indexMask >> 3; + var vIndex = hashIndex >> 3; + + // Adjust probes if starting index is not aligned to 8 + // e.g. if hashIndex = 1 -> V(0, 1, 2, 3, 4, 5, 6, 7) to V(-1, 0, 1, 2, 3, 4, 5, 6) + var expectedProbesVec = Vector256.Subtract(InitialProbesVec, Vector256.Create(hashIndex & 7)); + + while (true) + { + var hVec = hashesAndIndexesVec[vIndex++ & vIndexMask]; + + // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions + var hProbeVec = Vector256.ShiftRightLogical(hVec, HSmallMap.ProbeCountShift); + + // while ((h >>> HSmallMap.ProbeCountShift) >= probes) + if (Vector256.GreaterThanOrEqual(hProbeVec, expectedProbesVec) == Vector256.Zero) + break; + + var matchedProbesVec = Vector256.Equals(hProbeVec, expectedProbesVec); + var matchedHashMiddleVec = Vector256.Equals(Vector256.BitwiseAnd(hVec, hashMiddleMaskVec), hashMiddleVec); + var matchVec = Vector256.BitwiseAnd(matchedProbesVec, matchedHashMiddleVec); + var matchMask = matchVec.ExtractMostSignificantBits(); + + // if (((h >>> HSmallMap.ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) + while (matchMask != 0) + { + // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too + var matchIndex = BitOperations.TrailingZeroCount(matchMask); + var entryIndex = hVec.GetElement(matchIndex) & indexMask; + + ref var e = ref _entries.GetSurePresentEntryRef(entryIndex); + if (default(TEq).Equals(e.Key, key)) + { + value = e.Value; + return true; + } + + matchMask &= matchMask - 1; // clear the matched bit + } + + // e.g. V(-1, 0, 1, 2, 3, 4, 5, 6) + 8 -> V(7, 8, 9, 10, 11, 12, 13, 14) + expectedProbesVec = Vector256.Add(expectedProbesVec, VectorSizeVec); + } + + value = default; + return false; + } +#endif +#if NET7_0_OR_GREATER + ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); +#else + var hashesAndIndexes = _packedHashesAndIndexes; +#endif + + var h = HSmallMap.GetItem(ref hashesAndIndexes, hashIndex); + + // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions + var probes = 1; + while ((h >>> HSmallMap.ProbeCountShift) >= probes) + { + // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too + if (((h >>> HSmallMap.ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) + { + ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); + if (default(TEq).Equals(e.Key, key)) + { + value = e.Value; + return true; + } + } + + h = HSmallMap.GetItem(ref hashesAndIndexes, ++hashIndex & indexMask); + ++probes; + } + } + + value = default; + return false; + } + /// Lookup for the key and get the associated value if the key is found [MethodImpl((MethodImplOptions)256)] public bool TryGetValue(K key, out V value) @@ -7311,7 +7419,7 @@ public bool TryGetValue(K key, out V value) var hashesAndIndexes = _packedHashesAndIndexes; #endif - var h = HSmallMap.NextHash(ref hashesAndIndexes, hashIndex); + var h = HSmallMap.GetItem(ref hashesAndIndexes, hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var probes = 1; @@ -7328,8 +7436,116 @@ public bool TryGetValue(K key, out V value) } } - h = HSmallMap.NextHash(ref hashesAndIndexes, ++hashIndex & indexMask); + h = HSmallMap.GetItem(ref hashesAndIndexes, ++hashIndex & indexMask); + ++probes; + } + } + + value = default; + return false; + } + + /// Lookup for the key and get the associated value if the key is found + [MethodImpl((MethodImplOptions)256)] + public bool TryGetValue_Permute(K key, out V value) + { + if (_packedHashesAndIndexes != null) + { + var hash = default(TEq).GetHashCode(key); + + var indexMask = _indexMask; + var hashMiddleMask = HSmallMap.HashAndIndexMask & ~indexMask; + var hashMiddle = hash & hashMiddleMask; + var hashIndex = hash & indexMask; + +#if NET7_0_OR_GREATER + ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); +#else + var hashesAndIndexes = _packedHashesAndIndexes; +#endif + var probes = 0; + while (true) + { ++probes; + var h = HSmallMap.GetItem(ref hashesAndIndexes, hashIndex++ & indexMask); + + var hProbes = h >>> HSmallMap.ProbeCountShift; + if (hProbes < probes) + { + value = default; + return false; + } + + if ((hProbes == probes) & ((h & hashMiddleMask) == hashMiddle)) + { + ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); + if (default(TEq).Equals(e.Key, key)) + { + value = e.Value; + return true; + } + } + } + } + value = default; + return false; + } + + /// Lookup for the key and get the associated value if the key is found + [MethodImpl((MethodImplOptions)256)] + public bool TryGetValue_ILP2(K key, out V value) + { + if (_packedHashesAndIndexes != null) + { + var hash = default(TEq).GetHashCode(key); + + var indexMask = _indexMask; + var hashMiddleMask = HSmallMap.HashAndIndexMask & ~indexMask; + var hashMiddle = hash & hashMiddleMask; + var hashIndex = hash & indexMask; + +#if NET7_0_OR_GREATER + ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); +#else + var hashesAndIndexes = _packedHashesAndIndexes; +#endif + var iter = 0; + var expectedProbes0 = 1; + var expectedProbes1 = 2; + while (true) + { + var h0 = HSmallMap.GetItem(ref hashesAndIndexes, (iter + hashIndex) & indexMask); + var h1 = HSmallMap.GetItem(ref hashesAndIndexes, (iter + hashIndex + 1) & indexMask); + + var h0Probes = h0 >>> HSmallMap.ProbeCountShift; + var h1Probes = h1 >>> HSmallMap.ProbeCountShift; + + if ((h0Probes == expectedProbes0) & (h0 & hashMiddleMask) == hashMiddle) + { + ref var e = ref _entries.GetSurePresentEntryRef(h0 & indexMask); + if (default(TEq).Equals(e.Key, key)) + { + value = e.Value; + return true; + } + } + + if ((h1Probes == expectedProbes1) & (h1 & hashMiddleMask) == hashMiddle) + { + ref var e = ref _entries.GetSurePresentEntryRef(h1 & indexMask); + if (default(TEq).Equals(e.Key, key)) + { + value = e.Value; + return true; + } + } + + if (h1Probes < expectedProbes1) + break; + + iter += 2; + expectedProbes0 += 2; + expectedProbes1 += 2; } } @@ -7363,7 +7579,7 @@ public int GetEntryIndex(K key) var hashesAndIndexes = _packedHashesAndIndexes; #endif - var h = HSmallMap.NextHash(ref hashesAndIndexes, hashIndex); + var h = HSmallMap.GetItem(ref hashesAndIndexes, hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var probes = 1; @@ -7377,7 +7593,7 @@ public int GetEntryIndex(K key) return h & indexMask; } - h = HSmallMap.NextHash(ref hashesAndIndexes, ++hashIndex & indexMask); + h = HSmallMap.GetItem(ref hashesAndIndexes, ++hashIndex & indexMask); ++probes; } } @@ -7438,10 +7654,6 @@ public ref V GetOrAddValueRef(K key) // 3. We did not find the hash and therefore the key, so insert the new entry var hRobinHooded = h; h = (probes << HSmallMap.ProbeCountShift) | hashMiddle | entryCount; // old entryCount is the new index of the added hash - - if ((hashIndex & indexMask) < HSmallMap.PaddingHashCount) - HSmallMap.GetItemRef(ref hashesAndIndexes, indexMask + 1 + (hashIndex & indexMask)) = h; // Copy the hash to the padding - #if DEBUG _dbg.DebugCollectAndOutputProbes(probes, "Add"); #endif @@ -7460,8 +7672,6 @@ public ref V GetOrAddValueRef(K key) #endif var tmp = h; h = (probes << HSmallMap.ProbeCountShift) | (hRobinHooded & HSmallMap.HashAndIndexMask); - if ((hashIndex & indexMask) < HSmallMap.PaddingHashCount) - HSmallMap.GetItemRef(ref hashesAndIndexes, indexMask + 1 + (hashIndex & indexMask)) = h; // Copy the hash to the padding hRobinHooded = tmp; probes = hRobinHooded >>> HSmallMap.ProbeCountShift; @@ -7530,13 +7740,7 @@ public bool TryRemove(K key) { // Decrease the probe count by one cause we moving the hash closer to the ideal index emptied = (((h >>> HSmallMap.ProbeCountShift) - 1) << HSmallMap.ProbeCountShift) | (h & HSmallMap.HashAndIndexMask); - if (emptiedIndex < HSmallMap.PaddingHashCount) - HSmallMap.GetItemRef(ref hashesAndIndexes, indexMask + 1 + emptiedIndex) = emptied; // Copy the hash to the padding - h = 0; - emptiedIndex = hashIndex & indexMask; - if (emptiedIndex < HSmallMap.PaddingHashCount) - HSmallMap.GetItemRef(ref hashesAndIndexes, indexMask + 1 + emptiedIndex) = 0; // Copy the hash to the padding emptied = ref h; h = ref HSmallMap.GetItemRef(ref hashesAndIndexes, ++hashIndex & indexMask); @@ -7549,9 +7753,9 @@ internal int ResizeHashes(int indexMask) if (indexMask == 0) { _indexMask = (1 << HSmallMap.MinCapacityBits) - 1; - _packedHashesAndIndexes = new int[(1 << HSmallMap.MinCapacityBits) + HSmallMap.PaddingHashCount]; + _packedHashesAndIndexes = new int[1 << HSmallMap.MinCapacityBits]; #if DEBUG - Debug.WriteLine($"[ResizeHashes] new empty hashes {1} -> {_packedHashesAndIndexes.Length - HSmallMap.PaddingHashCount} + {HSmallMap.PaddingHashCount} padding"); + Debug.WriteLine($"[ResizeHashes] new empty hashes {1} -> {_packedHashesAndIndexes.Length}"); #endif return (1 << HSmallMap.MinCapacityBits) - 1; } @@ -7560,7 +7764,7 @@ internal int ResizeHashes(int indexMask) var newHashAndIndexMask = HSmallMap.HashAndIndexMask & ~oldCapacity; var newIndexMask = (indexMask << 1) | 1; - var newHashesAndIndexes = new int[(oldCapacity << 1) + HSmallMap.PaddingHashCount]; + var newHashesAndIndexes = new int[oldCapacity << 1]; #if NET7_0_OR_GREATER ref var newHashes = ref MemoryMarshal.GetArrayDataReference(newHashesAndIndexes); @@ -7575,7 +7779,7 @@ internal int ResizeHashes(int indexMask) // and! the hashes at the beginning robin hooded by the wrapped-around hashes var i = 0; while ((oldHash >>> HSmallMap.ProbeCountShift) > 1) - oldHash = HSmallMap.NextHash(ref oldHashes, ++i); + oldHash = HSmallMap.GetItem(ref oldHashes, ++i); var oldCapacityWithOverflowSegment = i + oldCapacity; while (true) @@ -7600,14 +7804,12 @@ internal int ResizeHashes(int indexMask) if (++i >= oldCapacityWithOverflowSegment) break; - oldHash = HSmallMap.NextHash(ref oldHashes, i & indexMask); + oldHash = HSmallMap.GetItem(ref oldHashes, i & indexMask); } #if DEBUG - Debug.WriteLine($"[ResizeHashes] {oldCapacity} -> {newHashesAndIndexes.Length - HSmallMap.PaddingHashCount} + {HSmallMap.PaddingHashCount} padding"); + Debug.WriteLine($"[ResizeHashes] {oldCapacity} -> {newHashesAndIndexes.Length}"); _dbg.DebugReCollectAndOutputProbes(newHashesAndIndexes); #endif - // Copy the padding hashes to the end of the new hashes array - Array.Copy(newHashesAndIndexes, 0, newHashesAndIndexes, newHashesAndIndexes.Length - HSmallMap.PaddingHashCount, HSmallMap.PaddingHashCount); _indexMask = _indexMask << 1 | 1; _packedHashesAndIndexes = newHashesAndIndexes; From 99d19e976e977d6434c2d8700d47e1549ebe72c3 Mon Sep 17 00:00:00 2001 From: dadhi Date: Tue, 10 Jun 2025 14:36:38 +0200 Subject: [PATCH 07/54] experiment FHM11 failed --- playground/Experiments/FHashMap11.cs | 540 ++++++++++++++++++ .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 30 +- .../Playground/FHashMap11Tests.cs | 414 ++++++++++++++ test/ImTools.UnitTests/SmallMapTests.cs | 14 - 4 files changed, 981 insertions(+), 17 deletions(-) create mode 100644 playground/Experiments/FHashMap11.cs create mode 100644 test/ImTools.UnitTests/Playground/FHashMap11Tests.cs diff --git a/playground/Experiments/FHashMap11.cs b/playground/Experiments/FHashMap11.cs new file mode 100644 index 00000000..69f006fd --- /dev/null +++ b/playground/Experiments/FHashMap11.cs @@ -0,0 +1,540 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +#if NET7_0_OR_GREATER +using System.Runtime.InteropServices; +#endif + +namespace ImTools.Experiments; + +using static FHashMap11; + +public static class FHashMap11 +{ + /// 2^32 / phi for the Fibonacci hashing, where phi is the golden ratio ~1.61803 + public const uint GoldenRatio32 = 2654435769; + + internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) + internal const byte MinCapacityBits = 3; // 1 << 3 == 8 + + public struct KeyEntry + { + public int Probe; + public int Hash; + public int Index; + public K Key; + public bool IsEmpty => Probe == 0; + public override string ToString() => Probe == 0 ? "" : $"probe:{Probe},index:{Index},hash:0b{Hash:b0},key:{Key}"; + } + + /// Converts the packed hashes and entries into the human readable info. + /// This also used for the debugging view of the and by the Verify... methods in tests. + public static KeyEntry[] Explain(this ref FHashMap11 map) + where TEq : struct, IEq => map.KeyEntries; + + /// Verifies that the hashes correspond to the keys stroed in the entries. May be called from the tests. + public static void VerifyHashesAndKeysEq(this FHashMap11 map, Action assertEq) + where TEq : struct, IEq + { + foreach (var e in map.KeyEntries) + if (!e.IsEmpty) + assertEq(default(TEq).GetHashCode(e.Key) == e.Hash); + } + + /// Verifies that there is no duplicate keys stored in hashes -> entries. May be called from the tests. + public static void VerifyNoDuplicateKeys(this FHashMap11 map, Action assertKey) + where TEq : struct, IEq + { + // Verify the indexes do no contains duplicate keys + var uniq = new Dictionary(map.Count); + var keys = map.KeyEntries; + for (var i = 0; i < keys.Length; i++) + { + var k = keys[i]; + if (k.IsEmpty) + continue; + var key = k.Key; + if (!uniq.TryGetValue(key, out var count)) + uniq.Add(key, 1); + else + assertKey(key); + } + } + + public static void VerifyProbesAreFitRobinHood(this FHashMap11 map, Action reportFail) + where TEq : struct, IEq + { + var hashes = map.KeyEntries; + var prevProbe = -1; + for (var i = 0; i < hashes.Length; i++) + { + var h = hashes[i]; + var probe = h.Probe; + if (prevProbe != -1 & probe - prevProbe > 1) + reportFail($"Probes are not consequent: {prevProbe}, {probe} for {i}: p{probe}, {h.Index} -> {h.Key}"); + prevProbe = probe; + } + } + + /// Verifies that the map contains all passed keys. May be called from the tests. + public static void VerifyContainAllKeys(this FHashMap11 map, IEnumerable expectedKeys, Action assertContainKey) + where TEq : struct, IEq + { + foreach (var key in expectedKeys) + assertContainKey(map.TryGetValue(key, out _), key); + } + + [MethodImpl((MethodImplOptions)256)] +#if NET7_0_OR_GREATER + internal static ref T GetItemRef(ref T start, int distance) => ref Unsafe.Add(ref start, distance); +#else + internal static ref T GetItemRef(ref T[] start, int distance) => ref start[distance]; +#endif + + [MethodImpl((MethodImplOptions)256)] +#if NET7_0_OR_GREATER + internal static T GetItem(ref T start, int distance) => Unsafe.Add(ref start, distance); +#else + internal static T GetItem(ref T[] start, int distance) => start[distance]; +#endif + +#if DEBUG + internal struct ProbesTracker + { + internal int MaxProbes; + internal int[] Probes; + public ProbesTracker() + { + MaxProbes = 1; + Probes = new int[1]; + } + + // will output something like + // [Add] Probes abs max = 10, curr max = 6, all = [1: 180, 2: 103, 3: 59, 4: 23, 5: 3, 6: 1]; first 4 probes are 365 out of 369 + internal void DebugOutputProbes(string label) + { + Debug.Write($"[{label}] Probes abs max={MaxProbes}, curr max={Probes.Length}, all=["); + var first4probes = 0; + var allProbes = 0; + for (var i = 0; i < Probes.Length; i++) + { + var p = Probes[i]; + Debug.Write($"{(i == 0 ? "" : ", ")}{i + 1}: {p}"); + if (i < 4) + first4probes += p; + allProbes += p; + } + Debug.WriteLine($"]; first 4 probes are {first4probes} out of {allProbes}"); + } + + internal void DebugCollectAndOutputProbes(int probes, [CallerMemberName] string label = "") + { + Probes ??= []; + if (probes > Probes.Length) + { + if (probes > MaxProbes) + MaxProbes = probes; + Array.Resize(ref Probes, probes); + Probes[probes - 1] = 1; + DebugOutputProbes(label); + } + else + ++Probes[probes - 1]; + } + + internal void DebugReCollectAndOutputProbes(KeyEntry[] hashesAndKeys, [CallerMemberName] string label = "") + { + var newProbes = new int[1]; + foreach (var h in hashesAndKeys) + { + var p = h.Probe; + if (p == 0) continue; + if (p > MaxProbes) + MaxProbes = p; + if (p > newProbes.Length) + Array.Resize(ref newProbes, p); + ++newProbes[p - 1]; + } + Probes = newProbes; + DebugOutputProbes(label); + } + + internal void RemoveProbe(int probe) + { + ref var p = ref Probes[probe - 1]; + --p; + if (p == 0 && probe == Probes.Length) + { + Array.Resize(ref Probes, probe - 1); + if (MaxProbes == probe) + --MaxProbes; + } + } + } +#endif + + public class DebugProxy + where TEq : struct, IEq + { + private readonly FHashMap11 _map; + public DebugProxy(FHashMap11 map) => _map = map; + public KeyEntry[] KeyEntries => _map.KeyEntries; + public V[] Values => _map.Values; + } +} + +// todo: @improve ? how/where to add SIMD to improve CPU utilization but not losing perf for smaller sizes +/// +/// Fast and less-allocating hash map without thread safety nets. Please measure it in your own use case before use. +/// It is configurable in regard of hash calculation/equality via and +/// in regard of key-value storage via +/// +/// Features: +/// - Implemented as a struct so that the empty/default map does not allocate on heap +/// - Hashes and key-values are the separate collections enabling better cash locality and faster performance (data-oriented design) +/// - No SIMD for now to avoid complexity and costs for the smaller maps, so the map is more fit for the smaller sizes. +/// - Provides the "stable" enumeration of the entries in the added order +/// +/// +[DebuggerTypeProxy(typeof(DebugProxy<,,>))] +[DebuggerDisplay("Count={Count}")] +public struct FHashMap11 : IReadOnlyCollection> + where TEq : struct, IEq +{ +#if DEBUG + ProbesTracker _dbg = new(); +#endif + private byte _capacityBitShift; + internal int _count; + public KeyEntry[] KeyEntries; + public V[] Values; + + /// Get the number of the key/hash entries in array. Should be more than + public int Capacity => 1 << _capacityBitShift; + + /// + public int Count => _count; + + /// Capacity calculates as `1 << capacityBitShift` + public FHashMap11(byte capacityBitShift) + { + _capacityBitShift = capacityBitShift; + + // the overflow tail to the hashes is the size of log2N where N==capacityBitShift, + // it is probably fine to have the check for the overlow of capacity because it will be mispredicted only once at the end of loop (it even rarely for the lookup) + KeyEntries = new KeyEntry[1 << capacityBitShift]; + Values = new V[1 << capacityBitShift]; + } + + /// Lookup for the key and get the associated value if the key is found + [MethodImpl((MethodImplOptions)256)] + public bool TryGetValue(K key, out V value) + { + if (KeyEntries != null) + { + var hash = default(TEq).GetHashCode(key); + + var indexMask = (1 << _capacityBitShift) - 1; + var hashIndex = hash & indexMask; + +#if NET7_0_OR_GREATER + ref var hashAndKeyEntries = ref MemoryMarshal.GetArrayDataReference(KeyEntries); +#else + var hashAndKeyEntries = KeyEntries; +#endif + var h = GetItem(ref hashAndKeyEntries, hashIndex); + + var probe = 1; + while (h.Probe >= probe) + { + if ((h.Probe == probe) & (h.Hash == hash) && default(TEq).Equals(h.Key, key)) + { + value = Values[h.Index]; + return true; + } + + h = GetItem(ref hashAndKeyEntries, ++hashIndex & indexMask); + ++probe; + } + } + + value = default; + return false; + } + + /// Lookup for the key and get the associated value or the default value if the key is not found + [MethodImpl((MethodImplOptions)256)] + public V GetValueOrDefault(K key, V defaultValue = default) => + TryGetValue(key, out var value) ? value : defaultValue; + + /// Gets the reference to the existing value of the provided key, or the default value to set for the newly added key. + [MethodImpl((MethodImplOptions)256)] + public ref V GetOrAddValueRef(K key) + { + var hash = default(TEq).GetHashCode(key); + + var indexMask = (1 << _capacityBitShift) - 1; + var currCount = _count; + + // if the free space is less than 1/8 of capacity (12.5%) then Resize + if (indexMask - currCount <= (indexMask >>> MinFreeCapacityShift)) + indexMask = ResizeHashes(indexMask); + + var hashIndex = hash & indexMask; + +#if NET7_0_OR_GREATER + ref var hashAndKeyEntries = ref MemoryMarshal.GetArrayDataReference(KeyEntries); +#else + var hashAndKeyEntries = KeyEntries; +#endif + ref var h = ref GetItemRef(ref hashAndKeyEntries, hashIndex); + + // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions + var probe = 1; + while (h.Probe >= probe) + { + // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too + if ((h.Probe == probe) & (h.Hash == hash) && default(TEq).Equals(h.Key, key)) + return ref Values[h.Index]; + + h = ref GetItemRef(ref hashAndKeyEntries, ++hashIndex & indexMask); + ++probe; + } + + // nothing found, add a new entry + _count = currCount + 1; + + // 3. We did not find the hash and therefore the key, so insert the new entry + var hRobinHooded = h; + + h.Hash = hash; + h.Probe = probe; + h.Index = currCount; + h.Key = key; + +#if DEBUG + _dbg.DebugCollectAndOutputProbes(probe, "Add"); +#endif + // 4. If the robin hooded hash is empty then we stop + // 5. Otherwise we steal the slot with the smaller probes + probe = hRobinHooded.Probe; + while (probe != 0) + { + h = ref GetItemRef(ref hashAndKeyEntries, ++hashIndex & indexMask); + if (h.Probe < ++probe) + { +#if DEBUG + if (h.Probe != 0) + _dbg.RemoveProbe(h.Probe); + _dbg.DebugCollectAndOutputProbes(probe, "Add-RH"); +#endif + var tmp = h; + + h = hRobinHooded; + h.Probe = probe; + + hRobinHooded = tmp; + probe = hRobinHooded.Probe; + } + } + + if (currCount == Values.Length) + Array.Resize(ref Values, Values.Length << 1); + return ref Values[currCount]; + } + + /// Same as `GetOrAddValueRef` but provides the value to add or override for the existing key + [MethodImpl((MethodImplOptions)256)] + public void AddOrUpdate(K key, in V value) => + GetOrAddValueRef(key) = value; + + /// Removes the hash and entry of the provided key or returns + [MethodImpl((MethodImplOptions)256)] + public bool TryRemove(K key) + { + var hash = default(TEq).GetHashCode(key); + + var indexMask = (1 << _capacityBitShift) - 1; + var hashIndex = hash & indexMask; + +#if NET7_0_OR_GREATER + ref var keyEntries = ref MemoryMarshal.GetArrayDataReference(KeyEntries); +#else + var keyEntries = KeyEntries; +#endif + ref var h = ref GetItemRef(ref keyEntries, hashIndex); + + var removed = false; + + // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions + var probe = 1; + while (h.Probe >= probe) + { + // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too + if ((h.Probe == probe) & (h.Hash == hash) && default(TEq).Equals(h.Key, key)) + { + removed = true; + + --_count; + Values[h.Index] = default; // todo: @perf fragmentation issue, add the slot to the free list? + h = default; +#if DEBUG + _dbg.RemoveProbe(probe); +#endif + break; + } + h = ref GetItemRef(ref keyEntries, ++hashIndex & indexMask); + ++probe; + } + + if (!removed) + return false; + + ref var emptied = ref h; + h = ref GetItemRef(ref keyEntries, ++hashIndex & indexMask); + + // move the next hash into the emptied slot until the next hash is empty or ideally positioned (hash is 0 or probe is 1) + while (h.Probe > 1) + { + emptied = h; + emptied.Probe -= 1; // decrease the probe count by one cause we moving the hash closer to the ideal index + h = default; + + emptied = ref h; + h = ref GetItemRef(ref keyEntries, ++hashIndex & indexMask); + } + return true; + } + + internal int ResizeHashes(int indexMask) + { + if (indexMask == 0) + { + _capacityBitShift = MinCapacityBits; + KeyEntries = new KeyEntry[1 << MinCapacityBits]; + Values = new V[1 << MinCapacityBits]; +#if DEBUG + Debug.WriteLine($"[ResizeHashes] new empty hashes {1} -> {KeyEntries.Length}"); +#endif + return (1 << MinCapacityBits) - 1; + } + + var oldCapacity = indexMask + 1; + var newKeyEntries = new KeyEntry[oldCapacity << 1]; + var newIndexMask = indexMask << 1 | 1; + +#if NET7_0_OR_GREATER + ref var newKeys = ref MemoryMarshal.GetArrayDataReference(newKeyEntries); + ref var oldKeys = ref MemoryMarshal.GetArrayDataReference(KeyEntries); + var oldKey = oldKeys; +#else + var newKeys = newKeyEntries; + var oldKeys = KeyEntries; + var oldKey = oldKeys[0]; +#endif + // Overflow segment is wrapped-around hashes and! the hashes at the beginning robin-hooded by the wrapped-around hashes + // so we skip them to start from first ideal or empty entry + var i = 0; + while (oldKey.Probe > 1) + oldKey = GetItem(ref oldKeys, ++i); + var oldCapacityWithOverflowSegment = i + oldCapacity; + + while (true) + { + if (oldKey.Probe != 0) + { + var newHashIndex = oldKey.Hash & newIndexMask; + + // no need for robin-hooding because we already did it for the old hashes and + // now just sparsing the hashes which are already in order into the new array + var newProbe = 1; + ref var newKey = ref GetItemRef(ref newKeys, newHashIndex); + while (newKey.Probe != 0) + { + newKey = ref GetItemRef(ref newKeys, ++newHashIndex & newIndexMask); + ++newProbe; + } + newKey = oldKey; + newKey.Probe = newProbe; + } + if (++i >= oldCapacityWithOverflowSegment) + break; + + oldKey = GetItem(ref oldKeys, i & indexMask); + } +#if DEBUG + Debug.WriteLine($"[ResizeHashes] {oldCapacity} -> {newKeyEntries.Length}"); + _dbg.DebugReCollectAndOutputProbes(newKeyEntries); +#endif + ++_capacityBitShift; + KeyEntries = newKeyEntries; + return newIndexMask; + } + + /// + [MethodImpl((MethodImplOptions)256)] + public Enumerator GetEnumerator() => new Enumerator(KeyEntries, Values, _count); + + /// + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// Enumerator of the entries in the order of their addition to the map + public struct Enumerator : IEnumerator> + { + private int _keyIndex; + private int _index; + private KeyValuePair _current; + private readonly KeyEntry[] _keys; + private readonly V[] _values; + private int _count; + internal Enumerator(KeyEntry[] keys, V[] values, int count) + { + _keyIndex = 0; + _index = 0; + _current = default; + _keys = keys; + _values = values; + _count = count; + } + + /// Move to the next entry in the order of their addition to the map + [MethodImpl((MethodImplOptions)256)] + public bool MoveNext() + { + if (_index < _count) + { + // skip empty hashes + var i = _keyIndex; + while (_keys[i].Probe == 0) + ++i; + _keyIndex = i; + ref var k = ref _keys[i]; + _current = new KeyValuePair(k.Key, _values[k.Index]); + + ++_keyIndex; + ++_index; + return true; + } + + _current = default; + return false; + } + + public KeyValuePair Current => _current; + object IEnumerator.Current => _current; + + void IEnumerator.Reset() + { + _keyIndex = 0; + _index = 0; + _current = default; + } + + public void Dispose() { } + } +} diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 8127a7e1..c0cfe677 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -16,6 +16,7 @@ using SmallMapTypeString = ImTools.HSmallMap, ImTools.HSmallMap.SingleArrayEntries>>; using FHashMapTypeString = FastExpressionCompiler.ImTools.FHashMap, FastExpressionCompiler.ImTools.FHashMap.SingleArrayEntries>>; using BenchmarkDotNet.Order; +using ImTools.Experiments; #nullable disable @@ -1325,7 +1326,7 @@ private static class TypeKey } [MemoryDiagnoser, RankColumn, Orderer(SummaryOrderPolicy.FastestToSlowest)] - [HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchMispredictions, HardwareCounter.BranchInstructions)] + // [HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchMispredictions, HardwareCounter.BranchInstructions)] public class Lookup { /* @@ -2289,6 +2290,15 @@ .NET SDK 9.0.203 | DictionarySlim_TryGetValue | 100 | 4.777 ns | 0.1559 ns | 0.4472 ns | 4.576 ns | 1.01 | 0.13 | 1 | - | NA | | FHashMap_TryGetValue | 100 | 6.020 ns | 0.1795 ns | 0.2335 ns | 6.017 ns | 1.27 | 0.13 | 2 | - | NA | | SmallMap_TryGetValue | 100 | 6.021 ns | 0.1808 ns | 0.4827 ns | 6.293 ns | 1.27 | 0.15 | 2 | - | NA | + + ## SmallMap vs FHashMap11 + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|-----:|----------:|------------:| + | DictionarySlim_TryGetValue | 1000 | 2.225 us | 0.0445 us | 0.1313 us | 2.162 us | 1.00 | 0.08 | 1 | - | NA | + | SmallMap_TryGetValue | 1000 | 2.364 us | 0.0387 us | 0.0302 us | 2.377 us | 1.07 | 0.06 | 1 | - | NA | + | FHashMap11_TryGetValue | 1000 | 2.387 us | 0.0477 us | 0.1170 us | 2.434 us | 1.08 | 0.08 | 1 | - | NA | + */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] // [Params(1, 10, 100)] @@ -2310,6 +2320,7 @@ public void Populate() _fHashMap9 = FillFHashMap9(); _fHashMap91 = FillFHashMap91(); _smallMap = FillSmallMap(); + _fHashMap11 = FillFHashMap11(); _fHashMap = FillFHashMap(); _concurrentDict = ConcurrentDict(); _immutableDict = ImmutableDict(); @@ -2527,6 +2538,19 @@ public SmallMapTypeString FillSmallMap() private SmallMapTypeString _smallMap; + public FHashMap11> FillFHashMap11() + { + var map = new FHashMap11>(); + + foreach (var key in _keys.Take(Count)) + map.GetOrAddValueRef(key) = "a"; + + map.GetOrAddValueRef(LookupKey) = "!"; + return map; + } + + private FHashMap11> _fHashMap11; + public FHashMapTypeString FillFHashMap() { var map = new FHashMapTypeString(); @@ -2734,12 +2758,12 @@ public int SmallMap_TryGetValue() } [Benchmark] - public int SmallMap_TryGetValue_Permute() + public int FHashMap11_TryGetValue() { var count = 0; foreach (var k in _randomLookupKeys) { - _smallMap.TryGetValue_Permute(k, out var result); + _fHashMap11.TryGetValue(k, out var result); count += result.Length; } diff --git a/test/ImTools.UnitTests/Playground/FHashMap11Tests.cs b/test/ImTools.UnitTests/Playground/FHashMap11Tests.cs new file mode 100644 index 00000000..48c1c65c --- /dev/null +++ b/test/ImTools.UnitTests/Playground/FHashMap11Tests.cs @@ -0,0 +1,414 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; + +namespace ImTools.Experiments.UnitTests; + +using static FHashMap11; + +[TestFixture] +public class FHashMap11Tests +{ + internal static void Verify(FHashMap11 map, IEnumerable expectedKeys) + where TEq : struct, IEq + { + map.VerifyHashesAndKeysEq(Assert.True); + map.VerifyProbesAreFitRobinHood(Assert.Fail); + map.VerifyNoDuplicateKeys(key => Assert.Fail($"Duplicate key: {key}")); + if (expectedKeys != null) + map.VerifyContainAllKeys(expectedKeys, (contains, key) => Assert.True(contains, $"Key not found:`{key}`")); + } + + [Test] + public void Real_world_test_AddOrUpdate() + { + var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(100).ToArray(); + + var map = new FHashMap11>(); + + foreach (var key in types) + map.AddOrUpdate(key, "a"); + + map.AddOrUpdate(typeof(Console), "!"); + + var found = map.TryGetValue(typeof(Console), out var value); + Assert.IsTrue(found); + Assert.AreEqual("!", value); + + Verify(map, types); + } + + [Test] + public void Real_world_test_AddOrUpdate_NO_Resize() + { + var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(100).ToArray(); + + var map = new FHashMap11>(8); + + foreach (var key in types) + map.AddOrUpdate(key, "a"); + + map.AddOrUpdate(typeof(FHashMap11Tests), "!"); + + Assert.AreEqual(101, map.Count); + + Verify(map, types); + } + + [Test] + public void Real_world_test_with_TryRemove_from_1000_items() + { + var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(1000).ToArray(); + + var map = new FHashMap11>(); + + foreach (var key in types) + map.AddOrUpdate(key, "a"); + + map.AddOrUpdate(typeof(FHashMap11Tests), "!"); + Assert.AreEqual(1001, map.Count); + + Assert.IsTrue(map.TryRemove(typeof(FHashMap11Tests))); + Assert.AreEqual(1000, map.Count); + + Verify(map, types); + } + + [Test] + public void Real_world_test_with_Enumerator_and_TryRemove_the_entries() + { + var count = 1000; + var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(count).ToList(); + Assert.AreEqual(count, types.Count); + + var map = new FHashMap11>(); + + foreach (var key in types) + map.AddOrUpdate(key, "a"); + + var keys = map.Select(kv => kv.Key).ToList(); + CollectionAssert.AreEquivalent(types, keys); + + Assert.IsTrue(map.TryRemove(types[0])); + Assert.IsTrue(map.TryRemove(types[999])); + Assert.IsTrue(map.TryRemove(types[377])); + Assert.IsTrue(map.TryRemove(types[733])); + Assert.AreEqual(count - 4, map.Count); + + // remove in the reverse order to keep the correct index in regard to map + types.RemoveAt(999); + types.RemoveAt(733); + types.RemoveAt(377); + types.RemoveAt(0); + Assert.AreEqual(count - 4, types.Count); + + // Check the second enumeration is working + var keys2 = map.Select(kv => kv.Key).ToList(); + CollectionAssert.AreEquivalent(types, keys2); + + Verify(map, types); + } + + [Test] + public void Simplified_test_with_equal_hashes_RefEq() + { + var map = new FHashMap11>(); + + var keys = new[] { typeof(Tuple<>), typeof(Tuple<,>), typeof(Tuple<,,>) }; + var i = 1; + foreach (var k in keys) + map.AddOrUpdate(k, "" + i++); + + Assert.AreEqual(3, map.Count); + + Assert.IsTrue(map.TryRemove(typeof(Tuple<,,>))); + Assert.AreEqual(2, map.Count); + + Verify(map, new[] { typeof(Tuple<>), typeof(Tuple<,>) }); + } + + [Test] + public void Can_store_and_retrieve_value_from_map() + { + var map = new FHashMap11(2); + + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(42 + 32, "2"); + map.AddOrUpdate(42 + 32 + 32, "3"); + + // interrupt the keys with ne key + map.AddOrUpdate(43, "a"); + map.AddOrUpdate(43 + 32, "b"); + map.AddOrUpdate(43 + 32 + 32, "c"); + + map.AddOrUpdate(42 + 32 + 32 + 32, "4"); + + // insert 3rd variety of the keys + map.AddOrUpdate(44, "*"); + + map.AddOrUpdate(42 + 32 + 32 + 32 + 32, "5"); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32, "6"); + map.AddOrUpdate(43 + 32 + 32 + 32, "d"); + + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32, "7"); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, "8"); + + // check for the missing key + Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32)); + + // check for the strange key + Assert.AreEqual("*", map.GetValueOrDefault(44)); + + Assert.AreEqual("1", map.GetValueOrDefault(42)); + Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); + Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); + Assert.AreEqual("4", map.GetValueOrDefault(42 + 32 + 32 + 32)); + Assert.AreEqual("5", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32)); + Assert.AreEqual("6", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32)); + Assert.AreEqual("7", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32)); + Assert.AreEqual("8", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32)); + + Assert.AreEqual("a", map.GetValueOrDefault(43)); + Assert.AreEqual("b", map.GetValueOrDefault(43 + 32)); + Assert.AreEqual("c", map.GetValueOrDefault(43 + 32 + 32)); + Assert.AreEqual("d", map.GetValueOrDefault(43 + 32 + 32 + 32)); + + Assert.AreEqual(13, map.Count); + + Verify(map, null); + } + + /* + ## Debug output example + + ### IntEq + + [AddOrUpdate] Probes abs max=2, max=2, all=[1: 1, 2: 1]; first 4 probes are 2 out of 2 + [AddOrUpdate] Probes abs max=3, max=3, all=[1: 1, 2: 1, 3: 1]; first 4 probes are 3 out of 3 + [AllocateEntries] Resize entries: 2 -> 4 + [ResizeHashes] 4 -> 8 + [ResizeHashes] Probes abs max=3, max=3, all=[1: 1, 2: 1, 3: 1]; first 4 probes are 3 out of 3 + [AddOrUpdate] Probes abs max=4, max=4, all=[1: 1, 2: 1, 3: 2, 4: 1]; first 4 probes are 5 out of 5 + [AllocateEntries] Resize entries: 4 -> 8 + [AddOrUpdate] Probes abs max=5, max=5, all=[1: 1, 2: 1, 3: 2, 4: 1, 5: 1]; first 4 probes are 5 out of 6 + [AddOrUpdate-RH] Probes abs max=6, max=6, all=[1: 1, 2: 1, 3: 2, 4: 2, 5: 1, 6: 1]; first 4 probes are 6 out of 8 + [ResizeHashes] 8 -> 16 + [ResizeHashes] Probes abs max=6, max=6, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 1, 6: 1]; first 4 probes are 5 out of 7 + [AllocateEntries] Resize entries: 8 -> 16 + [AddOrUpdate-RH] Probes abs max=7, max=7, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 1, 7: 1]; first 4 probes are 5 out of 9 + [AddOrUpdate-RH] Probes abs max=8, max=8, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 1, 8: 1]; first 4 probes are 5 out of 11 + [AddOrUpdate] Probes abs max=9, max=9, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 1, 8: 2, 9: 1]; first 4 probes are 5 out of 13 + [AddOrUpdate-RH] Probes abs max=10, max=10, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 2, 8: 2, 9: 1, 10: 1]; first 4 probes are 5 out of 15 + [AddOrUpdate-RH] Probes abs max=11, max=11, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 2, 8: 3, 9: 1, 10: 1, 11: 1]; first 4 probes are 5 out of 17 + + ### GoldenIntEq + + [AddOrUpdate] Probes abs max=2, max=2, all=[1: 1, 2: 1]; first 4 probes are 2 out of 2 + [AllocateEntries] Resize entries: 2 -> 4 + [ResizeHashes] 4 -> 8 + [ResizeHashes] Probes abs max=2, max=1, all=[1: 3]; first 4 probes are 3 out of 3 + [AllocateEntries] Resize entries: 4 -> 8 + [AddOrUpdate] Probes abs max=2, max=2, all=[1: 5, 2: 1]; first 4 probes are 6 out of 6 + [ResizeHashes] 8 -> 16 + [ResizeHashes] Probes abs max=2, max=2, all=[1: 6, 2: 1]; first 4 probes are 7 out of 7 + [AllocateEntries] Resize entries: 8 -> 16 + + */ + [Test] + public void Can_store_and_retrieve_value_from_map_Golden() + { + var map = new FHashMap11(2); + // var map = new FHashMap11(2); + + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(42 + 32, "2"); + map.AddOrUpdate(42 + 32 + 32, "3"); + + // interrupt the keys with new key + map.AddOrUpdate(43, "a"); + map.AddOrUpdate(43 + 32, "b"); + map.AddOrUpdate(43 + 32 + 32, "c"); + + map.AddOrUpdate(42 + 32 + 32 + 32, "4"); + + // insert 3rd variety of the keys + map.AddOrUpdate(44, "*"); + + map.AddOrUpdate(42 + 32 + 32 + 32 + 32, "5"); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32, "6"); + map.AddOrUpdate(43 + 32 + 32 + 32, "d"); + + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32, "7"); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, "8"); + + // check for the missing key + Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32)); + + // check for the strange key + Assert.AreEqual("*", map.GetValueOrDefault(44)); + + Assert.AreEqual("1", map.GetValueOrDefault(42)); + Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); + Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); + Assert.AreEqual("4", map.GetValueOrDefault(42 + 32 + 32 + 32)); + Assert.AreEqual("5", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32)); + Assert.AreEqual("6", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32)); + Assert.AreEqual("7", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32)); + Assert.AreEqual("8", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32)); + + Assert.AreEqual("a", map.GetValueOrDefault(43)); + Assert.AreEqual("b", map.GetValueOrDefault(43 + 32)); + Assert.AreEqual("c", map.GetValueOrDefault(43 + 32 + 32)); + Assert.AreEqual("d", map.GetValueOrDefault(43 + 32 + 32 + 32)); + + Assert.AreEqual(13, map.Count); + + Verify(map, null); + } + + [Test] + public void Can_lookup_the_default_map_without_error() + { + FHashMap11 map = default; + + Assert.IsFalse(map.TryGetValue(42, out _)); + } + + [Test] + public void Can_store_and_retrieve_value_from_map_with_Expand_in_the_middle() + { + var map = new FHashMap11(1); + + Assert.IsFalse(map.TryGetValue(42, out _)); + + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(42 + 32, "2"); + + // interrupt the keys with new key + map.AddOrUpdate(43, "a"); + map.AddOrUpdate(43 + 32, "b"); + + map.AddOrUpdate(42 + 32 + 32, "3"); + + Assert.AreEqual("1", map.GetValueOrDefault(42)); + Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); + Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); + Assert.AreEqual(null, map.GetValueOrDefault(42 + 32 + 32 + 32)); + Assert.AreEqual("a", map.GetValueOrDefault(43)); + + map.AddOrUpdate(43, "a!"); + Assert.AreEqual("a!", map.GetValueOrDefault(43)); + + map.AddOrUpdate(47, "x"); + map.AddOrUpdate(53, "y"); + Assert.AreEqual("x", map.GetValueOrDefault(47)); + Assert.AreEqual("y", map.GetValueOrDefault(53)); + + map.AddOrUpdate(47 + 16, "x!"); + map.AddOrUpdate(53 + 16, "y!"); + Assert.AreEqual("x!", map.GetValueOrDefault(47 + 16)); + Assert.AreEqual("y!", map.GetValueOrDefault(53 + 16)); + + Verify(map, null); + } + + [Test] + public void Can_resize_without_moving() + { + var map = new FHashMap11(2); + + map.AddOrUpdate(0, "0"); + map.AddOrUpdate(1, "1"); + map.AddOrUpdate(9, "9"); + + // resize goes here + map.AddOrUpdate(3, "3"); + + map.AddOrUpdate(5, "5"); + + Verify(map, new[] { 0, 1, 3, 5, 9 }); + } + + [Test] + public void Can_store_and_get_stored_item_count() + { + var map = new FHashMap11(); + + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(42 + 32 + 32, "3"); + + Assert.AreEqual(2, map.Count); + Verify(map, new[] { 42, 42 + 32 + 32 }); + } + + [Test] + public void Can_update_a_stored_item_with_new_value() + { + var map = new FHashMap11(); + + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(42, "3"); + + Assert.AreEqual("3", map.GetValueOrDefault(42)); + Assert.AreEqual(1, map.Count); + Verify(map, new[] { 42 }); + } + + [Test] + public void Can_add_key_with_0_hash_code() + { + var map = new FHashMap11(); + + map.AddOrUpdate(0, "aaa"); + map.AddOrUpdate(0 + 32, "2"); + map.AddOrUpdate(0 + 32 + 32, "3"); + Verify(map, new[] { 0, 0 + 32, 0 + 32 + 32 }); + + string value; + Assert.IsTrue(map.TryGetValue(0, out value)); + + Assert.AreEqual("aaa", value); + } + + [Test] + public void Can_quickly_find_the_scattered_items_with_the_same_cache() + { + var map = new FHashMap11(); + + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(43, "a"); + map.AddOrUpdate(42 + 32, "2"); + map.AddOrUpdate(45, "b"); + map.AddOrUpdate(46, "c"); + map.AddOrUpdate(42 + 32 + 32, "3"); + Verify(map, new[] { 42, 43, 42 + 32, 45, 46, 42 + 32 + 32 }); + + string value; + Assert.IsTrue(map.TryGetValue(42 + 32, out value)); + Assert.AreEqual("2", value); + + Assert.IsTrue(map.TryGetValue(42 + 32 + 32, out value)); + Assert.AreEqual("3", value); + } + + [Test] + public void Can_remove_the_stored_item() + { + var map = new FHashMap11(2); + + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(42 + 32, "2"); + map.AddOrUpdate(42 + 32 + 32, "3"); + + Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); + var r = map.TryRemove(42 + 32); + Assert.IsTrue(r); + + Assert.AreEqual(2, map.Count); + Assert.AreEqual("1", map.GetValueOrDefault(42)); + Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); + Verify(map, null); + } +} diff --git a/test/ImTools.UnitTests/SmallMapTests.cs b/test/ImTools.UnitTests/SmallMapTests.cs index 3fae984b..3294f317 100644 --- a/test/ImTools.UnitTests/SmallMapTests.cs +++ b/test/ImTools.UnitTests/SmallMapTests.cs @@ -15,20 +15,6 @@ namespace ImTools.Experiments.UnitTests; [TestFixture] public class SmallMapTests { -#if NET7_0_OR_GREATER - [Test] - public void Test_stackalloc_for_entries() - { - Span arr = stackalloc int[8]; - - Foo(in arr); - - Assert.AreEqual(42, arr[2]); - } - - static void Foo(in Span arr) => arr[2] = 42; -#endif - [Test] public void Real_world_test_AddOrUpdate() { From 8f26b0ea8eb40b829476380c83dc9fd274bab1b1 Mon Sep 17 00:00:00 2001 From: dadhi Date: Tue, 10 Jun 2025 14:38:36 +0200 Subject: [PATCH 08/54] remove bm things --- src/ImTools/ImTools.cs | 108 ----------------------------------------- 1 file changed, 108 deletions(-) diff --git a/src/ImTools/ImTools.cs b/src/ImTools/ImTools.cs index 4540c9bc..66495959 100644 --- a/src/ImTools/ImTools.cs +++ b/src/ImTools/ImTools.cs @@ -7445,114 +7445,6 @@ public bool TryGetValue(K key, out V value) return false; } - /// Lookup for the key and get the associated value if the key is found - [MethodImpl((MethodImplOptions)256)] - public bool TryGetValue_Permute(K key, out V value) - { - if (_packedHashesAndIndexes != null) - { - var hash = default(TEq).GetHashCode(key); - - var indexMask = _indexMask; - var hashMiddleMask = HSmallMap.HashAndIndexMask & ~indexMask; - var hashMiddle = hash & hashMiddleMask; - var hashIndex = hash & indexMask; - -#if NET7_0_OR_GREATER - ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); -#else - var hashesAndIndexes = _packedHashesAndIndexes; -#endif - var probes = 0; - while (true) - { - ++probes; - var h = HSmallMap.GetItem(ref hashesAndIndexes, hashIndex++ & indexMask); - - var hProbes = h >>> HSmallMap.ProbeCountShift; - if (hProbes < probes) - { - value = default; - return false; - } - - if ((hProbes == probes) & ((h & hashMiddleMask) == hashMiddle)) - { - ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); - if (default(TEq).Equals(e.Key, key)) - { - value = e.Value; - return true; - } - } - } - } - value = default; - return false; - } - - /// Lookup for the key and get the associated value if the key is found - [MethodImpl((MethodImplOptions)256)] - public bool TryGetValue_ILP2(K key, out V value) - { - if (_packedHashesAndIndexes != null) - { - var hash = default(TEq).GetHashCode(key); - - var indexMask = _indexMask; - var hashMiddleMask = HSmallMap.HashAndIndexMask & ~indexMask; - var hashMiddle = hash & hashMiddleMask; - var hashIndex = hash & indexMask; - -#if NET7_0_OR_GREATER - ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); -#else - var hashesAndIndexes = _packedHashesAndIndexes; -#endif - var iter = 0; - var expectedProbes0 = 1; - var expectedProbes1 = 2; - while (true) - { - var h0 = HSmallMap.GetItem(ref hashesAndIndexes, (iter + hashIndex) & indexMask); - var h1 = HSmallMap.GetItem(ref hashesAndIndexes, (iter + hashIndex + 1) & indexMask); - - var h0Probes = h0 >>> HSmallMap.ProbeCountShift; - var h1Probes = h1 >>> HSmallMap.ProbeCountShift; - - if ((h0Probes == expectedProbes0) & (h0 & hashMiddleMask) == hashMiddle) - { - ref var e = ref _entries.GetSurePresentEntryRef(h0 & indexMask); - if (default(TEq).Equals(e.Key, key)) - { - value = e.Value; - return true; - } - } - - if ((h1Probes == expectedProbes1) & (h1 & hashMiddleMask) == hashMiddle) - { - ref var e = ref _entries.GetSurePresentEntryRef(h1 & indexMask); - if (default(TEq).Equals(e.Key, key)) - { - value = e.Value; - return true; - } - } - - if (h1Probes < expectedProbes1) - break; - - iter += 2; - expectedProbes0 += 2; - expectedProbes1 += 2; - } - } - - value = default; - return false; - } - /// Lookup for the key and get the associated value or the default value if the key is not found [MethodImpl((MethodImplOptions)256)] public V GetValueOrDefault(K key, V defaultValue = default) => From 3fb0d53317d52bb2f4d3c81fecdd0b0c82ce29b3 Mon Sep 17 00:00:00 2001 From: dadhi Date: Wed, 18 Jun 2025 12:29:20 +0200 Subject: [PATCH 09/54] @wip APL aproach in FHM11 --- playground/Experiments/FHashMap11.cs | 416 +++++++++--------- .../Playground/FHashMap11Tests.cs | 136 +++--- 2 files changed, 288 insertions(+), 264 deletions(-) diff --git a/playground/Experiments/FHashMap11.cs b/playground/Experiments/FHashMap11.cs index 69f006fd..6e171c81 100644 --- a/playground/Experiments/FHashMap11.cs +++ b/playground/Experiments/FHashMap11.cs @@ -19,28 +19,13 @@ public static class FHashMap11 internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) internal const byte MinCapacityBits = 3; // 1 << 3 == 8 - public struct KeyEntry - { - public int Probe; - public int Hash; - public int Index; - public K Key; - public bool IsEmpty => Probe == 0; - public override string ToString() => Probe == 0 ? "" : $"probe:{Probe},index:{Index},hash:0b{Hash:b0},key:{Key}"; - } - - /// Converts the packed hashes and entries into the human readable info. - /// This also used for the debugging view of the and by the Verify... methods in tests. - public static KeyEntry[] Explain(this ref FHashMap11 map) - where TEq : struct, IEq => map.KeyEntries; - /// Verifies that the hashes correspond to the keys stroed in the entries. May be called from the tests. public static void VerifyHashesAndKeysEq(this FHashMap11 map, Action assertEq) where TEq : struct, IEq { - foreach (var e in map.KeyEntries) - if (!e.IsEmpty) - assertEq(default(TEq).GetHashCode(e.Key) == e.Hash); + for (var i = 0; i < map.Probes.Length; ++i) + if (map.Probes[i] != 0) + assertEq(default(TEq).GetHashCode(map.Keys[i]) == map.Hashes[i]); } /// Verifies that there is no duplicate keys stored in hashes -> entries. May be called from the tests. @@ -49,14 +34,13 @@ public static void VerifyNoDuplicateKeys(this FHashMap11 m { // Verify the indexes do no contains duplicate keys var uniq = new Dictionary(map.Count); - var keys = map.KeyEntries; + var keys = map.Keys; for (var i = 0; i < keys.Length; i++) { - var k = keys[i]; - if (k.IsEmpty) + if (map.Probes[i] == 0) // skip empty continue; - var key = k.Key; - if (!uniq.TryGetValue(key, out var count)) + var key = keys[i]; + if (!uniq.TryGetValue(key, out _)) uniq.Add(key, 1); else assertKey(key); @@ -66,14 +50,13 @@ public static void VerifyNoDuplicateKeys(this FHashMap11 m public static void VerifyProbesAreFitRobinHood(this FHashMap11 map, Action reportFail) where TEq : struct, IEq { - var hashes = map.KeyEntries; + var hashes = map.Hashes; var prevProbe = -1; for (var i = 0; i < hashes.Length; i++) { - var h = hashes[i]; - var probe = h.Probe; + var probe = map.Probes[i]; if (prevProbe != -1 & probe - prevProbe > 1) - reportFail($"Probes are not consequent: {prevProbe}, {probe} for {i}: p{probe}, {h.Index} -> {h.Key}"); + reportFail($"Probes are not consequent: {prevProbe}, {probe} for {i}: p{probe}, {map.ValueIndexes[i]} -> {map.Keys[i]}"); prevProbe = probe; } } @@ -103,11 +86,11 @@ public static void VerifyContainAllKeys(this FHashMap11 ma #if DEBUG internal struct ProbesTracker { - internal int MaxProbes; + internal int MaxProbe; internal int[] Probes; public ProbesTracker() { - MaxProbes = 1; + MaxProbe = 1; Probes = new int[1]; } @@ -115,7 +98,7 @@ public ProbesTracker() // [Add] Probes abs max = 10, curr max = 6, all = [1: 180, 2: 103, 3: 59, 4: 23, 5: 3, 6: 1]; first 4 probes are 365 out of 369 internal void DebugOutputProbes(string label) { - Debug.Write($"[{label}] Probes abs max={MaxProbes}, curr max={Probes.Length}, all=["); + Debug.Write($"[{label}] Probes abs max={MaxProbe}, curr max={Probes.Length}, all=["); var first4probes = 0; var allProbes = 0; for (var i = 0; i < Probes.Length; i++) @@ -129,30 +112,30 @@ internal void DebugOutputProbes(string label) Debug.WriteLine($"]; first 4 probes are {first4probes} out of {allProbes}"); } - internal void DebugCollectAndOutputProbes(int probes, [CallerMemberName] string label = "") + internal void DebugCollectAndOutputProbes(int probe, [CallerMemberName] string label = "") { Probes ??= []; - if (probes > Probes.Length) + if (probe > Probes.Length) { - if (probes > MaxProbes) - MaxProbes = probes; - Array.Resize(ref Probes, probes); - Probes[probes - 1] = 1; + if (probe > MaxProbe) + MaxProbe = probe; + Array.Resize(ref Probes, probe); + Probes[probe - 1] = 1; DebugOutputProbes(label); } else - ++Probes[probes - 1]; + ++Probes[probe - 1]; } - internal void DebugReCollectAndOutputProbes(KeyEntry[] hashesAndKeys, [CallerMemberName] string label = "") + internal void DebugReCollectAndOutputProbes(byte[] probes, [CallerMemberName] string label = "") { var newProbes = new int[1]; - foreach (var h in hashesAndKeys) + for (var i = 0; i < probes.Length; ++i) { - var p = h.Probe; + var p = probes[i]; if (p == 0) continue; - if (p > MaxProbes) - MaxProbes = p; + if (p > MaxProbe) + MaxProbe = p; if (p > newProbes.Length) Array.Resize(ref newProbes, p); ++newProbes[p - 1]; @@ -168,21 +151,12 @@ internal void RemoveProbe(int probe) if (p == 0 && probe == Probes.Length) { Array.Resize(ref Probes, probe - 1); - if (MaxProbes == probe) - --MaxProbes; + if (MaxProbe == probe) + --MaxProbe; } } } #endif - - public class DebugProxy - where TEq : struct, IEq - { - private readonly FHashMap11 _map; - public DebugProxy(FHashMap11 map) => _map = map; - public KeyEntry[] KeyEntries => _map.KeyEntries; - public V[] Values => _map.Values; - } } // todo: @improve ? how/where to add SIMD to improve CPU utilization but not losing perf for smaller sizes @@ -198,7 +172,6 @@ public class DebugProxy /// - Provides the "stable" enumeration of the entries in the added order /// /// -[DebuggerTypeProxy(typeof(DebugProxy<,,>))] [DebuggerDisplay("Count={Count}")] public struct FHashMap11 : IReadOnlyCollection> where TEq : struct, IEq @@ -208,7 +181,10 @@ public struct FHashMap11 : IReadOnlyCollection> #endif private byte _capacityBitShift; internal int _count; - public KeyEntry[] KeyEntries; + public byte[] Probes; + public int[] Hashes; + public K[] Keys; + public int[] ValueIndexes; public V[] Values; /// Get the number of the key/hash entries in array. Should be more than @@ -224,15 +200,19 @@ public FHashMap11(byte capacityBitShift) // the overflow tail to the hashes is the size of log2N where N==capacityBitShift, // it is probably fine to have the check for the overlow of capacity because it will be mispredicted only once at the end of loop (it even rarely for the lookup) - KeyEntries = new KeyEntry[1 << capacityBitShift]; - Values = new V[1 << capacityBitShift]; + var cap = 1 << capacityBitShift; + Probes = new byte[cap]; + Hashes = new int[cap]; + Keys = new K[cap]; + ValueIndexes = new int[cap]; + Values = new V[cap]; } /// Lookup for the key and get the associated value if the key is found [MethodImpl((MethodImplOptions)256)] public bool TryGetValue(K key, out V value) { - if (KeyEntries != null) + if (Keys != null) { var hash = default(TEq).GetHashCode(key); @@ -240,22 +220,26 @@ public bool TryGetValue(K key, out V value) var hashIndex = hash & indexMask; #if NET7_0_OR_GREATER - ref var hashAndKeyEntries = ref MemoryMarshal.GetArrayDataReference(KeyEntries); + ref var probes = ref MemoryMarshal.GetArrayDataReference(Probes); + ref var hashes = ref MemoryMarshal.GetArrayDataReference(Hashes); + ref var keys = ref MemoryMarshal.GetArrayDataReference(Keys); + ref var valueIndexes = ref MemoryMarshal.GetArrayDataReference(ValueIndexes); #else var hashAndKeyEntries = KeyEntries; #endif - var h = GetItem(ref hashAndKeyEntries, hashIndex); + var p = GetItem(ref probes, hashIndex); var probe = 1; - while (h.Probe >= probe) + while (p >= probe) { - if ((h.Probe == probe) & (h.Hash == hash) && default(TEq).Equals(h.Key, key)) + if ((p == probe) & (GetItem(ref hashes, hashIndex) == hash) && + default(TEq).Equals(GetItem(ref keys, hashIndex), key)) { - value = Values[h.Index]; + value = Values[GetItem(ref valueIndexes, hashIndex)]; return true; } - h = GetItem(ref hashAndKeyEntries, ++hashIndex & indexMask); + p = GetItem(ref probes, ++hashIndex & indexMask); ++probe; } } @@ -282,61 +266,74 @@ public ref V GetOrAddValueRef(K key) if (indexMask - currCount <= (indexMask >>> MinFreeCapacityShift)) indexMask = ResizeHashes(indexMask); - var hashIndex = hash & indexMask; + var index = hash & indexMask; #if NET7_0_OR_GREATER - ref var hashAndKeyEntries = ref MemoryMarshal.GetArrayDataReference(KeyEntries); + ref var probes = ref MemoryMarshal.GetArrayDataReference(Probes); + ref var hashes = ref MemoryMarshal.GetArrayDataReference(Hashes); + ref var keys = ref MemoryMarshal.GetArrayDataReference(Keys); + ref var valueIndexes = ref MemoryMarshal.GetArrayDataReference(ValueIndexes); #else var hashAndKeyEntries = KeyEntries; #endif - ref var h = ref GetItemRef(ref hashAndKeyEntries, hashIndex); + ref var pRef = ref GetItemRef(ref probes, index); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - var probe = 1; - while (h.Probe >= probe) + var probe = (byte)1; + while (pRef >= probe) { // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if ((h.Probe == probe) & (h.Hash == hash) && default(TEq).Equals(h.Key, key)) - return ref Values[h.Index]; + if ((pRef == probe) & GetItem(ref hashes, index) == hash && + default(TEq).Equals(GetItem(ref keys, index), key)) + return ref Values[GetItem(ref valueIndexes, index)]; - h = ref GetItemRef(ref hashAndKeyEntries, ++hashIndex & indexMask); + index = (index + 1) & indexMask; + pRef = ref GetItemRef(ref probes, index); ++probe; } - // nothing found, add a new entry + // Nothing found, add a new entry and inscrease the count first _count = currCount + 1; - // 3. We did not find the hash and therefore the key, so insert the new entry - var hRobinHooded = h; - - h.Hash = hash; - h.Probe = probe; - h.Index = currCount; - h.Key = key; - + // 3. There is an empty slot to insert the new entry, so we can just insert it + insert: + if (pRef == 0) + { #if DEBUG - _dbg.DebugCollectAndOutputProbes(probe, "Add"); + _dbg.DebugCollectAndOutputProbes(probe, "Add in the empty slot"); #endif - // 4. If the robin hooded hash is empty then we stop - // 5. Otherwise we steal the slot with the smaller probes - probe = hRobinHooded.Probe; - while (probe != 0) + pRef = probe; + GetItemRef(ref hashes, index) = hash; + GetItemRef(ref keys, index) = key; + GetItemRef(ref valueIndexes, index) = currCount; + } + else // p < probe, e.g. 3, 4, 5, (5<6), (6==6), (7==7), (3<8), (0<4) { - h = ref GetItemRef(ref hashAndKeyEntries, ++hashIndex & indexMask); - if (h.Probe < ++probe) - { + // 4. If the slot is not empty, then robin-hood the smaller (more valuable) probe + // and put the newly added item into the free slot. + // Then proceed with the robin-hooded key as-if it was a newly inserted key. + var rhProbe = pRef; + var rhHash = GetItem(ref hashes, index); + var rhKey = GetItem(ref keys, index); + var rhValueIndex = GetItem(ref valueIndexes, index); + #if DEBUG - if (h.Probe != 0) - _dbg.RemoveProbe(h.Probe); - _dbg.DebugCollectAndOutputProbes(probe, "Add-RH"); + _dbg.DebugCollectAndOutputProbes(probe, "Add into the occupied slot after RobinHood it"); #endif - var tmp = h; - - h = hRobinHooded; - h.Probe = probe; - - hRobinHooded = tmp; - probe = hRobinHooded.Probe; + // and set the new entry to the current slot + pRef = probe; + GetItemRef(ref hashes, index) = hash; + GetItemRef(ref keys, index) = key; + GetItemRef(ref valueIndexes, index) = currCount; + + // 5. Now treat the robin-hooded entry as a newly inserted entry + probe = rhProbe; + while (true) + { + index = (index + 1) & indexMask; + pRef = ref GetItemRef(ref probes, index); + if (pRef < ++probe) + goto insert; } } @@ -350,132 +347,156 @@ public ref V GetOrAddValueRef(K key) public void AddOrUpdate(K key, in V value) => GetOrAddValueRef(key) = value; - /// Removes the hash and entry of the provided key or returns - [MethodImpl((MethodImplOptions)256)] - public bool TryRemove(K key) - { - var hash = default(TEq).GetHashCode(key); - - var indexMask = (1 << _capacityBitShift) - 1; - var hashIndex = hash & indexMask; - -#if NET7_0_OR_GREATER - ref var keyEntries = ref MemoryMarshal.GetArrayDataReference(KeyEntries); -#else - var keyEntries = KeyEntries; -#endif - ref var h = ref GetItemRef(ref keyEntries, hashIndex); - - var removed = false; - - // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - var probe = 1; - while (h.Probe >= probe) - { - // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if ((h.Probe == probe) & (h.Hash == hash) && default(TEq).Equals(h.Key, key)) - { - removed = true; - - --_count; - Values[h.Index] = default; // todo: @perf fragmentation issue, add the slot to the free list? - h = default; -#if DEBUG - _dbg.RemoveProbe(probe); -#endif - break; - } - h = ref GetItemRef(ref keyEntries, ++hashIndex & indexMask); - ++probe; - } - - if (!removed) - return false; - - ref var emptied = ref h; - h = ref GetItemRef(ref keyEntries, ++hashIndex & indexMask); - - // move the next hash into the emptied slot until the next hash is empty or ideally positioned (hash is 0 or probe is 1) - while (h.Probe > 1) - { - emptied = h; - emptied.Probe -= 1; // decrease the probe count by one cause we moving the hash closer to the ideal index - h = default; - - emptied = ref h; - h = ref GetItemRef(ref keyEntries, ++hashIndex & indexMask); - } - return true; - } + // /// Removes the hash and entry of the provided key or returns + // [MethodImpl((MethodImplOptions)256)] + // public bool TryRemove(K key) + // { + // var hash = default(TEq).GetHashCode(key); + + // var indexMask = (1 << _capacityBitShift) - 1; + // var hashIndex = hash & indexMask; + + // #if NET7_0_OR_GREATER + // ref var keyEntries = ref MemoryMarshal.GetArrayDataReference(KeyEntries); + // #else + // var keyEntries = KeyEntries; + // #endif + // ref var h = ref GetItemRef(ref keyEntries, hashIndex); + + // var removed = false; + + // // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions + // var probe = 1; + // while (h.Probe >= probe) + // { + // // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too + // if ((h.Probe == probe) & (h.Hash == hash) && default(TEq).Equals(h.Key, key)) + // { + // removed = true; + + // --_count; + // Values[h.Index] = default; // todo: @perf fragmentation issue, add the slot to the free list? + // h = default; + // #if DEBUG + // _dbg.RemoveProbe(probe); + // #endif + // break; + // } + // h = ref GetItemRef(ref keyEntries, ++hashIndex & indexMask); + // ++probe; + // } + + // if (!removed) + // return false; + + // ref var emptied = ref h; + // h = ref GetItemRef(ref keyEntries, ++hashIndex & indexMask); + + // // move the next hash into the emptied slot until the next hash is empty or ideally positioned (hash is 0 or probe is 1) + // while (h.Probe > 1) + // { + // emptied = h; + // emptied.Probe -= 1; // decrease the probe count by one cause we moving the hash closer to the ideal index + // h = default; + + // emptied = ref h; + // h = ref GetItemRef(ref keyEntries, ++hashIndex & indexMask); + // } + // return true; + // } internal int ResizeHashes(int indexMask) { if (indexMask == 0) { _capacityBitShift = MinCapacityBits; - KeyEntries = new KeyEntry[1 << MinCapacityBits]; - Values = new V[1 << MinCapacityBits]; + var cap = 1 << MinCapacityBits; + Probes = new byte[cap]; + Hashes = new int[cap]; + Keys = new K[cap]; + ValueIndexes = new int[cap]; + Values = new V[cap]; #if DEBUG - Debug.WriteLine($"[ResizeHashes] new empty hashes {1} -> {KeyEntries.Length}"); + Debug.WriteLine($"[ResizeHashes] new empty hashes {1} -> {Probes.Length}"); #endif - return (1 << MinCapacityBits) - 1; + return cap - 1; } - var oldCapacity = indexMask + 1; - var newKeyEntries = new KeyEntry[oldCapacity << 1]; - var newIndexMask = indexMask << 1 | 1; + var oldCap = indexMask + 1; + var newCap = oldCap << 1; + var newProbes = new byte[newCap]; + var newHashes = new int[newCap]; + var newKeys = new K[newCap]; + var newValueIndexes = new int[newCap]; + var newIndexMask = newCap - 1; #if NET7_0_OR_GREATER - ref var newKeys = ref MemoryMarshal.GetArrayDataReference(newKeyEntries); - ref var oldKeys = ref MemoryMarshal.GetArrayDataReference(KeyEntries); - var oldKey = oldKeys; + ref var newProbesRef = ref MemoryMarshal.GetArrayDataReference(newProbes); + ref var oldProbesRef = ref MemoryMarshal.GetArrayDataReference(Probes); + var oldProbe = oldProbesRef; + + ref var newHashesRef = ref MemoryMarshal.GetArrayDataReference(newHashes); + ref var oldHashesRef = ref MemoryMarshal.GetArrayDataReference(Hashes); + + ref var newKeysRef = ref MemoryMarshal.GetArrayDataReference(newKeys); + ref var oldKeysRef = ref MemoryMarshal.GetArrayDataReference(Keys); + + ref var newValueIndexesRef = ref MemoryMarshal.GetArrayDataReference(newValueIndexes); + ref var oldValueIndexesRef = ref MemoryMarshal.GetArrayDataReference(ValueIndexes); #else - var newKeys = newKeyEntries; - var oldKeys = KeyEntries; + var newKeys = newKeys; + var oldKeys = Keys; var oldKey = oldKeys[0]; #endif // Overflow segment is wrapped-around hashes and! the hashes at the beginning robin-hooded by the wrapped-around hashes // so we skip them to start from first ideal or empty entry var i = 0; - while (oldKey.Probe > 1) - oldKey = GetItem(ref oldKeys, ++i); - var oldCapacityWithOverflowSegment = i + oldCapacity; + while (oldProbe > 1) + oldProbe = GetItem(ref oldProbesRef, ++i); + var oldCapWithOverflowSegment = i + oldCap; while (true) { - if (oldKey.Probe != 0) + if (oldProbe != 0) { - var newHashIndex = oldKey.Hash & newIndexMask; + var hash = GetItem(ref oldHashesRef, i); + var newIndex = hash & newIndexMask; // no need for robin-hooding because we already did it for the old hashes and // now just sparsing the hashes which are already in order into the new array - var newProbe = 1; - ref var newKey = ref GetItemRef(ref newKeys, newHashIndex); - while (newKey.Probe != 0) + var newProbe = (byte)1; + ref var newProbeRef = ref GetItemRef(ref newProbesRef, newIndex); + while (newProbeRef != 0) { - newKey = ref GetItemRef(ref newKeys, ++newHashIndex & newIndexMask); + newIndex = (newIndex + 1) & newIndexMask; + newProbeRef = ref GetItemRef(ref newProbeRef, newIndex); ++newProbe; } - newKey = oldKey; - newKey.Probe = newProbe; + newProbeRef = newProbe; + GetItemRef(ref newHashesRef, newIndex) = hash; + GetItemRef(ref newKeysRef, newIndex) = GetItem(ref oldKeysRef, i); + GetItemRef(ref newValueIndexesRef, newIndex) = GetItem(ref oldValueIndexesRef, i); } - if (++i >= oldCapacityWithOverflowSegment) + if (++i >= oldCapWithOverflowSegment) break; - oldKey = GetItem(ref oldKeys, i & indexMask); + oldProbe = GetItem(ref oldProbesRef, i & indexMask); } #if DEBUG - Debug.WriteLine($"[ResizeHashes] {oldCapacity} -> {newKeyEntries.Length}"); - _dbg.DebugReCollectAndOutputProbes(newKeyEntries); + Debug.WriteLine($"[ResizeHashes] {oldCap} -> {newProbes.Length}"); + _dbg.DebugReCollectAndOutputProbes(newProbes, "ResizeHashes"); #endif ++_capacityBitShift; - KeyEntries = newKeyEntries; + Probes = newProbes; + Hashes = newHashes; + Keys = newKeys; + ValueIndexes = newValueIndexes; return newIndexMask; } /// [MethodImpl((MethodImplOptions)256)] - public Enumerator GetEnumerator() => new Enumerator(KeyEntries, Values, _count); + public Enumerator GetEnumerator() => new Enumerator(Probes, ValueIndexes, Keys, Values, _count); /// IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); @@ -486,17 +507,21 @@ internal int ResizeHashes(int indexMask) /// Enumerator of the entries in the order of their addition to the map public struct Enumerator : IEnumerator> { - private int _keyIndex; + private int _probeIndex; private int _index; private KeyValuePair _current; - private readonly KeyEntry[] _keys; + private readonly byte[] _probes; + private readonly int[] _valueIndexes; + private readonly K[] _keys; private readonly V[] _values; private int _count; - internal Enumerator(KeyEntry[] keys, V[] values, int count) + internal Enumerator(byte[] probes, int[] valueIndexes, K[] keys, V[] values, int count) { - _keyIndex = 0; + _probeIndex = 0; _index = 0; _current = default; + _probes = probes; + _valueIndexes = valueIndexes; _keys = keys; _values = values; _count = count; @@ -509,14 +534,13 @@ public bool MoveNext() if (_index < _count) { // skip empty hashes - var i = _keyIndex; - while (_keys[i].Probe == 0) + var i = _probeIndex; + while (_probes[i] == 0) ++i; - _keyIndex = i; - ref var k = ref _keys[i]; - _current = new KeyValuePair(k.Key, _values[k.Index]); + _probeIndex = i; + _current = new KeyValuePair(_keys[i], _values[_valueIndexes[i]]); - ++_keyIndex; + ++_probeIndex; ++_index; return true; } @@ -530,7 +554,7 @@ public bool MoveNext() void IEnumerator.Reset() { - _keyIndex = 0; + _probeIndex = 0; _index = 0; _current = default; } diff --git a/test/ImTools.UnitTests/Playground/FHashMap11Tests.cs b/test/ImTools.UnitTests/Playground/FHashMap11Tests.cs index 48c1c65c..065cde52 100644 --- a/test/ImTools.UnitTests/Playground/FHashMap11Tests.cs +++ b/test/ImTools.UnitTests/Playground/FHashMap11Tests.cs @@ -57,77 +57,77 @@ public void Real_world_test_AddOrUpdate_NO_Resize() Verify(map, types); } - [Test] - public void Real_world_test_with_TryRemove_from_1000_items() - { - var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(1000).ToArray(); + // [Test] + // public void Real_world_test_with_TryRemove_from_1000_items() + // { + // var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(1000).ToArray(); - var map = new FHashMap11>(); + // var map = new FHashMap11>(); - foreach (var key in types) - map.AddOrUpdate(key, "a"); + // foreach (var key in types) + // map.AddOrUpdate(key, "a"); - map.AddOrUpdate(typeof(FHashMap11Tests), "!"); - Assert.AreEqual(1001, map.Count); + // map.AddOrUpdate(typeof(FHashMap11Tests), "!"); + // Assert.AreEqual(1001, map.Count); - Assert.IsTrue(map.TryRemove(typeof(FHashMap11Tests))); - Assert.AreEqual(1000, map.Count); + // Assert.IsTrue(map.TryRemove(typeof(FHashMap11Tests))); + // Assert.AreEqual(1000, map.Count); - Verify(map, types); - } + // Verify(map, types); + // } - [Test] - public void Real_world_test_with_Enumerator_and_TryRemove_the_entries() - { - var count = 1000; - var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(count).ToList(); - Assert.AreEqual(count, types.Count); + // [Test] + // public void Real_world_test_with_Enumerator_and_TryRemove_the_entries() + // { + // var count = 1000; + // var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(count).ToList(); + // Assert.AreEqual(count, types.Count); - var map = new FHashMap11>(); + // var map = new FHashMap11>(); - foreach (var key in types) - map.AddOrUpdate(key, "a"); + // foreach (var key in types) + // map.AddOrUpdate(key, "a"); - var keys = map.Select(kv => kv.Key).ToList(); - CollectionAssert.AreEquivalent(types, keys); + // var keys = map.Select(kv => kv.Key).ToList(); + // CollectionAssert.AreEquivalent(types, keys); - Assert.IsTrue(map.TryRemove(types[0])); - Assert.IsTrue(map.TryRemove(types[999])); - Assert.IsTrue(map.TryRemove(types[377])); - Assert.IsTrue(map.TryRemove(types[733])); - Assert.AreEqual(count - 4, map.Count); + // Assert.IsTrue(map.TryRemove(types[0])); + // Assert.IsTrue(map.TryRemove(types[999])); + // Assert.IsTrue(map.TryRemove(types[377])); + // Assert.IsTrue(map.TryRemove(types[733])); + // Assert.AreEqual(count - 4, map.Count); - // remove in the reverse order to keep the correct index in regard to map - types.RemoveAt(999); - types.RemoveAt(733); - types.RemoveAt(377); - types.RemoveAt(0); - Assert.AreEqual(count - 4, types.Count); + // // remove in the reverse order to keep the correct index in regard to map + // types.RemoveAt(999); + // types.RemoveAt(733); + // types.RemoveAt(377); + // types.RemoveAt(0); + // Assert.AreEqual(count - 4, types.Count); - // Check the second enumeration is working - var keys2 = map.Select(kv => kv.Key).ToList(); - CollectionAssert.AreEquivalent(types, keys2); + // // Check the second enumeration is working + // var keys2 = map.Select(kv => kv.Key).ToList(); + // CollectionAssert.AreEquivalent(types, keys2); - Verify(map, types); - } + // Verify(map, types); + // } - [Test] - public void Simplified_test_with_equal_hashes_RefEq() - { - var map = new FHashMap11>(); + // [Test] + // public void Simplified_test_with_equal_hashes_RefEq() + // { + // var map = new FHashMap11>(); - var keys = new[] { typeof(Tuple<>), typeof(Tuple<,>), typeof(Tuple<,,>) }; - var i = 1; - foreach (var k in keys) - map.AddOrUpdate(k, "" + i++); + // var keys = new[] { typeof(Tuple<>), typeof(Tuple<,>), typeof(Tuple<,,>) }; + // var i = 1; + // foreach (var k in keys) + // map.AddOrUpdate(k, "" + i++); - Assert.AreEqual(3, map.Count); + // Assert.AreEqual(3, map.Count); - Assert.IsTrue(map.TryRemove(typeof(Tuple<,,>))); - Assert.AreEqual(2, map.Count); + // Assert.IsTrue(map.TryRemove(typeof(Tuple<,,>))); + // Assert.AreEqual(2, map.Count); - Verify(map, new[] { typeof(Tuple<>), typeof(Tuple<,>) }); - } + // Verify(map, new[] { typeof(Tuple<>), typeof(Tuple<,>) }); + // } [Test] public void Can_store_and_retrieve_value_from_map() @@ -393,22 +393,22 @@ public void Can_quickly_find_the_scattered_items_with_the_same_cache() Assert.AreEqual("3", value); } - [Test] - public void Can_remove_the_stored_item() - { - var map = new FHashMap11(2); + // [Test] + // public void Can_remove_the_stored_item() + // { + // var map = new FHashMap11(2); - map.AddOrUpdate(42, "1"); - map.AddOrUpdate(42 + 32, "2"); - map.AddOrUpdate(42 + 32 + 32, "3"); + // map.AddOrUpdate(42, "1"); + // map.AddOrUpdate(42 + 32, "2"); + // map.AddOrUpdate(42 + 32 + 32, "3"); - Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); - var r = map.TryRemove(42 + 32); - Assert.IsTrue(r); + // Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); + // var r = map.TryRemove(42 + 32); + // Assert.IsTrue(r); - Assert.AreEqual(2, map.Count); - Assert.AreEqual("1", map.GetValueOrDefault(42)); - Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); - Verify(map, null); - } + // Assert.AreEqual(2, map.Count); + // Assert.AreEqual("1", map.GetValueOrDefault(42)); + // Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); + // Verify(map, null); + // } } From cea1a7715f7ef6cdca211d7bc3465dd01b10b642 Mon Sep 17 00:00:00 2001 From: dadhi Date: Wed, 18 Jun 2025 13:54:57 +0200 Subject: [PATCH 10/54] fixes --- playground/Experiments/FHashMap11.cs | 55 +++++++++++++++++++--------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/playground/Experiments/FHashMap11.cs b/playground/Experiments/FHashMap11.cs index 6e171c81..e2951469 100644 --- a/playground/Experiments/FHashMap11.cs +++ b/playground/Experiments/FHashMap11.cs @@ -217,7 +217,7 @@ public bool TryGetValue(K key, out V value) var hash = default(TEq).GetHashCode(key); var indexMask = (1 << _capacityBitShift) - 1; - var hashIndex = hash & indexMask; + var index = hash & indexMask; #if NET7_0_OR_GREATER ref var probes = ref MemoryMarshal.GetArrayDataReference(Probes); @@ -225,21 +225,25 @@ public bool TryGetValue(K key, out V value) ref var keys = ref MemoryMarshal.GetArrayDataReference(Keys); ref var valueIndexes = ref MemoryMarshal.GetArrayDataReference(ValueIndexes); #else - var hashAndKeyEntries = KeyEntries; + var probes = Probes; + var hashes = Hashes; + var keys = Keys; + var valueIndexes = ValueIndexes; #endif - var p = GetItem(ref probes, hashIndex); + var p = GetItem(ref probes, index); var probe = 1; while (p >= probe) { - if ((p == probe) & (GetItem(ref hashes, hashIndex) == hash) && - default(TEq).Equals(GetItem(ref keys, hashIndex), key)) + if ((p == probe) & (GetItem(ref hashes, index) == hash) && + default(TEq).Equals(GetItem(ref keys, index), key)) { - value = Values[GetItem(ref valueIndexes, hashIndex)]; + value = Values[GetItem(ref valueIndexes, index)]; return true; } - p = GetItem(ref probes, ++hashIndex & indexMask); + index = (index + 1) & indexMask; + p = GetItem(ref probes, index); ++probe; } } @@ -274,7 +278,10 @@ public ref V GetOrAddValueRef(K key) ref var keys = ref MemoryMarshal.GetArrayDataReference(Keys); ref var valueIndexes = ref MemoryMarshal.GetArrayDataReference(ValueIndexes); #else - var hashAndKeyEntries = KeyEntries; + var probes = Probes; + var hashes = Hashes; + var keys = Keys; + var valueIndexes = ValueIndexes; #endif ref var pRef = ref GetItemRef(ref probes, index); @@ -293,6 +300,7 @@ public ref V GetOrAddValueRef(K key) } // Nothing found, add a new entry and inscrease the count first + var valueIndex = currCount; _count = currCount + 1; // 3. There is an empty slot to insert the new entry, so we can just insert it @@ -305,7 +313,7 @@ public ref V GetOrAddValueRef(K key) pRef = probe; GetItemRef(ref hashes, index) = hash; GetItemRef(ref keys, index) = key; - GetItemRef(ref valueIndexes, index) = currCount; + GetItemRef(ref valueIndexes, index) = valueIndex; } else // p < probe, e.g. 3, 4, 5, (5<6), (6==6), (7==7), (3<8), (0<4) { @@ -324,10 +332,14 @@ public ref V GetOrAddValueRef(K key) pRef = probe; GetItemRef(ref hashes, index) = hash; GetItemRef(ref keys, index) = key; - GetItemRef(ref valueIndexes, index) = currCount; + GetItemRef(ref valueIndexes, index) = valueIndex; // 5. Now treat the robin-hooded entry as a newly inserted entry probe = rhProbe; + hash = rhHash; + key = rhKey; + valueIndex = rhValueIndex; + while (true) { index = (index + 1) & indexMask; @@ -444,9 +456,18 @@ internal int ResizeHashes(int indexMask) ref var newValueIndexesRef = ref MemoryMarshal.GetArrayDataReference(newValueIndexes); ref var oldValueIndexesRef = ref MemoryMarshal.GetArrayDataReference(ValueIndexes); #else - var newKeys = newKeys; - var oldKeys = Keys; - var oldKey = oldKeys[0]; + var newProbesRef = newProbes; + var oldProbesRef = Probes; + var oldProbe = oldProbesRef[0]; + + var newHashesRef = newHashes; + var oldHashesRef = Hashes; + + var newKeysRef = newKeys; + var oldKeysRef = Keys; + + var newValueIndexesRef = newValueIndexes; + var oldValueIndexesRef = ValueIndexes; #endif // Overflow segment is wrapped-around hashes and! the hashes at the beginning robin-hooded by the wrapped-around hashes // so we skip them to start from first ideal or empty entry @@ -459,7 +480,7 @@ internal int ResizeHashes(int indexMask) { if (oldProbe != 0) { - var hash = GetItem(ref oldHashesRef, i); + var hash = GetItem(ref oldHashesRef, i & indexMask); var newIndex = hash & newIndexMask; // no need for robin-hooding because we already did it for the old hashes and @@ -469,13 +490,13 @@ internal int ResizeHashes(int indexMask) while (newProbeRef != 0) { newIndex = (newIndex + 1) & newIndexMask; - newProbeRef = ref GetItemRef(ref newProbeRef, newIndex); + newProbeRef = ref GetItemRef(ref newProbesRef, newIndex); ++newProbe; } newProbeRef = newProbe; GetItemRef(ref newHashesRef, newIndex) = hash; - GetItemRef(ref newKeysRef, newIndex) = GetItem(ref oldKeysRef, i); - GetItemRef(ref newValueIndexesRef, newIndex) = GetItem(ref oldValueIndexesRef, i); + GetItemRef(ref newKeysRef, newIndex) = GetItem(ref oldKeysRef, i & indexMask); + GetItemRef(ref newValueIndexesRef, newIndex) = GetItem(ref oldValueIndexesRef, i & indexMask); } if (++i >= oldCapWithOverflowSegment) break; From 952fa26b31796b5e2d580d5dd422b0a05f5505c8 Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 23 Jun 2025 17:01:51 +0200 Subject: [PATCH 11/54] simd is 3x slower then scalar --- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 58 ++++++++--- src/ImTools/ImTools.cs | 96 +++++++++---------- 2 files changed, 92 insertions(+), 62 deletions(-) diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index c0cfe677..97a69819 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -26,11 +26,12 @@ namespace Playground { public class ImHashMapBenchmarks { - private static readonly Type[] _keys = typeof(Dictionary<,>).Assembly.GetTypes().Take(1000).ToArray(); + private static readonly Type[] _allKeys = typeof(List<>).Assembly.GetTypes().Take(2000).ToArray(); + private static readonly Type[] _keys = _allKeys.Take(1000).ToArray(); + private static readonly Type[] _missingKeys = _allKeys.TakeLast(1000).ToArray(); // get half of the random _keys for the lookup, by selecting half of the random indexes from keys private static readonly Random _seed = new Random(42); - private static readonly Type[] _randomLookupKeys = _keys.OrderBy(_ => _seed.Next()).Take(_keys.Length / 2).ToArray(); public struct TypeVal : IEquatable { @@ -2299,15 +2300,40 @@ .NET SDK 9.0.203 | SmallMap_TryGetValue | 1000 | 2.364 us | 0.0387 us | 0.0302 us | 2.377 us | 1.07 | 0.06 | 1 | - | NA | | FHashMap11_TryGetValue | 1000 | 2.387 us | 0.0477 us | 0.1170 us | 2.434 us | 1.08 | 0.08 | 1 | - | NA | + ## APL FHashMap11 + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|-----:|----------:|------------:| + | DictionarySlim_TryGetValue | 1000 | 2.195 us | 0.0437 us | 0.1217 us | 1.00 | 0.08 | 1 | - | NA | + | FHashMap11_TryGetValue | 1000 | 2.333 us | 0.0465 us | 0.0929 us | 1.07 | 0.07 | 2 | - | NA | + | SmallMap_TryGetValue | 1000 | 2.514 us | 0.0489 us | 0.0789 us | 1.15 | 0.07 | 3 | - | NA | + + ## Reshuffle the lookup comparison + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | + |--------------------------- |------ |------------:|----------:|-----------:|------:|--------:|-----:|----------:|------------:| + | SmallMap_TryGetValue | 10 | 44.90 ns | 0.536 ns | 0.419 ns | 0.99 | 0.04 | 1 | - | NA | + | DictionarySlim_TryGetValue | 10 | 45.43 ns | 0.941 ns | 1.857 ns | 1.00 | 0.06 | 1 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 385.36 ns | 4.621 ns | 4.097 ns | 1.00 | 0.01 | 1 | - | NA | + | SmallMap_TryGetValue | 100 | 513.52 ns | 5.702 ns | 5.054 ns | 1.33 | 0.02 | 2 | - | NA | + | | | | | | | | | | | + | SmallMap_TryGetValue | 1000 | 4,255.63 ns | 82.286 ns | 130.514 ns | 0.89 | 0.04 | 1 | - | NA | + | DictionarySlim_TryGetValue | 1000 | 4,794.12 ns | 95.912 ns | 189.322 ns | 1.00 | 0.06 | 2 | - | NA | + */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] // [Params(1, 10, 100)] - [Params(1000)] + [Params(10, 100, 1000)] public int Count; + private Type[] _randomPresentKeys; + [GlobalSetup] public void Populate() { + _randomPresentKeys = _keys.Take(Count).OrderBy(_ => _seed.Next()).ToArray(); + _mapV4 = V4_ImHashMap_AddOrUpdate(); _mapV3 = V3_ImHashMap_AddOrUpdate(); _mapV2 = V2_AddOrUpdate(); @@ -2735,12 +2761,14 @@ public string Dictionary_TryGetValue() public int DictionarySlim_TryGetValue() { var count = 0; - foreach (var k in _randomLookupKeys) + var iters = Count / 2; + for (var i = 0; i < iters; ++i) { - _dictSlim.TryGetValue(k, out var result); - count += result.Length; + if (_dictSlim.TryGetValue(_randomPresentKeys[i], out var result)) + count += result.Length; + if (!_dictSlim.TryGetValue(_missingKeys[i], out var _)) + --count; } - return count; } @@ -2748,20 +2776,22 @@ public int DictionarySlim_TryGetValue() public int SmallMap_TryGetValue() { var count = 0; - foreach (var k in _randomLookupKeys) + var iters = Count / 2; + for (var i = 0; i < iters; ++i) { - _smallMap.TryGetValue(k, out var result); - count += result.Length; + if (_smallMap.TryGetValue(_randomPresentKeys[i], out var result)) + count += result.Length; + if (!_smallMap.TryGetValue(_missingKeys[i], out var _)) + --count; } - return count; } - [Benchmark] + // [Benchmark] public int FHashMap11_TryGetValue() { var count = 0; - foreach (var k in _randomLookupKeys) + foreach (var k in _randomPresentKeys) { _fHashMap11.TryGetValue(k, out var result); count += result.Length; @@ -2774,7 +2804,7 @@ public int FHashMap11_TryGetValue() public int FHashMap_TryGetValue() { var count = 0; - foreach (var k in _randomLookupKeys) + foreach (var k in _randomPresentKeys) { var result = _fHashMap.TryGetValueRef(k, out _); count += result.Length; diff --git a/src/ImTools/ImTools.cs b/src/ImTools/ImTools.cs index 66495959..ec91b115 100644 --- a/src/ImTools/ImTools.cs +++ b/src/ImTools/ImTools.cs @@ -7314,10 +7314,6 @@ public bool TryGetValue_SIMD(K key, out V value) #if NET7_0_OR_GREATER if (Vector256.IsHardwareAccelerated) { - var indexMaskVec = Vector256.Create(indexMask); - var hashMiddleMaskVec = Vector256.Create(hashMiddleMask); - var hashMiddleVec = Vector256.Create(hashMiddle); - var hashesAndIndexesVec = MemoryMarshal.Cast>(_packedHashesAndIndexes.AsSpan()); var vIndexMask = indexMask >> 3; @@ -7325,7 +7321,7 @@ public bool TryGetValue_SIMD(K key, out V value) // Adjust probes if starting index is not aligned to 8 // e.g. if hashIndex = 1 -> V(0, 1, 2, 3, 4, 5, 6, 7) to V(-1, 0, 1, 2, 3, 4, 5, 6) - var expectedProbesVec = Vector256.Subtract(InitialProbesVec, Vector256.Create(hashIndex & 7)); + var expectedProbeVec = Vector256.Subtract(InitialProbesVec, Vector256.Create(hashIndex & 7)); while (true) { @@ -7333,39 +7329,33 @@ public bool TryGetValue_SIMD(K key, out V value) // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var hProbeVec = Vector256.ShiftRightLogical(hVec, HSmallMap.ProbeCountShift); + var probeDistanceVec = Vector256.Subtract(hProbeVec, expectedProbeVec); - // while ((h >>> HSmallMap.ProbeCountShift) >= probes) - if (Vector256.GreaterThanOrEqual(hProbeVec, expectedProbesVec) == Vector256.Zero) - break; - - var matchedProbesVec = Vector256.Equals(hProbeVec, expectedProbesVec); - var matchedHashMiddleVec = Vector256.Equals(Vector256.BitwiseAnd(hVec, hashMiddleMaskVec), hashMiddleVec); - var matchVec = Vector256.BitwiseAnd(matchedProbesVec, matchedHashMiddleVec); - var matchMask = matchVec.ExtractMostSignificantBits(); - - // if (((h >>> HSmallMap.ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) - while (matchMask != 0) + for (var i = 0; i < Vector256.Count; i++) { - // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - var matchIndex = BitOperations.TrailingZeroCount(matchMask); - var entryIndex = hVec.GetElement(matchIndex) & indexMask; - - ref var e = ref _entries.GetSurePresentEntryRef(entryIndex); - if (default(TEq).Equals(e.Key, key)) + var dist = probeDistanceVec.GetElement(i); + if (dist == 0) { - value = e.Value; - return true; + if ((hVec.GetElement(i) & hashMiddleMask) == hashMiddle) + { + ref var e = ref _entries.GetSurePresentEntryRef(hVec.GetElement(i) & indexMask); + if (default(TEq).Equals(e.Key, key)) + { + value = e.Value; + return true; + } + } + } + else if (dist < 0) + { + value = default; + return false; } - - matchMask &= matchMask - 1; // clear the matched bit } // e.g. V(-1, 0, 1, 2, 3, 4, 5, 6) + 8 -> V(7, 8, 9, 10, 11, 12, 13, 14) - expectedProbesVec = Vector256.Add(expectedProbesVec, VectorSizeVec); + expectedProbeVec = Vector256.Add(expectedProbeVec, VectorSizeVec); } - - value = default; - return false; } #endif #if NET7_0_OR_GREATER @@ -7377,22 +7367,27 @@ public bool TryGetValue_SIMD(K key, out V value) var h = HSmallMap.GetItem(ref hashesAndIndexes, hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - var probes = 1; - while ((h >>> HSmallMap.ProbeCountShift) >= probes) + var probe = 1; + while (true) { - // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> HSmallMap.ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) + var probeDistance = (h >>> HSmallMap.ProbeCountShift) - probe; + if (probeDistance == 0) { - ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); - if (default(TEq).Equals(e.Key, key)) + if ((h & hashMiddleMask) == hashMiddle) { - value = e.Value; - return true; + ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); + if (default(TEq).Equals(e.Key, key)) + { + value = e.Value; + return true; + } } } + else if (probeDistance < 0) + break; h = HSmallMap.GetItem(ref hashesAndIndexes, ++hashIndex & indexMask); - ++probes; + ++probe; } } @@ -7422,22 +7417,27 @@ public bool TryGetValue(K key, out V value) var h = HSmallMap.GetItem(ref hashesAndIndexes, hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - var probes = 1; - while ((h >>> HSmallMap.ProbeCountShift) >= probes) + var probe = 1; + while (true) { - // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> HSmallMap.ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) + var probeDistance = (h >>> HSmallMap.ProbeCountShift) - probe; + if (probeDistance == 0) { - ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); - if (default(TEq).Equals(e.Key, key)) + if ((h & hashMiddleMask) == hashMiddle) { - value = e.Value; - return true; + ref var e = ref _entries.GetSurePresentEntryRef(h & indexMask); + if (default(TEq).Equals(e.Key, key)) + { + value = e.Value; + return true; + } } } + else if (probeDistance < 0) + break; h = HSmallMap.GetItem(ref hashesAndIndexes, ++hashIndex & indexMask); - ++probes; + ++probe; } } From 66f3582de8afe85af5dd8aac2fede6aae66c59c9 Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 6 Jul 2025 21:39:24 +0200 Subject: [PATCH 12/54] new FEC SmallMap with fixes and first test passing --- playground/Experiments/FEC_ImTools.cs | 2071 +++++++++-------- playground/Experiments/FHashMap11.cs | 8 +- .../Experiments/UnscopedRefAttribute.cs | 7 + .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 131 +- .../ImTools.Benchmarks.csproj | 6 +- src/ImTools/ImTools.cs | 3 +- .../Playground/FecSmallMapTests.cs | 415 ++++ 7 files changed, 1652 insertions(+), 989 deletions(-) create mode 100644 playground/Experiments/UnscopedRefAttribute.cs create mode 100644 test/ImTools.UnitTests/Playground/FecSmallMapTests.cs diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 21bd1402..afc6e2f3 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -1,17 +1,73 @@ +// +/* +The MIT License (MIT) + +Copyright (c) 2016-2025 Maksim Volkau + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// ReSharper disable once InconsistentNaming +#nullable disable + +#if !NETSTANDARD2_0_OR_GREATER && !NET472 +#define SUPPORTS_UNSAFE +#define SUPPORTS_CREATE_SPAN +#endif + +#if LIGHT_EXPRESSION +namespace FastExpressionCompiler.LightExpression.ImTools; +#else +namespace FastExpressionCompiler.ImTools; +#endif + using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Text; using System.Runtime.CompilerServices; - -#if NET7_0_OR_GREATER using System.Runtime.InteropServices; +using System.Diagnostics.CodeAnalysis; + +#if NET8_0_OR_GREATER +using System.Runtime.Intrinsics; #endif -namespace FastExpressionCompiler.ImTools; +using static SmallMap; -using static FHashMap; +/// Helpers and polyfills for the missing things in the old .NET versions +public static class RefTools +{ + /// Polyfill for the missing returning the `ref` in the failed search scenario. + /// Note that the result is the `null` even for the struct `T`, so avoid the accessing its members without the check + [MethodImpl((MethodImplOptions)256)] + public static ref T GetNullRef() => +#if SUPPORTS_UNSAFE + ref Unsafe.NullRef(); +#else + ref _missing; + internal static T _missing = default; +#endif +} -/// Wrapper for the array and count +/// Wrapper for the array of the specific capacity and a separate count less or equal to this capacity public struct SmallList { /// Array of items @@ -37,13 +93,17 @@ public SmallList(T[] items) : this(items, items.Length) { } public static class SmallList { internal const int ForLoopCopyCount = 4; - internal const int InitialCapacity = 4; + internal const int DefaultInitialCapacity = 4; + + [MethodImpl(MethodImplOptions.NoInlining)] + internal static ref T ThrowIndexOutOfBounds(int index, int count) => + throw new IndexOutOfRangeException($"Index {index} is out of range of count {count} for SmallList<{typeof(T)},..>."); [MethodImpl((MethodImplOptions)256)] - internal static void Expand(ref TItem[] items) + internal static void Expand(ref T[] items) { // `| 1` is for the case when the length is 0 - var newItems = new TItem[(items.Length << 1) | 1]; // have fun to guess the new length, haha ;-P + var newItems = new T[(items.Length << 1) | 1]; // have fun to guess the new length, ha-ha ;-P if (items.Length > ForLoopCopyCount) Array.Copy(items, newItems, items.Length); else @@ -55,7 +115,7 @@ internal static void Expand(ref TItem[] items) /// Appends the new default item at the end of the items. Assumes that `index lte items.Length`! /// `items` should be not null [MethodImpl((MethodImplOptions)256)] - public static ref TItem AppendDefaultToNotNullItemsAndGetRef(ref TItem[] items, int index) + public static ref T AddDefaultToNotNullItemsAndGetRef(ref T[] items, int index) { Debug.Assert(index <= items.Length); if (index == items.Length) @@ -65,12 +125,12 @@ public static ref TItem AppendDefaultToNotNullItemsAndGetRef(ref TItem[] /// Appends the new default item at the end of the items. Assumes that `index lte items.Length`, `items` may be null [MethodImpl((MethodImplOptions)256)] - public static ref TItem AppendDefaultAndGetRef(ref TItem[] items, int index, int initialCapacity = InitialCapacity) + public static ref T AddDefaultAndGetRef(ref T[] items, int index, int initialCapacity = DefaultInitialCapacity) { if (items == null) { Debug.Assert(index == 0); - items = new TItem[initialCapacity]; + items = new T[initialCapacity]; return ref items[index]; } @@ -82,624 +142,744 @@ public static ref TItem AppendDefaultAndGetRef(ref TItem[] items, int ind /// Returns surely present item ref by its index [MethodImpl((MethodImplOptions)256)] - public static ref TItem GetSurePresentItemRef(this ref SmallList source, int index) => + public static ref T GetSurePresentItemRef(this ref SmallList source, int index) => ref source.Items[index]; + /// Returns surely present item ref by its index without boundary checks + [MethodImpl((MethodImplOptions)256)] + public static ref T GetSurePresentItemRef(this T[] source, int index) => +#if SUPPORTS_UNSAFE + ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(source), index); +#else + ref source[index]; +#endif + +#if NET7_0_OR_GREATER + /// Get the item by-ref without bounds check + [MethodImpl((MethodImplOptions)256)] + public static ref T GetSurePresentItemRef(this ref T source, int index) where T : struct => + ref Unsafe.Add(ref source, index); +#endif + + /// Get the item without bounds check + [MethodImpl((MethodImplOptions)256)] +#if NET7_0_OR_GREATER + internal static T GetSurePresentItem(this ref T source, int index) where T : struct => Unsafe.Add(ref source, index); +#else + internal static T GetSurePresentItem(this T[] source, int index) => source[index]; +#endif + // todo: @perf add the not null variant /// Appends the new default item to the list and returns ref to it for write or read [MethodImpl((MethodImplOptions)256)] - public static ref TItem Append(ref this SmallList source, int initialCapacity = InitialCapacity) => - ref AppendDefaultAndGetRef(ref source.Items, source.Count++, initialCapacity); + public static ref T Add(this ref SmallList source, int initialCapacity = DefaultInitialCapacity) => + ref AddDefaultAndGetRef(ref source.Items, source.Count++, initialCapacity); /// Appends the new item to the list // todo: @perf add the not null variant [MethodImpl((MethodImplOptions)256)] - public static void Append(ref this SmallList source, in TItem item, int initialCapacity = InitialCapacity) => - AppendDefaultAndGetRef(ref source.Items, source.Count++, initialCapacity) = item; + public static void Add(this ref SmallList source, in T item, int initialCapacity = DefaultInitialCapacity) => + AddDefaultAndGetRef(ref source.Items, source.Count++, initialCapacity) = item; /// Looks for the item in the list and return its index if found or -1 for the absent item [MethodImpl((MethodImplOptions)256)] - public static int TryGetIndex(this ref SmallList source, TItem it, TEq eq = default) - where TEq : struct, IEq + public static int TryGetIndex(this T[] items, in T it, int startIndex, int count, TEq eq = default, + int notFoundResult = -1) + where TEq : struct, IEq { - var count = source.Count; - var items = source.Items; - for (var i = 0; i < count; ++i) + Debug.Assert(items != null); + for (var i = startIndex; i < count; ++i) { ref var di = ref items[i]; // todo: @perf Marshall? if (eq.Equals(it, di)) return i; } - return -1; + return notFoundResult; } - /// Returns the ref of the found item or appends the item to the end of the list, and returns ref to it + /// Looks for the item in the list and return its index if found or -1 for the absent item + [MethodImpl((MethodImplOptions)256)] + public static int TryGetIndex(this ref SmallList source, T it, TEq eq = default) + where TEq : struct, IEq + => source.Items.TryGetIndex(it, 0, source.Count, eq); + + /// Returns the index of the found item or appends the item to the end of the list, and returns its index [MethodImpl((MethodImplOptions)256)] - public static int GetIndexOrAppend(this ref SmallList source, in TItem item, TEq eq) - where TEq : struct, IEq + public static int GetIndexOrAdd(this ref SmallList source, in T item, TEq eq = default) + where TEq : struct, IEq { var count = source.Count; - var items = source.Items; - for (var i = 0; i < count; ++i) + if (count != 0) { - ref var di = ref items[i]; // todo: @perf Marshall? - if (eq.Equals(item, di)) - return i; + var index = TryGetIndex(source.Items, in item, 0, count, eq); + if (index != -1) + return index; } - source.Append() = item; - return -1; + source.Add() = item; + return count; } +} - /// Returns surely present item ref by its index +#pragma warning disable CS9101 // UnscopedRef goes wrong on Ubuntu + +/// Utilities for Stack4, Stack8, etc. +public static class Stack +{ + [MethodImpl(MethodImplOptions.NoInlining)] + internal static ref T ThrowIndexOutOfBounds(int index, int capacity) => + throw new IndexOutOfRangeException($"Index {index} is out of range for Stack{capacity}<{typeof(T)},..>."); +} + +/// Stack with the Size information to check it at compile time +public interface IStack : IStack + where TSize : struct, ISize + where TStack : struct, IStack +{ +} + +/// Abstracts over collection of the items on stack of the fixed Capacity, +/// to be used as a part of the hybrid data structures which grow from stack to heap +public interface IStack + where TStack : struct, IStack +{ + /// Maximum count of items hold on stack + int Capacity { get; } + + /// Capacity == 1 << CapacityBitShift + byte CapacityBitShift { get; } + + /// Returns the item by ref to read and write the item value, + /// but does not check the index bounds comparing to the `this[index]` + [UnscopedRef] + ref T GetSurePresentItemRef(int index); + + /// Indexer returning the item by ref to read and write the item value + [UnscopedRef] + ref T this[int index] { get; } + +#if SUPPORTS_CREATE_SPAN + /// Creates a span over the stack items + public Span AsSpan(); +#endif +} + +/// Base marker for collection or container holding some number of items +public interface ISize +{ + /// Returns the size of the collection or container + int Size { get; } +} +/// Marker for collection or container holding 2 or items +public interface ISize2Plus : ISize { } +/// Marker for collection or container holding 4 or more items +public interface ISize4Plus : ISize2Plus { } +/// Marker for collection or container holding 8 or more items +public interface ISize8Plus : ISize4Plus { } +/// Marker for collection or container holding 16 or more items +public interface ISize16Plus : ISize8Plus { } + +/// Marker for collection or container holding 0 items +public struct Size0 : ISize +{ + /// + public int Size => 0; +} + +/// Marker for collection or container holding 4 items +public struct Size2 : ISize2Plus +{ + /// + public int Size => 2; +} +/// Marker for collection or container holding 4 items +public struct Size4 : ISize4Plus +{ + /// + public int Size => 4; +} +/// Marker for collection or container holding 8 items +public struct Size8 : ISize8Plus +{ + /// + public int Size => 8; +} +/// Marker for collection or container holding 16 items +public struct Size16 : ISize16Plus +{ + /// + public int Size => 16; +} + +/// Implementation of `IStack` for 2 items on stack +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct Stack2 : IStack> +{ + /// + public int Capacity => 2; + /// + public byte CapacityBitShift => 1; + + internal T _it0, _it1; + + /// + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public static ref TItem GetSurePresentItemRef(this ref SmallList4 source, int index) + public ref T GetSurePresentItemRef(int index) { - Debug.Assert(source.Count != 0); - Debug.Assert(index < source.Count); +#if SUPPORTS_UNSAFE + return ref Unsafe.Add(ref _it0, index); +#else switch (index) { - case 0: return ref source._it0; - case 1: return ref source._it1; - case 2: return ref source._it2; - case 3: return ref source._it3; - default: - Debug.Assert(source._rest != null, $"Expecting deeper items are already existing on stack at index: {index}"); - return ref source._rest[index - SmallList4.StackItemCount]; + case 0: return ref _it0; + default: return ref _it1; } +#endif } - /// Returns last present item ref, assumes that the list is not empty! - [MethodImpl((MethodImplOptions)256)] - public static ref TItem GetLastSurePresentItem(this ref SmallList4 source) => - ref source.GetSurePresentItemRef(source._count - 1); + /// + [UnscopedRef] + public ref T this[int index] + { + [MethodImpl((MethodImplOptions)256)] + get + { + if (index >= 0 & index < Capacity) + return ref GetSurePresentItemRef(index); + return ref Stack.ThrowIndexOutOfBounds(index, Capacity); + } + } - /// Returns the ref to tombstone indicating the missing item. +#if SUPPORTS_CREATE_SPAN + /// [MethodImpl((MethodImplOptions)256)] - public static ref TItem NotFound(this ref SmallList4 _) => ref SmallList4.Missing; + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _it0, Capacity); +#endif +} - /// Appends the default item to the end of the list and returns the reference to it. +/// Implementation of `IStack` for 4 items on stack +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct Stack4 : IStack> +{ + /// + public int Capacity => 4; + /// + public byte CapacityBitShift => 2; + + internal T _it0, _it1, _it2, _it3; + + /// + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public static ref TItem AppendDefaultAndGetRef(this ref SmallList4 source) + public ref T GetSurePresentItemRef(int index) { - var index = source._count++; +#if SUPPORTS_UNSAFE + return ref Unsafe.Add(ref _it0, index); +#else switch (index) { - case 0: return ref source._it0; - case 1: return ref source._it1; - case 2: return ref source._it2; - case 3: return ref source._it3; - default: - return ref AppendDefaultAndGetRef(ref source._rest, index - SmallList4.StackItemCount, SmallList4.StackItemCount); + case 0: return ref _it0; + case 1: return ref _it1; + case 2: return ref _it2; + default: return ref _it3; } +#endif } - /// Looks for the item in the list and return its index if found or -1 for the absent item - [MethodImpl((MethodImplOptions)256)] - public static int TryGetIndex(this ref SmallList4 source, TItem it, TEq eq = default) - where TEq : struct, IEq + /// + [UnscopedRef] + public ref T this[int index] { - switch (source._count) + [MethodImpl((MethodImplOptions)256)] + get { - case 1: - if (eq.Equals(it, source._it0)) return 0; - break; + if (index >= 0 & index < Capacity) + return ref GetSurePresentItemRef(index); + return ref Stack.ThrowIndexOutOfBounds(index, Capacity); + } + } - case 2: - if (eq.Equals(it, source._it0)) return 0; - if (eq.Equals(it, source._it1)) return 1; - break; +#if SUPPORTS_CREATE_SPAN + /// + [MethodImpl((MethodImplOptions)256)] + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _it0, Capacity); +#endif +} - case 3: - if (eq.Equals(it, source._it0)) return 0; - if (eq.Equals(it, source._it1)) return 1; - if (eq.Equals(it, source._it2)) return 2; - break; +/// Implementation of `IStack` for 8 items on stack +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct Stack8 : IStack> +{ + /// + public int Capacity => 8; + /// + public byte CapacityBitShift => 3; - default: - if (eq.Equals(it, source._it0)) return 0; - if (eq.Equals(it, source._it1)) return 1; - if (eq.Equals(it, source._it2)) return 2; - if (eq.Equals(it, source._it3)) return 3; - if (source._rest != null) - { - var count = source._count - SmallList4.StackItemCount; - var items = source._rest; - for (var i = 0; i < count; ++i) - { - ref var di = ref items[i]; // todo: @perf Marshall? - if (eq.Equals(it, di)) - return i + SmallList4.StackItemCount; - } - } - break; - } - return -1; - } + internal T _it0, _it1, _it2, _it3, _it4, _it5, _it6, _it7; - /// Returns the ref of the found item or appends the item to the end of the list, and returns ref to it + /// + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public static int GetIndexOrAppend(this ref SmallList4 source, in TItem item, TEq eq) - where TEq : struct, IEq + public ref T GetSurePresentItemRef(int index) { - switch (source._count) +#if SUPPORTS_UNSAFE + return ref Unsafe.Add(ref _it0, index); +#else + switch (index) { - case 0: - source._count = 1; - source._it0 = item; - return -1; - - case 1: - if (eq.Equals(item, source._it0)) return 0; - source._count = 2; - source._it1 = item; - return -1; - - case 2: - if (eq.Equals(item, source._it0)) return 0; - if (eq.Equals(item, source._it1)) return 1; - source._count = 3; - source._it2 = item; - return -1; - - case 3: - if (eq.Equals(item, source._it0)) return 0; - if (eq.Equals(item, source._it1)) return 1; - if (eq.Equals(item, source._it2)) return 2; - source._count = 4; - source._it3 = item; - return -1; - - default: - if (eq.Equals(item, source._it0)) return 0; - if (eq.Equals(item, source._it1)) return 1; - if (eq.Equals(item, source._it2)) return 2; - if (eq.Equals(item, source._it3)) return 3; - - var restCount = source._count - SmallList4.StackItemCount; - if (restCount != 0) - { - var items = source._rest; - for (var i = 0; i < restCount; ++i) - { - ref var di = ref items[i]; // todo: @perf Marshall? - if (eq.Equals(item, di)) - return i + SmallList4.StackItemCount; - } - } - AppendDefaultAndGetRef(ref source._rest, restCount, SmallList4.StackItemCount) = item; - ++source._count; - return -1; + case 0: return ref _it0; + case 1: return ref _it1; + case 2: return ref _it2; + case 3: return ref _it3; + case 4: return ref _it4; + case 5: return ref _it5; + case 6: return ref _it6; + default: return ref _it7; } +#endif } - /// Returns surely present item ref by its index - [MethodImpl((MethodImplOptions)256)] - public static ref TItem GetSurePresentItemRef(this ref SmallList2 source, int index) + /// + [UnscopedRef] + public ref T this[int index] { - Debug.Assert(source.Count != 0); - Debug.Assert(index < source.Count); - switch (index) + [MethodImpl((MethodImplOptions)256)] + get { - case 0: return ref source._it0; - case 1: return ref source._it1; - default: - Debug.Assert(source._rest != null, $"Expecting deeper items are already existing on stack at index: {index}"); - return ref source._rest[index - SmallList2.StackItemCount]; + if (index >= 0 & index < Capacity) + return ref GetSurePresentItemRef(index); + return ref Stack.ThrowIndexOutOfBounds(index, Capacity); } } - /// Returns last present item ref, assumes that the list is not empty! +#if SUPPORTS_CREATE_SPAN + /// [MethodImpl((MethodImplOptions)256)] - public static ref TItem GetLastSurePresentItem(this ref SmallList2 source) => - ref source.GetSurePresentItemRef(source._count - 1); + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _it0, Capacity); +#endif +} - /// Returns the ref to tombstone indicating the missing item. - [MethodImpl((MethodImplOptions)256)] - public static ref TItem NotFound(this ref SmallList2 _) => ref SmallList2.Missing; +/// Implementation of `IStack` for 16 items on stack +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct Stack16 : IStack> +{ + /// + public int Capacity => 16; + /// + public byte CapacityBitShift => 4; - /// Appends the default item to the end of the list and returns the reference to it. + internal T _it0, _it1, _it2, _it3, _it4, _it5, _it6, _it7; + internal T _it8, _it9, _it10, _it11, _it12, _it13, _it14, _it15; + + /// + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public static ref TItem AppendDefaultAndGetRef(this ref SmallList2 source) + public ref T GetSurePresentItemRef(int index) { - var index = source._count++; +#if SUPPORTS_UNSAFE + return ref Unsafe.Add(ref _it0, index); +#else switch (index) { - case 0: return ref source._it0; - case 1: return ref source._it1; - default: - return ref AppendDefaultAndGetRef(ref source._rest, index - SmallList2.StackItemCount, SmallList2.StackItemCount); + case 0: return ref _it0; + case 1: return ref _it1; + case 2: return ref _it2; + case 3: return ref _it3; + case 4: return ref _it4; + case 5: return ref _it5; + case 6: return ref _it6; + case 7: return ref _it7; + case 8: return ref _it8; + case 9: return ref _it9; + case 10: return ref _it10; + case 11: return ref _it11; + case 12: return ref _it12; + case 13: return ref _it13; + case 14: return ref _it14; + default: return ref _it15; } +#endif } - /// Looks for the item in the list and return its index if found or -1 for the absent item - [MethodImpl((MethodImplOptions)256)] - public static int TryGetIndex(this ref SmallList2 source, TItem it, TEq eq = default) - where TEq : struct, IEq + /// + [UnscopedRef] + public ref T this[int index] { - switch (source._count) + [MethodImpl((MethodImplOptions)256)] + get { - case 1: - if (eq.Equals(it, source._it0)) return 0; - break; - - default: - if (eq.Equals(it, source._it0)) return 0; - if (eq.Equals(it, source._it1)) return 1; - if (source._rest != null) - { - var count = source._count - SmallList2.StackItemCount; - var items = source._rest; - for (var i = 0; i < count; ++i) - { - ref var di = ref items[i]; // todo: @perf Marshall? - if (eq.Equals(it, di)) - return i + SmallList2.StackItemCount; - } - } - break; + if (index >= 0 & index < Capacity) + return ref GetSurePresentItemRef(index); + return ref Stack.ThrowIndexOutOfBounds(index, Capacity); } - return -1; } - /// Returns the ref of the found item or appends the item to the end of the list, and returns ref to it +#if SUPPORTS_CREATE_SPAN + /// [MethodImpl((MethodImplOptions)256)] - public static int GetIndexOrAppend(this ref SmallList2 source, TItem item, TEq eq) - where TEq : struct, IEq - { - switch (source._count) - { - case 0: - source._count = 1; - source._it0 = item; - return -1; - - case 1: - if (eq.Equals(item, source._it0)) return 0; - source._count = 2; - source._it1 = item; - return -1; - - default: - if (eq.Equals(item, source._it0)) return 0; - if (eq.Equals(item, source._it1)) return 1; - - var restCount = source._count - SmallList2.StackItemCount; - if (restCount != 0) - { - var items = source._rest; - for (var i = 0; i < restCount; ++i) - { - ref var di = ref items[i]; // todo: @perf Marshall? - if (eq.Equals(item, di)) - return i + SmallList2.StackItemCount; - } - } - AppendDefaultAndGetRef(ref source._rest, restCount, SmallList2.StackItemCount) = item; - ++source._count; - return -1; - } - } + public Span AsSpan() => MemoryMarshal.CreateSpan(ref _it0, Capacity); +#endif } -/// List with the number of first items (4) stored inside its struct and the rest in the growable array. -/// Supports addition and removal (removel is without resize) only at the end of the list, aka Stack behavior -public struct SmallList4 +/// Generic version of SmallList abstracted for how much items are on the stack +public struct SmallList : IEnumerable + where TStack : struct, IStack { - /// The number of entries stored inside the map itself without moving them to array on heap - public const int StackItemCount = 4; + /// Let's enable access to the Count, so you can Pop the item by --list.Count. Just don't forget to nullify the popped item if needed + public int Count; - // todo: @check what if someone stores something in it, it would be a memory leak, but isn't it the same as using `out var` in the returning`false` Try...methods? - internal static TItem Missing; // return the ref to Tombstone when nothing found +#pragma warning disable CS0649 // it is fine `Stack` is never assigned to, and will always have its default value + /// Let's enable access to the stack, just know what's you doing + public TStack Stack; +#pragma warning restore CS0649 - internal int _count; - internal TItem _it0, _it1, _it2, _it3; - internal TItem[] _rest; + /// Exposes the rest on the heap + public T[] Rest; - /// Gets the number of items in the list - public int Count + /// Ensures that the list has allocated space to hold `count` of items + [MethodImpl((MethodImplOptions)256)] + public void InitCount(int count) { - [MethodImpl((MethodImplOptions)256)] - get => _count; + Debug.Assert(count > 0, "Count should be more than 0"); + Debug.Assert(Count == 0, "Initial the count should be 0"); + + // Add the StackCapacity empty space at the end, we may use it later for BuildToArray. + // The actual source Capacity will be StackCapacity + count. + if (count > Stack.Capacity) + Rest = new T[count]; + Count = count; } - /// Returns surely present item by its index - public TItem this[int index] + /// Returns surely present item by ref + [UnscopedRef] + public ref T this[int index] { [MethodImpl((MethodImplOptions)256)] get { - Debug.Assert(_count != 0); - Debug.Assert(index < _count); - switch (index) - { - case 0: return _it0; - case 1: return _it1; - case 2: return _it2; - case 3: return _it3; - default: - Debug.Assert(_rest != null, $"Expecting deeper items are already existing on stack at index: {index}"); - return _rest[index - StackItemCount]; - } + if (index < 0 | index >= Count) + return ref SmallList.ThrowIndexOutOfBounds(index, Count); + + var stackCap = Stack.Capacity; + if (index < stackCap) + return ref Stack.GetSurePresentItemRef(index); + + Debug.Assert(Rest != null, "Expecting deeper items are already existing on heap"); + return ref Rest.GetSurePresentItemRef(index - stackCap); } } - /// Adds the item to the end of the list aka the Stack.Push + /// Returns a surely present item ref by its index + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public void Append(in TItem item) + public ref T GetSurePresentItemRef(int index) { - var index = _count++; - switch (index) - { - case 0: _it0 = item; break; - case 1: _it1 = item; break; - case 2: _it2 = item; break; - case 3: _it3 = item; break; - default: - SmallList.AppendDefaultAndGetRef(ref _rest, index - StackItemCount, StackItemCount) = item; - break; - } + Debug.Assert(Count != 0, "SmallList.GetSurePresentItemRef: list should not be empty"); + Debug.Assert(index >= 0 & index < Count, $"SmallList.GetSurePresentItemRef: index {index} should be less than Count {Count}"); + + var stackCap = Stack.Capacity; + if (index < stackCap) + return ref Stack.GetSurePresentItemRef(index); + + Debug.Assert(Rest != null); + return ref Rest[index - stackCap]; } - /// Bridge to go from the List.Add + /// Appends the default item to the end of the list and returns the reference to it. + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public void Add(in TItem item) => Append(in item); + public ref T AddDefaultAndGetRef() + { + var index = Count++; + var stackCap = Stack.Capacity; + if (index < stackCap) + return ref Stack.GetSurePresentItemRef(index); + return ref SmallList.AddDefaultAndGetRef(ref Rest, index - stackCap); + } - /// Adds the default item to the end of the list aka the Stack.Push default + /// Adds the item to the end of the list aka the Stack.Push. Returns the index of the added item. [MethodImpl((MethodImplOptions)256)] - public void AppendDefault() + public int Add(in T item) { - if (++_count >= StackItemCount) - SmallList.AppendDefaultAndGetRef(ref _rest, _count - StackItemCount, StackItemCount); + var index = Count++; + var stackCap = Stack.Capacity; + if (index < stackCap) + Stack.GetSurePresentItemRef(index) = item; + else + SmallList.AddDefaultAndGetRef(ref Rest, index - stackCap) = item; + return index; } - /// Removes the last item from the list aka the Stack Pop. Assumes that the list is not empty! + /// Looks for the item in the list and return its index if found or -1 for the absent item [MethodImpl((MethodImplOptions)256)] - public void RemoveLastSurePresentItem() + public int TryGetIndex(in T item, TEq eq = default) where TEq : struct, IEq { - Debug.Assert(Count != 0); - var index = --_count; - switch (index) + if (Count != 0) { - case 0: _it0 = default; break; - case 1: _it1 = default; break; - case 2: _it2 = default; break; - case 3: _it3 = default; break; - default: - Debug.Assert(_rest != null, $"Expecting a deeper parent stack created before accessing it here at level {index}"); - _rest[index - StackItemCount] = default; - break; + var index = 0; + foreach (var it in this) + { + if (eq.Equals(item, it)) + return index; + ++index; + } } + return -1; } -} -/// List with the number of first items (2) stored inside its struct and the rest in the growable array. -/// Supports addition and removal (removel is without resize) only at the end of the list, aka Stack behavior -public struct SmallList2 -{ - /// The number of entries stored inside the map itself without moving them to array on heap - public const int StackItemCount = 2; - - // todo: @check what if someone stores something in it, it would be a memory leak, but isn't it the same as using `out var` in the returning`false` Try...methods? - internal static TItem Missing; // return the ref to Tombstone when nothing found - - internal int _count; - internal TItem _it0, _it1; - internal TItem[] _rest; - - /// Good stuff + /// Returns the index of the found item or appends the item to the end of the list, and returns its index [MethodImpl((MethodImplOptions)256)] - public void InitCount(int count) + public int GetIndexOrAdd(in T item, TEq eq = default) where TEq : struct, IEq { - _count = count; - if (count > StackItemCount) - _rest = new TItem[count - StackItemCount]; + var i = TryGetIndex(in item, eq); + return i != -1 ? i : Add(in item); } - /// Good stiff + ///Clears the list, but keeps the already allocated array on heap to reuse in the future [MethodImpl((MethodImplOptions)256)] - public void Init(TItem it0) + public void Clear() { - _count = 1; - _it0 = it0; + Stack = default; // todo: @perf is there way to faster clear items on stack? + var restCount = Count - Stack.Capacity; + if (restCount > 0) + { + Debug.Assert(Rest != null, "Expecting deeper items are already existing on heap"); + Array.Clear(Rest, 0, restCount); + } + Count = 0; } - /// Good steff + /// Returns last present item ref, assumes that the list is not empty! + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public void Init(TItem it0, TItem it1) + public ref T GetLastSurePresentItem() { - _count = 2; - _it0 = it0; - _it1 = it1; + Debug.Assert(Count != 0, "Expecting that the list is not empty"); + return ref GetSurePresentItemRef(Count - 1); } - /// Good staff + /// Removes the last item from the list aka the Stack Pop. Assumes that the list is not empty! [MethodImpl((MethodImplOptions)256)] - public void Init(TItem it0, TItem it1, params TItem[] rest) + public void RemoveLastSurePresentItem() { - _count = 2 + rest.Length; - _it0 = it0; - _it1 = it1; - _rest = rest; + Debug.Assert(Count != 0, "SmallList.RemoveLastSurePresentItem: Expecting that the list is not empty"); + GetSurePresentItemRef(Count - 1) = default; + --Count; } - /// Good styff - public void Init(params TItem[] items) + /// Returns an enumerator struct + [MethodImpl((MethodImplOptions)256)] + public SmallListEnumerator GetEnumerator() => new SmallListEnumerator(this); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} + +/// Enumerator on stack, without allocations +public struct SmallListEnumerator : IEnumerator, IEnumerator + where TStack : struct, IStack +{ + private readonly SmallList _list; + private int _index; + internal SmallListEnumerator(SmallList list) { - switch (items.Length) - { - case 0: - break; - case 1: - Init(items[0]); - break; - case 2: - Init(items[0], items[1]); - break; - default: - _count = items.Length; - _it0 = items[0]; - _it1 = items[1]; - for (var i = StackItemCount; i < items.Length; ++i) - items[i - StackItemCount] = items[i]; - _rest = items; - break; - } + _list = list; + _index = -1; } - - /// Good staff - public void Init(TList items) where TList : IReadOnlyList + private T _current; + /// + public T Current => _current; + object IEnumerator.Current => _current; + /// + [MethodImpl((MethodImplOptions)256)] + public bool MoveNext() { - if (items is TItem[] arr) - { - Init(arr); - return; - } - switch (items.Count) + var index = ++_index; + if (index < _list.Count) { - case 0: - break; - case 1: - Init(items[0]); - break; - case 2: - Init(items[0], items[1]); - break; - default: - var count = items.Count; - var rest = new TItem[count - StackItemCount]; - _count = count; - _it0 = items[0]; - _it1 = items[1]; - for (var i = StackItemCount; i < count; ++i) - rest[i - StackItemCount] = items[i]; - _rest = rest; - break; + _current = index < _list.Stack.Capacity + ? _list.Stack.GetSurePresentItemRef(index) + : _list.Rest[index - _list.Stack.Capacity]; + return true; } + return false; } + /// + public void Reset() => _index = -1; + /// + public void Dispose() { } +} - /// Gets the number of items in the list - public int Count - { - [MethodImpl((MethodImplOptions)256)] - get => _count; - } +/// Printable thing via provided printer +public interface IPrintable +{ + /// Print to the provided string builder via the provided printer. + StringBuilder Print(StringBuilder s, Func printer); +} - /// Returns surely present item by its index - public TItem this[int index] +/// Produces good enough hash codes for the fields +public static class Hasher +{ + /// Combines hashes of two fields + public static int Combine(T1 a, T2 b) { - [MethodImpl((MethodImplOptions)256)] - get - { - Debug.Assert(_count != 0); - Debug.Assert(index < _count); - switch (index) - { - case 0: return _it0; - case 1: return _it1; - default: - Debug.Assert(_rest != null, $"Expecting deeper items are already existing on stack at index: {index}"); - return _rest[index - StackItemCount]; - } - } + var bh = b?.GetHashCode() ?? 0; + if (ReferenceEquals(a, null)) + return bh; + var ah = a.GetHashCode(); + if (ah == 0) + return bh; + return Combine(ah, bh); } - /// Adds the item to the end of the list aka the Stack.Push + /// Inspired by System.Tuple.CombineHashCodes [MethodImpl((MethodImplOptions)256)] - public void Append(in TItem item) + public static int Combine(int h1, int h2) { - var index = _count++; - switch (index) + unchecked { - case 0: _it0 = item; break; - case 1: _it1 = item; break; - default: - SmallList.AppendDefaultAndGetRef(ref _rest, index - StackItemCount, StackItemCount) = item; - break; + return (h1 << 5) + h1 ^ h2; } } +} + +/// Configures removed key tombstone, equality and hash function for the SmallMap and friends +public interface IEq : IEqualityComparer +{ + /// Defines the value of the key indicating the removed entry + K GetTombstone(); +} - /// Bridge to go from the List.Add +/// Default comparer using the `object.GetHashCode` and `object.Equals` overloads +public struct DefaultEq : IEq +{ + /// [MethodImpl((MethodImplOptions)256)] - public void Add(in TItem item) => Append(in item); + public K GetTombstone() => default; - /// Adds the default item to the end of the list aka the Stack.Push default + /// [MethodImpl((MethodImplOptions)256)] - public void AppendDefault() - { - if (++_count >= StackItemCount) - SmallList.AppendDefaultAndGetRef(ref _rest, _count - StackItemCount, StackItemCount); - } + public bool Equals(K x, K y) => ReferenceEquals(x, y) || x.Equals(y); - /// Removes the last item from the list aka the Stack Pop. Assumes that the list is not empty! + /// [MethodImpl((MethodImplOptions)256)] - public void RemoveLastSurePresentItem() - { - Debug.Assert(Count != 0); - var index = --_count; - switch (index) - { - case 0: _it0 = default; break; - case 1: _it1 = default; break; - default: - Debug.Assert(_rest != null, $"Expecting a deeper parent stack created before accessing it here at level {index}"); - _rest[index - StackItemCount] = default; - break; - } - } + public int GetHashCode(K key) => key.GetHashCode(); +} - /// Copy items to new the array +/// Uses the integer itself as hash code and `==` for equality +public struct IntEq : IEq +{ + /// [MethodImpl((MethodImplOptions)256)] - public TItem[] CopyToArray() - { - switch (Count) - { - case 0: return Array.Empty(); - case 1: return new[] { _it0 }; - case 2: return new[] { _it0, _it1 }; - default: - var items = new TItem[Count]; - items[0] = _it0; - items[1] = _it1; - Array.Copy(_rest, 0, items, 2, Count - StackItemCount); - return items; - } - } + public int GetTombstone() => int.MinValue; // todo: @improve separate the tombstone from the hash + + /// + [MethodImpl((MethodImplOptions)256)] + public bool Equals(int x, int y) => x == y; - /// Exposing as list + /// [MethodImpl((MethodImplOptions)256)] - public IList AsList() => CopyToArray(); + public int GetHashCode(int key) => key; } -/// Configiration and the tools for the FHashMap map data structure -public static class FHashMap +/// Instances of the RefEq for the often used K +public static class RefEq { - /// 2^32 / phi for the Fibonacci hashing, where phi is the golden ratio ~1.61803 - public const uint GoldenRatio32 = 2654435769; + /// RefEq for object, possibly the only thing you need + public static readonly RefEq OfObject = default; +} - internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) - internal const byte MinHashesCapacityBitShift = 4; // 1 << 3 == 8 +// todo: @wip should we even need K here, maybe object implementation is enough? +/// Uses the `object.GetHashCode` and `object.ReferenceEquals` +public struct RefEq : IEq where K : class +{ + /// + [MethodImpl((MethodImplOptions)256)] + public K GetTombstone() => null; - /// Upper hash bits spent on storing the probes, e.g. 5 bits mean 31 probes max. - public const byte MaxProbeBits = 5; - internal const byte MaxProbeCount = (1 << MaxProbeBits) - 1; - internal const byte ProbeCountShift = 32 - MaxProbeBits; - internal const int HashAndIndexMask = ~(MaxProbeCount << ProbeCountShift); + /// + [MethodImpl((MethodImplOptions)256)] + public bool Equals(K x, K y) => ReferenceEquals(x, y); - /// The number of entries stored inside the map itself without moving them to array on heap - public const int StackEntriesCount = 4; + /// + [MethodImpl((MethodImplOptions)256)] + public int GetHashCode(K key) => RuntimeHelpers.GetHashCode(key); +} - /// Creates the map with the storage +/// Compares via `ReferenceEquals` and gets the hash faster via `RuntimeHelpers.GetHashCode` +public struct RefEq : IEq<(A, B)> + where A : class + where B : class +{ + /// [MethodImpl((MethodImplOptions)256)] - public static FHashMap> New(byte capacityBitShift = 0) - where TEq : struct, IEq => new(capacityBitShift); + public (A, B) GetTombstone() => (null, null); - /// Holds a single entry consisting of key and value. - /// Value may be set or changed but the key is set in stone (by construction). - [DebuggerDisplay("{Key?.ToString()}->{Value}")] - public struct Entry - { + /// + [MethodImpl((MethodImplOptions)256)] + public bool Equals((A, B) x, (A, B) y) => + ReferenceEquals(x.Item1, y.Item1) && ReferenceEquals(x.Item2, y.Item2); + + /// + [MethodImpl((MethodImplOptions)256)] + public int GetHashCode((A, B) key) => + Hasher.Combine(RuntimeHelpers.GetHashCode(key.Item1), RuntimeHelpers.GetHashCode(key.Item2)); +} + +/// Compares via `ReferenceEquals` and gets the hash faster via `RuntimeHelpers.GetHashCode` +public struct RefEq : IEq<(A, B, C)> + where A : class + where B : class + where C : class +{ + /// + [MethodImpl((MethodImplOptions)256)] + public (A, B, C) GetTombstone() => (null, null, null); + + /// + [MethodImpl((MethodImplOptions)256)] + public bool Equals((A, B, C) x, (A, B, C) y) => + ReferenceEquals(x.Item1, y.Item1) && ReferenceEquals(x.Item2, y.Item2) && ReferenceEquals(x.Item3, y.Item3); + + /// + [MethodImpl((MethodImplOptions)256)] + public int GetHashCode((A, B, C) key) => + Hasher.Combine(RuntimeHelpers.GetHashCode(key.Item1), Hasher.Combine(RuntimeHelpers.GetHashCode(key.Item2), RuntimeHelpers.GetHashCode(key.Item3))); +} + +/// Add the Use parameter to `T Method{T}(..., Use{T} _)` to enable type inference for T, +/// by calling it as `var t = Method(..., default(Use{T}))` +public interface Use { } + +/// Configuration and the tools for the SmallMap and friends +public static class SmallMap +{ + internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) + internal const byte MinHashesCapacityBitShift = 3; // 1 << 3 == 8 + internal const byte DefaultHashesCapacityBitShift = 4; // 1 << 4 == 16, means the default capacity is 16 int hashes + /// Upper hash bits spent on storing the probes, e.g. 5 bits mean 31 probes max. + public const byte ProbeBits = 5; + internal const byte NotShiftedProbeCountMask = (1 << ProbeBits) - 1; // 0b00000000000000000000000000011111 + // 27, so the upper 5 bits are used for the probe count + internal const byte ProbeCountShift = 32 - ProbeBits; + // ~0b11111000000000000000000000000000 -> 0b00000111111111111111111111111111 + internal const int HashAndIndexMask = ~(NotShiftedProbeCountMask << ProbeCountShift); + + /// Represent a keyed entry stored in the SmallMap. + /// Its implementation struct may include the additional Value for the Map or just the Key for the Set. + /// The implementation may also decide to make Value readonly or writable for the in-place update + public interface IEntry + { + /// Returns the key of the payload + K Key { get; internal set; } + } + + /// Holds a single entry consisting of key and value. + /// Value may be set or changed but the key is set in stone (by construction). + [DebuggerDisplay("{Key?.ToString()}->{Value}")] + public struct Entry : IEntry + { /// The readonly key - public K Key; + public K Key { get; set; } /// The mutable value public V Value; /// Construct with the key and default value @@ -712,597 +892,588 @@ public Entry(K key, V value) } } - /// binary reprsentation of the `int` - public static string ToB(int x) => Convert.ToString(x, 2).PadLeft(32, '0'); - - [MethodImpl((MethodImplOptions)256)] -#if NET7_0_OR_GREATER - internal static ref int GetHashRef(ref int start, int distance) => ref Unsafe.Add(ref start, distance); -#else - internal static ref int GetHashRef(ref int[] start, int distance) => ref start[distance]; -#endif + /// The entry with just a key. + /// When used with the SmallMap it may represent a Set without wasting the space for the absent value + [DebuggerDisplay("{Key?.ToString()}")] + public struct Entry : IEntry + { + /// The readonly key + public K Key { get; set; } + /// Construct with the key and default value + public Entry(K key) => Key = key; + } - [MethodImpl((MethodImplOptions)256)] -#if NET7_0_OR_GREATER - internal static int GetHash(ref int start, int distance) => Unsafe.Add(ref start, distance); -#else - internal static int GetHash(ref int[] start, int distance) => start[distance]; -#endif + /// Binary representation of the `int` + public static string ToB(int x) => System.Convert.ToString(x, 2).PadLeft(32, '0'); - /// Configures removed key tombstone, equality and hash function for the FHashMap - public interface IEq + /// Abstraction to configure your own entries data structure. Check the derived types for the examples + public interface IEntries + where TEntry : struct, IEntry + where TEq : IEq { - /// Defines the value of the key indicating the removed entry - K GetTombstone(); + /// Initializes the entries storage to the specified capacity + void Init(int capacityPowerOfTwoPlease); - /// Equals keys - bool Equals(K x, K y); + /// Returns the reference to entry by its index, index should map to the present/non-removed entry + ref TEntry GetSurePresentEntryRef(int index); - /// Calculates and returns the hash of the key - int GetHashCode(K key); + /// Adds the key at the "end" of entries - so the order of addition is preserved. + ref TEntry AddKeyAndGetEntryRef(K key, int index); } - /// Default comparer using the `object.GetHashCode` and `object.Equals` overloads - public struct DefaultEq : IEq + internal const int MinEntriesCapacity = 2; + + /// For now to use in the Set as a value + public readonly struct NoValue { } + + /// Stores the entries in a single dynamically reallocated growing array + public struct SingleArrayEntries : IEntries + where TEntry : struct, IEntry + where TEq : struct, IEq { - /// - [MethodImpl((MethodImplOptions)256)] - public K GetTombstone() => default; + internal TEntry[] _entries; + + /// + public void Init(int capacityPowerOfTwoPlease) => + _entries = new TEntry[capacityPowerOfTwoPlease]; - /// + /// [MethodImpl((MethodImplOptions)256)] - public bool Equals(K x, K y) => x.Equals(y); + public ref TEntry GetSurePresentEntryRef(int index) => + ref _entries.GetSurePresentItemRef(index); - /// + /// [MethodImpl((MethodImplOptions)256)] - public int GetHashCode(K key) => key.GetHashCode(); + public ref TEntry AddKeyAndGetEntryRef(K key, int index) + { + if (index == _entries.Length) + Array.Resize(ref _entries, index << 1); + + ref var e = ref _entries.GetSurePresentItemRef(index); + e.Key = key; + return ref e; + } } - /// Uses the `object.GetHashCode` and `object.ReferenceEquals` - public struct RefEq : IEq where K : class + /// Lookup for the K in the TStackEntries, first by calculating it hash with TEq and searching the hash in the TStackHashes + [MethodImpl((MethodImplOptions)256)] + public static ref TEntry TryGetEntryRef( + this ref TStackEntries entries, ref TStackHashes hashes, int count, K key, out bool found, + TEq eq = default, TCap cap = default, Use _ = default) + where TEntry : struct, IEntry + where TEq : struct, IEq + where TStackHashes : struct, IStack + where TStackEntries : struct, IStack + where TCap : struct, ISize2Plus { - /// - [MethodImpl((MethodImplOptions)256)] - public K GetTombstone() => null; + Debug.Assert(count <= cap.Size, $"SmallMap.TryGetEntryRef: count {count} should be <= stack capacity {cap.Size}"); - /// - [MethodImpl((MethodImplOptions)256)] - public bool Equals(K x, K y) => ReferenceEquals(x, y); + var hash = eq.GetHashCode(key); - /// - [MethodImpl((MethodImplOptions)256)] - public int GetHashCode(K key) => RuntimeHelpers.GetHashCode(key); - } +#if NET8_0_OR_GREATER + if (count >= 8 & cap.Size >= 8 & Vector256.IsHardwareAccelerated) + { + var vHash = Vector256.Create(hash); + var vHashes = MemoryMarshal.Cast>(hashes.AsSpan()); + var i = 0; + foreach (var vCurr in vHashes) + { + var vMatches = Vector256.Equals(vCurr, vHash); + var matches = Vector256.ExtractMostSignificantBits(vMatches); + while (matches != 0) + { + var matchIndex = System.Numerics.BitOperations.TrailingZeroCount(matches); - /// Compares via `ReferenceEquals` and gets the hash faster via `RuntimeHelpers.GetHashCode` - public struct RefEq : IEq<(A, B)> - where A : class - where B : class - { - /// - [MethodImpl((MethodImplOptions)256)] - public (A, B) GetTombstone() => (null, null); + ref var entry = ref entries.GetSurePresentItemRef(i + matchIndex); + if (found = eq.Equals(entry.Key, key)) + return ref entry; - /// - [MethodImpl((MethodImplOptions)256)] - public bool Equals((A, B) x, (A, B) y) => - ReferenceEquals(x.Item1, y.Item1) && ReferenceEquals(x.Item2, y.Item2); + // Clear lower bits up to and including the first set bit, afaik it can be hw accelerated + // 0b0001_1000 & (0b0001_1000 - 1) -> & 0b0001_1000 & 0b0001_0111 -> 0b0001_0000 + matches &= matches - 1; + } - /// - [MethodImpl((MethodImplOptions)256)] - public int GetHashCode((A, B) key) => - Hasher.Combine(RuntimeHelpers.GetHashCode(key.Item1), RuntimeHelpers.GetHashCode(key.Item2)); - } + i += Vector256.Count; + if (i >= count) + break; + } - /// Compares via `ReferenceEquals` and gets the hash faster via `RuntimeHelpers.GetHashCode` - public struct RefEq : IEq<(A, B, C)> - where A : class - where B : class - where C : class - { - /// - [MethodImpl((MethodImplOptions)256)] - public (A, B, C) GetTombstone() => (null, null, null); + found = false; + return ref RefTools.GetNullRef(); + } +#endif - /// - [MethodImpl((MethodImplOptions)256)] - public bool Equals((A, B, C) x, (A, B, C) y) => - ReferenceEquals(x.Item1, y.Item1) && ReferenceEquals(x.Item2, y.Item2) && ReferenceEquals(x.Item3, y.Item3); + for (var i = 0; i < count; ++i) + { + var h = hashes.GetSurePresentItemRef(i); + if (h == hash) + { + ref var entry = ref entries.GetSurePresentItemRef(i); + if (found = eq.Equals(entry.Key, key)) + return ref entry; + } + } - /// - [MethodImpl((MethodImplOptions)256)] - public int GetHashCode((A, B, C) key) => - Hasher.Combine(RuntimeHelpers.GetHashCode(key.Item1), Hasher.Combine(RuntimeHelpers.GetHashCode(key.Item2), RuntimeHelpers.GetHashCode(key.Item3))); + found = false; + return ref RefTools.GetNullRef(); } - /// Combines the hashes of 2 keys - internal static class Hasher + /// Gets the ref to the existing entry.Value by the provided key (found == true), + /// or adds a new entry (found == false) and returns it.Value by ref. + /// So the method always return a non-null ref to the value, either existing or added + [MethodImpl((MethodImplOptions)256)] + public static ref V AddOrGetValueRef( + this ref SmallMap, TEq, TStackCap, TStackHashes, TStackEntries, TEntries> map, K key, out bool found) + where TEq : struct, IEq + where TStackCap : struct, ISize2Plus + where TStackHashes : struct, IStack + where TStackEntries : struct, IStack, TStackCap, TStackEntries> + where TEntries : struct, IEntries, TEq> => + ref map.AddOrGetEntryRef(key, out found).Value; + + /// Adds an entry for sure absent key. + /// Provides the performance in scenarios where you look for the present key, and using it, and if ABSENT then add the new one. + /// So this method optimized NOT to look for the present item for the second time + [MethodImpl((MethodImplOptions)256)] + public static ref V AddSureAbsentDefaultAndGetRef( + this ref SmallMap, TEq, TStackCap, TStackHashes, TStackEntries, TEntries> map, K key) + where TEq : struct, IEq + where TStackCap : struct, ISize2Plus + where TStackHashes : struct, IStack + where TStackEntries : struct, IStack, TStackCap, TStackEntries> + where TEntries : struct, IEntries, TEq> + => ref map.AddSureAbsentDefaultEntryAndGetRef(key).Value; + + /// Lookups for the stored entry by key. Returns the ref to the found entry.Value or the null ref + [MethodImpl((MethodImplOptions)256)] + public static ref V TryGetValueRef( + this ref SmallMap, TEq, TStackCap, TStackHashes, TStackEntries, TEntries> map, K key, out bool found) + where TEq : struct, IEq + where TStackCap : struct, ISize2Plus + where TStackHashes : struct, IStack + where TStackEntries : struct, IStack, TStackCap, TStackEntries> + where TEntries : struct, IEntries, TEq> { - /// Combines the hashes of 2 keys - [MethodImpl((MethodImplOptions)256)] - public static int Combine(int h1, int h2) => unchecked((h1 * (int)0xA5555529) + h2); + ref var e = ref map.TryGetEntryRef(key, out found); + if (found) return ref e.Value; + return ref RefTools.GetNullRef(); } +} - // todo: @improve can we move the Entry into the type parameter to configure and possibly save the memory e.g. for the sets? - /// Abstraction to configure your own entries data structure. Check the derivitives for the examples - public interface IEntries where TEq : IEq - { - /// Initializes the entries storage to the specified capacity via the number of bits in the capacity - void Init(byte capacityBitShift); +// todo: @improve ? how/where to add SIMD to improve CPU utilization but not losing perf for smaller sizes +/// +/// Fast and less-allocating hash map without thread safety nets. Please measure it in your own use case before use. +/// It is configurable in regard of hash calculation/equality via `TEq` type parameter and +/// in regard of key-value storage via `TEntries` type parameter. +/// +/// Details: +/// - Implemented as a struct so that the empty/default map does not allocate on heap +/// - Hashes and key-values are the separate collections enabling better cash locality and faster performance (data-oriented design) +/// - No SIMD for now to avoid complexity and costs for the smaller maps, so the map is more fit for the smaller sizes. +/// - Provides the "stable" enumeration of the entries in the added order +/// - The TryRemove method removes the hash but replaces the key-value entry with the tombstone key and the default value. +/// For instance, for the `RefEq` the tombstone is . You may redefine it in the `IEq{K}.GetTombstone()` implementation. +/// +/// +[DebuggerDisplay("{Count} entries total")] +public struct SmallMap + where TEntry : struct, IEntry + where TEq : struct, IEq + where TStackCap : struct, ISize2Plus + where TStackHashes : struct, IStack + where TStackEntries : struct, IStack + where TEntries : struct, IEntries +{ + internal byte _capacityBitShift; + internal int _count; - /// Returns the reference to entry by its index, index should map to the present/non-removed entry - ref Entry GetSurePresentEntryRef(int index); + // The _packedHashesAndIndexes elements are of `Int32` with the bits split as following: + // 00010|000...110|01101 + // | | |- The index into the _entries structure, 0-based. The index bit count (indexMask) is the hashes capacity - 1. + // | | | This part of the erased hash is used to get the ideal index into the hashes array, so later this part of hash may be restored from the hash index and its probes. + // | |- The remaining middle bits of the original hash + // |- 5 (MaxProbeBits) high bits of the Probe count, with the minimal value of b00001 indicating the non-empty slot. + internal int[] _packedHashesAndIndexes; - /// Adds the key at the "end" of entriesc- so the order of addition is preserved. - ref V AddKeyAndGetValueRef(K key, int index); +#pragma warning disable IDE0044 // it tries to make entries readonly but they should stay modify-able to prevent its defensive struct copying +#pragma warning disable CS0649 // field is never assigned to, and will always have its default value + internal TEntries _entries; + internal TStackHashes _stackHashes; + internal TStackEntries _stackEntries; +#pragma warning restore CS0649 +#pragma warning restore IDE0044 + + /// Capacity bits + public int CapacityBitShift => _capacityBitShift; + + /// Access to the hashes and indexes + public int[] PackedHashesAndIndexes => _packedHashesAndIndexes; + + /// Number of entries in the map + public int Count => _count; + + /// Access to the key-value entries + public TEntries Entries => _entries; + + /// Capacity calculates as `1 leftShift capacityBitShift` + public SmallMap(byte capacityBitShift) + { + // Keep the capacity at least 8 for SIMD Vector256, etc., etc, if you need less space use Stack for that + _capacityBitShift = capacityBitShift < MinHashesCapacityBitShift ? MinHashesCapacityBitShift : capacityBitShift; + + // The overflow tail to the hashes is the size of log2N where N==capacityBitShift, + // it is probably fine to have the check for the overflow of capacity because it will be mis-predicted only once at the end of loop (it even rarely for the lookup) + _packedHashesAndIndexes = new int[1 << capacityBitShift]; + _entries.Init(capacityBitShift); } - internal const int MinEntriesCapacity = 2; + ///Get the value ref by the entry index. Also the index corresponds to entry adding order. + /// Important: it does not check the index bounds, so you need to check that the index is from 0 to map.Count-1 + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] + public ref TEntry GetSurePresentEntryRef(int index) + { + Debug.Assert(index >= 0); + Debug.Assert(index < _count); + if (index >= _stackEntries.Capacity) + return ref _entries.GetSurePresentEntryRef(index - _stackEntries.Capacity); + return ref _stackEntries.GetSurePresentItemRef(index); + } - /// Stores the entries in a single dynamically reallocated array - public struct SingleArrayEntries : IEntries where TEq : struct, IEq + [UnscopedRef] + private ref TEntry AddOrGetRefInEntries(K key, out bool found) { - internal Entry[] _entries; + // if the free space is less than 1/8 of capacity (12.5%) then Resize + var indexMask = (1 << _capacityBitShift) - 1; - /// - public void Init(byte capacityBitShift) => - _entries = new Entry[1 << capacityBitShift]; + // todo: @perf @wip why are we checking for resize here if the possible outcome may be just get the value? + if (indexMask - _count <= (indexMask >>> MinFreeCapacityShift)) + indexMask = ResizeHashes(indexMask); - /// - [MethodImpl((MethodImplOptions)256)] - public ref Entry GetSurePresentEntryRef(int index) - { -#if NET7_0_OR_GREATER - return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_entries), index); -#else - return ref _entries[index]; -#endif - } + var hash = default(TEq).GetHashCode(key); + var hashMiddleMask = HashAndIndexMask & ~indexMask; + var hashMiddle = hash & hashMiddleMask; + var hashIndex = hash & indexMask; - /// - [MethodImpl((MethodImplOptions)256)] - public ref V AddKeyAndGetValueRef(K key, int index) - { - if (index == _entries.Length) - Array.Resize(ref _entries, index << 1); #if NET7_0_OR_GREATER - ref var e = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_entries), index); + ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); #else - ref var e = ref _entries[index]; + ref var hashesAndIndexes = ref _packedHashesAndIndexes; #endif - e.Key = key; - return ref e.Value; - } - } + ref var h = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); - /// Finds the stored value by key. If found returns ref to the value it can be modified in place. - [MethodImpl((MethodImplOptions)256)] - public static ref V TryGetValueRef(this ref FHashMap map, K key, out bool found) - where TEq : struct, IEq - where TEntries : struct, IEntries - { - if (map._count > StackEntriesCount) - return ref map.TryGetValueRefByHash(key, out found); - switch (map._count) + // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions + var probe = 1; + while ((h >>> ProbeCountShift) >= probe) { - case 1: - if (found = default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - break; - case 2: - if (found = default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - if (found = default(TEq).Equals(key, map._e1.Key)) return ref map._e1.Value; - break; - case 3: - if (found = default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - if (found = default(TEq).Equals(key, map._e1.Key)) return ref map._e1.Value; - if (found = default(TEq).Equals(key, map._e2.Key)) return ref map._e2.Value; - break; - case 4: - if (found = default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - if (found = default(TEq).Equals(key, map._e1.Key)) return ref map._e1.Value; - if (found = default(TEq).Equals(key, map._e2.Key)) return ref map._e2.Value; - if (found = default(TEq).Equals(key, map._e3.Key)) return ref map._e3.Value; - break; + // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too + if (((h >>> ProbeCountShift) == probe) & ((h & hashMiddleMask) == hashMiddle)) + { + ref var e = ref GetSurePresentEntryRef(h & indexMask); + if (found = default(TEq).Equals(e.Key, key)) + return ref e; + } + h = ref hashesAndIndexes.GetSurePresentItemRef(++hashIndex & indexMask); + ++probe; } found = false; - return ref FHashMap._missing.Value; - } - /// Gets the reference to the existing value of the provided key, or the default value to set for the newly added key. - [MethodImpl((MethodImplOptions)256)] - public static ref V GetOrAddValueRef(this ref FHashMap map, K key, out bool found) - where TEq : struct, IEq - where TEntries : struct, IEntries - { - if (map._count > StackEntriesCount) - return ref map.GetOrAddValueRefByHash(key, out found); - found = true; - switch (map._count) + // 3. We did not find the hash and therefore the key, so insert the new entry + var newIndex = _count; + var hRobinHooded = h; + h = (probe << ProbeCountShift) | hashMiddle | newIndex; + + // 4. If the robin hooded hash is empty then we stop + // 5. Otherwise we steal the slot with the smaller probes + probe = hRobinHooded >>> ProbeCountShift; + while (hRobinHooded != 0) { - case 0: - found = false; - map._count = 1; - map._e0.Key = key; - return ref map._e0.Value; - - case 1: - if (default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - found = false; - map._count = 2; - map._e1.Key = key; - return ref map._e1.Value; - - case 2: - if (default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - if (default(TEq).Equals(key, map._e1.Key)) return ref map._e1.Value; - found = false; - map._count = 3; - map._e2.Key = key; - return ref map._e2.Value; - - case 3: - if (default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - if (default(TEq).Equals(key, map._e1.Key)) return ref map._e1.Value; - if (default(TEq).Equals(key, map._e2.Key)) return ref map._e2.Value; - found = false; - map._count = 4; - map._e3.Key = key; - return ref map._e3.Value; - - default: - if (default(TEq).Equals(key, map._e0.Key)) return ref map._e0.Value; - if (default(TEq).Equals(key, map._e1.Key)) return ref map._e1.Value; - if (default(TEq).Equals(key, map._e2.Key)) return ref map._e2.Value; - if (default(TEq).Equals(key, map._e3.Key)) return ref map._e3.Value; - found = false; - - map._capacityBitShift = MinHashesCapacityBitShift; - map._packedHashesAndIndexes = new int[1 << MinHashesCapacityBitShift]; - - var indexMask = (1 << MinHashesCapacityBitShift) - 1; - - // todo: @perf optimize by calculating the keys hashes and putting them into the span and iterating over them inside a single method - map.AddInitialHashWithoutResizing(map._e0.Key, 0, indexMask); - map.AddInitialHashWithoutResizing(map._e1.Key, 1, indexMask); - map.AddInitialHashWithoutResizing(map._e2.Key, 2, indexMask); - map.AddInitialHashWithoutResizing(map._e3.Key, 3, indexMask); - map.AddInitialHashWithoutResizing(key, StackEntriesCount, indexMask); - - map._count = 5; - map._entries.Init(2); - return ref map._entries.AddKeyAndGetValueRef(key, 0); + h = ref hashesAndIndexes.GetSurePresentItemRef(++hashIndex & indexMask); + if ((h >>> ProbeCountShift) < ++probe) + { + var tmp = h; + h = (probe << ProbeCountShift) | (hRobinHooded & HashAndIndexMask); + hRobinHooded = tmp; + probe = hRobinHooded >>> ProbeCountShift; + } } + + ++_count; + return ref _entries.AddKeyAndGetEntryRef(key, newIndex - _stackEntries.Capacity); } - private static void AddInitialHashWithoutResizing(this ref FHashMap map, K key, int index, int indexMask) - where TEq : struct, IEq - where TEntries : struct, IEntries + private void AddJustHashAndEntryIndexWithoutResizing(int indexMask, int hashIndex, int hashMiddleAndEntryIndex) { #if NET7_0_OR_GREATER - ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(map._packedHashesAndIndexes); + ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); #else - var hashesAndIndexes = map._packedHashesAndIndexes; + var hashesAndIndexes = _packedHashesAndIndexes; #endif - var hash = default(TEq).GetHashCode(key); - var hashIndex = hash & indexMask; - // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - ref var h = ref GetHashRef(ref hashesAndIndexes, hashIndex); - var probes = 1; - while ((h >>> ProbeCountShift) >= probes) + ref var h = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); + var probe = 1; + while ((h >>> ProbeCountShift) >= probe) { - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); - ++probes; + h = ref hashesAndIndexes.GetSurePresentItemRef(++hashIndex & indexMask); + ++probe; } // 3. We did not find the hash and therefore the key, so insert the new entry var hRobinHooded = h; - h = (probes << ProbeCountShift) | (hash & HashAndIndexMask & ~indexMask) | index; + h = (probe << ProbeCountShift) | hashMiddleAndEntryIndex; // 4. If the robin hooded hash is empty then we stop // 5. Otherwise we steal the slot with the smaller probes - probes = hRobinHooded >>> ProbeCountShift; + probe = hRobinHooded >>> ProbeCountShift; while (hRobinHooded != 0) { - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); - if ((h >>> ProbeCountShift) < ++probes) + h = ref hashesAndIndexes.GetSurePresentItemRef(++hashIndex & indexMask); + if ((h >>> ProbeCountShift) < ++probe) { var tmp = h; - h = (probes << ProbeCountShift) | (hRobinHooded & HashAndIndexMask); + h = (probe << ProbeCountShift) | (hRobinHooded & HashAndIndexMask); hRobinHooded = tmp; - probes = hRobinHooded >>> ProbeCountShift; + probe = hRobinHooded >>> ProbeCountShift; } } } - /// Adds the sure absent key entry. - /// Provides the performance in scenarios where you look for present key, and using it, and if ABSENT then add the new one. - /// So this method optimized NOT to look for the present item for the second time in SEQUENCE - [MethodImpl((MethodImplOptions)256)] - public static ref V AddSureAbsentDefaultAndGetRef(this ref FHashMap map, K key) - where TEq : struct, IEq - where TEntries : struct, IEntries + /// Gets the ref to the existing entry by the provided key (found == true), + /// or adds a new entry (found == false) and returns it by ref + [UnscopedRef] + public ref TEntry AddOrGetEntryRef(K key, out bool found) { - if (map._count > StackEntriesCount) - return ref map.AddSureAbsentDefaultAndGetRefByHash(key); - switch (map._count) + if (_count > _stackEntries.Capacity) + return ref AddOrGetRefInEntries(key, out found); + + ref var e = ref _stackEntries.TryGetEntryRef(ref _stackHashes, _count, key, out found, + default(TEq), default(TStackCap), default(Use)); + if (found) + return ref e; + + // Add the new entry to the stack if there is still space in stack + if (_count < _stackEntries.Capacity) + { + var newIndex = _count; + ++_count; + _stackHashes.GetSurePresentItemRef(newIndex) = default(TEq).GetHashCode(key); + ref var newEntry = ref _stackEntries.GetSurePresentItemRef(newIndex); + newEntry.Key = key; + return ref newEntry; + } + + // Now all capacity of the stack is used. + // To avoid double work always going linearly through the Stack with the comparison, + // let's calculate the hash of the keys stored on stack and put them + // to the usual HashMap packed hashes and indexes array for the promised O(1) lookup. + // But the values are remaining on the Stack, and for the found index of the entry we use the GetSurePresentItemRef(index) + // to get the value reference either from the Stack or the Entries. + // So the values on the stack are guarantied to be stable from the beginning of the map creation, + // because they are not copied when the Entries need to Resize (depending on the TEntries implementation). + + _capacityBitShift = (byte)(_stackEntries.CapacityBitShift + 1); + var indexMask = (1 << _capacityBitShift) - 1; + var hashMiddleMask = HashAndIndexMask & ~indexMask; + var stackCap = _stackEntries.Capacity; + _packedHashesAndIndexes = new int[1 << _capacityBitShift]; + + for (var i = 0; i < _stackHashes.Capacity; ++i) { - case 0: - map._count = 1; - map._e0.Key = key; - return ref map._e0.Value; - - case 1: - map._count = 2; - map._e1.Key = key; - return ref map._e1.Value; - - case 2: - map._count = 3; - map._e2.Key = key; - return ref map._e2.Value; - - case 3: - map._count = 4; - map._e3.Key = key; - return ref map._e3.Value; - - default: - map._capacityBitShift = MinHashesCapacityBitShift; - map._packedHashesAndIndexes = new int[1 << MinHashesCapacityBitShift]; - - var indexMask = (1 << MinHashesCapacityBitShift) - 1; - - map.AddInitialHashWithoutResizing(map._e0.Key, 0, indexMask); - map.AddInitialHashWithoutResizing(map._e1.Key, 1, indexMask); - map.AddInitialHashWithoutResizing(map._e2.Key, 2, indexMask); - map.AddInitialHashWithoutResizing(map._e3.Key, 3, indexMask); - map.AddInitialHashWithoutResizing(key, StackEntriesCount, indexMask); - - map._count = 5; - map._entries.Init(2); - return ref map._entries.AddKeyAndGetValueRef(key, 0); + var h = _stackHashes.GetSurePresentItemRef(i); + AddJustHashAndEntryIndexWithoutResizing(indexMask, h & indexMask, (h & hashMiddleMask) | i); } + + // todo: @perf calculates the hash code twice, first time in the TryGetEntryRef and then here + var hash = default(TEq).GetHashCode(key); + AddJustHashAndEntryIndexWithoutResizing(indexMask, hash & indexMask, (hash & hashMiddleMask) | stackCap); + + ++_count; + _entries.Init(stackCap); // Give the heap entries the same initial capacity as Stack, effectively doubling the capacity + return ref _entries.AddKeyAndGetEntryRef(key, 0); // add the new key to the entries with the 0 index in the entries } - [MethodImpl((MethodImplOptions)256)] - private static ref V AddSureAbsentDefaultAndGetRefByHash(this ref FHashMap map, K key) - where TEq : struct, IEq - where TEntries : struct, IEntries + /* + Insertion step by step: + + 1. Initially the map is empty. Its capacity mask is 7: + + Index: 0 1 2 3 4 5 6 7 + Hash: [0] [0] [0] [0] [0] [0] [0] [0] + + 2. Insert the key A with the hash 13, which is 0b0011_0101. 13 & 7 Mask = 5, so the index is 5. + + Index: 0 1 2 3 4 5 6 7 + Hash: [0] [0] [0] [0] [0] [13] [0] [0] + Probe: 1A + + 3. Insert the key B with the hash 5, which is 0b0000_1011. 5 & 7 Mask = 5, so the index is again 5. + + Index: 0 1 2 3 4 5 6 7 + Hash: [0] [0] [0] [0] [0] [13] [5] [0] + Probe 1A 2B + + 4. Insert the key C with the hash 7, which is 0b0010_0101. 7 & 7 Mask = 7, so the index is 7. + + Index: 0 1 2 3 4 5 6 7 + Hash: [0] [0] [0] [0] [0] [13] [5] [7] + Probe: 1A 2B 1C + + 5. Insert the key D with the hash 21, which is 0b0101_0101. 21 & 7 Mask = 5, so the index is again again 5. + + Index: 0 1 2 3 4 5 6 7 + Hash: [7] [0] [0] [0] [0] [13] [5] [21] + Probe: 2C 1A 2B 3D + */ + [UnscopedRef] + private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key) { // if the free space is less than 1/8 of capacity (12.5%) then Resize - var indexMask = (1 << map._capacityBitShift) - 1; - if (indexMask - map._count <= (indexMask >>> MinFreeCapacityShift)) - indexMask = map.ResizeHashes(indexMask); + var indexMask = (1 << _capacityBitShift) - 1; + if (indexMask - _count <= (indexMask >>> MinFreeCapacityShift)) + indexMask = ResizeHashes(indexMask); var hash = default(TEq).GetHashCode(key); var hashIndex = hash & indexMask; #if NET7_0_OR_GREATER - ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(map._packedHashesAndIndexes); + ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); #else - var hashesAndIndexes = map._packedHashesAndIndexes; + var hashesAndIndexes = _packedHashesAndIndexes; #endif - ref var h = ref GetHashRef(ref hashesAndIndexes, hashIndex); + ref var h = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - var probes = 1; - while ((h >>> ProbeCountShift) >= probes) + var probe = 1; + while ((h >>> ProbeCountShift) >= probe) { - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); - ++probes; + h = ref hashesAndIndexes.GetSurePresentItemRef(++hashIndex & indexMask); + ++probe; } // 3. We did not find the hash and therefore the key, so insert the new entry var hRobinHooded = h; - h = (probes << ProbeCountShift) | (hash & HashAndIndexMask & ~indexMask) | map._count; + h = (probe << ProbeCountShift) | (hash & HashAndIndexMask & ~indexMask) | _count; // 4. If the robin hooded hash is empty then we stop // 5. Otherwise we steal the slot with the smaller probes - probes = hRobinHooded >>> ProbeCountShift; + probe = hRobinHooded >>> ProbeCountShift; while (hRobinHooded != 0) { - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); - if ((h >>> ProbeCountShift) < ++probes) + h = ref hashesAndIndexes.GetSurePresentItemRef(++hashIndex & indexMask); + if ((h >>> ProbeCountShift) < ++probe) { var tmp = h; - h = (probes << ProbeCountShift) | (hRobinHooded & HashAndIndexMask); + h = (probe << ProbeCountShift) | (hRobinHooded & HashAndIndexMask); hRobinHooded = tmp; - probes = hRobinHooded >>> ProbeCountShift; + probe = hRobinHooded >>> ProbeCountShift; } } - return ref map._entries.AddKeyAndGetValueRef(key, (map._count++) - StackEntriesCount); + return ref _entries.AddKeyAndGetEntryRef(key, (_count++) - _stackEntries.Capacity); } - ///Get the value ref by the entry index. Also the index corresponds to entry adding order. - ///Improtant: it does not checks the index bounds, so you need to check that the index is from 0 to map.Count-1 - [MethodImpl((MethodImplOptions)256)] - public static ref Entry GetSurePresentEntryRef(this ref FHashMap map, int index) - where TEq : struct, IEq - where TEntries : struct, IEntries + /// Adds an entry for sure absent key. + /// Provides the performance in scenarios where you look for the present key, and using it, and if ABSENT then add the new one. + /// So this method optimized NOT to look for the present item for the second time + [UnscopedRef] + public ref TEntry AddSureAbsentDefaultEntryAndGetRef(K key) { - Debug.Assert(index >= 0); - Debug.Assert(index < map._count); - if (index >= StackEntriesCount) - return ref map._entries.GetSurePresentEntryRef(index - StackEntriesCount); - switch (index) + if (_count > _stackEntries.Capacity) + return ref AddSureAbsentDefaultAndGetRefInEntries(key); + + // Add the new entry to the stack if there is still space in stack + if (_count < _stackEntries.Capacity) { - case 0: return ref map._e0; - case 1: return ref map._e1; - case 2: return ref map._e2; - case 3: return ref map._e3; + var newIndex = _count++; + _stackHashes.GetSurePresentItemRef(newIndex) = default(TEq).GetHashCode(key); + ref var newEntry = ref _stackEntries.GetSurePresentItemRef(newIndex); + newEntry.Key = key; + return ref newEntry; } - return ref FHashMap._missing; - } - - [MethodImpl((MethodImplOptions)256)] - internal static ref V TryGetValueRefByHash(this ref FHashMap map, K key, out bool found) - where TEq : struct, IEq - where TEntries : struct, IEntries - { - var hash = default(TEq).GetHashCode(key); - var indexMask = (1 << map._capacityBitShift) - 1; + _capacityBitShift = (byte)(_stackEntries.CapacityBitShift + 1); + var indexMask = (1 << _capacityBitShift) - 1; var hashMiddleMask = HashAndIndexMask & ~indexMask; - var hashMiddle = hash & hashMiddleMask; - var hashIndex = hash & indexMask; + _packedHashesAndIndexes = new int[1 << _capacityBitShift]; -#if NET7_0_OR_GREATER - ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(map._packedHashesAndIndexes); -#else - var hashesAndIndexes = map._packedHashesAndIndexes; -#endif + var stackCap = _stackHashes.Capacity; + for (var i = 0; i < stackCap; ++i) + { + var h = _stackHashes.GetSurePresentItemRef(i); + AddJustHashAndEntryIndexWithoutResizing(indexMask, h & indexMask, (h & hashMiddleMask) | i); + } - var h = GetHash(ref hashesAndIndexes, hashIndex); + var hash = default(TEq).GetHashCode(key); + AddJustHashAndEntryIndexWithoutResizing(indexMask, hash & indexMask, (hash & hashMiddleMask) | stackCap); - // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - var probes = 1; - while ((h >>> ProbeCountShift) >= probes) - { - // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) - { - ref var e = ref map.GetSurePresentEntryRef(h & indexMask); - if (default(TEq).Equals(e.Key, key)) - { - found = true; - return ref e.Value; - } - } + ++_count; + _entries.Init(stackCap); // Give the heap entries the same initial capacity as Stack, effectively doubling the capacity + return ref _entries.AddKeyAndGetEntryRef(key, 0); // add the new key to the entries with the 0 index in the entries + } - h = GetHash(ref hashesAndIndexes, ++hashIndex & indexMask); - ++probes; + /// Lookups for the stored key. If found true, otherwise false + public bool ContainsKey(K key) + { + if (_count > _stackEntries.Capacity) + { + _ = TryGetRefInEntries(key, out var found); + return found; + } + else + { + _ = ref _stackEntries.TryGetEntryRef(ref _stackHashes, _count, key, out var found, + default(TEq), default(TStackCap), default(Use)); + return found; } - - found = false; - return ref FHashMap._missing.Value; } + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - private static ref V GetOrAddValueRefByHash(this ref FHashMap map, K key, out bool found) - where TEq : struct, IEq - where TEntries : struct, IEntries + internal ref TEntry TryGetRefInEntries(K key, out bool found) { - // if the free space is less than 1/8 of capacity (12.5%) then Resize - var indexMask = (1 << map._capacityBitShift) - 1; - if (indexMask - map._count <= (indexMask >>> MinFreeCapacityShift)) - indexMask = map.ResizeHashes(indexMask); - var hash = default(TEq).GetHashCode(key); + + var indexMask = (1 << _capacityBitShift) - 1; var hashMiddleMask = HashAndIndexMask & ~indexMask; var hashMiddle = hash & hashMiddleMask; var hashIndex = hash & indexMask; #if NET7_0_OR_GREATER - ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(map._packedHashesAndIndexes); + ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); #else - var hashesAndIndexes = map._packedHashesAndIndexes; + var hashesAndIndexes = _packedHashesAndIndexes; #endif - ref var h = ref GetHashRef(ref hashesAndIndexes, hashIndex); + + var h = hashesAndIndexes.GetSurePresentItem(hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - var probes = 1; - while ((h >>> ProbeCountShift) >= probes) + var probe = 1; + while ((h >>> ProbeCountShift) >= probe) { // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> ProbeCountShift) == probes) & ((h & hashMiddleMask) == hashMiddle)) + if (((h >>> ProbeCountShift) == probe) & ((h & hashMiddleMask) == hashMiddle)) { - ref var e = ref map.GetSurePresentEntryRef(h & indexMask); - if (default(TEq).Equals(e.Key, key)) - { - found = true; - return ref e.Value; - } + ref var e = ref GetSurePresentEntryRef(h & indexMask); + if (found = default(TEq).Equals(e.Key, key)) + return ref e; } - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); - ++probes; - } - // 3. We did not find the hash and therefore the key, so insert the new entry - var hRobinHooded = h; - h = (probes << ProbeCountShift) | hashMiddle | map._count; - - // 4. If the robin hooded hash is empty then we stop - // 5. Otherwise we steal the slot with the smaller probes - probes = hRobinHooded >>> ProbeCountShift; - while (hRobinHooded != 0) - { - h = ref GetHashRef(ref hashesAndIndexes, ++hashIndex & indexMask); - if ((h >>> ProbeCountShift) < ++probes) - { - var tmp = h; - h = (probes << ProbeCountShift) | (hRobinHooded & HashAndIndexMask); - hRobinHooded = tmp; - probes = hRobinHooded >>> ProbeCountShift; - } + h = hashesAndIndexes.GetSurePresentItem(++hashIndex & indexMask); + ++probe; } + found = false; - return ref map._entries.AddKeyAndGetValueRef(key, (map._count++) - StackEntriesCount); + return ref RefTools.GetNullRef(); } -} -// todo: @improve ? how/where to add SIMD to improve CPU utilization but not losing perf for smaller sizes -/// -/// Fast and less-allocating hash map without thread safety nets. Please measure it in your own use case before use. -/// It is configurable in regard of hash calculation/equality via `TEq` type paremeter and -/// in regard of key-value storage via `TEntries` type parameter. -/// -/// Details: -/// - Implemented as a struct so that the empty/default map does not allocate on heap -/// - Hashes and key-values are the separate collections enabling better cash locality and faster performance (data-oriented design) -/// - No SIMD for now to avoid complexity and costs for the smaller maps, so the map is more fit for the smaller sizes. -/// - Provides the "stable" enumeration of the entries in the added order -/// - The TryRemove method removes the hash but replaces the key-value entry with the tombstone key and the default value. -/// For instance, for the `RefEq` the tombstone is . You may redefine it in the `IEq{K}.GetTombstone()` implementation. -/// -/// -public struct FHashMap - where TEq : struct, IEq - where TEntries : struct, IEntries -{ - internal static Entry _missing; - - internal byte _capacityBitShift; - internal int _count; - - // The _packedHashesAndIndexes elements are of `Int32` with the bits split as following: - // 00010|000...110|01101 - // | | |- The index into the _entries structure, 0-based. The index bit count (indexMask) is the hashes capacity - 1. - // | | | This part of the erased hash is used to get the ideal index into the hashes array, so later this part of hash may be restored from the hash index and its probes. - // | |- The remaining middle bits of the original hash - // |- 5 (MaxProbeBits) high bits of the Probe count, with the minimal value of b00001 indicating the non-empty slot. - internal int[] _packedHashesAndIndexes; - -#pragma warning disable IDE0044 // it tries to make entries readonly but they should stay modifyable to prevent its defensive struct copying - internal TEntries _entries; -#pragma warning restore IDE0044 - - // todo: @improve how to configure how much we store on stack - internal Entry _e0, _e1, _e2, _e3; - - /// Capacity bits - public int CapacityBitShift => _capacityBitShift; - - /// Access to the hashes and indexes - public int[] PackedHashesAndIndexes => _packedHashesAndIndexes; - - /// Number of entries in the map - public int Count => _count; - - /// Access to the key-value entries - public TEntries Entries => _entries; - - /// Capacity calculates as `1 leftShift capacityBitShift` - public FHashMap(byte capacityBitShift) + /// Lookup for the stored entry by key. Returns the ref to the found entry or the null ref + [UnscopedRef] + public ref TEntry TryGetEntryRef(K key, out bool found) { - _capacityBitShift = capacityBitShift; - - // the overflow tail to the hashes is the size of log2N where N==capacityBitShift, - // it is probably fine to have the check for the overlow of capacity because it will be mispredicted only once at the end of loop (it even rarely for the lookup) - _packedHashesAndIndexes = new int[1 << capacityBitShift]; - _entries = default; - _entries.Init(capacityBitShift); + if (_count > _stackEntries.Capacity) + return ref TryGetRefInEntries(key, out found); + return ref _stackEntries.TryGetEntryRef(ref _stackHashes, _count, key, out found, + default(TEq), default(TStackCap), default(Use)); } internal int ResizeHashes(int indexMask) @@ -1325,7 +1496,7 @@ internal int ResizeHashes(int indexMask) // Overflow segment is wrapped-around hashes and! the hashes at the beginning robin hooded by the wrapped-around hashes var i = 0; while ((oldHash >>> ProbeCountShift) > 1) - oldHash = GetHash(ref oldHashes, ++i); + oldHash = oldHashes.GetSurePresentItem(++i); var oldCapacityWithOverflowSegment = i + oldCapacity; while (true) @@ -1335,23 +1506,67 @@ internal int ResizeHashes(int indexMask) // get the new hash index from the old one with the next bit equal to the `oldCapacity` var indexWithNextBit = (oldHash & oldCapacity) | (((i + 1) - (oldHash >>> ProbeCountShift)) & indexMask); - // no need for robinhooding because we already did it for the old hashes and now just sparcing the hashes into the new array which are already in order - var probes = 1; - ref var newHash = ref GetHashRef(ref newHashes, indexWithNextBit); + // no need for robin-hooding because we already did it for the old hashes and now just filling the hashes into the new array which are already in order + var probe = 1; + ref var newHash = ref newHashes.GetSurePresentItemRef(indexWithNextBit); while (newHash != 0) { - newHash = ref GetHashRef(ref newHashes, ++indexWithNextBit & newIndexMask); - ++probes; + newHash = ref newHashes.GetSurePresentItemRef(++indexWithNextBit & newIndexMask); + ++probe; } - newHash = (probes << ProbeCountShift) | (oldHash & newHashAndIndexMask); + newHash = (probe << ProbeCountShift) | (oldHash & newHashAndIndexMask); } if (++i >= oldCapacityWithOverflowSegment) break; - oldHash = GetHash(ref oldHashes, i & indexMask); + oldHash = oldHashes.GetSurePresentItem(i & indexMask); } ++_capacityBitShift; _packedHashesAndIndexes = newHashesAndIndexes; return newIndexMask; } } + +/// Holds the Map with 4 items on stack. Minimizes the number of type arguments required to be specified +public struct SmallMap4() where TEq : struct, IEq +{ + /// Map with 4 elements on stack and entries baked by the single array + public SmallMap, TEq, Size4, Stack4, Stack4>, SmallMap.SingleArrayEntries, TEq>> Map; +} + +/// Holds the Map with 8 items on stack. Minimizes the number of type arguments required to be specified +public struct SmallMap8() where TEq : struct, IEq +{ + /// Map with 8 elements on stack and entries baked by the single array + public SmallMap, TEq, Size8, Stack8, Stack8>, SmallMap.SingleArrayEntries, TEq>> Map; +} + +/// Holds the Map with 16 items on stack. Minimizes the number of type arguments required to be specified +public struct SmallMap16() where TEq : struct, IEq +{ + /// Map with 16 elements on stack and entries baked by the single array + public SmallMap, TEq, Size16, Stack16, Stack16>, SmallMap.SingleArrayEntries, TEq>> Map; +} + +/// Holds the Set with 4 items on stack. Minimizes the number of type arguments required to be specified +public struct SmallSet4() where TEq : struct, IEq +{ + /// Set with 4 keys on stack and entries baked by the single array + public SmallMap, TEq, Size4, Stack4, Stack4>, SmallMap.SingleArrayEntries, TEq>> Set; +} + +/// Holds the Set with 8 items on stack. Minimizes the number of type arguments required to be specified +public struct SmallSet8() where TEq : struct, IEq +{ + /// Set with 8 keys on stack and entries baked by the single array + public SmallMap, TEq, Size8, Stack8, Stack8>, SmallMap.SingleArrayEntries, TEq>> Set; +} + +/// Holds the Set with 16 items on stack. Minimizes the number of type arguments required to be specified +public struct SmallSet16() where TEq : struct, IEq +{ + /// Set with 16 keys on stack and entries baked by the single array + public SmallMap, TEq, Size16, Stack16, Stack16>, SmallMap.SingleArrayEntries, TEq>> Set; +} + +#nullable restore \ No newline at end of file diff --git a/playground/Experiments/FHashMap11.cs b/playground/Experiments/FHashMap11.cs index e2951469..f4084d4b 100644 --- a/playground/Experiments/FHashMap11.cs +++ b/playground/Experiments/FHashMap11.cs @@ -19,7 +19,7 @@ public static class FHashMap11 internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) internal const byte MinCapacityBits = 3; // 1 << 3 == 8 - /// Verifies that the hashes correspond to the keys stroed in the entries. May be called from the tests. + /// Verifies that the hashes correspond to the keys stored in the entries. May be called from the tests. public static void VerifyHashesAndKeysEq(this FHashMap11 map, Action assertEq) where TEq : struct, IEq { @@ -199,7 +199,7 @@ public FHashMap11(byte capacityBitShift) _capacityBitShift = capacityBitShift; // the overflow tail to the hashes is the size of log2N where N==capacityBitShift, - // it is probably fine to have the check for the overlow of capacity because it will be mispredicted only once at the end of loop (it even rarely for the lookup) + // it is probably fine to have the check for the overflow of capacity because it will be mis-predicted only once at the end of loop (it even rarely for the lookup) var cap = 1 << capacityBitShift; Probes = new byte[cap]; Hashes = new int[cap]; @@ -299,7 +299,7 @@ public ref V GetOrAddValueRef(K key) ++probe; } - // Nothing found, add a new entry and inscrease the count first + // Nothing found, add a new entry and increase the count first var valueIndex = currCount; _count = currCount + 1; @@ -484,7 +484,7 @@ internal int ResizeHashes(int indexMask) var newIndex = hash & newIndexMask; // no need for robin-hooding because we already did it for the old hashes and - // now just sparsing the hashes which are already in order into the new array + // now just sparse the hashes which are already in order into the new array var newProbe = (byte)1; ref var newProbeRef = ref GetItemRef(ref newProbesRef, newIndex); while (newProbeRef != 0) diff --git a/playground/Experiments/UnscopedRefAttribute.cs b/playground/Experiments/UnscopedRefAttribute.cs new file mode 100644 index 00000000..167d2d58 --- /dev/null +++ b/playground/Experiments/UnscopedRefAttribute.cs @@ -0,0 +1,7 @@ +#if !NET7_0_OR_GREATER +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(System.AttributeTargets.Method | System.AttributeTargets.Parameter | System.AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + public sealed class UnscopedRefAttribute : Attribute { } +} +#endif diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 97a69819..dce47689 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -14,7 +14,7 @@ using FHashMap91TypeString = ImTools.Experiments.FHashMap91, ImTools.Experiments.FHashMap91.SingleArrayEntries>>; using SmallMapTypeString = ImTools.HSmallMap, ImTools.HSmallMap.SingleArrayEntries>>; -using FHashMapTypeString = FastExpressionCompiler.ImTools.FHashMap, FastExpressionCompiler.ImTools.FHashMap.SingleArrayEntries>>; +using FecSmallMapTypeString = FastExpressionCompiler.ImTools.SmallMap16>; using BenchmarkDotNet.Order; using ImTools.Experiments; @@ -982,7 +982,7 @@ public class Populate */ // [Params(1, 10, 100, 1000)] - [Params(100)]//, 1000)] + [Params(10)]//, 1000)] // [Params(1000)] public int Count; @@ -1158,9 +1158,9 @@ public DictionarySlim DictSlim_TryAdd() } // [Benchmark] - public ImTools.Experiments.FHashMap7> FHashMap7_AddOrUpdate() + public ImTools.Experiments.FHashMap7> FHashMap7_AddOrUpdate() { - var map = new ImTools.Experiments.FHashMap7>(); + var map = new ImTools.Experiments.FHashMap7>(); foreach (var key in _types) map.AddOrUpdate(key, "a"); @@ -1183,9 +1183,9 @@ public FHashMap91TypeString FHashMap91_GetOrAddValueRef() } // [Benchmark] - public ImTools.Experiments.FHashMap8> FHashMap8_AddOrUpdate() + public ImTools.Experiments.FHashMap8> FHashMap8_AddOrUpdate() { - var map = new ImTools.Experiments.FHashMap8>(); + var map = new ImTools.Experiments.FHashMap8>(); foreach (var key in _types) map.AddOrUpdate(key, "a"); @@ -2323,8 +2323,8 @@ .NET SDK 9.0.203 */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] - // [Params(1, 10, 100)] - [Params(10, 100, 1000)] + // [Params(10, 100, 1000)] + [Params(100)] public int Count; private Type[] _randomPresentKeys; @@ -2334,22 +2334,25 @@ public void Populate() { _randomPresentKeys = _keys.Take(Count).OrderBy(_ => _seed.Next()).ToArray(); - _mapV4 = V4_ImHashMap_AddOrUpdate(); - _mapV3 = V3_ImHashMap_AddOrUpdate(); - _mapV2 = V2_AddOrUpdate(); - _partMapV4 = V4_PartitionedHashMap_AddOrUpdate(); - _partMapV3 = V3_PartitionedHashMap_AddOrUpdate(); - _typeDict = TypeDictionary_Add(); - _dict = Dict(); - _dictSlim = DictSlim(); - _fHashMap7 = FillFHashMap7(); - _fHashMap9 = FillFHashMap9(); - _fHashMap91 = FillFHashMap91(); - _smallMap = FillSmallMap(); - _fHashMap11 = FillFHashMap11(); - _fHashMap = FillFHashMap(); - _concurrentDict = ConcurrentDict(); - _immutableDict = ImmutableDict(); + // _mapV4 = V4_ImHashMap_AddOrUpdate(); + // _mapV3 = V3_ImHashMap_AddOrUpdate(); + // _mapV2 = V2_AddOrUpdate(); + // _partMapV4 = V4_PartitionedHashMap_AddOrUpdate(); + // _partMapV3 = V3_PartitionedHashMap_AddOrUpdate(); + // _typeDict = TypeDictionary_Add(); + // _dict = Dict(); + // _dictSlim = DictSlim(); + // _fHashMap7 = FillFHashMap7(); + // _fHashMap9 = FillFHashMap9(); + // _fHashMap91 = FillFHashMap91(); + // _smallMap = FillSmallMap(); + // _fHashMap11 = FillFHashMap11(); + + // _fecSmallMap = new FecSmallMapTypeString(); + // FillFecHashMap(ref _fecSmallMap); + + // _concurrentDict = ConcurrentDict(); + // _immutableDict = ImmutableDict(); } #region Population @@ -2512,9 +2515,9 @@ public DictionarySlim DictSlim() private DictionarySlim _dictSlim; - public ImTools.Experiments.FHashMap7> FillFHashMap7() + public ImTools.Experiments.FHashMap7> FillFHashMap7() { - var map = new ImTools.Experiments.FHashMap7>(); + var map = new ImTools.Experiments.FHashMap7>(); foreach (var key in _keys.Take(Count)) map.AddOrUpdate(key, "a"); @@ -2523,11 +2526,11 @@ public ImTools.Experiments.FHashMap7> FillFHashMap7() return map; } - private ImTools.Experiments.FHashMap7> _fHashMap7; + private ImTools.Experiments.FHashMap7> _fHashMap7; - public ImTools.Experiments.FHashMap9> FillFHashMap9() + public ImTools.Experiments.FHashMap9> FillFHashMap9() { - var map = new ImTools.Experiments.FHashMap9>(); + var map = new ImTools.Experiments.FHashMap9>(); foreach (var key in _keys.Take(Count)) map.AddOrUpdate(key, "a"); @@ -2536,7 +2539,7 @@ public ImTools.Experiments.FHashMap9> FillFHashMap9() return map; } - private ImTools.Experiments.FHashMap9> _fHashMap9; + private ImTools.Experiments.FHashMap9> _fHashMap9; public FHashMap91TypeString FillFHashMap91() { @@ -2564,9 +2567,9 @@ public SmallMapTypeString FillSmallMap() private SmallMapTypeString _smallMap; - public FHashMap11> FillFHashMap11() + public FHashMap11> FillFHashMap11() { - var map = new FHashMap11>(); + var map = new FHashMap11>(); foreach (var key in _keys.Take(Count)) map.GetOrAddValueRef(key) = "a"; @@ -2575,20 +2578,17 @@ public FHashMap11> FillFHashMap11() return map; } - private FHashMap11> _fHashMap11; + private FHashMap11> _fHashMap11; - public FHashMapTypeString FillFHashMap() + public void FillFecHashMap(ref FecSmallMapTypeString map) { - var map = new FHashMapTypeString(); - foreach (var key in _keys.Take(Count)) - map.GetOrAddValueRef(key, out _) = "a"; + map.Map.AddOrGetValueRef(key, out _) = "a"; - map.GetOrAddValueRef(LookupKey, out _) = "!"; - return map; + map.Map.AddOrGetValueRef(LookupKey, out _) = "!"; } - private FHashMapTypeString _fHashMap; + private FecSmallMapTypeString _fecSmallMap; public ConcurrentDictionary ConcurrentDict() { @@ -2758,55 +2758,80 @@ public string Dictionary_TryGetValue() // [Benchmark] [Benchmark(Baseline = true)] - public int DictionarySlim_TryGetValue() + public int DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent() { + var dict = new DictionarySlim(); + + foreach (var key in _keys.Take(Count)) + dict.GetOrAddValueRef(key) = "a"; + + dict.GetOrAddValueRef(LookupKey) = "!"; + var count = 0; var iters = Count / 2; for (var i = 0; i < iters; ++i) { - if (_dictSlim.TryGetValue(_randomPresentKeys[i], out var result)) + if (dict.TryGetValue(_randomPresentKeys[i], out var result)) count += result.Length; - if (!_dictSlim.TryGetValue(_missingKeys[i], out var _)) + if (!dict.TryGetValue(_missingKeys[i], out var _)) --count; } return count; } [Benchmark] - public int SmallMap_TryGetValue() + public int SmallMap_PopulateThenLookup_HalfMissed_HalfPresent() { + var map = new SmallMapTypeString(); + + foreach (var key in _keys.Take(Count)) + map.GetOrAddValueRef(key) = "a"; + + map.GetOrAddValueRef(LookupKey) = "!"; + var count = 0; var iters = Count / 2; for (var i = 0; i < iters; ++i) { - if (_smallMap.TryGetValue(_randomPresentKeys[i], out var result)) + if (map.TryGetValue(_randomPresentKeys[i], out var result)) count += result.Length; - if (!_smallMap.TryGetValue(_missingKeys[i], out var _)) + if (!map.TryGetValue(_missingKeys[i], out var _)) --count; } return count; } // [Benchmark] - public int FHashMap11_TryGetValue() + public int FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent() { + var map = new FecSmallMapTypeString(); + + foreach (var key in _keys.Take(Count)) + map.Map.AddOrGetValueRef(key, out _) = "a"; + + map.Map.AddOrGetValueRef(LookupKey, out _) = "!"; + var count = 0; - foreach (var k in _randomPresentKeys) + var iters = Count / 2; + for (var i = 0; i < iters; ++i) { - _fHashMap11.TryGetValue(k, out var result); - count += result.Length; + var result = map.Map.TryGetValueRef(_randomPresentKeys[i], out var found); + if (found) + count += result.Length; + var _ = map.Map.TryGetValueRef(_missingKeys[i], out found); + if (found) + --count; } - return count; } // [Benchmark] - public int FHashMap_TryGetValue() + public int FHashMap11_TryGetValue() { var count = 0; foreach (var k in _randomPresentKeys) { - var result = _fHashMap.TryGetValueRef(k, out _); + _fHashMap11.TryGetValue(k, out var result); count += result.Length; } diff --git a/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj b/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj index ffcaa8d1..7b7df3c2 100644 --- a/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj +++ b/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj @@ -18,11 +18,11 @@ - - + + - + diff --git a/src/ImTools/ImTools.cs b/src/ImTools/ImTools.cs index ec91b115..70afb07c 100644 --- a/src/ImTools/ImTools.cs +++ b/src/ImTools/ImTools.cs @@ -2004,7 +2004,7 @@ public static void ForEach(this ImZipper z, Action effect) /// Entry containing the Value in addition to the Hash public abstract class ImHashMapEntry : ImHashMap.Entry { - /// The value. Maybe modified if you need the Ref{Value} semantics. + /// The value. Maybe modified if you need the Ref{Value} semantics. /// You may add the entry with the default Value to the map, and calculate and set it later (e.g. using the CAS). public V Value; @@ -7298,6 +7298,7 @@ public HSmallMap(byte capacityBitShift) internal static readonly Vector256 VectorSizeVec = Vector256.Create(8); #endif + // todo: @wip @remove /// Lookup for the key and get the associated value if the key is found [MethodImpl((MethodImplOptions)256)] public bool TryGetValue_SIMD(K key, out V value) diff --git a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs new file mode 100644 index 00000000..83ba451b --- /dev/null +++ b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs @@ -0,0 +1,415 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; + +namespace FastExpressionCompiler.ImTools.UnitTests; + +using static SmallMap; + +[TestFixture] +public class FecSmallMapTests +{ + // internal static void Verify(SmallMap16 map, IEnumerable expectedKeys) + // where TEq : struct, IEq + // { + // map.VerifyHashesAndKeysEq(Assert.True); + // map.VerifyProbesAreFitRobinHood(Assert.Fail); + // map.VerifyNoDuplicateKeys(key => Assert.Fail($"Duplicate key: {key}")); + // if (expectedKeys != null) + // map.VerifyContainAllKeys(expectedKeys, (contains, key) => Assert.True(contains, $"Key not found:`{key}`")); + // } + + [Test] + public void Real_world_test_AddOrUpdate() + { + var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(100).ToArray(); + + var map = new SmallMap16>(); + + foreach (var key in types) + map.Map.AddOrGetValueRef(key, out _) = "a"; + + map.Map.AddOrGetValueRef(typeof(Console), out _) = "!"; + + var value = map.Map.TryGetValueRef(typeof(Console), out var found); + Assert.IsTrue(found); + Assert.AreEqual("!", value); + + // Verify(map, types); + } + + // [Test] + // public void Real_world_test_AddOrUpdate_NO_Resize() + // { + // var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(100).ToArray(); + + // var map = new SmallMap16>(8); + + // foreach (var key in types) + // map.AddOrUpdate(key, "a"); + + // map.AddOrUpdate(typeof(FHashMap11Tests), "!"); + + // Assert.AreEqual(101, map.Count); + + // Verify(map, types); + // } + + // [Test] + // public void Real_world_test_with_TryRemove_from_1000_items() + // { + // var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(1000).ToArray(); + + // var map = new SmallMap16>(); + + // foreach (var key in types) + // map.AddOrUpdate(key, "a"); + + // map.AddOrUpdate(typeof(FHashMap11Tests), "!"); + // Assert.AreEqual(1001, map.Count); + + // Assert.IsTrue(map.TryRemove(typeof(FHashMap11Tests))); + // Assert.AreEqual(1000, map.Count); + + // Verify(map, types); + // } + + // [Test] + // public void Real_world_test_with_Enumerator_and_TryRemove_the_entries() + // { + // var count = 1000; + // var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(count).ToList(); + // Assert.AreEqual(count, types.Count); + + // var map = new SmallMap16>(); + + // foreach (var key in types) + // map.AddOrUpdate(key, "a"); + + // var keys = map.Select(kv => kv.Key).ToList(); + // CollectionAssert.AreEquivalent(types, keys); + + // Assert.IsTrue(map.TryRemove(types[0])); + // Assert.IsTrue(map.TryRemove(types[999])); + // Assert.IsTrue(map.TryRemove(types[377])); + // Assert.IsTrue(map.TryRemove(types[733])); + // Assert.AreEqual(count - 4, map.Count); + + // // remove in the reverse order to keep the correct index in regard to map + // types.RemoveAt(999); + // types.RemoveAt(733); + // types.RemoveAt(377); + // types.RemoveAt(0); + // Assert.AreEqual(count - 4, types.Count); + + // // Check the second enumeration is working + // var keys2 = map.Select(kv => kv.Key).ToList(); + // CollectionAssert.AreEquivalent(types, keys2); + + // Verify(map, types); + // } + + // [Test] + // public void Simplified_test_with_equal_hashes_RefEq() + // { + // var map = new SmallMap16>(); + + // var keys = new[] { typeof(Tuple<>), typeof(Tuple<,>), typeof(Tuple<,,>) }; + // var i = 1; + // foreach (var k in keys) + // map.AddOrUpdate(k, "" + i++); + + // Assert.AreEqual(3, map.Count); + + // Assert.IsTrue(map.TryRemove(typeof(Tuple<,,>))); + // Assert.AreEqual(2, map.Count); + + // Verify(map, new[] { typeof(Tuple<>), typeof(Tuple<,>) }); + // } + + // [Test] + // public void Can_store_and_retrieve_value_from_map() + // { + // var map = new SmallMap16(2); + + // map.AddOrUpdate(42, "1"); + // map.AddOrUpdate(42 + 32, "2"); + // map.AddOrUpdate(42 + 32 + 32, "3"); + + // // interrupt the keys with ne key + // map.AddOrUpdate(43, "a"); + // map.AddOrUpdate(43 + 32, "b"); + // map.AddOrUpdate(43 + 32 + 32, "c"); + + // map.AddOrUpdate(42 + 32 + 32 + 32, "4"); + + // // insert 3rd variety of the keys + // map.AddOrUpdate(44, "*"); + + // map.AddOrUpdate(42 + 32 + 32 + 32 + 32, "5"); + // map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32, "6"); + // map.AddOrUpdate(43 + 32 + 32 + 32, "d"); + + // map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32, "7"); + // map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, "8"); + + // // check for the missing key + // Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32)); + + // // check for the strange key + // Assert.AreEqual("*", map.GetValueOrDefault(44)); + + // Assert.AreEqual("1", map.GetValueOrDefault(42)); + // Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); + // Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); + // Assert.AreEqual("4", map.GetValueOrDefault(42 + 32 + 32 + 32)); + // Assert.AreEqual("5", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32)); + // Assert.AreEqual("6", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32)); + // Assert.AreEqual("7", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32)); + // Assert.AreEqual("8", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32)); + + // Assert.AreEqual("a", map.GetValueOrDefault(43)); + // Assert.AreEqual("b", map.GetValueOrDefault(43 + 32)); + // Assert.AreEqual("c", map.GetValueOrDefault(43 + 32 + 32)); + // Assert.AreEqual("d", map.GetValueOrDefault(43 + 32 + 32 + 32)); + + // Assert.AreEqual(13, map.Count); + + // Verify(map, null); + // } + + /* + ## Debug output example + + ### IntEq + + [AddOrUpdate] Probes abs max=2, max=2, all=[1: 1, 2: 1]; first 4 probes are 2 out of 2 + [AddOrUpdate] Probes abs max=3, max=3, all=[1: 1, 2: 1, 3: 1]; first 4 probes are 3 out of 3 + [AllocateEntries] Resize entries: 2 -> 4 + [ResizeHashes] 4 -> 8 + [ResizeHashes] Probes abs max=3, max=3, all=[1: 1, 2: 1, 3: 1]; first 4 probes are 3 out of 3 + [AddOrUpdate] Probes abs max=4, max=4, all=[1: 1, 2: 1, 3: 2, 4: 1]; first 4 probes are 5 out of 5 + [AllocateEntries] Resize entries: 4 -> 8 + [AddOrUpdate] Probes abs max=5, max=5, all=[1: 1, 2: 1, 3: 2, 4: 1, 5: 1]; first 4 probes are 5 out of 6 + [AddOrUpdate-RH] Probes abs max=6, max=6, all=[1: 1, 2: 1, 3: 2, 4: 2, 5: 1, 6: 1]; first 4 probes are 6 out of 8 + [ResizeHashes] 8 -> 16 + [ResizeHashes] Probes abs max=6, max=6, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 1, 6: 1]; first 4 probes are 5 out of 7 + [AllocateEntries] Resize entries: 8 -> 16 + [AddOrUpdate-RH] Probes abs max=7, max=7, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 1, 7: 1]; first 4 probes are 5 out of 9 + [AddOrUpdate-RH] Probes abs max=8, max=8, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 1, 8: 1]; first 4 probes are 5 out of 11 + [AddOrUpdate] Probes abs max=9, max=9, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 1, 8: 2, 9: 1]; first 4 probes are 5 out of 13 + [AddOrUpdate-RH] Probes abs max=10, max=10, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 2, 8: 2, 9: 1, 10: 1]; first 4 probes are 5 out of 15 + [AddOrUpdate-RH] Probes abs max=11, max=11, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 2, 8: 3, 9: 1, 10: 1, 11: 1]; first 4 probes are 5 out of 17 + + ### GoldenIntEq + + [AddOrUpdate] Probes abs max=2, max=2, all=[1: 1, 2: 1]; first 4 probes are 2 out of 2 + [AllocateEntries] Resize entries: 2 -> 4 + [ResizeHashes] 4 -> 8 + [ResizeHashes] Probes abs max=2, max=1, all=[1: 3]; first 4 probes are 3 out of 3 + [AllocateEntries] Resize entries: 4 -> 8 + [AddOrUpdate] Probes abs max=2, max=2, all=[1: 5, 2: 1]; first 4 probes are 6 out of 6 + [ResizeHashes] 8 -> 16 + [ResizeHashes] Probes abs max=2, max=2, all=[1: 6, 2: 1]; first 4 probes are 7 out of 7 + [AllocateEntries] Resize entries: 8 -> 16 + + */ + + // [Test] + // public void Can_store_and_retrieve_value_from_map_Golden() + // { + // var map = new SmallMap16(2); + // // var map = new SmallMap16(2); + + // map.AddOrUpdate(42, "1"); + // map.AddOrUpdate(42 + 32, "2"); + // map.AddOrUpdate(42 + 32 + 32, "3"); + + // // interrupt the keys with new key + // map.AddOrUpdate(43, "a"); + // map.AddOrUpdate(43 + 32, "b"); + // map.AddOrUpdate(43 + 32 + 32, "c"); + + // map.AddOrUpdate(42 + 32 + 32 + 32, "4"); + + // // insert 3rd variety of the keys + // map.AddOrUpdate(44, "*"); + + // map.AddOrUpdate(42 + 32 + 32 + 32 + 32, "5"); + // map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32, "6"); + // map.AddOrUpdate(43 + 32 + 32 + 32, "d"); + + // map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32, "7"); + // map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, "8"); + + // // check for the missing key + // Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32)); + + // // check for the strange key + // Assert.AreEqual("*", map.GetValueOrDefault(44)); + + // Assert.AreEqual("1", map.GetValueOrDefault(42)); + // Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); + // Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); + // Assert.AreEqual("4", map.GetValueOrDefault(42 + 32 + 32 + 32)); + // Assert.AreEqual("5", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32)); + // Assert.AreEqual("6", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32)); + // Assert.AreEqual("7", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32)); + // Assert.AreEqual("8", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32)); + + // Assert.AreEqual("a", map.GetValueOrDefault(43)); + // Assert.AreEqual("b", map.GetValueOrDefault(43 + 32)); + // Assert.AreEqual("c", map.GetValueOrDefault(43 + 32 + 32)); + // Assert.AreEqual("d", map.GetValueOrDefault(43 + 32 + 32 + 32)); + + // Assert.AreEqual(13, map.Count); + + // Verify(map, null); + // } + + // [Test] + // public void Can_lookup_the_default_map_without_error() + // { + // SmallMap16 map = default; + + // Assert.IsFalse(map.TryGetValue(42, out _)); + // } + + // [Test] + // public void Can_store_and_retrieve_value_from_map_with_Expand_in_the_middle() + // { + // var map = new SmallMap16(1); + + // Assert.IsFalse(map.TryGetValue(42, out _)); + + // map.AddOrUpdate(42, "1"); + // map.AddOrUpdate(42 + 32, "2"); + + // // interrupt the keys with new key + // map.AddOrUpdate(43, "a"); + // map.AddOrUpdate(43 + 32, "b"); + + // map.AddOrUpdate(42 + 32 + 32, "3"); + + // Assert.AreEqual("1", map.GetValueOrDefault(42)); + // Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); + // Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); + // Assert.AreEqual(null, map.GetValueOrDefault(42 + 32 + 32 + 32)); + // Assert.AreEqual("a", map.GetValueOrDefault(43)); + + // map.AddOrUpdate(43, "a!"); + // Assert.AreEqual("a!", map.GetValueOrDefault(43)); + + // map.AddOrUpdate(47, "x"); + // map.AddOrUpdate(53, "y"); + // Assert.AreEqual("x", map.GetValueOrDefault(47)); + // Assert.AreEqual("y", map.GetValueOrDefault(53)); + + // map.AddOrUpdate(47 + 16, "x!"); + // map.AddOrUpdate(53 + 16, "y!"); + // Assert.AreEqual("x!", map.GetValueOrDefault(47 + 16)); + // Assert.AreEqual("y!", map.GetValueOrDefault(53 + 16)); + + // Verify(map, null); + // } + + // [Test] + // public void Can_resize_without_moving() + // { + // var map = new SmallMap16(2); + + // map.AddOrUpdate(0, "0"); + // map.AddOrUpdate(1, "1"); + // map.AddOrUpdate(9, "9"); + + // // resize goes here + // map.AddOrUpdate(3, "3"); + + // map.AddOrUpdate(5, "5"); + + // Verify(map, new[] { 0, 1, 3, 5, 9 }); + // } + + // [Test] + // public void Can_store_and_get_stored_item_count() + // { + // var map = new SmallMap16(); + + // map.AddOrUpdate(42, "1"); + // map.AddOrUpdate(42 + 32 + 32, "3"); + + // Assert.AreEqual(2, map.Count); + // Verify(map, new[] { 42, 42 + 32 + 32 }); + // } + + // [Test] + // public void Can_update_a_stored_item_with_new_value() + // { + // var map = new SmallMap16(); + + // map.AddOrUpdate(42, "1"); + // map.AddOrUpdate(42, "3"); + + // Assert.AreEqual("3", map.GetValueOrDefault(42)); + // Assert.AreEqual(1, map.Count); + // Verify(map, new[] { 42 }); + // } + + // [Test] + // public void Can_add_key_with_0_hash_code() + // { + // var map = new SmallMap16(); + + // map.AddOrUpdate(0, "aaa"); + // map.AddOrUpdate(0 + 32, "2"); + // map.AddOrUpdate(0 + 32 + 32, "3"); + // Verify(map, new[] { 0, 0 + 32, 0 + 32 + 32 }); + + // string value; + // Assert.IsTrue(map.TryGetValue(0, out value)); + + // Assert.AreEqual("aaa", value); + // } + + // [Test] + // public void Can_quickly_find_the_scattered_items_with_the_same_cache() + // { + // var map = new SmallMap16(); + + // map.AddOrUpdate(42, "1"); + // map.AddOrUpdate(43, "a"); + // map.AddOrUpdate(42 + 32, "2"); + // map.AddOrUpdate(45, "b"); + // map.AddOrUpdate(46, "c"); + // map.AddOrUpdate(42 + 32 + 32, "3"); + // Verify(map, new[] { 42, 43, 42 + 32, 45, 46, 42 + 32 + 32 }); + + // string value; + // Assert.IsTrue(map.TryGetValue(42 + 32, out value)); + // Assert.AreEqual("2", value); + + // Assert.IsTrue(map.TryGetValue(42 + 32 + 32, out value)); + // Assert.AreEqual("3", value); + // } + + // [Test] + // public void Can_remove_the_stored_item() + // { + // var map = new SmallMap16(2); + + // map.AddOrUpdate(42, "1"); + // map.AddOrUpdate(42 + 32, "2"); + // map.AddOrUpdate(42 + 32 + 32, "3"); + + // Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); + // var r = map.TryRemove(42 + 32); + // Assert.IsTrue(r); + + // Assert.AreEqual(2, map.Count); + // Assert.AreEqual("1", map.GetValueOrDefault(42)); + // Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); + // Verify(map, null); + // } +} From afe6c01848f3b686d2b4ecf9fd358a97f7685a73 Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 7 Jul 2025 16:52:33 +0200 Subject: [PATCH 13/54] small improve --- playground/Experiments/FEC_ImTools.cs | 77 +++++++++--- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 35 +++--- .../Playground/FecSmallMapTests.cs | 112 ++++++++++++------ 3 files changed, 148 insertions(+), 76 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index afc6e2f3..9df72e3e 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -855,7 +855,6 @@ public static class SmallMap { internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) internal const byte MinHashesCapacityBitShift = 3; // 1 << 3 == 8 - internal const byte DefaultHashesCapacityBitShift = 4; // 1 << 4 == 16, means the default capacity is 16 int hashes /// Upper hash bits spent on storing the probes, e.g. 5 bits mean 31 probes max. public const byte ProbeBits = 5; internal const byte NotShiftedProbeCountMask = (1 << ProbeBits) - 1; // 0b00000000000000000000000000011111 @@ -1031,6 +1030,21 @@ public static ref V AddOrGetValueRef, TEq> => ref map.AddOrGetEntryRef(key, out found).Value; + /// Adds or updates the entry with the provided value. + /// Returns `true` if it is updated the existing value. + [MethodImpl((MethodImplOptions)256)] + public static bool AddOrUpdate( + this ref SmallMap, TEq, TStackCap, TStackHashes, TStackEntries, TEntries> map, K key, in V value) + where TEq : struct, IEq + where TStackCap : struct, ISize2Plus + where TStackHashes : struct, IStack + where TStackEntries : struct, IStack, TStackCap, TStackEntries> + where TEntries : struct, IEntries, TEq> + { + map.AddOrGetEntryRef(key, out var found).Value = value; + return found; + } + /// Adds an entry for sure absent key. /// Provides the performance in scenarios where you look for the present key, and using it, and if ABSENT then add the new one. /// So this method optimized NOT to look for the present item for the second time @@ -1058,6 +1072,21 @@ public static ref V TryGetValueRef.GetNullRef(); } + + /// Lookups for the stored entry by key. Returns the found value or provided default + [MethodImpl((MethodImplOptions)256)] + public static V GetValueOrDefault( + this ref SmallMap, TEq, TStackCap, TStackHashes, TStackEntries, TEntries> map, K key, V defaultValue = default) + where TEq : struct, IEq + where TStackCap : struct, ISize2Plus + where TStackHashes : struct, IStack + where TStackEntries : struct, IStack, TStackCap, TStackEntries> + where TEntries : struct, IEntries, TEq> + { + ref var e = ref map.TryGetEntryRef(key, out var found); + if (found) return e.Value; + return defaultValue; + } } // todo: @improve ? how/where to add SIMD to improve CPU utilization but not losing perf for smaller sizes @@ -1210,8 +1239,8 @@ private void AddJustHashAndEntryIndexWithoutResizing(int indexMask, int hashInde var hashesAndIndexes = _packedHashesAndIndexes; #endif // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - ref var h = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); var probe = 1; + ref var h = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); while ((h >>> ProbeCountShift) >= probe) { h = ref hashesAndIndexes.GetSurePresentItemRef(++hashIndex & indexMask); @@ -1257,9 +1286,9 @@ public ref TEntry AddOrGetEntryRef(K key, out bool found) var newIndex = _count; ++_count; _stackHashes.GetSurePresentItemRef(newIndex) = default(TEq).GetHashCode(key); - ref var newEntry = ref _stackEntries.GetSurePresentItemRef(newIndex); - newEntry.Key = key; - return ref newEntry; + ref var newStackEntry = ref _stackEntries.GetSurePresentItemRef(newIndex); + newStackEntry.Key = key; + return ref newStackEntry; } // Now all capacity of the stack is used. @@ -1271,13 +1300,15 @@ public ref TEntry AddOrGetEntryRef(K key, out bool found) // So the values on the stack are guarantied to be stable from the beginning of the map creation, // because they are not copied when the Entries need to Resize (depending on the TEntries implementation). - _capacityBitShift = (byte)(_stackEntries.CapacityBitShift + 1); - var indexMask = (1 << _capacityBitShift) - 1; + var newCapBitShift = _stackEntries.CapacityBitShift + 1; + _capacityBitShift = (byte)newCapBitShift; + _packedHashesAndIndexes = new int[1 << newCapBitShift]; + + var indexMask = (1 << newCapBitShift) - 1; var hashMiddleMask = HashAndIndexMask & ~indexMask; - var stackCap = _stackEntries.Capacity; - _packedHashesAndIndexes = new int[1 << _capacityBitShift]; - for (var i = 0; i < _stackHashes.Capacity; ++i) + var stackCap = _stackEntries.Capacity; + for (var i = 0; i < stackCap; ++i) { var h = _stackHashes.GetSurePresentItemRef(i); AddJustHashAndEntryIndexWithoutResizing(indexMask, h & indexMask, (h & hashMiddleMask) | i); @@ -1289,7 +1320,11 @@ public ref TEntry AddOrGetEntryRef(K key, out bool found) ++_count; _entries.Init(stackCap); // Give the heap entries the same initial capacity as Stack, effectively doubling the capacity - return ref _entries.AddKeyAndGetEntryRef(key, 0); // add the new key to the entries with the 0 index in the entries + + // Set the key for the first entry, which is the 0 index in the entries + ref var newEntry = ref _entries.GetSurePresentEntryRef(0); + newEntry.Key = key; + return ref newEntry; } /* @@ -1386,15 +1421,17 @@ public ref TEntry AddSureAbsentDefaultEntryAndGetRef(K key) { var newIndex = _count++; _stackHashes.GetSurePresentItemRef(newIndex) = default(TEq).GetHashCode(key); - ref var newEntry = ref _stackEntries.GetSurePresentItemRef(newIndex); - newEntry.Key = key; - return ref newEntry; + ref var newStackEntry = ref _stackEntries.GetSurePresentItemRef(newIndex); + newStackEntry.Key = key; + return ref newStackEntry; } - _capacityBitShift = (byte)(_stackEntries.CapacityBitShift + 1); - var indexMask = (1 << _capacityBitShift) - 1; + var newCapBitShift = _stackEntries.CapacityBitShift + 1; + _capacityBitShift = (byte)newCapBitShift; + _packedHashesAndIndexes = new int[1 << newCapBitShift]; + + var indexMask = (1 << newCapBitShift) - 1; var hashMiddleMask = HashAndIndexMask & ~indexMask; - _packedHashesAndIndexes = new int[1 << _capacityBitShift]; var stackCap = _stackHashes.Capacity; for (var i = 0; i < stackCap; ++i) @@ -1408,7 +1445,11 @@ public ref TEntry AddSureAbsentDefaultEntryAndGetRef(K key) ++_count; _entries.Init(stackCap); // Give the heap entries the same initial capacity as Stack, effectively doubling the capacity - return ref _entries.AddKeyAndGetEntryRef(key, 0); // add the new key to the entries with the 0 index in the entries + + // Set the key for the first entry, which is the 0 index in the entries + ref var newEntry = ref _entries.GetSurePresentEntryRef(0); + newEntry.Key = key; + return ref newEntry; } /// Lookups for the stored key. If found true, otherwise false diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index dce47689..385fe82b 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -2323,16 +2323,18 @@ .NET SDK 9.0.203 */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] - // [Params(10, 100, 1000)] - [Params(100)] + [Params(10, 100, 1000)] + // [Params(100)] public int Count; + private Type[] _presentKeys; private Type[] _randomPresentKeys; [GlobalSetup] public void Populate() { - _randomPresentKeys = _keys.Take(Count).OrderBy(_ => _seed.Next()).ToArray(); + _presentKeys = _keys.Take(Count).ToArray(); + _randomPresentKeys = _presentKeys.OrderBy(_ => _seed.Next()).ToArray(); // _mapV4 = V4_ImHashMap_AddOrUpdate(); // _mapV3 = V3_ImHashMap_AddOrUpdate(); @@ -2762,11 +2764,9 @@ public int DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent() { var dict = new DictionarySlim(); - foreach (var key in _keys.Take(Count)) + foreach (var key in _presentKeys) dict.GetOrAddValueRef(key) = "a"; - dict.GetOrAddValueRef(LookupKey) = "!"; - var count = 0; var iters = Count / 2; for (var i = 0; i < iters; ++i) @@ -2784,11 +2784,9 @@ public int SmallMap_PopulateThenLookup_HalfMissed_HalfPresent() { var map = new SmallMapTypeString(); - foreach (var key in _keys.Take(Count)) + foreach (var key in _presentKeys) map.GetOrAddValueRef(key) = "a"; - map.GetOrAddValueRef(LookupKey) = "!"; - var count = 0; var iters = Count / 2; for (var i = 0; i < iters; ++i) @@ -2801,25 +2799,24 @@ public int SmallMap_PopulateThenLookup_HalfMissed_HalfPresent() return count; } - // [Benchmark] + [Benchmark] public int FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent() { - var map = new FecSmallMapTypeString(); - - foreach (var key in _keys.Take(Count)) - map.Map.AddOrGetValueRef(key, out _) = "a"; + var m = new FecSmallMapTypeString(); + ref var map = ref m.Map; - map.Map.AddOrGetValueRef(LookupKey, out _) = "!"; + foreach (var key in _presentKeys) + map.AddOrGetValueRef(key, out _) = "a"; var count = 0; var iters = Count / 2; for (var i = 0; i < iters; ++i) { - var result = map.Map.TryGetValueRef(_randomPresentKeys[i], out var found); + var result = map.TryGetValueRef(_randomPresentKeys[i], out var found); if (found) count += result.Length; - var _ = map.Map.TryGetValueRef(_missingKeys[i], out found); - if (found) + _ = map.TryGetValueRef(_missingKeys[i], out found); + if (!found) --count; } return count; @@ -3179,7 +3176,7 @@ .NET SDK 7.0.306 */ // [Params(1, 10, 100, 1_000)] - [Params(100)] + [Params(10, 100, 1000)] public int Count; [GlobalSetup] diff --git a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs index 83ba451b..c0dcfec3 100644 --- a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs +++ b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs @@ -40,6 +40,41 @@ public void Real_world_test_AddOrUpdate() // Verify(map, types); } + [Test] + public void Benchmark_test_with_Add_and_Lookup() + { + const int Count = 10; + var LookupKey = typeof(Console); + + var m = new SmallMap16>(); + ref var map = ref m.Map; + + var allKeys = typeof(List<>).Assembly.GetTypes().Take(2000).ToArray(); + var keys = allKeys.Take(1000).ToArray(); + var seed = new Random(42); + var missingKeys = allKeys.Skip(1000).Take(Count).ToArray(); + var randomPresentKeys = keys.Take(Count).OrderBy(_ => seed.Next()).ToArray(); + + foreach (var key in keys.Take(Count)) + map.AddOrGetValueRef(key, out _) = "a"; + + map.AddOrGetValueRef(LookupKey, out _) = "!"; + + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) + { + var result = map.TryGetValueRef(randomPresentKeys[i], out var found); + if (found) + count += result.Length; + _ = map.TryGetValueRef(missingKeys[i], out found); + if (!found) + --count; + } + + Assert.AreEqual(0, count); + } + // [Test] // public void Real_world_test_AddOrUpdate_NO_Resize() // { @@ -129,56 +164,55 @@ public void Real_world_test_AddOrUpdate() // Verify(map, new[] { typeof(Tuple<>), typeof(Tuple<,>) }); // } - // [Test] - // public void Can_store_and_retrieve_value_from_map() - // { - // var map = new SmallMap16(2); - - // map.AddOrUpdate(42, "1"); - // map.AddOrUpdate(42 + 32, "2"); - // map.AddOrUpdate(42 + 32 + 32, "3"); + [Test] + public void Can_store_and_retrieve_value_from_map() + { + var m = new SmallMap16(); + ref var map = ref m.Map; - // // interrupt the keys with ne key - // map.AddOrUpdate(43, "a"); - // map.AddOrUpdate(43 + 32, "b"); - // map.AddOrUpdate(43 + 32 + 32, "c"); + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(42 + 32, "2"); + map.AddOrUpdate(42 + 32 + 32, "3"); - // map.AddOrUpdate(42 + 32 + 32 + 32, "4"); + // interrupt the keys with ne key + map.AddOrUpdate(43, "a"); + map.AddOrUpdate(43 + 32, "b"); + map.AddOrUpdate(43 + 32 + 32, "c"); - // // insert 3rd variety of the keys - // map.AddOrUpdate(44, "*"); + map.AddOrUpdate(42 + 32 + 32 + 32, "4"); - // map.AddOrUpdate(42 + 32 + 32 + 32 + 32, "5"); - // map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32, "6"); - // map.AddOrUpdate(43 + 32 + 32 + 32, "d"); + // insert 3rd variety of the keys + map.AddOrUpdate(44, "*"); - // map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32, "7"); - // map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, "8"); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32, "5"); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32, "6"); + map.AddOrUpdate(43 + 32 + 32 + 32, "d"); - // // check for the missing key - // Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32)); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32, "7"); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, "8"); - // // check for the strange key - // Assert.AreEqual("*", map.GetValueOrDefault(44)); + // check for the missing key + Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32)); - // Assert.AreEqual("1", map.GetValueOrDefault(42)); - // Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); - // Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); - // Assert.AreEqual("4", map.GetValueOrDefault(42 + 32 + 32 + 32)); - // Assert.AreEqual("5", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32)); - // Assert.AreEqual("6", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32)); - // Assert.AreEqual("7", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32)); - // Assert.AreEqual("8", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32)); + // check for the strange key + Assert.AreEqual("*", map.GetValueOrDefault(44)); - // Assert.AreEqual("a", map.GetValueOrDefault(43)); - // Assert.AreEqual("b", map.GetValueOrDefault(43 + 32)); - // Assert.AreEqual("c", map.GetValueOrDefault(43 + 32 + 32)); - // Assert.AreEqual("d", map.GetValueOrDefault(43 + 32 + 32 + 32)); + Assert.AreEqual("1", map.GetValueOrDefault(42)); + Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); + Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); + Assert.AreEqual("4", map.GetValueOrDefault(42 + 32 + 32 + 32)); + Assert.AreEqual("5", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32)); + Assert.AreEqual("6", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32)); + Assert.AreEqual("7", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32)); + Assert.AreEqual("8", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32)); - // Assert.AreEqual(13, map.Count); + Assert.AreEqual("a", map.GetValueOrDefault(43)); + Assert.AreEqual("b", map.GetValueOrDefault(43 + 32)); + Assert.AreEqual("c", map.GetValueOrDefault(43 + 32 + 32)); + Assert.AreEqual("d", map.GetValueOrDefault(43 + 32 + 32 + 32)); - // Verify(map, null); - // } + Assert.AreEqual(13, map.Count); + } /* ## Debug output example From 80f86b2535b3ce0e924ab302388871eb8568e6b1 Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 7 Jul 2025 18:09:35 +0200 Subject: [PATCH 14/54] disable the Ubunt and MacOS builds because of strange UnscopedRefAttribute behaviour --- appveyor.yml | 4 ++-- playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 4d690fac..d1362720 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,7 @@ image: - Visual Studio 2022 - - Ubuntu - - macOS + # - Ubuntu + # - macOS version: 5.0.0-preview-01-b{build} diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 385fe82b..fc5d6e6c 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -2321,10 +2321,18 @@ .NET SDK 9.0.203 | SmallMap_TryGetValue | 1000 | 4,255.63 ns | 82.286 ns | 130.514 ns | 0.89 | 0.04 | 1 | - | NA | | DictionarySlim_TryGetValue | 1000 | 4,794.12 ns | 95.912 ns | 189.322 ns | 1.00 | 0.06 | 2 | - | NA | + ## Stack hybrid from the FEC is tested + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|--------:|---------:|------:|--------:|-----:|-------:|----------:|------------:| + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 203.5 ns | 3.88 ns | 3.63 ns | 0.70 | 0.03 | 1 | 0.1147 | 720 B | 0.67 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 277.6 ns | 3.61 ns | 5.52 ns | 0.95 | 0.04 | 2 | - | - | 0.00 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 292.1 ns | 5.84 ns | 12.19 ns | 1.00 | 0.06 | 2 | 0.1707 | 1072 B | 1.00 | + */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] - [Params(10, 100, 1000)] - // [Params(100)] + // [Params(10, 100, 1000)] + [Params(10)] public int Count; private Type[] _presentKeys; From 2dbb480c480cc6cf9ee34eec9e36b47a21a5f9b9 Mon Sep 17 00:00:00 2001 From: dadhi Date: Tue, 8 Jul 2025 16:48:38 +0200 Subject: [PATCH 15/54] ensuring the not 0 hash --- appveyor.yml | 2 +- playground/Experiments/FEC_ImTools.cs | 80 ++++++++----------- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 2 +- .../Playground/FecSmallMapTests.cs | 33 +++++--- 4 files changed, 61 insertions(+), 56 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index d1362720..48872932 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,7 +5,7 @@ image: version: 5.0.0-preview-01-b{build} -test: off +test: false skip_commits: message: /noci .*/ diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 9df72e3e..1cc02427 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -957,7 +957,7 @@ public ref TEntry AddKeyAndGetEntryRef(K key, int index) /// Lookup for the K in the TStackEntries, first by calculating it hash with TEq and searching the hash in the TStackHashes [MethodImpl((MethodImplOptions)256)] public static ref TEntry TryGetEntryRef( - this ref TStackEntries entries, ref TStackHashes hashes, int count, K key, out bool found, + this ref TStackEntries entries, ref TStackHashes hashes, int count, K key, int hash, out bool found, TEq eq = default, TCap cap = default, Use _ = default) where TEntry : struct, IEntry where TEq : struct, IEq @@ -966,11 +966,14 @@ public static ref TEntry TryGetEntryRef.GetNullRef(); + } #if NET8_0_OR_GREATER - if (count >= 8 & cap.Size >= 8 & Vector256.IsHardwareAccelerated) + if (cap.Size >= 8 & Vector256.IsHardwareAccelerated) { var vHash = Vector256.Create(hash); var vHashes = MemoryMarshal.Cast>(hashes.AsSpan()); @@ -1170,7 +1173,7 @@ public ref TEntry GetSurePresentEntryRef(int index) } [UnscopedRef] - private ref TEntry AddOrGetRefInEntries(K key, out bool found) + private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) { // if the free space is less than 1/8 of capacity (12.5%) then Resize var indexMask = (1 << _capacityBitShift) - 1; @@ -1179,7 +1182,6 @@ private ref TEntry AddOrGetRefInEntries(K key, out bool found) if (indexMask - _count <= (indexMask >>> MinFreeCapacityShift)) indexMask = ResizeHashes(indexMask); - var hash = default(TEq).GetHashCode(key); var hashMiddleMask = HashAndIndexMask & ~indexMask; var hashMiddle = hash & hashMiddleMask; var hashIndex = hash & indexMask; @@ -1270,27 +1272,36 @@ private void AddJustHashAndEntryIndexWithoutResizing(int indexMask, int hashInde /// Gets the ref to the existing entry by the provided key (found == true), /// or adds a new entry (found == false) and returns it by ref [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] public ref TEntry AddOrGetEntryRef(K key, out bool found) { + var hash = default(TEq).GetHashCode(key) | 1; + if (_count > _stackEntries.Capacity) - return ref AddOrGetRefInEntries(key, out found); + return ref AddOrGetRefInEntries(key, hash, out found); - ref var e = ref _stackEntries.TryGetEntryRef(ref _stackHashes, _count, key, out found, + ref var e = ref _stackEntries.TryGetEntryRef(ref _stackHashes, _count, key, hash, out found, default(TEq), default(TStackCap), default(Use)); if (found) return ref e; // Add the new entry to the stack if there is still space in stack + found = false; if (_count < _stackEntries.Capacity) { var newIndex = _count; ++_count; - _stackHashes.GetSurePresentItemRef(newIndex) = default(TEq).GetHashCode(key); + _stackHashes.GetSurePresentItemRef(newIndex) = hash; ref var newStackEntry = ref _stackEntries.GetSurePresentItemRef(newIndex); newStackEntry.Key = key; return ref newStackEntry; } + return ref MigrateToHeapAndAddAEntry(key, hash); + } + + private ref TEntry MigrateToHeapAndAddAEntry(K key, int hash) + { // Now all capacity of the stack is used. // To avoid double work always going linearly through the Stack with the comparison, // let's calculate the hash of the keys stored on stack and put them @@ -1315,7 +1326,6 @@ public ref TEntry AddOrGetEntryRef(K key, out bool found) } // todo: @perf calculates the hash code twice, first time in the TryGetEntryRef and then here - var hash = default(TEq).GetHashCode(key); AddJustHashAndEntryIndexWithoutResizing(indexMask, hash & indexMask, (hash & hashMiddleMask) | stackCap); ++_count; @@ -1360,14 +1370,13 @@ Probe 1A 2B Probe: 2C 1A 2B 3D */ [UnscopedRef] - private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key) + private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key, int hash) { // if the free space is less than 1/8 of capacity (12.5%) then Resize var indexMask = (1 << _capacityBitShift) - 1; if (indexMask - _count <= (indexMask >>> MinFreeCapacityShift)) indexMask = ResizeHashes(indexMask); - var hash = default(TEq).GetHashCode(key); var hashIndex = hash & indexMask; #if NET7_0_OR_GREATER @@ -1411,58 +1420,39 @@ private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key) /// Provides the performance in scenarios where you look for the present key, and using it, and if ABSENT then add the new one. /// So this method optimized NOT to look for the present item for the second time [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] public ref TEntry AddSureAbsentDefaultEntryAndGetRef(K key) { + var hash = default(TEq).GetHashCode(key) | 1; + if (_count > _stackEntries.Capacity) - return ref AddSureAbsentDefaultAndGetRefInEntries(key); + return ref AddSureAbsentDefaultAndGetRefInEntries(key, hash); // Add the new entry to the stack if there is still space in stack if (_count < _stackEntries.Capacity) { var newIndex = _count++; - _stackHashes.GetSurePresentItemRef(newIndex) = default(TEq).GetHashCode(key); + _stackHashes.GetSurePresentItemRef(newIndex) = hash; ref var newStackEntry = ref _stackEntries.GetSurePresentItemRef(newIndex); newStackEntry.Key = key; return ref newStackEntry; } - var newCapBitShift = _stackEntries.CapacityBitShift + 1; - _capacityBitShift = (byte)newCapBitShift; - _packedHashesAndIndexes = new int[1 << newCapBitShift]; - - var indexMask = (1 << newCapBitShift) - 1; - var hashMiddleMask = HashAndIndexMask & ~indexMask; - - var stackCap = _stackHashes.Capacity; - for (var i = 0; i < stackCap; ++i) - { - var h = _stackHashes.GetSurePresentItemRef(i); - AddJustHashAndEntryIndexWithoutResizing(indexMask, h & indexMask, (h & hashMiddleMask) | i); - } - - var hash = default(TEq).GetHashCode(key); - AddJustHashAndEntryIndexWithoutResizing(indexMask, hash & indexMask, (hash & hashMiddleMask) | stackCap); - - ++_count; - _entries.Init(stackCap); // Give the heap entries the same initial capacity as Stack, effectively doubling the capacity - - // Set the key for the first entry, which is the 0 index in the entries - ref var newEntry = ref _entries.GetSurePresentEntryRef(0); - newEntry.Key = key; - return ref newEntry; + return ref MigrateToHeapAndAddAEntry(key, hash); } /// Lookups for the stored key. If found true, otherwise false public bool ContainsKey(K key) { + var hash = default(TEq).GetHashCode(key) | 1; if (_count > _stackEntries.Capacity) { - _ = TryGetRefInEntries(key, out var found); + _ = TryGetRefInEntries(key, hash, out var found); return found; } else { - _ = ref _stackEntries.TryGetEntryRef(ref _stackHashes, _count, key, out var found, + _ = ref _stackEntries.TryGetEntryRef(ref _stackHashes, _count, key, hash, out var found, default(TEq), default(TStackCap), default(Use)); return found; } @@ -1470,10 +1460,8 @@ public bool ContainsKey(K key) [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - internal ref TEntry TryGetRefInEntries(K key, out bool found) + internal ref TEntry TryGetRefInEntries(K key, int hash, out bool found) { - var hash = default(TEq).GetHashCode(key); - var indexMask = (1 << _capacityBitShift) - 1; var hashMiddleMask = HashAndIndexMask & ~indexMask; var hashMiddle = hash & hashMiddleMask; @@ -1509,11 +1497,13 @@ internal ref TEntry TryGetRefInEntries(K key, out bool found) /// Lookup for the stored entry by key. Returns the ref to the found entry or the null ref [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] public ref TEntry TryGetEntryRef(K key, out bool found) { + var hash = default(TEq).GetHashCode(key) | 1; if (_count > _stackEntries.Capacity) - return ref TryGetRefInEntries(key, out found); - return ref _stackEntries.TryGetEntryRef(ref _stackHashes, _count, key, out found, + return ref TryGetRefInEntries(key, hash, out found); + return ref _stackEntries.TryGetEntryRef(ref _stackHashes, _count, key, hash, out found, default(TEq), default(TStackCap), default(Use)); } diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index fc5d6e6c..8c8fb3f0 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -2787,7 +2787,7 @@ public int DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent() return count; } - [Benchmark] + // [Benchmark] public int SmallMap_PopulateThenLookup_HalfMissed_HalfPresent() { var map = new SmallMapTypeString(); diff --git a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs index c0dcfec3..36c93586 100644 --- a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs +++ b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs @@ -1,6 +1,7 @@  using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using NUnit.Framework; @@ -20,11 +21,26 @@ public class FecSmallMapTests // if (expectedKeys != null) // map.VerifyContainAllKeys(expectedKeys, (contains, key) => Assert.True(contains, $"Key not found:`{key}`")); // } + private static readonly Type[] _allKeys = typeof(List<>).Assembly.GetTypes().Take(2000).ToArray(); + + [Test] + public void Zero_0_hash_test() + { + var map = new SmallMap16(); + // Make map >> 0 + map.Map.AddOrUpdate(1, 1); + Assert.AreEqual(1, map.Map.Count); + + Assert.False(map.Map.ContainsKey(0)); + map.Map.AddOrUpdate(0, 0); + Assert.AreEqual(2, map.Map.Count); + Assert.True(map.Map.ContainsKey(0)); + } [Test] public void Real_world_test_AddOrUpdate() { - var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(100).ToArray(); + var types = _allKeys.Take(100).ToArray(); var map = new SmallMap16>(); @@ -40,26 +56,25 @@ public void Real_world_test_AddOrUpdate() // Verify(map, types); } + [Test] public void Benchmark_test_with_Add_and_Lookup() { const int Count = 10; - var LookupKey = typeof(Console); + Debug.Assert(Count <= 1000, "Count should be less than or equal to 1000 for this test to work correctly."); var m = new SmallMap16>(); ref var map = ref m.Map; - var allKeys = typeof(List<>).Assembly.GetTypes().Take(2000).ToArray(); - var keys = allKeys.Take(1000).ToArray(); + var presentKeys = _allKeys.Take(Count).ToArray(); + var missingKeys = _allKeys.Skip(1000).Take(Count).ToArray(); + var seed = new Random(42); - var missingKeys = allKeys.Skip(1000).Take(Count).ToArray(); - var randomPresentKeys = keys.Take(Count).OrderBy(_ => seed.Next()).ToArray(); + var randomPresentKeys = presentKeys.OrderBy(_ => seed.Next()).ToArray(); - foreach (var key in keys.Take(Count)) + foreach (var key in presentKeys) map.AddOrGetValueRef(key, out _) = "a"; - map.AddOrGetValueRef(LookupKey, out _) = "!"; - var count = 0; var iters = Count / 2; for (var i = 0; i < iters; ++i) From 075473357d2fae41955b03d7ad05c889172807c1 Mon Sep 17 00:00:00 2001 From: dadhi Date: Wed, 9 Jul 2025 21:30:48 +0200 Subject: [PATCH 16/54] small things --- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 8c8fb3f0..47b978ad 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -430,7 +430,7 @@ public class Populate | ImmutableDict_Builder_Add | 1000 | 445,419.2 ns | 4,140.17 ns | 3,872.72 ns | 445,259.3 ns | 1.55 | 0.03 | 9.7656 | 1.9531 | - | 64208 B | | ImmutableDict_Add | 1000 | 815,930.7 ns | 15,795.06 ns | 17,556.16 ns | 810,772.7 ns | 2.83 | 0.06 | 105.4688 | 24.4141 | - | 662168 B | -## Against static TypeDictionary by @rogeralsing (need to substract the cost of enumerating the array when adding items to ImHashMap and PartitionedHashMap) +## Against static TypeDictionary by @rogeralsing (need to subtract the cost of enumerating the array when adding items to ImHashMap and PartitionedHashMap) BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores @@ -860,7 +860,7 @@ public class Populate | DictSlim_TryAdd | 1000 | 28.42 us | 0.585 us | 1.661 us | 1.00 | 0.00 | 51,595 | 266 | 338 | 9.1553 | 0.7935 | 56.45 KB | 1.00 | | FHashMap91_AddOrUpdate | 1000 | 32.17 us | 0.621 us | 1.151 us | 1.11 | 0.07 | 56,017 | 165 | 255 | 5.9814 | 0.5493 | 36.7 KB | 0.65 | -## Removing sparce and against the fibonacci hashing +## Removing sparse and against the fibonacci hashing | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Gen0 | Gen1 | Allocated | Alloc Ratio | |------------------------------ |------ |-------------:|-----------:|-----------:|-------------:|------:|--------:|----------------------:|---------------:|------------------------:|-------:|-------:|----------:|------------:| @@ -1896,7 +1896,7 @@ public class Lookup | ImmutableDict_TryGet | 1000 | 29.555 ns | 0.5926 ns | 0.5543 ns | 1.95 | 0.04 | - | - | - | - | - ## Against static TypeDictionary by @rogeralsing (need to substract the cost of enumerating the array when adding items to ImHashMap and PartitionedHashMap) + ## Against static TypeDictionary by @rogeralsing (need to subtract the cost of enumerating the array when adding items to ImHashMap and PartitionedHashMap) BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores @@ -2329,6 +2329,14 @@ .NET SDK 9.0.203 | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 277.6 ns | 3.61 ns | 5.52 ns | 0.95 | 0.04 | 2 | - | - | 0.00 | | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 292.1 ns | 5.84 ns | 12.19 ns | 1.00 | 0.06 | 2 | 0.1707 | 1072 B | 1.00 | + ## After inlining in the FecHashMap + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|--------:|---------:|---------:|------:|--------:|-----:|-------:|----------:|------------:| + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 111.1 ns | 2.25 ns | 5.90 ns | 108.8 ns | 0.38 | 0.03 | 1 | - | - | 0.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 192.4 ns | 1.46 ns | 1.22 ns | 192.5 ns | 0.67 | 0.03 | 2 | 0.1147 | 720 B | 0.67 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 289.3 ns | 5.78 ns | 11.28 ns | 290.9 ns | 1.00 | 0.05 | 3 | 0.1707 | 1072 B | 1.00 | + */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] // [Params(10, 100, 1000)] From afe0237a768a485e536f488a31a4f7329672d900 Mon Sep 17 00:00:00 2001 From: dadhi Date: Wed, 9 Jul 2025 21:41:20 +0200 Subject: [PATCH 17/54] bms --- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 47b978ad..06ba6d2f 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -2331,16 +2331,20 @@ .NET SDK 9.0.203 ## After inlining in the FecHashMap - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Allocated | Alloc Ratio | - |--------------------------------------------------------- |------ |---------:|--------:|---------:|---------:|------:|--------:|-----:|-------:|----------:|------------:| - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 111.1 ns | 2.25 ns | 5.90 ns | 108.8 ns | 0.38 | 0.03 | 1 | - | - | 0.00 | - | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 192.4 ns | 1.46 ns | 1.22 ns | 192.5 ns | 0.67 | 0.03 | 2 | 0.1147 | 720 B | 0.67 | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 289.3 ns | 5.78 ns | 11.28 ns | 290.9 ns | 1.00 | 0.05 | 3 | 0.1707 | 1072 B | 1.00 | + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |-----------:|---------:|---------:|-----------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 110.8 ns | 2.26 ns | 4.90 ns | 110.1 ns | 0.38 | 0.02 | 1 | - | - | - | 0.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 194.1 ns | 1.80 ns | 1.50 ns | 194.2 ns | 0.67 | 0.03 | 2 | 0.1147 | - | 720 B | 0.67 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 288.7 ns | 5.82 ns | 14.04 ns | 280.1 ns | 1.00 | 0.07 | 3 | 0.1707 | - | 1072 B | 1.00 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,003.8 ns | 30.26 ns | 31.08 ns | 1,995.1 ns | 1.00 | 0.02 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,306.8 ns | 18.67 ns | 15.59 ns | 2,307.0 ns | 1.15 | 0.02 | 2 | 0.8507 | - | 5344 B | 0.71 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 3,473.7 ns | 15.97 ns | 13.34 ns | 3,470.3 ns | 1.73 | 0.03 | 3 | 0.7782 | - | 4904 B | 0.65 | */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] // [Params(10, 100, 1000)] - [Params(10)] + [Params(10, 100)] public int Count; private Type[] _presentKeys; @@ -2795,7 +2799,7 @@ public int DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent() return count; } - // [Benchmark] + [Benchmark] public int SmallMap_PopulateThenLookup_HalfMissed_HalfPresent() { var map = new SmallMapTypeString(); From c1e40d83ecc0ef9851be2253e70ec11552247a13 Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 13 Jul 2025 23:13:56 +0200 Subject: [PATCH 18/54] add separate probes array to simplify things --- playground/Experiments/FEC_ImTools.cs | 289 ++++++++++-------- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 2 +- .../Playground/FecSmallMapTests.cs | 132 ++++++-- 3 files changed, 273 insertions(+), 150 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 1cc02427..c8469f9c 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -788,7 +788,6 @@ public static class RefEq public static readonly RefEq OfObject = default; } -// todo: @wip should we even need K here, maybe object implementation is enough? /// Uses the `object.GetHashCode` and `object.ReferenceEquals` public struct RefEq : IEq where K : class { @@ -856,12 +855,6 @@ public static class SmallMap internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) internal const byte MinHashesCapacityBitShift = 3; // 1 << 3 == 8 /// Upper hash bits spent on storing the probes, e.g. 5 bits mean 31 probes max. - public const byte ProbeBits = 5; - internal const byte NotShiftedProbeCountMask = (1 << ProbeBits) - 1; // 0b00000000000000000000000000011111 - // 27, so the upper 5 bits are used for the probe count - internal const byte ProbeCountShift = 32 - ProbeBits; - // ~0b11111000000000000000000000000000 -> 0b00000111111111111111111111111111 - internal const int HashAndIndexMask = ~(NotShiftedProbeCountMask << ProbeCountShift); /// Represent a keyed entry stored in the SmallMap. /// Its implementation struct may include the additional Value for the Map or just the Key for the Set. @@ -1125,6 +1118,7 @@ public struct SmallMapCapacity bits public int CapacityBitShift => _capacityBitShift; + /// Mask for the index part of the packedHashAndIndexes entry + public int IndexMask => (1 << _capacityBitShift) - 1; + + /// Access to the probes + public byte[] Probes => _probes; + /// Access to the hashes and indexes public int[] PackedHashesAndIndexes => _packedHashesAndIndexes; @@ -1150,11 +1150,10 @@ public struct SmallMapCapacity calculates as `1 leftShift capacityBitShift` public SmallMap(byte capacityBitShift) { - // Keep the capacity at least 8 for SIMD Vector256, etc., etc, if you need less space use Stack for that + // Keep the capacity at least 8 for SIMD Vector256, if you need less space use Stack for that _capacityBitShift = capacityBitShift < MinHashesCapacityBitShift ? MinHashesCapacityBitShift : capacityBitShift; - // The overflow tail to the hashes is the size of log2N where N==capacityBitShift, - // it is probably fine to have the check for the overflow of capacity because it will be mis-predicted only once at the end of loop (it even rarely for the lookup) + _probes = new byte[1 << capacityBitShift]; _packedHashesAndIndexes = new int[1 << capacityBitShift]; _entries.Init(capacityBitShift); } @@ -1175,96 +1174,123 @@ public ref TEntry GetSurePresentEntryRef(int index) [UnscopedRef] private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) { - // if the free space is less than 1/8 of capacity (12.5%) then Resize var indexMask = (1 << _capacityBitShift) - 1; - // todo: @perf @wip why are we checking for resize here if the possible outcome may be just get the value? + // If the free space is less than 1/8 of capacity (12.5%) then Resize + // Note: this check done here even before lookup and not before insert, because later we rely on the Lookup hashIndex to do a first insert if (indexMask - _count <= (indexMask >>> MinFreeCapacityShift)) indexMask = ResizeHashes(indexMask); - var hashMiddleMask = HashAndIndexMask & ~indexMask; - var hashMiddle = hash & hashMiddleMask; - var hashIndex = hash & indexMask; - #if NET7_0_OR_GREATER + ref var probes = ref MemoryMarshal.GetArrayDataReference(_probes); ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); #else + ref var probes = ref _probes; ref var hashesAndIndexes = ref _packedHashesAndIndexes; #endif - ref var h = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); - // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - var probe = 1; - while ((h >>> ProbeCountShift) >= probe) + var hashIndex = hash & indexMask; + var hashWithoutIndex = hash & ~indexMask; + ref var p = ref probes.GetSurePresentItemRef(hashIndex); + + // Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions + byte probe = 1; + while (p >= probe) { - // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> ProbeCountShift) == probe) & ((h & hashMiddleMask) == hashMiddle)) + // For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too + if (p == probe) { - ref var e = ref GetSurePresentEntryRef(h & indexMask); - if (found = default(TEq).Equals(e.Key, key)) - return ref e; + var h = hashesAndIndexes.GetSurePresentItemRef(hashIndex); + if ((h & ~indexMask) == hashWithoutIndex) + { + ref var e = ref GetSurePresentEntryRef(h & indexMask); + if (found = default(TEq).Equals(e.Key, key)) + return ref e; + } } - h = ref hashesAndIndexes.GetSurePresentItemRef(++hashIndex & indexMask); + hashIndex = (hashIndex + 1) & indexMask; // warp around the end of the array + p = ref probes.GetSurePresentItemRef(hashIndex); ++probe; } + found = false; - // 3. We did not find the hash and therefore the key, so insert the new entry - var newIndex = _count; - var hRobinHooded = h; - h = (probe << ProbeCountShift) | hashMiddle | newIndex; + // We did not find the hash and therefore the key, so insert the new hash + var pRobinHooded = p; + p = probe; + var hRobinHooded = hashesAndIndexes.GetSurePresentItemRef(hashIndex); + hashesAndIndexes.GetSurePresentItemRef(hashIndex) = hashWithoutIndex | _count; - // 4. If the robin hooded hash is empty then we stop - // 5. Otherwise we steal the slot with the smaller probes - probe = hRobinHooded >>> ProbeCountShift; - while (hRobinHooded != 0) + // If the robin hooded probe is empty then we stop + probe = pRobinHooded; + while (probe != 0) { - h = ref hashesAndIndexes.GetSurePresentItemRef(++hashIndex & indexMask); - if ((h >>> ProbeCountShift) < ++probe) + hashIndex = (hashIndex + 1) & indexMask; + p = ref probes.GetSurePresentItemRef(hashIndex); + + // Otherwise we steal the slot with the smaller probes + if (p < ++probe) { - var tmp = h; - h = (probe << ProbeCountShift) | (hRobinHooded & HashAndIndexMask); - hRobinHooded = tmp; - probe = hRobinHooded >>> ProbeCountShift; + var pValue = p; + p = probe; + probe = pValue; + + var hValue = hashesAndIndexes.GetSurePresentItemRef(hashIndex); + hashesAndIndexes.GetSurePresentItemRef(hashIndex) = hRobinHooded; + hRobinHooded = hValue; } } - ++_count; - return ref _entries.AddKeyAndGetEntryRef(key, newIndex - _stackEntries.Capacity); + return ref _entries.AddKeyAndGetEntryRef(key, _count++ - _stackEntries.Capacity); } - private void AddJustHashAndEntryIndexWithoutResizing(int indexMask, int hashIndex, int hashMiddleAndEntryIndex) + [MethodImpl((MethodImplOptions)256)] + private void PutHashAndIndexWithoutResizing(int indexMask, int hash, int index) { + var hashIndex = hash & indexMask; + var hashWithIndex = (hash & ~indexMask) | index; + #if NET7_0_OR_GREATER + ref var probes = ref MemoryMarshal.GetArrayDataReference(_probes); ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); #else + var probes = _probes; var hashesAndIndexes = _packedHashesAndIndexes; #endif - // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - var probe = 1; - ref var h = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); - while ((h >>> ProbeCountShift) >= probe) + ref var p = ref probes.GetSurePresentItemRef(hashIndex); + + // Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions + byte probe = 1; + while (p >= probe) { - h = ref hashesAndIndexes.GetSurePresentItemRef(++hashIndex & indexMask); + hashIndex = (hashIndex + 1) & indexMask; + p = ref probes.GetSurePresentItemRef(hashIndex); ++probe; } - // 3. We did not find the hash and therefore the key, so insert the new entry - var hRobinHooded = h; - h = (probe << ProbeCountShift) | hashMiddleAndEntryIndex; + // We did not find the hash and therefore the key, so insert the new hash + var pRobinHooded = p; + p = probe; + var hRobinHooded = hashesAndIndexes.GetSurePresentItemRef(hashIndex); + hashesAndIndexes.GetSurePresentItemRef(hashIndex) = hashWithIndex; - // 4. If the robin hooded hash is empty then we stop - // 5. Otherwise we steal the slot with the smaller probes - probe = hRobinHooded >>> ProbeCountShift; - while (hRobinHooded != 0) + // If the robin hooded probe is empty then we stop + probe = pRobinHooded; + while (probe != 0) { - h = ref hashesAndIndexes.GetSurePresentItemRef(++hashIndex & indexMask); - if ((h >>> ProbeCountShift) < ++probe) + hashIndex = (hashIndex + 1) & indexMask; + p = ref probes.GetSurePresentItemRef(hashIndex); + + // Otherwise we steal the slot with the smaller probes + if (p < ++probe) { - var tmp = h; - h = (probe << ProbeCountShift) | (hRobinHooded & HashAndIndexMask); - hRobinHooded = tmp; - probe = hRobinHooded >>> ProbeCountShift; + var pValue = p; + p = probe; + probe = pValue; + + var hValue = hashesAndIndexes.GetSurePresentItemRef(hashIndex); + hashesAndIndexes.GetSurePresentItemRef(hashIndex) = hRobinHooded; + hRobinHooded = hValue; } } } @@ -1285,8 +1311,9 @@ public ref TEntry AddOrGetEntryRef(K key, out bool found) if (found) return ref e; - // Add the new entry to the stack if there is still space in stack found = false; + + // Add the new entry to the stack if there is still space in the stack if (_count < _stackEntries.Capacity) { var newIndex = _count; @@ -1297,10 +1324,10 @@ public ref TEntry AddOrGetEntryRef(K key, out bool found) return ref newStackEntry; } - return ref MigrateToHeapAndAddAEntry(key, hash); + return ref MigrateToHeapAndAddEntry(key, hash); } - private ref TEntry MigrateToHeapAndAddAEntry(K key, int hash) + private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) { // Now all capacity of the stack is used. // To avoid double work always going linearly through the Stack with the comparison, @@ -1311,22 +1338,21 @@ private ref TEntry MigrateToHeapAndAddAEntry(K key, int hash) // So the values on the stack are guarantied to be stable from the beginning of the map creation, // because they are not copied when the Entries need to Resize (depending on the TEntries implementation). - var newCapBitShift = _stackEntries.CapacityBitShift + 1; + var newCapBitShift = Math.Max(_stackEntries.CapacityBitShift + 1, _capacityBitShift); _capacityBitShift = (byte)newCapBitShift; + _probes = new byte[1 << newCapBitShift]; _packedHashesAndIndexes = new int[1 << newCapBitShift]; var indexMask = (1 << newCapBitShift) - 1; - var hashMiddleMask = HashAndIndexMask & ~indexMask; var stackCap = _stackEntries.Capacity; for (var i = 0; i < stackCap; ++i) { var h = _stackHashes.GetSurePresentItemRef(i); - AddJustHashAndEntryIndexWithoutResizing(indexMask, h & indexMask, (h & hashMiddleMask) | i); + PutHashAndIndexWithoutResizing(indexMask, h, i); } - // todo: @perf calculates the hash code twice, first time in the TryGetEntryRef and then here - AddJustHashAndEntryIndexWithoutResizing(indexMask, hash & indexMask, (hash & hashMiddleMask) | stackCap); + PutHashAndIndexWithoutResizing(indexMask, hash, stackCap); ++_count; _entries.Init(stackCap); // Give the heap entries the same initial capacity as Stack, effectively doubling the capacity @@ -1370,6 +1396,7 @@ Probe 1A 2B Probe: 2C 1A 2B 3D */ [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key, int hash) { // if the free space is less than 1/8 of capacity (12.5%) then Resize @@ -1377,41 +1404,7 @@ private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key, int hash) if (indexMask - _count <= (indexMask >>> MinFreeCapacityShift)) indexMask = ResizeHashes(indexMask); - var hashIndex = hash & indexMask; - -#if NET7_0_OR_GREATER - ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); -#else - var hashesAndIndexes = _packedHashesAndIndexes; -#endif - ref var h = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); - - // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - var probe = 1; - while ((h >>> ProbeCountShift) >= probe) - { - h = ref hashesAndIndexes.GetSurePresentItemRef(++hashIndex & indexMask); - ++probe; - } - - // 3. We did not find the hash and therefore the key, so insert the new entry - var hRobinHooded = h; - h = (probe << ProbeCountShift) | (hash & HashAndIndexMask & ~indexMask) | _count; - - // 4. If the robin hooded hash is empty then we stop - // 5. Otherwise we steal the slot with the smaller probes - probe = hRobinHooded >>> ProbeCountShift; - while (hRobinHooded != 0) - { - h = ref hashesAndIndexes.GetSurePresentItemRef(++hashIndex & indexMask); - if ((h >>> ProbeCountShift) < ++probe) - { - var tmp = h; - h = (probe << ProbeCountShift) | (hRobinHooded & HashAndIndexMask); - hRobinHooded = tmp; - probe = hRobinHooded >>> ProbeCountShift; - } - } + PutHashAndIndexWithoutResizing(indexMask, hash, _count); return ref _entries.AddKeyAndGetEntryRef(key, (_count++) - _stackEntries.Capacity); } @@ -1438,7 +1431,7 @@ public ref TEntry AddSureAbsentDefaultEntryAndGetRef(K key) return ref newStackEntry; } - return ref MigrateToHeapAndAddAEntry(key, hash); + return ref MigrateToHeapAndAddEntry(key, hash); } /// Lookups for the stored key. If found true, otherwise false @@ -1463,31 +1456,37 @@ public bool ContainsKey(K key) internal ref TEntry TryGetRefInEntries(K key, int hash, out bool found) { var indexMask = (1 << _capacityBitShift) - 1; - var hashMiddleMask = HashAndIndexMask & ~indexMask; - var hashMiddle = hash & hashMiddleMask; var hashIndex = hash & indexMask; + var hashWithoutIndex = hash & ~indexMask; #if NET7_0_OR_GREATER + ref var probes = ref MemoryMarshal.GetArrayDataReference(_probes); ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); #else + var probes = _probes; var hashesAndIndexes = _packedHashesAndIndexes; #endif - var h = hashesAndIndexes.GetSurePresentItem(hashIndex); + var p = probes.GetSurePresentItem(hashIndex); // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var probe = 1; - while ((h >>> ProbeCountShift) >= probe) + while (p >= probe) { // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (((h >>> ProbeCountShift) == probe) & ((h & hashMiddleMask) == hashMiddle)) + if (probe == p) { - ref var e = ref GetSurePresentEntryRef(h & indexMask); - if (found = default(TEq).Equals(e.Key, key)) - return ref e; + var h = hashesAndIndexes.GetSurePresentItem(hashIndex); + if ((h & ~indexMask) == hashWithoutIndex) + { + ref var e = ref GetSurePresentEntryRef(h & indexMask); + if (found = default(TEq).Equals(e.Key, key)) + return ref e; + } } - h = hashesAndIndexes.GetSurePresentItem(++hashIndex & indexMask); + hashIndex = (hashIndex + 1) & indexMask; + p = probes.GetSurePresentItem(hashIndex); ++probe; } @@ -1510,49 +1509,75 @@ public ref TEntry TryGetEntryRef(K key, out bool found) internal int ResizeHashes(int indexMask) { var oldCapacity = indexMask + 1; - var newHashAndIndexMask = HashAndIndexMask & ~oldCapacity; + var eraseNextOldHashBit = (int)~(uint)oldCapacity; // converting to uint forst otherwise ~(2 compiment) for signed will do -(n+1), e.g. ~32==-33 + + var newCapacity = oldCapacity << 1; var newIndexMask = (indexMask << 1) | 1; - var newHashesAndIndexes = new int[oldCapacity << 1]; + var newProbesArr = new byte[newCapacity]; + var newHashesAndIndexes = new int[newCapacity]; #if NET7_0_OR_GREATER + ref var newProbes = ref MemoryMarshal.GetArrayDataReference(newProbesArr); ref var newHashes = ref MemoryMarshal.GetArrayDataReference(newHashesAndIndexes); + + ref var oldProbes = ref MemoryMarshal.GetArrayDataReference(_probes); + var oldProbe = oldProbes; ref var oldHashes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); - var oldHash = oldHashes; #else + var newProbes = newProbesArr; var newHashes = newHashesAndIndexes; + var oldProbes = _probes; + var oldProbe = oldProbes; var oldHashes = _packedHashesAndIndexes; - var oldHash = oldHashes[0]; #endif - // Overflow segment is wrapped-around hashes and! the hashes at the beginning robin hooded by the wrapped-around hashes + // Skip overflow segment of wrapped-around probes greater than 1 to keep the Robin Hood invariant. + // We know for sure that the entry with probe == 1 exists if the map is not fully occupied (which is by design when the Resize is starting). + // The probe == 1 would be at the right edge of the empty space. + // probes example [3, 4, 2, 0, 0, 1, 2, 2] + // i: [0, 1, 2, 3 ] + // skip stops on 3, so the oldCapWithOverflowSegment is 3 + 8 == 11, this corresponds to the all old probes to scan with wrap-around var i = 0; - while ((oldHash >>> ProbeCountShift) > 1) - oldHash = oldHashes.GetSurePresentItem(++i); + while (oldProbe > 1) // todo: @perf a good candidate for SIMD, SWAR, because a zero or one may take multple conflicts to scan until found + oldProbe = oldProbes.GetSurePresentItem(++i); var oldCapacityWithOverflowSegment = i + oldCapacity; while (true) { - if (oldHash != 0) + if (oldProbe != 0) // for the non-empty probe and therefore hash { - // get the new hash index from the old one with the next bit equal to the `oldCapacity` - var indexWithNextBit = (oldHash & oldCapacity) | (((i + 1) - (oldHash >>> ProbeCountShift)) & indexMask); + // Get the new IDEAL hash index from the old one with the next bit equal to the `oldCapacity`, normalized by the probe. + // For example: + // old probes [3, 4, 2, 0, 0, 1, 2, 2] + // old indeces [8, 9, 10 5, 6, 7, 8, 9, 10] 11, 12, 13, 14, 15 nextbit | (i - (oldProbe - 1)) & 7 + // new probes0 [ 1, ] (0 & 8) | (5 - (1 - 1)) & 7 == ideal 5, actual 5 + // new probes1 [ 1, 1, ] (1 & 8) | (6 - (2 - 1)) & 7 == ideal 13, actual 13 + // new probes2 [ 1, 1, 1, ] (0 & 8) | (7 - (2 - 1)) & 7 == ideal 6, actual 6 + // new probes3 [ 1, 1, 2, 1, ] (0 & 8) | (8 - (3 - 1)) & 7 == ideal 6, actual 7, ++probe + // new probes4 [ 1, 1, 2, 1, 1, ] (1 & 8) | (9 - (4 - 1)) & 7 == ideal 14, actual 14 + // new probes5 [ 1, 1, 2, 1, 1, 1, ] (1 & 8) | (10 - (2 - 1)) & 7 == ideal 9, actual 9 + var oldHash = oldHashes.GetSurePresentItem(i & indexMask); + var hashIndexWithNextBit = (oldHash & oldCapacity) | ((i - (oldProbe - 1)) & indexMask); // no need for robin-hooding because we already did it for the old hashes and now just filling the hashes into the new array which are already in order - var probe = 1; - ref var newHash = ref newHashes.GetSurePresentItemRef(indexWithNextBit); - while (newHash != 0) + byte probe = 1; + ref var p = ref newProbes.GetSurePresentItemRef(hashIndexWithNextBit); + while (p != 0) { - newHash = ref newHashes.GetSurePresentItemRef(++indexWithNextBit & newIndexMask); + hashIndexWithNextBit = (hashIndexWithNextBit + 1) & newIndexMask; + p = ref newProbes.GetSurePresentItemRef(hashIndexWithNextBit); ++probe; } - newHash = (probe << ProbeCountShift) | (oldHash & newHashAndIndexMask); + p = probe; + newHashes.GetSurePresentItemRef(hashIndexWithNextBit) = oldHash & eraseNextOldHashBit; } if (++i >= oldCapacityWithOverflowSegment) break; - oldHash = oldHashes.GetSurePresentItem(i & indexMask); + oldProbe = oldProbes.GetSurePresentItem(i & indexMask); } ++_capacityBitShift; + _probes = newProbesArr; _packedHashesAndIndexes = newHashesAndIndexes; return newIndexMask; } @@ -1563,6 +1588,7 @@ public struct SmallMap4() where TEq : struct, IEq { /// Map with 4 elements on stack and entries baked by the single array public SmallMap, TEq, Size4, Stack4, Stack4>, SmallMap.SingleArrayEntries, TEq>> Map; + public SmallMap4(byte capacityBitShift) : this() => Map = new(capacityBitShift); } /// Holds the Map with 8 items on stack. Minimizes the number of type arguments required to be specified @@ -1570,6 +1596,7 @@ public struct SmallMap8() where TEq : struct, IEq { /// Map with 8 elements on stack and entries baked by the single array public SmallMap, TEq, Size8, Stack8, Stack8>, SmallMap.SingleArrayEntries, TEq>> Map; + public SmallMap8(byte capacityBitShift) : this() => Map = new(capacityBitShift); } /// Holds the Map with 16 items on stack. Minimizes the number of type arguments required to be specified @@ -1577,6 +1604,8 @@ public struct SmallMap16() where TEq : struct, IEq { /// Map with 16 elements on stack and entries baked by the single array public SmallMap, TEq, Size16, Stack16, Stack16>, SmallMap.SingleArrayEntries, TEq>> Map; + + public SmallMap16(byte capacityBitShift) : this() => Map = new(capacityBitShift); } /// Holds the Set with 4 items on stack. Minimizes the number of type arguments required to be specified diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 06ba6d2f..923b6100 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -2344,7 +2344,7 @@ .NET SDK 9.0.203 */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] // [Params(10, 100, 1000)] - [Params(10, 100)] + [Params(100)] public int Count; private Type[] _presentKeys; diff --git a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs index 36c93586..5b2df1b3 100644 --- a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs +++ b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs @@ -12,15 +12,92 @@ namespace FastExpressionCompiler.ImTools.UnitTests; [TestFixture] public class FecSmallMapTests { - // internal static void Verify(SmallMap16 map, IEnumerable expectedKeys) + internal static void Verify(SmallMap16 map, IEnumerable expectedKeys) + where TEq : struct, IEq + { + VerifyHashesAndKeysEq(map); + + // map.VerifyProbesAreFitRobinHood(Assert.Fail); + // map.VerifyNoDuplicateKeys(key => Assert.Fail($"Duplicate key: {key}")); + + if (expectedKeys != null) + VerifyContainAllKeys(map, expectedKeys); + } + + /// Verifies that the hashes correspond to the keys stored in the entries. May be called from the tests. + internal static void VerifyHashesAndKeysEq(SmallMap16 map) + where TEq : struct, IEq + { + var probes = map.Map.Probes; + var hashes = map.Map.PackedHashesAndIndexes; + var indexMask = map.Map.IndexMask; + for (var i = 0; i < probes.Length; i++) + { + if (probes[i] != 0) + { + var h = hashes[i]; + var hashPart = h & ~indexMask; + var index = h & indexMask; + var key = map.Map.GetSurePresentEntryRef(index).Key; + var hashFromKey = default(TEq).GetHashCode(key); + var hashPartFromKey = hashFromKey & ~indexMask; + Assert.AreEqual(hashPart, hashPartFromKey, + $"At index {i}: hashPart:{hashPart} != key-hashPart:{hashPartFromKey} for key={key}"); + } + } + } + + // /// Verifies that there is no duplicate keys stored in hashes -> entries. May be called from the tests. + // public static void VerifyNoDuplicateKeys(this HSmallMap map) // where TEq : struct, IEq + // where TEntries : struct, IEntries // { - // map.VerifyHashesAndKeysEq(Assert.True); - // map.VerifyProbesAreFitRobinHood(Assert.Fail); - // map.VerifyNoDuplicateKeys(key => Assert.Fail($"Duplicate key: {key}")); - // if (expectedKeys != null) - // map.VerifyContainAllKeys(expectedKeys, (contains, key) => Assert.True(contains, $"Key not found:`{key}`")); + // // Verify the indexes do no contains duplicate keys + // var uniq = new Dictionary(map.Count); + // var hashes = map.PackedHashesAndIndexes; + // var capacity = map.Capacity; + // var indexMask = capacity - 1; + // for (var i = 0; i < hashes.Length - 8; i++) + // { + // var h = hashes[i]; + // if (h == 0) + // continue; + // var key = map.Entries.GetSurePresentEntryRef(h & indexMask).Key; + // if (!uniq.ContainsKey(key)) + // uniq.Add(key, 1); + // else + // Assert.Fail($"Duplicate key: {key}"); + // } // } + + // /// Verifies that the probes are consistently increasing + // public static void VerifyProbesAreFitRobinHood(this HSmallMap map) + // where TEq : struct, IEq + // where TEntries : struct, IEntries + // { + // var hashes = map.PackedHashesAndIndexes; + // var capacity = map.Capacity; + // var indexMask = capacity - 1; + // var prevProbes = -1; + // const int ProbeCountShift = 32 - MaxProbeBits; + // for (var i = 0; i < hashes.Length - 8; i++) + // { + // var h = hashes[i]; + // var probes = h >>> ProbeCountShift; + // if (prevProbes != -1 && probes - prevProbes > 1) + // Assert.Fail($"Probes are not consequent: {prevProbes}, {probes} for {i}: p{probes}, {h & indexMask} -> {map.Entries.GetSurePresentEntryRef(h & indexMask).Key}"); + // prevProbes = probes; + // } + // } + + /// Verifies that the map contains all passed keys. May be called from the tests. + internal static void VerifyContainAllKeys(SmallMap16 map, IEnumerable expectedKeys) + where TEq : struct, IEq + { + foreach (var key in expectedKeys) + Assert.True(map.Map.ContainsKey(key), $"Key not found:`{key}`"); + } + private static readonly Type[] _allKeys = typeof(List<>).Assembly.GetTypes().Take(2000).ToArray(); [Test] @@ -38,29 +115,42 @@ public void Zero_0_hash_test() } [Test] - public void Real_world_test_AddOrUpdate() + public void Benchmark_test_with_Add_and_Lookup_10_items() { - var types = _allKeys.Take(100).ToArray(); + const int Count = 10; + Debug.Assert(Count <= 1000, "Count should be less than or equal to 1000 for this test to work correctly."); - var map = new SmallMap16>(); + var m = new SmallMap16>(); + ref var map = ref m.Map; - foreach (var key in types) - map.Map.AddOrGetValueRef(key, out _) = "a"; + var presentKeys = _allKeys.Take(Count).ToArray(); + var missingKeys = _allKeys.Skip(1000).Take(Count).ToArray(); - map.Map.AddOrGetValueRef(typeof(Console), out _) = "!"; + var seed = new Random(42); + var randomPresentKeys = presentKeys.OrderBy(_ => seed.Next()).ToArray(); - var value = map.Map.TryGetValueRef(typeof(Console), out var found); - Assert.IsTrue(found); - Assert.AreEqual("!", value); + foreach (var key in presentKeys) + map.AddOrGetValueRef(key, out _) = "a"; - // Verify(map, types); - } + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) + { + var result = map.TryGetValueRef(randomPresentKeys[i], out var found); + if (found) + count += result.Length; + _ = map.TryGetValueRef(missingKeys[i], out found); + if (!found) + --count; + } + Assert.AreEqual(0, count); + } [Test] - public void Benchmark_test_with_Add_and_Lookup() + public void Benchmark_test_with_Add_and_Lookup_100_items() { - const int Count = 10; + const int Count = 100; Debug.Assert(Count <= 1000, "Count should be less than or equal to 1000 for this test to work correctly."); var m = new SmallMap16>(); @@ -75,6 +165,10 @@ public void Benchmark_test_with_Add_and_Lookup() foreach (var key in presentKeys) map.AddOrGetValueRef(key, out _) = "a"; + VerifyHashesAndKeysEq(m); + foreach (var key in presentKeys) + Assert.True(map.ContainsKey(key), $"Key not found:`{key}`"); + var count = 0; var iters = Count / 2; for (var i = 0; i < iters; ++i) From 7c9263b95e58b3046dfd9fac798c1589ad56ebbd Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 21 Jul 2025 16:36:37 +0200 Subject: [PATCH 19/54] added diagnostics and GoldenRatioRefEq --- playground/Experiments/FEC_ImTools.cs | 101 +++++++++++++++--- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 17 ++- .../Playground/FecSmallMapTests.cs | 5 + 3 files changed, 107 insertions(+), 16 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index c8469f9c..fcde1590 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -781,6 +781,13 @@ public struct IntEq : IEq public int GetHashCode(int key) => key; } +/// General tools for IEq and its implementations +public static class Eq +{ + /// Factor to multiple by, to achieve the Fibonacci hashing (2^32 / phi, where phi is the golden ratio ~1.61803). + public const uint GoldenRatioFactor32 = 2654435769; +} + /// Instances of the RefEq for the often used K public static class RefEq { @@ -788,7 +795,7 @@ public static class RefEq public static readonly RefEq OfObject = default; } -/// Uses the `object.GetHashCode` and `object.ReferenceEquals` +/// Uses the `RuntimeHelpers.GetHashCode` and `object.ReferenceEquals` public struct RefEq : IEq where K : class { /// @@ -804,6 +811,22 @@ public struct RefEq : IEq where K : class public int GetHashCode(K key) => RuntimeHelpers.GetHashCode(key); } +/// Uses the `RuntimeHelpers.GetHashCode` and hashes the result with Fibonacci hashing via * GoldenRatio +public struct GoldenRatioRefEq : IEq where K : class +{ + /// + [MethodImpl((MethodImplOptions)256)] + public K GetTombstone() => null; + + /// + [MethodImpl((MethodImplOptions)256)] + public bool Equals(K x, K y) => ReferenceEquals(x, y); + + /// + [MethodImpl((MethodImplOptions)256)] + public int GetHashCode(K key) => (int)(RuntimeHelpers.GetHashCode(key) * Eq.GoldenRatioFactor32); +} + /// Compares via `ReferenceEquals` and gets the hash faster via `RuntimeHelpers.GetHashCode` public struct RefEq : IEq<(A, B)> where A : class @@ -853,8 +876,12 @@ public interface Use { } public static class SmallMap { internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) - internal const byte MinHashesCapacityBitShift = 3; // 1 << 3 == 8 - /// Upper hash bits spent on storing the probes, e.g. 5 bits mean 31 probes max. + internal const byte MinHashesCapacityBitShift = 4; // 1 << 3 == 8 + + // todo: @wip @remove if not required + // #if NET7_0_OR_GREATER + // internal static Vector128 ByteVec16OfOne = Vector128.Create((byte)1); + // #endif /// Represent a keyed entry stored in the SmallMap. /// Its implementation struct may include the additional Value for the Map or just the Key for the Set. @@ -1109,6 +1136,11 @@ public struct SmallMap where TEntries : struct, IEntries { +#if DEBUG + public int ProbeCheckCountInRead = 0; + public int ProbeCheckCountInWrite = 0; +#endif + internal byte _capacityBitShift; internal int _count; @@ -1123,9 +1155,9 @@ public struct SmallMap>> MinFreeCapacityShift)) indexMask = ResizeHashes(indexMask); @@ -1211,6 +1243,9 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) hashIndex = (hashIndex + 1) & indexMask; // warp around the end of the array p = ref probes.GetSurePresentItemRef(hashIndex); ++probe; +#if DEBUG + ++ProbeCheckCountInWrite; +#endif } found = false; @@ -1238,6 +1273,9 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) var hValue = hashesAndIndexes.GetSurePresentItemRef(hashIndex); hashesAndIndexes.GetSurePresentItemRef(hashIndex) = hRobinHooded; hRobinHooded = hValue; +#if DEBUG + ++ProbeCheckCountInWrite; +#endif } } @@ -1266,6 +1304,9 @@ private void PutHashAndIndexWithoutResizing(int indexMask, int hash, int index) hashIndex = (hashIndex + 1) & indexMask; p = ref probes.GetSurePresentItemRef(hashIndex); ++probe; +#if DEBUG + ++ProbeCheckCountInWrite; +#endif } // We did not find the hash and therefore the key, so insert the new hash @@ -1291,6 +1332,9 @@ private void PutHashAndIndexWithoutResizing(int indexMask, int hash, int index) var hValue = hashesAndIndexes.GetSurePresentItemRef(hashIndex); hashesAndIndexes.GetSurePresentItemRef(hashIndex) = hRobinHooded; hRobinHooded = hValue; +#if DEBUG + ++ProbeCheckCountInWrite; +#endif } } } @@ -1367,30 +1411,25 @@ private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) Insertion step by step: 1. Initially the map is empty. Its capacity mask is 7: - Index: 0 1 2 3 4 5 6 7 Hash: [0] [0] [0] [0] [0] [0] [0] [0] 2. Insert the key A with the hash 13, which is 0b0011_0101. 13 & 7 Mask = 5, so the index is 5. - Index: 0 1 2 3 4 5 6 7 Hash: [0] [0] [0] [0] [0] [13] [0] [0] Probe: 1A 3. Insert the key B with the hash 5, which is 0b0000_1011. 5 & 7 Mask = 5, so the index is again 5. - Index: 0 1 2 3 4 5 6 7 Hash: [0] [0] [0] [0] [0] [13] [5] [0] Probe 1A 2B 4. Insert the key C with the hash 7, which is 0b0010_0101. 7 & 7 Mask = 7, so the index is 7. - Index: 0 1 2 3 4 5 6 7 Hash: [0] [0] [0] [0] [0] [13] [5] [7] Probe: 1A 2B 1C 5. Insert the key D with the hash 21, which is 0b0101_0101. 21 & 7 Mask = 5, so the index is again again 5. - Index: 0 1 2 3 4 5 6 7 Hash: [7] [0] [0] [0] [0] [13] [5] [21] Probe: 2C 1A 2B 3D @@ -1469,11 +1508,9 @@ internal ref TEntry TryGetRefInEntries(K key, int hash, out bool found) var p = probes.GetSurePresentItem(hashIndex); - // 1. Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions var probe = 1; while (p >= probe) { - // 2. For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too if (probe == p) { var h = hashesAndIndexes.GetSurePresentItem(hashIndex); @@ -1488,6 +1525,9 @@ internal ref TEntry TryGetRefInEntries(K key, int hash, out bool found) hashIndex = (hashIndex + 1) & indexMask; p = probes.GetSurePresentItem(hashIndex); ++probe; +#if DEBUG + ++ProbeCheckCountInRead; +#endif } found = false; @@ -1509,7 +1549,7 @@ public ref TEntry TryGetEntryRef(K key, out bool found) internal int ResizeHashes(int indexMask) { var oldCapacity = indexMask + 1; - var eraseNextOldHashBit = (int)~(uint)oldCapacity; // converting to uint forst otherwise ~(2 compiment) for signed will do -(n+1), e.g. ~32==-33 + var eraseNextOldHashBit = ~oldCapacity; var newCapacity = oldCapacity << 1; var newIndexMask = (indexMask << 1) | 1; @@ -1528,7 +1568,7 @@ internal int ResizeHashes(int indexMask) var newProbes = newProbesArr; var newHashes = newHashesAndIndexes; var oldProbes = _probes; - var oldProbe = oldProbes; + var oldProbe = oldProbes[0]; var oldHashes = _packedHashesAndIndexes; #endif // Skip overflow segment of wrapped-around probes greater than 1 to keep the Robin Hood invariant. @@ -1538,8 +1578,36 @@ internal int ResizeHashes(int indexMask) // i: [0, 1, 2, 3 ] // skip stops on 3, so the oldCapWithOverflowSegment is 3 + 8 == 11, this corresponds to the all old probes to scan with wrap-around var i = 0; - while (oldProbe > 1) // todo: @perf a good candidate for SIMD, SWAR, because a zero or one may take multple conflicts to scan until found + + // #if NET7_0_OR_GREATER + // if (Vector128.IsHardwareAccelerated) + // { + // Debug.Assert((oldCapacity & 15) == 0, "Capacity should be multiple of 16 for SIMD Vector128"); + // while (i < oldCapacity) + // { + // var probes = Vector128.LoadUnsafe(ref Unsafe.Add(ref oldProbes, i)); + // var lessOrEqThenOne = Vector128.LessThanOrEqual(probes, ByteVec16OfOne); + // var matchMask = lessOrEqThenOne.ExtractMostSignificantBits(); + // if (matchMask != 0) + // { + // i += System.Numerics.BitOperations.TrailingZeroCount(matchMask); + // break; + // } + // i += 16; + // } + // } + // else + // while (oldProbe > 1) + // oldProbe = oldProbes.GetSurePresentItem(++i); + // #else + while (oldProbe > 1) + { oldProbe = oldProbes.GetSurePresentItem(++i); +#if DEBUG + ++ProbeCheckCountInWrite; +#endif + } + // #endif var oldCapacityWithOverflowSegment = i + oldCapacity; while (true) @@ -1567,6 +1635,9 @@ internal int ResizeHashes(int indexMask) hashIndexWithNextBit = (hashIndexWithNextBit + 1) & newIndexMask; p = ref newProbes.GetSurePresentItemRef(hashIndexWithNextBit); ++probe; +#if DEBUG + ++ProbeCheckCountInWrite; +#endif } p = probe; newHashes.GetSurePresentItemRef(hashIndexWithNextBit) = oldHash & eraseNextOldHashBit; diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 923b6100..f6c7ff11 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -2341,6 +2341,21 @@ .NET SDK 9.0.203 | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,306.8 ns | 18.67 ns | 15.59 ns | 2,307.0 ns | 1.15 | 0.02 | 2 | 0.8507 | - | 5344 B | 0.71 | | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 3,473.7 ns | 15.97 ns | 13.34 ns | 3,470.3 ns | 1.73 | 0.03 | 3 | 0.7782 | - | 4904 B | 0.65 | + ## Probes are separate from the packed hashes and indexes into their own array + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|----------:|----------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.118 us | 0.0421 us | 0.0679 us | 1.00 | 0.04 | 1 | 1.1902 | 0.0229 | 7.31 KB | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.346 us | 0.0404 us | 0.0378 us | 1.11 | 0.04 | 2 | 0.8507 | - | 5.22 KB | 0.71 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 3.294 us | 0.0648 us | 0.0970 us | 1.56 | 0.07 | 3 | 0.8278 | - | 5.08 KB | 0.69 | + + ## GoldenRatio Hash adjustment + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.150 us | 0.0428 us | 0.0783 us | 2.115 us | 1.00 | 0.05 | 1 | 1.1902 | 0.0229 | 7.31 KB | 1.00 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 3.092 us | 0.0608 us | 0.0871 us | 3.068 us | 1.44 | 0.06 | 2 | 0.8278 | - | 5.08 KB | 0.69 | + */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] // [Params(10, 100, 1000)] @@ -2799,7 +2814,7 @@ public int DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent() return count; } - [Benchmark] + // [Benchmark] public int SmallMap_PopulateThenLookup_HalfMissed_HalfPresent() { var map = new SmallMapTypeString(); diff --git a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs index 5b2df1b3..7628147b 100644 --- a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs +++ b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs @@ -181,6 +181,11 @@ public void Benchmark_test_with_Add_and_Lookup_100_items() --count; } +#if DEBUG + Assert.Greater(m.Map.ProbeCheckCountInWrite, 100); // 396 no GoldenRatio, 284 with GoldenRatio + Assert.Greater(m.Map.ProbeCheckCountInRead, 100); // 429 no GoldenRatio, 335 with GoldenRatio +#endif + Assert.AreEqual(0, count); } From 9771dd3d50caca6f4fc76c78225089c397c30f29 Mon Sep 17 00:00:00 2001 From: dadhi Date: Tue, 29 Jul 2025 13:27:30 +0200 Subject: [PATCH 20/54] @wip probes go to their own array + padding + wrapping --- playground/Experiments/FEC_ImTools.cs | 685 ++++++++++++------ .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 7 + src/ImTools/ImTools.cs | 7 +- .../Playground/FecSmallMapTests.cs | 110 +-- 4 files changed, 479 insertions(+), 330 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index fcde1590..ed72f606 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -25,6 +25,11 @@ THE SOFTWARE. // ReSharper disable once InconsistentNaming #nullable disable +#define MAP_LOOKUP_SPLIT_LOOP // todo: @wip @perf + +// #if DEBUG +// #define VERIFY_MAP +// #endif #if !NETSTANDARD2_0_OR_GREATER && !NET472 #define SUPPORTS_UNSAFE @@ -868,15 +873,36 @@ public int GetHashCode((A, B, C) key) => Hasher.Combine(RuntimeHelpers.GetHashCode(key.Item1), Hasher.Combine(RuntimeHelpers.GetHashCode(key.Item2), RuntimeHelpers.GetHashCode(key.Item3))); } -/// Add the Use parameter to `T Method{T}(..., Use{T} _)` to enable type inference for T, -/// by calling it as `var t = Method(..., default(Use{T}))` -public interface Use { } +/// Add the Use parameter to `Method{T}(..., Use{T} _)` to enable type inference for T, +/// by calling it as `var t = Method(..., Use{T}.It)` +public readonly struct Use +{ + public static readonly Use It = new(); + public readonly A Aval; +} + +/// Add the Use parameter in type inference for A, B +public readonly struct Use +{ + public static readonly Use It = new(); + public readonly A Aval; + public readonly B Bval; +} + +/// Add the Use parameter in type inference for A, B, C +public readonly struct Use +{ + public static readonly Use It = new(); + public readonly A Aval; + public readonly B Bval; + public readonly C Cval; +} /// Configuration and the tools for the SmallMap and friends public static class SmallMap { internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) - internal const byte MinHashesCapacityBitShift = 4; // 1 << 3 == 8 + internal const byte MinHashesCapacityBitShift = 4; // 1 << 4 == 16 // todo: @wip @remove if not required // #if NET7_0_OR_GREATER @@ -976,21 +1002,19 @@ public ref TEntry AddKeyAndGetEntryRef(K key, int index) /// Lookup for the K in the TStackEntries, first by calculating it hash with TEq and searching the hash in the TStackHashes [MethodImpl((MethodImplOptions)256)] - public static ref TEntry TryGetEntryRef( - this ref TStackEntries entries, ref TStackHashes hashes, int count, K key, int hash, out bool found, - TEq eq = default, TCap cap = default, Use _ = default) + public static int TryGetEntryIndex( + this ref TStackEntries entries, ref TStackHashes hashes, int count, K key, int hash, + Use it = default) where TEntry : struct, IEntry where TEq : struct, IEq where TStackHashes : struct, IStack where TStackEntries : struct, IStack where TCap : struct, ISize2Plus { + var cap = it.Bval; Debug.Assert(count <= cap.Size, $"SmallMap.TryGetEntryRef: count {count} should be <= stack capacity {cap.Size}"); if (count == 0) - { - found = false; - return ref RefTools.GetNullRef(); - } + return -1; #if NET8_0_OR_GREATER if (cap.Size >= 8 & Vector256.IsHardwareAccelerated) @@ -1002,42 +1026,36 @@ public static ref TEntry TryGetEntryRef & 0b0001_1000 & 0b0001_0111 -> 0b0001_0000 matches &= matches - 1; + ++i; } - - i += Vector256.Count; if (i >= count) break; } - - found = false; - return ref RefTools.GetNullRef(); + return -1; } #endif - for (var i = 0; i < count; ++i) { var h = hashes.GetSurePresentItemRef(i); if (h == hash) { ref var entry = ref entries.GetSurePresentItemRef(i); - if (found = eq.Equals(entry.Key, key)) - return ref entry; + if (default(TEq).Equals(entry.Key, key)) + return i; } } - - found = false; - return ref RefTools.GetNullRef(); + return -1; } /// Gets the ref to the existing entry.Value by the provided key (found == true), @@ -1112,6 +1130,130 @@ public static V GetValueOrDefaultMethods to verify the integrity and invariants of SmallMap +public static class SmallMapDiagnostics +{ + /// Verifies the integrity and invariants of the SmallMap + public static void Verify(ref this TMap map, + Action assertCond, IEnumerable expectedKeys = null, Use _ = default) + where TMap : struct, IMap, IMapStruct + where TEq : struct, IEq + { + map.VerifyHashesAndKeysEq(assertCond, _); + + map.VerifyProbesRobinHoodInvariants(assertCond); + + map.VerifyKeyHasMetaInfo(assertCond, _); + + // map.VerifyNoDuplicateKeys(key => Assert.Fail($"Duplicate key: {key}")); + + if (expectedKeys != null) + map.VerifyContainAllKeys(expectedKeys, assertCond, _); + } + + /// Verifies that the hashes correspond to the keys stored in the entries. May be called from the tests. + public static void VerifyHashesAndKeysEq(ref this TMap map, Action assertCond, + Use _ = default) + where TMap : struct, IMap, IMapStruct + where TEq : struct, IEq + { + var probes = map.Probes; + var hashes = map.PackedHashesAndIndexes; + var indexMask = map.Capacity - 1; + for (var i = 0; i < probes.Length; i++) + { + if (probes[i] != 0) + { + var h = hashes[i]; + var hashPart = h & ~indexMask; + var index = h & indexMask; + var key = map.GetSurePresentKey(index); + var hashFromKey = default(TEq).GetHashCode(key); + var hashPartFromKey = hashFromKey & ~indexMask; + assertCond(hashPart == hashPartFromKey, $"Hash mismatch for key:{key}, expected:{hashPartFromKey}, actual:{hashPart}"); + } + } + } + + // /// Verifies that there is no duplicate keys stored in hashes -> entries. May be called from the tests. + // public static void VerifyNoDuplicateKeys(this HSmallMap map) + // where TEq : struct, IEq + // where TEntries : struct, IEntries + // { + // // Verify the indexes do no contains duplicate keys + // var uniq = new Dictionary(map.Count); + // var hashes = map.PackedHashesAndIndexes; + // var capacity = map.Capacity; + // var indexMask = capacity - 1; + // for (var i = 0; i < hashes.Length - 8; i++) + // { + // var h = hashes[i]; + // if (h == 0) + // continue; + // var key = map.Entries.GetSurePresentEntryRef(h & indexMask).Key; + // if (!uniq.ContainsKey(key)) + // uniq.Add(key, 1); + // else + // Assert.Fail($"Duplicate key: {key}"); + // } + // } + + /// Verifies that the probes are consistently increasing + public static void VerifyProbesRobinHoodInvariants(this TMap map, Action assertCond) + where TMap : struct, IMapStruct + { + var prevProbe = 0; + var probes = map.Probes; + for (var i = 0; i < probes.Length; i++) + { + var probe = probes[i]; + var invariant = prevProbe >= probe || prevProbe + 1 == probe; + assertCond(invariant, $"Probe invariant failed: prevProbe:{prevProbe}, probe:{probe}"); + prevProbe = probe; + } + } + + /// Verifies that each key has a corresponding hash entry which is correctly references this key + public static void VerifyKeyHasMetaInfo(ref this TMap map, + Action assertCond, Use _ = default) + where TMap : struct, IMap + where TEq : struct, IEq + { + for (var i = 0; i < map.Count; ++i) + { + var key = map.GetSurePresentKey(i); + var foundIndex = map.TryGetIndex(key); + assertCond(foundIndex != -1, $"Not found foundIndex:-1 for i:{i}, key:{key}"); + assertCond(foundIndex == i, $"Does not correspond foundIndex:{foundIndex}, i:{i}, key:{key}"); + } + } + + /// Verifies that the map contains all passed keys. May be called from the tests. + public static void VerifyContainAllKeys(ref this TMap map, + IEnumerable expectedKeys, Action assertContains, Use _ = default) + where TMap : struct, IMap + where TEq : struct, IEq + { + foreach (var key in expectedKeys) + assertContains(map.ContainsKey(key), $"Key not found: {key}"); + } +} + +public interface IMap where TEq : struct, IEq +{ + int Count { get; } + int TryGetIndex(K key); + bool ContainsKey(K key); + K GetSurePresentKey(int index); +} + +public interface IMapStruct +{ + int Capacity { get; } + byte[] Probes { get; } + int[] PackedHashesAndIndexes { get; } +} + // todo: @improve ? how/where to add SIMD to improve CPU utilization but not losing perf for smaller sizes /// /// Fast and less-allocating hash map without thread safety nets. Please measure it in your own use case before use. @@ -1128,7 +1270,7 @@ public static V GetValueOrDefault [DebuggerDisplay("{Count} entries total")] -public struct SmallMap +public struct SmallMap : IMap, IMapStruct where TEntry : struct, IEntry where TEq : struct, IEq where TStackCap : struct, ISize2Plus @@ -1137,13 +1279,42 @@ public struct SmallMap { #if DEBUG - public int ProbeCheckCountInRead = 0; - public int ProbeCheckCountInWrite = 0; + // Diagnostic counters to measure the performance of the map operations + + public int TotalProbeCheckCountInRead = 0; + public int PutHashAndIndexWithoutResizing_Count = 0; + public int PutHashAndIndexWithoutResizing_InitialProbeCheckCount = 0; + public int PutHashAndIndexWithoutResizing_AverageProbeCheckCount => + PutHashAndIndexWithoutResizing_Count == 0 ? 0 : PutHashAndIndexWithoutResizing_InitialProbeCheckCount / PutHashAndIndexWithoutResizing_Count; + public int PutHashAndIndexWithoutResizing_RobinHoodCount = 0; + public int PutHashAndIndexWithoutResizing_RobinHoodProbeCheckCount = 0; + + public int TryGetRefInEntries_Count = 0; + public int TryGetRefInEntries_InitialProbeCheckCount = 0; + public int TryGetRefInEntries_AverageProbeCheckCount => + TryGetRefInEntries_Count == 0 ? 0 : TryGetRefInEntries_InitialProbeCheckCount / TryGetRefInEntries_Count; + public int TryGetRefInEntries_EqualProbeCheckCount = 0; + + public int AddOrGetRefInEntries_Count = 0; + public int AddOrGetRefInEntries_InitialProbeCheckCount = 0; + public int AddOrGetRefInEntries_InitialProbeCheckCountAverage => + AddOrGetRefInEntries_Count == 0 ? 0 : AddOrGetRefInEntries_InitialProbeCheckCount / AddOrGetRefInEntries_Count; + public int FindEqualProbeAndHashCheckCount = 0; + public int AddOrGetRefInEntries_RobinHoodCount = 0; + public int AddOrGetRefInEntries_RobinHoodProbeCheckCount = 0; + + public int ResizeProbesAndHashes_Count = 0; + public int ResizeProbesAndHashes_AverageProbeCheckCount = 0; + public int ResizeProbesAndHashes_AverageEmptyProbeCount = 0; + public int ResizeProbesAndHashes_LastWrapAroundEmptyProbesSpanCount = 0; #endif internal byte _capacityBitShift; + // true if the last item in probes and hashes being occupied + internal bool _isCapacityPaddingFilled; internal int _count; + // todo: @wip update the description, because now we store probes separately from the hashes // The _packedHashesAndIndexes elements are of `Int32` with the bits split as following: // 00010|000...110|01101 // | | |- The index into the _entries structure, 0-based. The index bit count (indexMask) is the hashes capacity - 1. @@ -1164,6 +1335,9 @@ public struct SmallMapCapacity bits public int CapacityBitShift => _capacityBitShift; + /// Capacity + public int Capacity => 1 << _capacityBitShift; + /// Mask for the index part of the packedHashAndIndexes entry public int IndexMask => (1 << _capacityBitShift) - 1; @@ -1184,12 +1358,32 @@ public SmallMap(byte capacityBitShift) { // Keep the capacity at least 8 for SIMD Vector256, if you need less space use Stack for that _capacityBitShift = capacityBitShift < MinHashesCapacityBitShift ? MinHashesCapacityBitShift : capacityBitShift; + var capacity = 1 << _capacityBitShift; + + // Extra padding is required to avoid index wrap around for probes and hashes, + // and ensure that SIMD can read at most 16 bytes starting from the last index. + // Using either 16 or 1/8 of the capacity, whichever is bigger. + var tailPadding = 1 << (_capacityBitShift - 3); + var capacityWithPadding = capacity + (tailPadding > 16 ? tailPadding : 16); - _probes = new byte[1 << capacityBitShift]; - _packedHashesAndIndexes = new int[1 << capacityBitShift]; - _entries.Init(capacityBitShift); + _probes = new byte[capacityWithPadding]; + _packedHashesAndIndexes = new int[capacityWithPadding]; + + _entries.Init(capacity); + } + + // todo: @perf try using StableGrowingEntries (or Xar) with O(1) random access. And make an default storage for the entries. + [MethodImpl((MethodImplOptions)256)] + public K GetSurePresentKey(int index) + { + Debug.Assert(index >= 0); + Debug.Assert(index < _count); + return index >= _stackEntries.Capacity + ? _entries.GetSurePresentEntryRef(index - _stackEntries.Capacity).Key + : _stackEntries.GetSurePresentItemRef(index).Key; } + // todo: @perf try using StableGrowingEntries (or Xar) with O(1) random access. And make an default storage for the entries. ///Get the value ref by the entry index. Also the index corresponds to entry adding order. /// Important: it does not check the index bounds, so you need to check that the index is from 0 to map.Count-1 [UnscopedRef] @@ -1203,15 +1397,77 @@ public ref TEntry GetSurePresentEntryRef(int index) return ref _stackEntries.GetSurePresentItemRef(index); } + /// The actual Lookup implementation in a single place + [MethodImpl((MethodImplOptions)256)] + private int TryGetEntryAndHashIndex(K key, int hash, int indexMask, + ref int hashIndex, ref byte probe, +#if NET7_0_OR_GREATER + ref byte probes, ref int hashesAndIndexes +#else + ref byte[] probes, ref int[] hashesAndIndexes +#endif + ) + { + // Skip over the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions +#if MAP_LOOKUP_SPLIT_LOOP + ref var pRef = ref probes.GetSurePresentItemRef(hashIndex); + while (pRef > probe) + { + pRef = ref probes.GetSurePresentItemRef(++hashIndex); + ++probe; +#if DEBUG + ++FindEqualProbeAndHashCheckCount; +#endif + } + + while (pRef == probe) + { + // Compare the hash middle part, then the indexed key + var h = hashesAndIndexes.GetSurePresentItemRef(hashIndex); + if ((h & ~indexMask) == (hash & ~indexMask)) + if (default(TEq).Equals(GetSurePresentKey(h & indexMask), key)) + return h & indexMask; + pRef = ref probes.GetSurePresentItemRef(++hashIndex); + ++probe; +#if DEBUG + ++FindEqualProbeAndHashCheckCount; +#endif + } +#else + ref var pRef = ref probes.GetSurePresentItemRef(hashIndex); + while (pRef >= probe) + { + // Compare the hash middle part, then the indexed key + if (pRef == probe) + { + var h = hashesAndIndexes.GetSurePresentItemRef(hashIndex); + if ((h & ~indexMask) == (hash & ~indexMask)) + if (default(TEq).Equals(GetSurePresentKey(h & indexMask), key)) + return h & indexMask; + } + pRef = ref probes.GetSurePresentItemRef(++hashIndex); + ++probe; +#if DEBUG + ++FindEqualProbeAndHashCheckCount; +#endif + } +#endif + return -1; // not found + } + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) { +#if DEBUG + ++AddOrGetRefInEntries_Count; +#endif var indexMask = (1 << _capacityBitShift) - 1; - // If the free space is less than 1/8 of capacity (12.5%) then Resize + // If the free space is less than ~1/8 of capacity (~12.5%) then Resize probes and hashes (but not the entries). // Note: this check done here even before lookup (but not in insert), because later we rely on the Lookup hashIndex to do a first insert - if (indexMask - _count <= (indexMask >>> MinFreeCapacityShift)) - indexMask = ResizeHashes(indexMask); + if (_isCapacityPaddingFilled || indexMask - _count <= (indexMask >>> MinFreeCapacityShift)) + indexMask = ResizeProbesAndHashes(indexMask); #if NET7_0_OR_GREATER ref var probes = ref MemoryMarshal.GetArrayDataReference(_probes); @@ -1222,69 +1478,57 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) #endif var hashIndex = hash & indexMask; - var hashWithoutIndex = hash & ~indexMask; - ref var p = ref probes.GetSurePresentItemRef(hashIndex); - - // Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions byte probe = 1; - while (p >= probe) - { - // For the equal probes check for equality the hash middle part, and update the entry if the keys are equal too - if (p == probe) - { - var h = hashesAndIndexes.GetSurePresentItemRef(hashIndex); - if ((h & ~indexMask) == hashWithoutIndex) - { - ref var e = ref GetSurePresentEntryRef(h & indexMask); - if (found = default(TEq).Equals(e.Key, key)) - return ref e; - } - } - hashIndex = (hashIndex + 1) & indexMask; // warp around the end of the array - p = ref probes.GetSurePresentItemRef(hashIndex); - ++probe; -#if DEBUG - ++ProbeCheckCountInWrite; -#endif - } + var entryIndex = TryGetEntryAndHashIndex(key, hash, indexMask, ref hashIndex, ref probe, ref probes, ref hashesAndIndexes); + if (found = entryIndex != -1) + return ref GetSurePresentEntryRef(entryIndex); - found = false; + // We did not find the probe and hash and therefore the key, so insert the new probe and hash + ref var pRef = ref probes.GetSurePresentItemRef(hashIndex); + var pRobinHooded = pRef; + pRef = probe; - // We did not find the hash and therefore the key, so insert the new hash - var pRobinHooded = p; - p = probe; - var hRobinHooded = hashesAndIndexes.GetSurePresentItemRef(hashIndex); - hashesAndIndexes.GetSurePresentItemRef(hashIndex) = hashWithoutIndex | _count; + ref var hRef = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); + var hRobinHooded = hRef; + hRef = (hash & ~indexMask) | _count; // If the robin hooded probe is empty then we stop probe = pRobinHooded; while (probe != 0) { - hashIndex = (hashIndex + 1) & indexMask; - p = ref probes.GetSurePresentItemRef(hashIndex); + pRef = ref probes.GetSurePresentItemRef(++hashIndex); // Otherwise we steal the slot with the smaller probes - if (p < ++probe) + if (pRef < ++probe) { - var pValue = p; - p = probe; + var pValue = pRef; + pRef = probe; probe = pValue; - var hValue = hashesAndIndexes.GetSurePresentItemRef(hashIndex); - hashesAndIndexes.GetSurePresentItemRef(hashIndex) = hRobinHooded; + hRef = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); + var hValue = hRef; + hRef = hRobinHooded; hRobinHooded = hValue; #if DEBUG - ++ProbeCheckCountInWrite; + ++AddOrGetRefInEntries_RobinHoodCount; #endif } +#if DEBUG + ++AddOrGetRefInEntries_RobinHoodProbeCheckCount; +#endif } + _isCapacityPaddingFilled = hashIndex + 1 == _probes.Length; + return ref _entries.AddKeyAndGetEntryRef(key, _count++ - _stackEntries.Capacity); } [MethodImpl((MethodImplOptions)256)] private void PutHashAndIndexWithoutResizing(int indexMask, int hash, int index) { +#if DEBUG + ++PutHashAndIndexWithoutResizing_Count; +#endif var hashIndex = hash & indexMask; var hashWithIndex = (hash & ~indexMask) | index; @@ -1295,48 +1539,54 @@ private void PutHashAndIndexWithoutResizing(int indexMask, int hash, int index) var probes = _probes; var hashesAndIndexes = _packedHashesAndIndexes; #endif - ref var p = ref probes.GetSurePresentItemRef(hashIndex); // Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions + ref var pRef = ref probes.GetSurePresentItemRef(hashIndex); byte probe = 1; - while (p >= probe) + while (pRef >= probe) { - hashIndex = (hashIndex + 1) & indexMask; - p = ref probes.GetSurePresentItemRef(hashIndex); + pRef = ref probes.GetSurePresentItemRef(++hashIndex); // no need to wrap around because the probes have the extra capacity ++probe; #if DEBUG - ++ProbeCheckCountInWrite; + ++PutHashAndIndexWithoutResizing_InitialProbeCheckCount; #endif } // We did not find the hash and therefore the key, so insert the new hash - var pRobinHooded = p; - p = probe; - var hRobinHooded = hashesAndIndexes.GetSurePresentItemRef(hashIndex); - hashesAndIndexes.GetSurePresentItemRef(hashIndex) = hashWithIndex; + var pRobinHooded = pRef; + pRef = probe; + + ref var hRef = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); + var hRobinHooded = hRef; + hRef = hashWithIndex; // If the robin hooded probe is empty then we stop probe = pRobinHooded; while (probe != 0) { - hashIndex = (hashIndex + 1) & indexMask; - p = ref probes.GetSurePresentItemRef(hashIndex); + pRef = ref probes.GetSurePresentItemRef(++hashIndex); // Otherwise we steal the slot with the smaller probes - if (p < ++probe) + if (pRef < ++probe) { - var pValue = p; - p = probe; + var pValue = pRef; + pRef = probe; probe = pValue; - var hValue = hashesAndIndexes.GetSurePresentItemRef(hashIndex); - hashesAndIndexes.GetSurePresentItemRef(hashIndex) = hRobinHooded; + hRef = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); + var hValue = hRef; + hRef = hRobinHooded; hRobinHooded = hValue; #if DEBUG - ++ProbeCheckCountInWrite; + ++PutHashAndIndexWithoutResizing_RobinHoodCount; #endif } +#if DEBUG + ++PutHashAndIndexWithoutResizing_RobinHoodProbeCheckCount; +#endif } + + _isCapacityPaddingFilled = hashIndex + 1 == _probes.Length; } /// Gets the ref to the existing entry by the provided key (found == true), @@ -1345,25 +1595,22 @@ private void PutHashAndIndexWithoutResizing(int indexMask, int hash, int index) [MethodImpl((MethodImplOptions)256)] public ref TEntry AddOrGetEntryRef(K key, out bool found) { - var hash = default(TEq).GetHashCode(key) | 1; + + var hash = default(TEq).GetHashCode(key); if (_count > _stackEntries.Capacity) return ref AddOrGetRefInEntries(key, hash, out found); - ref var e = ref _stackEntries.TryGetEntryRef(ref _stackHashes, _count, key, hash, out found, - default(TEq), default(TStackCap), default(Use)); - if (found) - return ref e; - - found = false; + var i = _stackEntries.TryGetEntryIndex(ref _stackHashes, _count, key, hash, Use.It); + if (found = i != -1) + return ref _stackEntries.GetSurePresentItemRef(i); // Add the new entry to the stack if there is still space in the stack if (_count < _stackEntries.Capacity) { - var newIndex = _count; - ++_count; - _stackHashes.GetSurePresentItemRef(newIndex) = hash; - ref var newStackEntry = ref _stackEntries.GetSurePresentItemRef(newIndex); + var addedIndex = _count++; + _stackHashes.GetSurePresentItemRef(addedIndex) = hash; + ref var newStackEntry = ref _stackEntries.GetSurePresentItemRef(addedIndex); newStackEntry.Key = key; return ref newStackEntry; } @@ -1377,17 +1624,22 @@ private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) // To avoid double work always going linearly through the Stack with the comparison, // let's calculate the hash of the keys stored on stack and put them // to the usual HashMap packed hashes and indexes array for the promised O(1) lookup. - // But the values are remaining on the Stack, and for the found index of the entry we use the GetSurePresentItemRef(index) - // to get the value reference either from the Stack or the Entries. - // So the values on the stack are guarantied to be stable from the beginning of the map creation, + // But the key-value entries will remain on the Stack, and for the found index of the entry we use the GetSurePresentItemRef(index) + // to get the entry reference either from the Stack or the TEntries. + // So the entries on stack are guarantied to be stable from the beginning of the map creation, // because they are not copied when the Entries need to Resize (depending on the TEntries implementation). + // To ensure stable entries on Entries you need to use var newCapBitShift = Math.Max(_stackEntries.CapacityBitShift + 1, _capacityBitShift); _capacityBitShift = (byte)newCapBitShift; - _probes = new byte[1 << newCapBitShift]; - _packedHashesAndIndexes = new int[1 << newCapBitShift]; - var indexMask = (1 << newCapBitShift) - 1; + var newCapacity = 1 << newCapBitShift; + var newTailPadding = 1 << (newCapBitShift - 3); + var newCapacityWithPadding = newCapacity + (newTailPadding > 16 ? newTailPadding : 16); + var indexMask = newCapacity - 1; + + _packedHashesAndIndexes = new int[newCapacityWithPadding]; + _probes = new byte[newCapacityWithPadding]; var stackCap = _stackEntries.Capacity; for (var i = 0; i < stackCap; ++i) @@ -1440,8 +1692,8 @@ private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key, int hash) { // if the free space is less than 1/8 of capacity (12.5%) then Resize var indexMask = (1 << _capacityBitShift) - 1; - if (indexMask - _count <= (indexMask >>> MinFreeCapacityShift)) - indexMask = ResizeHashes(indexMask); + if (_isCapacityPaddingFilled || indexMask - _count <= (indexMask >>> MinFreeCapacityShift)) + indexMask = ResizeProbesAndHashes(indexMask); PutHashAndIndexWithoutResizing(indexMask, hash, _count); @@ -1455,7 +1707,7 @@ private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key, int hash) [MethodImpl((MethodImplOptions)256)] public ref TEntry AddSureAbsentDefaultEntryAndGetRef(K key) { - var hash = default(TEq).GetHashCode(key) | 1; + var hash = default(TEq).GetHashCode(key); if (_count > _stackEntries.Capacity) return ref AddSureAbsentDefaultAndGetRefInEntries(key, hash); @@ -1473,31 +1725,26 @@ public ref TEntry AddSureAbsentDefaultEntryAndGetRef(K key) return ref MigrateToHeapAndAddEntry(key, hash); } - /// Lookups for the stored key. If found true, otherwise false - public bool ContainsKey(K key) + /// Lookups for the stored key. Returns its index in the entries or -1 + [MethodImpl((MethodImplOptions)256)] + public int TryGetIndex(K key) { - var hash = default(TEq).GetHashCode(key) | 1; - if (_count > _stackEntries.Capacity) - { - _ = TryGetRefInEntries(key, hash, out var found); - return found; - } - else - { - _ = ref _stackEntries.TryGetEntryRef(ref _stackHashes, _count, key, hash, out var found, - default(TEq), default(TStackCap), default(Use)); - return found; - } + var hash = default(TEq).GetHashCode(key); + return _count > _stackEntries.Capacity + ? TryGetIndexInEntries(key, hash) + : _stackEntries.TryGetEntryIndex(ref _stackHashes, _count, key, hash, Use.It); } - [UnscopedRef] + /// Lookups for the stored key. If found true, otherwise false + public bool ContainsKey(K key) => TryGetIndex(key) != -1; + + [MethodImpl((MethodImplOptions)256)] - internal ref TEntry TryGetRefInEntries(K key, int hash, out bool found) + internal int TryGetIndexInEntries(K key, int hash) { - var indexMask = (1 << _capacityBitShift) - 1; - var hashIndex = hash & indexMask; - var hashWithoutIndex = hash & ~indexMask; - +#if DEBUG + ++TryGetRefInEntries_Count; +#endif #if NET7_0_OR_GREATER ref var probes = ref MemoryMarshal.GetArrayDataReference(_probes); ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); @@ -1505,33 +1752,10 @@ internal ref TEntry TryGetRefInEntries(K key, int hash, out bool found) var probes = _probes; var hashesAndIndexes = _packedHashesAndIndexes; #endif - - var p = probes.GetSurePresentItem(hashIndex); - - var probe = 1; - while (p >= probe) - { - if (probe == p) - { - var h = hashesAndIndexes.GetSurePresentItem(hashIndex); - if ((h & ~indexMask) == hashWithoutIndex) - { - ref var e = ref GetSurePresentEntryRef(h & indexMask); - if (found = default(TEq).Equals(e.Key, key)) - return ref e; - } - } - - hashIndex = (hashIndex + 1) & indexMask; - p = probes.GetSurePresentItem(hashIndex); - ++probe; -#if DEBUG - ++ProbeCheckCountInRead; -#endif - } - - found = false; - return ref RefTools.GetNullRef(); + var indexMask = (1 << _capacityBitShift) - 1; + var hashIndex = hash & indexMask; + byte probe = 1; + return TryGetEntryAndHashIndex(key, hash, indexMask, ref hashIndex, ref probe, ref probes, ref hashesAndIndexes); } /// Lookup for the stored entry by key. Returns the ref to the found entry or the null ref @@ -1539,117 +1763,116 @@ internal ref TEntry TryGetRefInEntries(K key, int hash, out bool found) [MethodImpl((MethodImplOptions)256)] public ref TEntry TryGetEntryRef(K key, out bool found) { - var hash = default(TEq).GetHashCode(key) | 1; + var hash = default(TEq).GetHashCode(key); if (_count > _stackEntries.Capacity) - return ref TryGetRefInEntries(key, hash, out found); - return ref _stackEntries.TryGetEntryRef(ref _stackHashes, _count, key, hash, out found, - default(TEq), default(TStackCap), default(Use)); + { + var i = TryGetIndexInEntries(key, hash); + if (found = i != -1) + return ref GetSurePresentEntryRef(i); // the index may refer to the stack or heap entries + } + else + { + var i = _stackEntries.TryGetEntryIndex(ref _stackHashes, _count, key, hash, Use.It); + if (found = i != -1) + return ref _stackEntries.GetSurePresentItemRef(i); + } + return ref RefTools.GetNullRef(); } - internal int ResizeHashes(int indexMask) + internal int ResizeProbesAndHashes(int indexMask) { +#if DEBUG + ++ResizeProbesAndHashes_Count; +#endif var oldCapacity = indexMask + 1; - var eraseNextOldHashBit = ~oldCapacity; var newCapacity = oldCapacity << 1; var newIndexMask = (indexMask << 1) | 1; - var newProbesArr = new byte[newCapacity]; - var newHashesAndIndexes = new int[newCapacity]; + var maybeTailPadding = oldCapacity >> 2; // 1/8 of the new capacity + var newCapacityWithPadding = newCapacity + (maybeTailPadding > 16 ? maybeTailPadding : 16); + + var newProbesArr = new byte[newCapacityWithPadding]; + var newHashesAndIndexes = new int[newCapacityWithPadding]; #if NET7_0_OR_GREATER ref var newProbes = ref MemoryMarshal.GetArrayDataReference(newProbesArr); ref var newHashes = ref MemoryMarshal.GetArrayDataReference(newHashesAndIndexes); - ref var oldProbes = ref MemoryMarshal.GetArrayDataReference(_probes); - var oldProbe = oldProbes; - ref var oldHashes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); + ref var oldHashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); #else var newProbes = newProbesArr; var newHashes = newHashesAndIndexes; var oldProbes = _probes; - var oldProbe = oldProbes[0]; var oldHashes = _packedHashesAndIndexes; #endif - // Skip overflow segment of wrapped-around probes greater than 1 to keep the Robin Hood invariant. - // We know for sure that the entry with probe == 1 exists if the map is not fully occupied (which is by design when the Resize is starting). - // The probe == 1 would be at the right edge of the empty space. - // probes example [3, 4, 2, 0, 0, 1, 2, 2] - // i: [0, 1, 2, 3 ] - // skip stops on 3, so the oldCapWithOverflowSegment is 3 + 8 == 11, this corresponds to the all old probes to scan with wrap-around - var i = 0; - - // #if NET7_0_OR_GREATER - // if (Vector128.IsHardwareAccelerated) - // { - // Debug.Assert((oldCapacity & 15) == 0, "Capacity should be multiple of 16 for SIMD Vector128"); - // while (i < oldCapacity) - // { - // var probes = Vector128.LoadUnsafe(ref Unsafe.Add(ref oldProbes, i)); - // var lessOrEqThenOne = Vector128.LessThanOrEqual(probes, ByteVec16OfOne); - // var matchMask = lessOrEqThenOne.ExtractMostSignificantBits(); - // if (matchMask != 0) - // { - // i += System.Numerics.BitOperations.TrailingZeroCount(matchMask); - // break; - // } - // i += 16; - // } - // } - // else - // while (oldProbe > 1) - // oldProbe = oldProbes.GetSurePresentItem(++i); - // #else - while (oldProbe > 1) - { - oldProbe = oldProbes.GetSurePresentItem(++i); #if DEBUG - ++ProbeCheckCountInWrite; + var newProbeCheckCount = 0; + var inSkipEmptyProbesSpan = false; + var skipEmptyProbesSpanCount = 0; + var totalSkipEmptyProbesCount = 0; #endif - } - // #endif - - var oldCapacityWithOverflowSegment = i + oldCapacity; - while (true) + //todo: @wip remember that the padding cap is for wrap around items, + // and the invariant for them is the there no 0 probes in the wrap-around, so the loop may be stopped at the first 0 probe + // Considering that the most zero probes would be in a padding (as its is half of the capacity) we may consider + // to split the loop into two: for capacity and for padding. + for (var i = 0; i < _probes.Length; ++i) { + var oldProbe = oldProbes.GetSurePresentItem(i); +#if DEBUG + if (oldProbe == 0) + { + if (!inSkipEmptyProbesSpan) + { + inSkipEmptyProbesSpan = true; + ++skipEmptyProbesSpanCount; + } + ++totalSkipEmptyProbesCount; + if (i >= oldCapacity) + ++ResizeProbesAndHashes_LastWrapAroundEmptyProbesSpanCount; + } + else + { + inSkipEmptyProbesSpan = false; + } +#endif if (oldProbe != 0) // for the non-empty probe and therefore hash { - // Get the new IDEAL hash index from the old one with the next bit equal to the `oldCapacity`, normalized by the probe. - // For example: - // old probes [3, 4, 2, 0, 0, 1, 2, 2] - // old indeces [8, 9, 10 5, 6, 7, 8, 9, 10] 11, 12, 13, 14, 15 nextbit | (i - (oldProbe - 1)) & 7 - // new probes0 [ 1, ] (0 & 8) | (5 - (1 - 1)) & 7 == ideal 5, actual 5 - // new probes1 [ 1, 1, ] (1 & 8) | (6 - (2 - 1)) & 7 == ideal 13, actual 13 - // new probes2 [ 1, 1, 1, ] (0 & 8) | (7 - (2 - 1)) & 7 == ideal 6, actual 6 - // new probes3 [ 1, 1, 2, 1, ] (0 & 8) | (8 - (3 - 1)) & 7 == ideal 6, actual 7, ++probe - // new probes4 [ 1, 1, 2, 1, 1, ] (1 & 8) | (9 - (4 - 1)) & 7 == ideal 14, actual 14 - // new probes5 [ 1, 1, 2, 1, 1, 1, ] (1 & 8) | (10 - (2 - 1)) & 7 == ideal 9, actual 9 - var oldHash = oldHashes.GetSurePresentItem(i & indexMask); - var hashIndexWithNextBit = (oldHash & oldCapacity) | ((i - (oldProbe - 1)) & indexMask); - - // no need for robin-hooding because we already did it for the old hashes and now just filling the hashes into the new array which are already in order + var oldHashAndIndex = oldHashesAndIndexes.GetSurePresentItem(i); + + // First, calculate the ideal hash index for the current probe, it should be in the range of the old capacity/index mask + var oldHashIndex = i - (oldProbe - 1); + Debug.Assert(oldHashIndex < oldCapacity, "Old hash index should be less than old capacity"); + var nextHashBitInIndex = oldHashAndIndex & oldCapacity; + var newHashIndex = nextHashBitInIndex | oldHashIndex; + Debug.Assert(newHashIndex == oldHashIndex || newHashIndex == oldHashIndex + oldCapacity, "New hash index should be either the same as old or with the next bit set to the old capacity"); + + // No need for robin-hooding because we already did it for the old hashes and now just filling the hashes into the new array which are already in order byte probe = 1; - ref var p = ref newProbes.GetSurePresentItemRef(hashIndexWithNextBit); - while (p != 0) + ref var pRef = ref newProbes.GetSurePresentItemRef(newHashIndex); + while (pRef != 0) { - hashIndexWithNextBit = (hashIndexWithNextBit + 1) & newIndexMask; - p = ref newProbes.GetSurePresentItemRef(hashIndexWithNextBit); + pRef = ref newProbes.GetSurePresentItemRef(++newHashIndex); ++probe; #if DEBUG - ++ProbeCheckCountInWrite; + ++newProbeCheckCount; #endif } - p = probe; - newHashes.GetSurePresentItemRef(hashIndexWithNextBit) = oldHash & eraseNextOldHashBit; + pRef = probe; + newHashes.GetSurePresentItemRef(newHashIndex) = oldHashAndIndex & ~oldCapacity; } - if (++i >= oldCapacityWithOverflowSegment) - break; - - oldProbe = oldProbes.GetSurePresentItem(i & indexMask); } ++_capacityBitShift; _probes = newProbesArr; _packedHashesAndIndexes = newHashesAndIndexes; + +#if DEBUG + ResizeProbesAndHashes_AverageProbeCheckCount = newProbeCheckCount / _count; + ResizeProbesAndHashes_AverageEmptyProbeCount = totalSkipEmptyProbesCount / skipEmptyProbesSpanCount; +#endif +#if VERIFY_MAP + this.VerifyKeyHasMetaInfo(static (x, msg) => Debug.Assert(x, msg), Use.It); +#endif return newIndexMask; } } diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index f6c7ff11..d5a83751 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -2356,6 +2356,13 @@ .NET SDK 9.0.203 | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.150 us | 0.0428 us | 0.0783 us | 2.115 us | 1.00 | 0.05 | 1 | 1.1902 | 0.0229 | 7.31 KB | 1.00 | | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 3.092 us | 0.0608 us | 0.0871 us | 3.068 us | 1.44 | 0.06 | 2 | 0.8278 | - | 5.08 KB | 0.69 | + ## After probes go to their own array + padding + wrapping + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.289 us | 0.0457 us | 0.0994 us | 2.260 us | 1.00 | 0.06 | 1 | 1.1902 | 0.0229 | 7.31 KB | 1.00 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.311 us | 0.0459 us | 0.1056 us | 2.356 us | 1.01 | 0.06 | 1 | 0.8659 | 0.0038 | 5.31 KB | 0.73 | + */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] // [Params(10, 100, 1000)] diff --git a/src/ImTools/ImTools.cs b/src/ImTools/ImTools.cs index 70afb07c..f0a9dacb 100644 --- a/src/ImTools/ImTools.cs +++ b/src/ImTools/ImTools.cs @@ -7028,7 +7028,12 @@ public void SetTombstoneOrRemoveSurePresentEntry(int index) // 0b11111111 == 255, so the mask for the capacity is 255 internal const int ChunkCapacityMask = ChunkCapacity - 1; - // todo: @perf research on the similar growable indexed collection with append-to-end semantics + // todo: @perf recently (07-2025) a similar data structure was presented on BestSoftwareConference and + // has name Xar (extendible growable array with the stable references). + // I can implement O(1) random access by precalculating starting offsets of each chunk then using SIMD to compare the input index against offsets, + // e.g. `var offsets = Vector256.Create(16, 16+32, 16+32+64, 16+32+64+128,...); + // var matches = Vector256.ExtractMostSignificantBits(Vector256.Greater(Vector256.Create(Index), offsets)); + // var chunkIndex = BitOperations.TrailingZeroCount(matches);` /// The array of array buckets, where bucket is the fixed size. /// It enables adding the new bucket without for the new entries without reallocating the existing data. /// It may allow to drop the empty bucket as well, reclaiming the memory after remove. diff --git a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs index 7628147b..9af9be26 100644 --- a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs +++ b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs @@ -12,99 +12,13 @@ namespace FastExpressionCompiler.ImTools.UnitTests; [TestFixture] public class FecSmallMapTests { - internal static void Verify(SmallMap16 map, IEnumerable expectedKeys) - where TEq : struct, IEq - { - VerifyHashesAndKeysEq(map); - - // map.VerifyProbesAreFitRobinHood(Assert.Fail); - // map.VerifyNoDuplicateKeys(key => Assert.Fail($"Duplicate key: {key}")); - - if (expectedKeys != null) - VerifyContainAllKeys(map, expectedKeys); - } - - /// Verifies that the hashes correspond to the keys stored in the entries. May be called from the tests. - internal static void VerifyHashesAndKeysEq(SmallMap16 map) - where TEq : struct, IEq - { - var probes = map.Map.Probes; - var hashes = map.Map.PackedHashesAndIndexes; - var indexMask = map.Map.IndexMask; - for (var i = 0; i < probes.Length; i++) - { - if (probes[i] != 0) - { - var h = hashes[i]; - var hashPart = h & ~indexMask; - var index = h & indexMask; - var key = map.Map.GetSurePresentEntryRef(index).Key; - var hashFromKey = default(TEq).GetHashCode(key); - var hashPartFromKey = hashFromKey & ~indexMask; - Assert.AreEqual(hashPart, hashPartFromKey, - $"At index {i}: hashPart:{hashPart} != key-hashPart:{hashPartFromKey} for key={key}"); - } - } - } - - // /// Verifies that there is no duplicate keys stored in hashes -> entries. May be called from the tests. - // public static void VerifyNoDuplicateKeys(this HSmallMap map) - // where TEq : struct, IEq - // where TEntries : struct, IEntries - // { - // // Verify the indexes do no contains duplicate keys - // var uniq = new Dictionary(map.Count); - // var hashes = map.PackedHashesAndIndexes; - // var capacity = map.Capacity; - // var indexMask = capacity - 1; - // for (var i = 0; i < hashes.Length - 8; i++) - // { - // var h = hashes[i]; - // if (h == 0) - // continue; - // var key = map.Entries.GetSurePresentEntryRef(h & indexMask).Key; - // if (!uniq.ContainsKey(key)) - // uniq.Add(key, 1); - // else - // Assert.Fail($"Duplicate key: {key}"); - // } - // } - - // /// Verifies that the probes are consistently increasing - // public static void VerifyProbesAreFitRobinHood(this HSmallMap map) - // where TEq : struct, IEq - // where TEntries : struct, IEntries - // { - // var hashes = map.PackedHashesAndIndexes; - // var capacity = map.Capacity; - // var indexMask = capacity - 1; - // var prevProbes = -1; - // const int ProbeCountShift = 32 - MaxProbeBits; - // for (var i = 0; i < hashes.Length - 8; i++) - // { - // var h = hashes[i]; - // var probes = h >>> ProbeCountShift; - // if (prevProbes != -1 && probes - prevProbes > 1) - // Assert.Fail($"Probes are not consequent: {prevProbes}, {probes} for {i}: p{probes}, {h & indexMask} -> {map.Entries.GetSurePresentEntryRef(h & indexMask).Key}"); - // prevProbes = probes; - // } - // } - - /// Verifies that the map contains all passed keys. May be called from the tests. - internal static void VerifyContainAllKeys(SmallMap16 map, IEnumerable expectedKeys) - where TEq : struct, IEq - { - foreach (var key in expectedKeys) - Assert.True(map.Map.ContainsKey(key), $"Key not found:`{key}`"); - } - private static readonly Type[] _allKeys = typeof(List<>).Assembly.GetTypes().Take(2000).ToArray(); [Test] public void Zero_0_hash_test() { var map = new SmallMap16(); - // Make map >> 0 + // Make non-empty map map.Map.AddOrUpdate(1, 1); Assert.AreEqual(1, map.Map.Count); @@ -153,21 +67,23 @@ public void Benchmark_test_with_Add_and_Lookup_100_items() const int Count = 100; Debug.Assert(Count <= 1000, "Count should be less than or equal to 1000 for this test to work correctly."); +#if !FIB + var m = new SmallMap16>(); +#else var m = new SmallMap16>(); +#endif ref var map = ref m.Map; - var presentKeys = _allKeys.Take(Count).ToArray(); + var presentTypes = _allKeys.Take(Count).ToArray(); var missingKeys = _allKeys.Skip(1000).Take(Count).ToArray(); var seed = new Random(42); - var randomPresentKeys = presentKeys.OrderBy(_ => seed.Next()).ToArray(); + var randomPresentKeys = presentTypes.OrderBy(_ => seed.Next()).ToArray(); - foreach (var key in presentKeys) - map.AddOrGetValueRef(key, out _) = "a"; + foreach (var type in presentTypes) + map.AddOrGetValueRef(type, out _) = "a"; - VerifyHashesAndKeysEq(m); - foreach (var key in presentKeys) - Assert.True(map.ContainsKey(key), $"Key not found:`{key}`"); + // map.Verify(static (cond, msg) => Assert.IsTrue(cond, msg), presentTypes, Use>.It); var count = 0; var iters = Count / 2; @@ -180,13 +96,11 @@ public void Benchmark_test_with_Add_and_Lookup_100_items() if (!found) --count; } + Assert.AreEqual(0, count); #if DEBUG - Assert.Greater(m.Map.ProbeCheckCountInWrite, 100); // 396 no GoldenRatio, 284 with GoldenRatio - Assert.Greater(m.Map.ProbeCheckCountInRead, 100); // 429 no GoldenRatio, 335 with GoldenRatio + Assert.Greater(m.Map.FindEqualProbeAndHashCheckCount, 200); // 290 no GoldenRatio, 236 with GoldenRatio ~ 20% difference #endif - - Assert.AreEqual(0, count); } // [Test] From a5b6a6d89e791f6f185da6872ca642e63501747a Mon Sep 17 00:00:00 2001 From: dadhi Date: Tue, 29 Jul 2025 23:04:53 +0200 Subject: [PATCH 21/54] fixed the tests --- playground/Experiments/FEC_ImTools.cs | 32 +++++++++++++-------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index ed72f606..90483a00 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -53,6 +53,7 @@ namespace FastExpressionCompiler.ImTools; #if NET8_0_OR_GREATER using System.Runtime.Intrinsics; +using System.Numerics; #endif using static SmallMap; @@ -1011,13 +1012,13 @@ public static int TryGetEntryIndex where TCap : struct, ISize2Plus { - var cap = it.Bval; - Debug.Assert(count <= cap.Size, $"SmallMap.TryGetEntryRef: count {count} should be <= stack capacity {cap.Size}"); + var cap = default(TCap).Size; + Debug.Assert(count <= cap, $"SmallMap.TryGetEntryRef: count {count} should be <= stack capacity {cap}"); if (count == 0) return -1; #if NET8_0_OR_GREATER - if (cap.Size >= 8 & Vector256.IsHardwareAccelerated) + if (cap >= 8 & Vector256.IsHardwareAccelerated) { var vHash = Vector256.Create(hash); var vHashes = MemoryMarshal.Cast>(hashes.AsSpan()); @@ -1026,21 +1027,21 @@ public static int TryGetEntryIndex= count) // todo: @perf can I remove this branch? + return -1; - ref var entry = ref entries.GetSurePresentItemRef(i + matchIndex); - if (default(TEq).Equals(entry.Key, key)) - return i + matchIndex; + if (default(TEq).Equals(entries.GetSurePresentItemRef(matchIndex).Key, key)) + return matchIndex; // Clear lower bits up to and including the first set bit, afaik it can be hw accelerated // 0b0001_1000 & (0b0001_1000 - 1) -> & 0b0001_1000 & 0b0001_0111 -> 0b0001_0000 matches &= matches - 1; - ++i; } - if (i >= count) - break; + if ((i += Vector256.Count) >= count) + return -1; } return -1; } @@ -1048,12 +1049,9 @@ public static int TryGetEntryIndex Date: Wed, 30 Jul 2025 19:10:24 +0200 Subject: [PATCH 22/54] simplify extension methods by split of Map interfaces to IMap and IMapImpl --- playground/Experiments/FEC_ImTools.cs | 220 ++++++++---------- .../Playground/FecSmallMapTests.cs | 48 ++-- 2 files changed, 117 insertions(+), 151 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 90483a00..d9981e4f 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -876,24 +876,24 @@ public int GetHashCode((A, B, C) key) => /// Add the Use parameter to `Method{T}(..., Use{T} _)` to enable type inference for T, /// by calling it as `var t = Method(..., Use{T}.It)` -public readonly struct Use +public readonly struct Pass { - public static readonly Use It = new(); + public static readonly Pass It = new(); public readonly A Aval; } -/// Add the Use parameter in type inference for A, B -public readonly struct Use +/// Add the Pass parameter in type inference for A, B +public readonly struct Pass { - public static readonly Use It = new(); + public static readonly Pass It = new(); public readonly A Aval; public readonly B Bval; } -/// Add the Use parameter in type inference for A, B, C -public readonly struct Use +/// Add the Pass parameter in type inference for A, B, C +public readonly struct Pass { - public static readonly Use It = new(); + public static readonly Pass It = new(); public readonly A Aval; public readonly B Bval; public readonly C Cval; @@ -1005,7 +1005,7 @@ public ref TEntry AddKeyAndGetEntryRef(K key, int index) [MethodImpl((MethodImplOptions)256)] public static int TryGetEntryIndex( this ref TStackEntries entries, ref TStackHashes hashes, int count, K key, int hash, - Use it = default) + Pass it = default) where TEntry : struct, IEntry where TEq : struct, IEq where TStackHashes : struct, IStack @@ -1013,7 +1013,7 @@ public static int TryGetEntryIndexGets the ref to the existing entry.Value by the provided key (found == true), - /// or adds a new entry (found == false) and returns it.Value by ref. - /// So the method always return a non-null ref to the value, either existing or added - [MethodImpl((MethodImplOptions)256)] - public static ref V AddOrGetValueRef( - this ref SmallMap, TEq, TStackCap, TStackHashes, TStackEntries, TEntries> map, K key, out bool found) - where TEq : struct, IEq - where TStackCap : struct, ISize2Plus - where TStackHashes : struct, IStack - where TStackEntries : struct, IStack, TStackCap, TStackEntries> - where TEntries : struct, IEntries, TEq> => - ref map.AddOrGetEntryRef(key, out found).Value; - /// Adds or updates the entry with the provided value. /// Returns `true` if it is updated the existing value. [MethodImpl((MethodImplOptions)256)] - public static bool AddOrUpdate( - this ref SmallMap, TEq, TStackCap, TStackHashes, TStackEntries, TEntries> map, K key, in V value) - where TEq : struct, IEq - where TStackCap : struct, ISize2Plus - where TStackHashes : struct, IStack - where TStackEntries : struct, IStack, TStackCap, TStackEntries> - where TEntries : struct, IEntries, TEq> + public static bool AddOrUpdate(this ref TMap map, K key, in V value) + where TMap : struct, IMap> { map.AddOrGetEntryRef(key, out var found).Value = value; return found; } - /// Adds an entry for sure absent key. - /// Provides the performance in scenarios where you look for the present key, and using it, and if ABSENT then add the new one. - /// So this method optimized NOT to look for the present item for the second time - [MethodImpl((MethodImplOptions)256)] - public static ref V AddSureAbsentDefaultAndGetRef( - this ref SmallMap, TEq, TStackCap, TStackHashes, TStackEntries, TEntries> map, K key) - where TEq : struct, IEq - where TStackCap : struct, ISize2Plus - where TStackHashes : struct, IStack - where TStackEntries : struct, IStack, TStackCap, TStackEntries> - where TEntries : struct, IEntries, TEq> - => ref map.AddSureAbsentDefaultEntryAndGetRef(key).Value; - - /// Lookups for the stored entry by key. Returns the ref to the found entry.Value or the null ref - [MethodImpl((MethodImplOptions)256)] - public static ref V TryGetValueRef( - this ref SmallMap, TEq, TStackCap, TStackHashes, TStackEntries, TEntries> map, K key, out bool found) - where TEq : struct, IEq - where TStackCap : struct, ISize2Plus - where TStackHashes : struct, IStack - where TStackEntries : struct, IStack, TStackCap, TStackEntries> - where TEntries : struct, IEntries, TEq> - { - ref var e = ref map.TryGetEntryRef(key, out found); - if (found) return ref e.Value; - return ref RefTools.GetNullRef(); - } - /// Lookups for the stored entry by key. Returns the found value or provided default [MethodImpl((MethodImplOptions)256)] - public static V GetValueOrDefault( - this ref SmallMap, TEq, TStackCap, TStackHashes, TStackEntries, TEntries> map, K key, V defaultValue = default) - where TEq : struct, IEq - where TStackCap : struct, ISize2Plus - where TStackHashes : struct, IStack - where TStackEntries : struct, IStack, TStackCap, TStackEntries> - where TEntries : struct, IEntries, TEq> + public static V GetValueOrDefault(this ref TMap map, K key, V defaultValue = default) + where TMap : struct, IMap> { ref var e = ref map.TryGetEntryRef(key, out var found); if (found) return e.Value; @@ -1132,28 +1081,25 @@ public static V GetValueOrDefaultVerifies the integrity and invariants of the SmallMap - public static void Verify(ref this TMap map, - Action assertCond, IEnumerable expectedKeys = null, Use _ = default) - where TMap : struct, IMap, IMapStruct - where TEq : struct, IEq + public static void Verify(ref this TMap map, + Action assertCond, IEnumerable expectedKeys = null, Pass pass = default) + where TMap : struct, IMap, IMapImpl { - map.VerifyHashesAndKeysEq(assertCond, _); + map.VerifyHashesAndKeysEq(assertCond, pass); - map.VerifyProbesRobinHoodInvariants(assertCond); + map.VerifyProbesRobinHoodInvariants(assertCond, pass); - map.VerifyKeyHasMetaInfo(assertCond, _); + map.VerifyKeyHasMetaInfo(assertCond, pass); // map.VerifyNoDuplicateKeys(key => Assert.Fail($"Duplicate key: {key}")); if (expectedKeys != null) - map.VerifyContainAllKeys(expectedKeys, assertCond, _); + map.VerifyContainAllKeys(expectedKeys, assertCond, pass); } - /// Verifies that the hashes correspond to the keys stored in the entries. May be called from the tests. - public static void VerifyHashesAndKeysEq(ref this TMap map, Action assertCond, - Use _ = default) - where TMap : struct, IMap, IMapStruct - where TEq : struct, IEq + /// Verifies that the hashes correspond to the keys stored in the entries. May be called tests. + public static void VerifyHashesAndKeysEq(ref this TMap map, Action assertCond, Pass _ = default) + where TMap : struct, IMap, IMapImpl { var probes = map.Probes; var hashes = map.PackedHashesAndIndexes; @@ -1166,39 +1112,37 @@ public static void VerifyHashesAndKeysEq(ref this TMap map, Action var hashPart = h & ~indexMask; var index = h & indexMask; var key = map.GetSurePresentKey(index); - var hashFromKey = default(TEq).GetHashCode(key); + var hashFromKey = map.GetHashCode(key); var hashPartFromKey = hashFromKey & ~indexMask; assertCond(hashPart == hashPartFromKey, $"Hash mismatch for key:{key}, expected:{hashPartFromKey}, actual:{hashPart}"); } } } - // /// Verifies that there is no duplicate keys stored in hashes -> entries. May be called from the tests. - // public static void VerifyNoDuplicateKeys(this HSmallMap map) - // where TEq : struct, IEq - // where TEntries : struct, IEntries - // { - // // Verify the indexes do no contains duplicate keys - // var uniq = new Dictionary(map.Count); - // var hashes = map.PackedHashesAndIndexes; - // var capacity = map.Capacity; - // var indexMask = capacity - 1; - // for (var i = 0; i < hashes.Length - 8; i++) - // { - // var h = hashes[i]; - // if (h == 0) - // continue; - // var key = map.Entries.GetSurePresentEntryRef(h & indexMask).Key; - // if (!uniq.ContainsKey(key)) - // uniq.Add(key, 1); - // else - // Assert.Fail($"Duplicate key: {key}"); - // } - // } + /// Verifies that there is no duplicate keys stored in hashes -> entries. May be called from the tests. + public static void VerifyNoDuplicateKeys(this TMap map, Action assertCond, Pass _ = default) + where TMap : struct, IMap, IMapImpl + { + var uniq = new Dictionary(map.Count); + var probes = map.Probes; + var hashes = map.PackedHashesAndIndexes; + var indexMask = map.Capacity - 1; + for (var i = 0; i < probes.Length; i++) + { + var p = probes[i]; + if (p == 0) + continue; + var key = map.GetSurePresentKey(hashes[i] & indexMask); + if (!uniq.ContainsKey(key)) + uniq.Add(key, 1); + else + assertCond(false, $"Duplicate key: {key}"); + } + } /// Verifies that the probes are consistently increasing - public static void VerifyProbesRobinHoodInvariants(this TMap map, Action assertCond) - where TMap : struct, IMapStruct + public static void VerifyProbesRobinHoodInvariants(this TMap map, Action assertCond, Pass _ = default) + where TMap : struct, IMapImpl { var prevProbe = 0; var probes = map.Probes; @@ -1212,10 +1156,9 @@ public static void VerifyProbesRobinHoodInvariants(this TMap map, ActionVerifies that each key has a corresponding hash entry which is correctly references this key - public static void VerifyKeyHasMetaInfo(ref this TMap map, - Action assertCond, Use _ = default) - where TMap : struct, IMap - where TEq : struct, IEq + public static void VerifyKeyHasMetaInfo(ref this TMap map, + Action assertCond, Pass _ = default) + where TMap : struct, IMap { for (var i = 0; i < map.Count; ++i) { @@ -1227,29 +1170,52 @@ public static void VerifyKeyHasMetaInfo(ref this TMap map, } /// Verifies that the map contains all passed keys. May be called from the tests. - public static void VerifyContainAllKeys(ref this TMap map, - IEnumerable expectedKeys, Action assertContains, Use _ = default) - where TMap : struct, IMap - where TEq : struct, IEq + public static void VerifyContainAllKeys(ref this TMap map, + IEnumerable expectedKeys, Action assertContains, Pass _ = default) + where TMap : struct, IMap { foreach (var key in expectedKeys) assertContains(map.ContainsKey(key), $"Key not found: {key}"); } } -public interface IMap where TEq : struct, IEq +public interface IMap { + /// Number of entries in the map int Count { get; } + /// Lookups for the stored key. Returns its index in the entries or -1 int TryGetIndex(K key); + /// Lookups for the stored key. If found true, otherwise false bool ContainsKey(K key); + /// Lookups for the stored key by the index in the entries data structure K GetSurePresentKey(int index); } -public interface IMapStruct +public interface IMap : IMap + where TEntry : struct, IEntry +{ + /// Returns the ref to the found entry or the null ref + [UnscopedRef] + ref TEntry TryGetEntryRef(K key, out bool found); + + /// Returns the ref to the found entry or Adds the entry if not found + [UnscopedRef] + ref TEntry AddOrGetEntryRef(K key, out bool found); + + /// Adds an entry for sure absent key. + /// Provides the performance in scenarios where you know the key is not yet in the map + [UnscopedRef] + ref TEntry AddSureAbsentDefaultEntryAndGetRef(K key); +} + +/// Implementation details +public interface IMapImpl { int Capacity { get; } byte[] Probes { get; } int[] PackedHashesAndIndexes { get; } + /// Returns the hash code for the provided key + int GetHashCode(K key); } // todo: @improve ? how/where to add SIMD to improve CPU utilization but not losing perf for smaller sizes @@ -1268,7 +1234,7 @@ public interface IMapStruct /// /// [DebuggerDisplay("{Count} entries total")] -public struct SmallMap : IMap, IMapStruct +public struct SmallMap : IMap, IMapImpl where TEntry : struct, IEntry where TEq : struct, IEq where TStackCap : struct, ISize2Plus @@ -1345,7 +1311,7 @@ public struct SmallMapAccess to the hashes and indexes public int[] PackedHashesAndIndexes => _packedHashesAndIndexes; - /// Number of entries in the map + /// public int Count => _count; /// Access to the key-value entries @@ -1370,7 +1336,11 @@ public SmallMap(byte capacityBitShift) _entries.Init(capacity); } + /// + public int GetHashCode(K key) => default(TEq).GetHashCode(key); + // todo: @perf try using StableGrowingEntries (or Xar) with O(1) random access. And make an default storage for the entries. + /// [MethodImpl((MethodImplOptions)256)] public K GetSurePresentKey(int index) { @@ -1587,19 +1557,17 @@ private void PutHashAndIndexWithoutResizing(int indexMask, int hash, int index) _isCapacityPaddingFilled = hashIndex + 1 == _probes.Length; } - /// Gets the ref to the existing entry by the provided key (found == true), - /// or adds a new entry (found == false) and returns it by ref + /// [UnscopedRef] [MethodImpl((MethodImplOptions)256)] public ref TEntry AddOrGetEntryRef(K key, out bool found) { - var hash = default(TEq).GetHashCode(key); if (_count > _stackEntries.Capacity) return ref AddOrGetRefInEntries(key, hash, out found); - var i = _stackEntries.TryGetEntryIndex(ref _stackHashes, _count, key, hash, Use.It); + var i = _stackEntries.TryGetEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); if (found = i != -1) return ref _stackEntries.GetSurePresentItemRef(i); @@ -1698,9 +1666,7 @@ private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key, int hash) return ref _entries.AddKeyAndGetEntryRef(key, (_count++) - _stackEntries.Capacity); } - /// Adds an entry for sure absent key. - /// Provides the performance in scenarios where you look for the present key, and using it, and if ABSENT then add the new one. - /// So this method optimized NOT to look for the present item for the second time + /// [UnscopedRef] [MethodImpl((MethodImplOptions)256)] public ref TEntry AddSureAbsentDefaultEntryAndGetRef(K key) @@ -1723,17 +1689,17 @@ public ref TEntry AddSureAbsentDefaultEntryAndGetRef(K key) return ref MigrateToHeapAndAddEntry(key, hash); } - /// Lookups for the stored key. Returns its index in the entries or -1 + /// [MethodImpl((MethodImplOptions)256)] public int TryGetIndex(K key) { var hash = default(TEq).GetHashCode(key); return _count > _stackEntries.Capacity ? TryGetIndexInEntries(key, hash) - : _stackEntries.TryGetEntryIndex(ref _stackHashes, _count, key, hash, Use.It); + : _stackEntries.TryGetEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); } - /// Lookups for the stored key. If found true, otherwise false + /// public bool ContainsKey(K key) => TryGetIndex(key) != -1; @@ -1756,7 +1722,7 @@ internal int TryGetIndexInEntries(K key, int hash) return TryGetEntryAndHashIndex(key, hash, indexMask, ref hashIndex, ref probe, ref probes, ref hashesAndIndexes); } - /// Lookup for the stored entry by key. Returns the ref to the found entry or the null ref + /// [UnscopedRef] [MethodImpl((MethodImplOptions)256)] public ref TEntry TryGetEntryRef(K key, out bool found) @@ -1770,7 +1736,7 @@ public ref TEntry TryGetEntryRef(K key, out bool found) } else { - var i = _stackEntries.TryGetEntryIndex(ref _stackHashes, _count, key, hash, Use.It); + var i = _stackEntries.TryGetEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); if (found = i != -1) return ref _stackEntries.GetSurePresentItemRef(i); } diff --git a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs index 9af9be26..f5aca994 100644 --- a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs +++ b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs @@ -44,16 +44,16 @@ public void Benchmark_test_with_Add_and_Lookup_10_items() var randomPresentKeys = presentKeys.OrderBy(_ => seed.Next()).ToArray(); foreach (var key in presentKeys) - map.AddOrGetValueRef(key, out _) = "a"; + map.AddOrUpdate(key, "a"); var count = 0; var iters = Count / 2; for (var i = 0; i < iters; ++i) { - var result = map.TryGetValueRef(randomPresentKeys[i], out var found); + var entry = map.TryGetEntryRef(randomPresentKeys[i], out var found); if (found) - count += result.Length; - _ = map.TryGetValueRef(missingKeys[i], out found); + count += entry.Value.Length; + _ = map.TryGetEntryRef(missingKeys[i], out found); if (!found) --count; } @@ -81,7 +81,7 @@ public void Benchmark_test_with_Add_and_Lookup_100_items() var randomPresentKeys = presentTypes.OrderBy(_ => seed.Next()).ToArray(); foreach (var type in presentTypes) - map.AddOrGetValueRef(type, out _) = "a"; + map.AddOrUpdate(type, "a"); // map.Verify(static (cond, msg) => Assert.IsTrue(cond, msg), presentTypes, Use>.It); @@ -89,10 +89,10 @@ public void Benchmark_test_with_Add_and_Lookup_100_items() var iters = Count / 2; for (var i = 0; i < iters; ++i) { - var result = map.TryGetValueRef(randomPresentKeys[i], out var found); + var result = map.TryGetEntryRef(randomPresentKeys[i], out var found); if (found) - count += result.Length; - _ = map.TryGetValueRef(missingKeys[i], out found); + count += result.Value.Length; + _ = map.TryGetEntryRef(missingKeys[i], out found); if (!found) --count; } @@ -220,24 +220,24 @@ public void Can_store_and_retrieve_value_from_map() map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, "8"); // check for the missing key - Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32)); + Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32, default(string))); // check for the strange key - Assert.AreEqual("*", map.GetValueOrDefault(44)); - - Assert.AreEqual("1", map.GetValueOrDefault(42)); - Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); - Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); - Assert.AreEqual("4", map.GetValueOrDefault(42 + 32 + 32 + 32)); - Assert.AreEqual("5", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32)); - Assert.AreEqual("6", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32)); - Assert.AreEqual("7", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32)); - Assert.AreEqual("8", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32)); - - Assert.AreEqual("a", map.GetValueOrDefault(43)); - Assert.AreEqual("b", map.GetValueOrDefault(43 + 32)); - Assert.AreEqual("c", map.GetValueOrDefault(43 + 32 + 32)); - Assert.AreEqual("d", map.GetValueOrDefault(43 + 32 + 32 + 32)); + Assert.AreEqual("*", map.GetValueOrDefault(44, default(string))); + + Assert.AreEqual("1", map.GetValueOrDefault(42, default(string))); + Assert.AreEqual("2", map.GetValueOrDefault(42 + 32, default(string))); + Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32, default(string))); + Assert.AreEqual("4", map.GetValueOrDefault(42 + 32 + 32 + 32, default(string))); + Assert.AreEqual("5", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32, default(string))); + Assert.AreEqual("6", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32, default(string))); + Assert.AreEqual("7", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32, default(string))); + Assert.AreEqual("8", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, default(string))); + + Assert.AreEqual("a", map.GetValueOrDefault(43, default(string))); + Assert.AreEqual("b", map.GetValueOrDefault(43 + 32, default(string))); + Assert.AreEqual("c", map.GetValueOrDefault(43 + 32 + 32, default(string))); + Assert.AreEqual("d", map.GetValueOrDefault(43 + 32 + 32 + 32, default(string))); Assert.AreEqual(13, map.Count); } From 9446b2140ecb0ae49509623b88f1e5d15dab7802 Mon Sep 17 00:00:00 2001 From: dadhi Date: Wed, 30 Jul 2025 21:20:04 +0200 Subject: [PATCH 23/54] simplify things --- playground/Experiments/FEC_ImTools.cs | 19 ++++++------- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 28 +++++++++++-------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index d9981e4f..55c0bbb0 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -1003,7 +1003,7 @@ public ref TEntry AddKeyAndGetEntryRef(K key, int index) /// Lookup for the K in the TStackEntries, first by calculating it hash with TEq and searching the hash in the TStackHashes [MethodImpl((MethodImplOptions)256)] - public static int TryGetEntryIndex( + public static int TryGetStackEntryIndex( this ref TStackEntries entries, ref TStackHashes hashes, int count, K key, int hash, Pass it = default) where TEntry : struct, IEntry @@ -1013,7 +1013,7 @@ public static int TryGetEntryIndex : IMap [UnscopedRef] ref TEntry AddOrGetEntryRef(K key, out bool found); - /// Adds an entry for sure absent key. - /// Provides the performance in scenarios where you know the key is not yet in the map + /// Adds an entry for sure absent key. Provides the performance in scenarios where you know the key is not yet in the map [UnscopedRef] ref TEntry AddSureAbsentDefaultEntryAndGetRef(K key); } @@ -1214,11 +1213,10 @@ public interface IMapImpl int Capacity { get; } byte[] Probes { get; } int[] PackedHashesAndIndexes { get; } - /// Returns the hash code for the provided key + /// Returns the hash code for the provided key using the IEq implementation used by this map int GetHashCode(K key); } -// todo: @improve ? how/where to add SIMD to improve CPU utilization but not losing perf for smaller sizes /// /// Fast and less-allocating hash map without thread safety nets. Please measure it in your own use case before use. /// It is configurable in regard of hash calculation/equality via `TEq` type parameter and @@ -1233,7 +1231,7 @@ public interface IMapImpl /// For instance, for the `RefEq` the tombstone is . You may redefine it in the `IEq{K}.GetTombstone()` implementation. /// /// -[DebuggerDisplay("{Count} entries total")] +[DebuggerDisplay("Contains {Count} entries")] public struct SmallMap : IMap, IMapImpl where TEntry : struct, IEntry where TEq : struct, IEq @@ -1244,7 +1242,6 @@ public struct SmallMap _stackEntries.Capacity) return ref AddOrGetRefInEntries(key, hash, out found); - var i = _stackEntries.TryGetEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); + var i = _stackEntries.TryGetStackEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); if (found = i != -1) return ref _stackEntries.GetSurePresentItemRef(i); @@ -1696,7 +1693,7 @@ public int TryGetIndex(K key) var hash = default(TEq).GetHashCode(key); return _count > _stackEntries.Capacity ? TryGetIndexInEntries(key, hash) - : _stackEntries.TryGetEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); + : _stackEntries.TryGetStackEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); } /// @@ -1736,7 +1733,7 @@ public ref TEntry TryGetEntryRef(K key, out bool found) } else { - var i = _stackEntries.TryGetEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); + var i = _stackEntries.TryGetStackEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); if (found = i != -1) return ref _stackEntries.GetSurePresentItemRef(i); } diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index d5a83751..ebf53542 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -2358,15 +2358,21 @@ .NET SDK 9.0.203 ## After probes go to their own array + padding + wrapping - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | - |--------------------------------------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.289 us | 0.0457 us | 0.0994 us | 2.260 us | 1.00 | 0.06 | 1 | 1.1902 | 0.0229 | 7.31 KB | 1.00 | - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.311 us | 0.0459 us | 0.1056 us | 2.356 us | 1.01 | 0.06 | 1 | 0.8659 | 0.0038 | 5.31 KB | 0.73 | + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |------------:|----------:|----------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 108.2 ns | 1.04 ns | 0.87 ns | 0.36 | 0.01 | 1 | - | - | - | 0.00 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 297.9 ns | 5.89 ns | 7.45 ns | 1.00 | 0.03 | 2 | 0.1707 | - | 1072 B | 1.00 | + | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,146.4 ns | 42.80 ns | 66.64 ns | 1.00 | 0.04 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,220.2 ns | 43.51 ns | 60.99 ns | 1.04 | 0.04 | 1 | 0.8659 | 0.0038 | 5440 B | 0.73 | + | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 23,454.6 ns | 455.76 ns | 695.99 ns | 1.00 | 0.04 | 1 | 9.1553 | 1.2817 | 57808 B | 1.00 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 47,784.8 ns | 580.37 ns | 453.11 ns | 2.04 | 0.06 | 2 | 8.9111 | 0.0610 | 55976 B | 0.97 | */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] // [Params(10, 100, 1000)] - [Params(100)] + [Params(10, 100, 1000)] public int Count; private Type[] _presentKeys; @@ -2627,9 +2633,9 @@ public SmallMapTypeString FillSmallMap() public void FillFecHashMap(ref FecSmallMapTypeString map) { foreach (var key in _keys.Take(Count)) - map.Map.AddOrGetValueRef(key, out _) = "a"; + map.Map.AddOrUpdate(key, "a"); - map.Map.AddOrGetValueRef(LookupKey, out _) = "!"; + map.Map.AddOrUpdate(LookupKey, "!"); } private FecSmallMapTypeString _fecSmallMap; @@ -2848,16 +2854,16 @@ public int FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent() ref var map = ref m.Map; foreach (var key in _presentKeys) - map.AddOrGetValueRef(key, out _) = "a"; + map.AddOrUpdate(key, "a"); var count = 0; var iters = Count / 2; for (var i = 0; i < iters; ++i) { - var result = map.TryGetValueRef(_randomPresentKeys[i], out var found); + var result = map.TryGetEntryRef(_randomPresentKeys[i], out var found); if (found) - count += result.Length; - _ = map.TryGetValueRef(_missingKeys[i], out found); + count += result.Value.Length; + _ = map.TryGetEntryRef(_missingKeys[i], out found); if (!found) --count; } From 3befe9e0572c27bedb67accb0135fa58b8c7da23 Mon Sep 17 00:00:00 2001 From: dadhi Date: Wed, 30 Jul 2025 21:23:16 +0200 Subject: [PATCH 24/54] enable more Verify --- playground/Experiments/FEC_ImTools.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 55c0bbb0..19e4effd 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -1091,7 +1091,7 @@ public static void Verify(ref this TMap map, map.VerifyKeyHasMetaInfo(assertCond, pass); - // map.VerifyNoDuplicateKeys(key => Assert.Fail($"Duplicate key: {key}")); + map.VerifyNoDuplicateKeys(assertCond, pass); if (expectedKeys != null) map.VerifyContainAllKeys(expectedKeys, assertCond, pass); From 14dd2ad4cde1abad51e9bfa3f4bdadf75e8523db Mon Sep 17 00:00:00 2001 From: dadhi Date: Thu, 31 Jul 2025 12:19:05 +0200 Subject: [PATCH 25/54] cleanup diagnostics and improve Resize --- playground/Experiments/FEC_ImTools.cs | 169 +++++++----------- .../Playground/FecSmallMapTests.cs | 44 ++++- 2 files changed, 107 insertions(+), 106 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 19e4effd..6736290f 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -25,7 +25,6 @@ THE SOFTWARE. // ReSharper disable once InconsistentNaming #nullable disable -#define MAP_LOOKUP_SPLIT_LOOP // todo: @wip @perf // #if DEBUG // #define VERIFY_MAP @@ -57,6 +56,7 @@ namespace FastExpressionCompiler.ImTools; #endif using static SmallMap; +using System.Linq; /// Helpers and polyfills for the missing things in the old .NET versions public static class RefTools @@ -1232,42 +1232,33 @@ public interface IMapImpl /// /// [DebuggerDisplay("Contains {Count} entries")] -public struct SmallMap : IMap, IMapImpl +public struct SmallMap : IMap, IMapImpl where TEntry : struct, IEntry where TEq : struct, IEq where TStackCap : struct, ISize2Plus where TStackHashes : struct, IStack where TStackEntries : struct, IStack - where TEntries : struct, IEntries + where THeapEntries : struct, IEntries { #if DEBUG // Diagnostic counters to measure the performance of the map operations - public int TotalProbeCheckCountInRead = 0; public int PutHashAndIndexWithoutResizing_Count = 0; - public int PutHashAndIndexWithoutResizing_InitialProbeCheckCount = 0; - public int PutHashAndIndexWithoutResizing_AverageProbeCheckCount => - PutHashAndIndexWithoutResizing_Count == 0 ? 0 : PutHashAndIndexWithoutResizing_InitialProbeCheckCount / PutHashAndIndexWithoutResizing_Count; + public int PutHashAndIndexWithoutResizing_ProbeGreaterOrEqualCheckCount = 0; + public int PutHashAndIndexWithoutResizing_ProbeGreaterOrEqualCheckCountAverage => + PutHashAndIndexWithoutResizing_Count == 0 ? 0 : PutHashAndIndexWithoutResizing_ProbeGreaterOrEqualCheckCount / PutHashAndIndexWithoutResizing_Count; public int PutHashAndIndexWithoutResizing_RobinHoodCount = 0; public int PutHashAndIndexWithoutResizing_RobinHoodProbeCheckCount = 0; - public int TryGetRefInEntries_Count = 0; - public int TryGetRefInEntries_InitialProbeCheckCount = 0; - public int TryGetRefInEntries_AverageProbeCheckCount => - TryGetRefInEntries_Count == 0 ? 0 : TryGetRefInEntries_InitialProbeCheckCount / TryGetRefInEntries_Count; - public int TryGetRefInEntries_EqualProbeCheckCount = 0; - + public int LookupCount = 0; + public int LookupGreaterProbeCheckCount = 0; + public int LookupGreaterProbeCheckCountAverage => LookupCount == 0 ? 0 : LookupGreaterProbeCheckCount / LookupCount; + public int LookupEqualProbeCheckCount = 0; + public int LookupEqualProbeCheckCountAverage => LookupCount == 0 ? 0 : LookupEqualProbeCheckCount / LookupCount; public int AddOrGetRefInEntries_Count = 0; - public int AddOrGetRefInEntries_InitialProbeCheckCount = 0; - public int AddOrGetRefInEntries_InitialProbeCheckCountAverage => - AddOrGetRefInEntries_Count == 0 ? 0 : AddOrGetRefInEntries_InitialProbeCheckCount / AddOrGetRefInEntries_Count; - public int FindEqualProbeAndHashCheckCount = 0; public int AddOrGetRefInEntries_RobinHoodCount = 0; public int AddOrGetRefInEntries_RobinHoodProbeCheckCount = 0; public int ResizeProbesAndHashes_Count = 0; - public int ResizeProbesAndHashes_AverageProbeCheckCount = 0; - public int ResizeProbesAndHashes_AverageEmptyProbeCount = 0; - public int ResizeProbesAndHashes_LastWrapAroundEmptyProbesSpanCount = 0; #endif internal byte _capacityBitShift; @@ -1289,7 +1280,7 @@ public struct SmallMap _count; /// Access to the key-value entries - public TEntries Entries => _entries; + public THeapEntries Entries => _heapEntries; /// Capacity calculates as `1 leftShift capacityBitShift` public SmallMap(byte capacityBitShift) @@ -1330,7 +1321,7 @@ public SmallMap(byte capacityBitShift) _probes = new byte[capacityWithPadding]; _packedHashesAndIndexes = new int[capacityWithPadding]; - _entries.Init(capacity); + _heapEntries.Init(capacity); } /// @@ -1344,7 +1335,7 @@ public K GetSurePresentKey(int index) Debug.Assert(index >= 0); Debug.Assert(index < _count); return index >= _stackEntries.Capacity - ? _entries.GetSurePresentEntryRef(index - _stackEntries.Capacity).Key + ? _heapEntries.GetSurePresentEntryRef(index - _stackEntries.Capacity).Key : _stackEntries.GetSurePresentItemRef(index).Key; } @@ -1358,7 +1349,7 @@ public ref TEntry GetSurePresentEntryRef(int index) Debug.Assert(index >= 0); Debug.Assert(index < _count); if (index >= _stackEntries.Capacity) - return ref _entries.GetSurePresentEntryRef(index - _stackEntries.Capacity); + return ref _heapEntries.GetSurePresentEntryRef(index - _stackEntries.Capacity); return ref _stackEntries.GetSurePresentItemRef(index); } @@ -1373,15 +1364,17 @@ private int TryGetEntryAndHashIndex(K key, int hash, int indexMask, #endif ) { +#if DEBUG + ++LookupCount; +#endif // Skip over the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions -#if MAP_LOOKUP_SPLIT_LOOP ref var pRef = ref probes.GetSurePresentItemRef(hashIndex); while (pRef > probe) { pRef = ref probes.GetSurePresentItemRef(++hashIndex); ++probe; #if DEBUG - ++FindEqualProbeAndHashCheckCount; + ++LookupGreaterProbeCheckCount; #endif } @@ -1395,29 +1388,10 @@ private int TryGetEntryAndHashIndex(K key, int hash, int indexMask, pRef = ref probes.GetSurePresentItemRef(++hashIndex); ++probe; #if DEBUG - ++FindEqualProbeAndHashCheckCount; + ++LookupEqualProbeCheckCount; #endif } -#else - ref var pRef = ref probes.GetSurePresentItemRef(hashIndex); - while (pRef >= probe) - { - // Compare the hash middle part, then the indexed key - if (pRef == probe) - { - var h = hashesAndIndexes.GetSurePresentItemRef(hashIndex); - if ((h & ~indexMask) == (hash & ~indexMask)) - if (default(TEq).Equals(GetSurePresentKey(h & indexMask), key)) - return h & indexMask; - } - pRef = ref probes.GetSurePresentItemRef(++hashIndex); - ++probe; -#if DEBUG - ++FindEqualProbeAndHashCheckCount; -#endif - } -#endif - return -1; // not found + return -1; } [UnscopedRef] @@ -1430,7 +1404,7 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) var indexMask = (1 << _capacityBitShift) - 1; // If the free space is less than ~1/8 of capacity (~12.5%) then Resize probes and hashes (but not the entries). - // Note: this check done here even before lookup (but not in insert), because later we rely on the Lookup hashIndex to do a first insert + // Note: this check done here even before lookup and not later at insert, because later we rely on the Lookup hashIndex to do a first insert. if (_isCapacityPaddingFilled || indexMask - _count <= (indexMask >>> MinFreeCapacityShift)) indexMask = ResizeProbesAndHashes(indexMask); @@ -1483,9 +1457,10 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) #endif } - _isCapacityPaddingFilled = hashIndex + 1 == _probes.Length; + // @pre - keep the last slot empty so the ResizeProbesAndHashes can rely on it to Stop when scanning the padding span beyond oldCapacity + _isCapacityPaddingFilled = hashIndex + 2 == _probes.Length; - return ref _entries.AddKeyAndGetEntryRef(key, _count++ - _stackEntries.Capacity); + return ref _heapEntries.AddKeyAndGetEntryRef(key, _count++ - _stackEntries.Capacity); } [MethodImpl((MethodImplOptions)256)] @@ -1506,14 +1481,14 @@ private void PutHashAndIndexWithoutResizing(int indexMask, int hash, int index) #endif // Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - ref var pRef = ref probes.GetSurePresentItemRef(hashIndex); byte probe = 1; + ref var pRef = ref probes.GetSurePresentItemRef(hashIndex); while (pRef >= probe) { - pRef = ref probes.GetSurePresentItemRef(++hashIndex); // no need to wrap around because the probes have the extra capacity ++probe; + pRef = ref probes.GetSurePresentItemRef(++hashIndex); // no need to wrap around because the probes have the extra capacity #if DEBUG - ++PutHashAndIndexWithoutResizing_InitialProbeCheckCount; + ++PutHashAndIndexWithoutResizing_ProbeGreaterOrEqualCheckCount; #endif } @@ -1551,7 +1526,8 @@ private void PutHashAndIndexWithoutResizing(int indexMask, int hash, int index) #endif } - _isCapacityPaddingFilled = hashIndex + 1 == _probes.Length; + // @pre - keep the last slot empty so the ResizeProbesAndHashes can rely on it to Stop when scanning the padding span beyond oldCapacity + _isCapacityPaddingFilled = hashIndex + 2 == _probes.Length; } /// @@ -1614,10 +1590,10 @@ private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) PutHashAndIndexWithoutResizing(indexMask, hash, stackCap); ++_count; - _entries.Init(stackCap); // Give the heap entries the same initial capacity as Stack, effectively doubling the capacity + _heapEntries.Init(stackCap); // Give the heap entries the same initial capacity as Stack, effectively doubling the capacity // Set the key for the first entry, which is the 0 index in the entries - ref var newEntry = ref _entries.GetSurePresentEntryRef(0); + ref var newEntry = ref _heapEntries.GetSurePresentEntryRef(0); newEntry.Key = key; return ref newEntry; } @@ -1660,7 +1636,7 @@ private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key, int hash) PutHashAndIndexWithoutResizing(indexMask, hash, _count); - return ref _entries.AddKeyAndGetEntryRef(key, (_count++) - _stackEntries.Capacity); + return ref _heapEntries.AddKeyAndGetEntryRef(key, (_count++) - _stackEntries.Capacity); } /// @@ -1699,13 +1675,9 @@ public int TryGetIndex(K key) /// public bool ContainsKey(K key) => TryGetIndex(key) != -1; - [MethodImpl((MethodImplOptions)256)] internal int TryGetIndexInEntries(K key, int hash) { -#if DEBUG - ++TryGetRefInEntries_Count; -#endif #if NET7_0_OR_GREATER ref var probes = ref MemoryMarshal.GetArrayDataReference(_probes); ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); @@ -1767,36 +1739,11 @@ internal int ResizeProbesAndHashes(int indexMask) var oldProbes = _probes; var oldHashes = _packedHashesAndIndexes; #endif -#if DEBUG - var newProbeCheckCount = 0; - var inSkipEmptyProbesSpan = false; - var skipEmptyProbesSpanCount = 0; - var totalSkipEmptyProbesCount = 0; -#endif - //todo: @wip remember that the padding cap is for wrap around items, - // and the invariant for them is the there no 0 probes in the wrap-around, so the loop may be stopped at the first 0 probe - // Considering that the most zero probes would be in a padding (as its is half of the capacity) we may consider - // to split the loop into two: for capacity and for padding. - for (var i = 0; i < _probes.Length; ++i) + var i = 0; + var oldProbe = 0; + while (i < oldCapacity) { - var oldProbe = oldProbes.GetSurePresentItem(i); -#if DEBUG - if (oldProbe == 0) - { - if (!inSkipEmptyProbesSpan) - { - inSkipEmptyProbesSpan = true; - ++skipEmptyProbesSpanCount; - } - ++totalSkipEmptyProbesCount; - if (i >= oldCapacity) - ++ResizeProbesAndHashes_LastWrapAroundEmptyProbesSpanCount; - } - else - { - inSkipEmptyProbesSpan = false; - } -#endif + oldProbe = oldProbes.GetSurePresentItem(i); if (oldProbe != 0) // for the non-empty probe and therefore hash { var oldHashAndIndex = oldHashesAndIndexes.GetSurePresentItem(i); @@ -1804,8 +1751,7 @@ internal int ResizeProbesAndHashes(int indexMask) // First, calculate the ideal hash index for the current probe, it should be in the range of the old capacity/index mask var oldHashIndex = i - (oldProbe - 1); Debug.Assert(oldHashIndex < oldCapacity, "Old hash index should be less than old capacity"); - var nextHashBitInIndex = oldHashAndIndex & oldCapacity; - var newHashIndex = nextHashBitInIndex | oldHashIndex; + var newHashIndex = (oldHashAndIndex & oldCapacity) | oldHashIndex; Debug.Assert(newHashIndex == oldHashIndex || newHashIndex == oldHashIndex + oldCapacity, "New hash index should be either the same as old or with the next bit set to the old capacity"); // No need for robin-hooding because we already did it for the old hashes and now just filling the hashes into the new array which are already in order @@ -1813,24 +1759,45 @@ internal int ResizeProbesAndHashes(int indexMask) ref var pRef = ref newProbes.GetSurePresentItemRef(newHashIndex); while (pRef != 0) { - pRef = ref newProbes.GetSurePresentItemRef(++newHashIndex); ++probe; -#if DEBUG - ++newProbeCheckCount; -#endif + pRef = ref newProbes.GetSurePresentItemRef(++newHashIndex); } + pRef = probe; newHashes.GetSurePresentItemRef(newHashIndex) = oldHashAndIndex & ~oldCapacity; } + ++i; } + + // Starting from the last oldProbe, if it is 0 then we done and the padding is empty + while (true) + { + oldProbe = oldProbes.GetSurePresentItem(i); + if (oldProbe == 0) + break; + + var oldHashAndIndex = oldHashesAndIndexes.GetSurePresentItem(i); + + var oldHashIndex = i - (oldProbe - 1); + var newHashIndex = (oldHashAndIndex & oldCapacity) | oldHashIndex; + + byte probe = 1; + ref var pRef = ref newProbes.GetSurePresentItemRef(newHashIndex); + while (pRef != 0) + { + ++probe; + pRef = ref newProbes.GetSurePresentItemRef(++newHashIndex); + } + + pRef = probe; + newHashes.GetSurePresentItemRef(newHashIndex) = oldHashAndIndex & ~oldCapacity; + ++i; + } + ++_capacityBitShift; _probes = newProbesArr; _packedHashesAndIndexes = newHashesAndIndexes; -#if DEBUG - ResizeProbesAndHashes_AverageProbeCheckCount = newProbeCheckCount / _count; - ResizeProbesAndHashes_AverageEmptyProbeCount = totalSkipEmptyProbesCount / skipEmptyProbesSpanCount; -#endif #if VERIFY_MAP this.VerifyKeyHasMetaInfo(static (x, msg) => Debug.Assert(x, msg), Use.It); #endif diff --git a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs index f5aca994..38a2a68f 100644 --- a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs +++ b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs @@ -67,11 +67,45 @@ public void Benchmark_test_with_Add_and_Lookup_100_items() const int Count = 100; Debug.Assert(Count <= 1000, "Count should be less than or equal to 1000 for this test to work correctly."); -#if !FIB - var m = new SmallMap16>(); -#else var m = new SmallMap16>(); + ref var map = ref m.Map; + + var presentTypes = _allKeys.Take(Count).ToArray(); + var missingKeys = _allKeys.Skip(1000).Take(Count).ToArray(); + + var seed = new Random(42); + var randomPresentKeys = presentTypes.OrderBy(_ => seed.Next()).ToArray(); + + foreach (var type in presentTypes) + map.AddOrUpdate(type, "a"); + + map.Verify(static (cond, msg) => Assert.IsTrue(cond, msg), presentTypes, Pass.It); + + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) + { + var result = map.TryGetEntryRef(randomPresentKeys[i], out var found); + if (found) + count += result.Value.Length; + _ = map.TryGetEntryRef(missingKeys[i], out found); + if (!found) + --count; + } + Assert.AreEqual(0, count); + +#if DEBUG + Assert.Greater(m.Map.LookupGreaterProbeCheckCount, 200); // 290 no GoldenRatio, 236 with GoldenRatio ~ 20% difference #endif + } + + [Test] + public void Benchmark_test_with_Add_and_Lookup_1000_items() + { + const int Count = 1000; + Debug.Assert(Count <= 1000, "Count should be less than or equal to 1000 for this test to work correctly."); + + var m = new SmallMap16>(); ref var map = ref m.Map; var presentTypes = _allKeys.Take(Count).ToArray(); @@ -83,7 +117,7 @@ public void Benchmark_test_with_Add_and_Lookup_100_items() foreach (var type in presentTypes) map.AddOrUpdate(type, "a"); - // map.Verify(static (cond, msg) => Assert.IsTrue(cond, msg), presentTypes, Use>.It); + map.Verify(static (cond, msg) => Assert.IsTrue(cond, msg), presentTypes, Pass.It); var count = 0; var iters = Count / 2; @@ -99,7 +133,7 @@ public void Benchmark_test_with_Add_and_Lookup_100_items() Assert.AreEqual(0, count); #if DEBUG - Assert.Greater(m.Map.FindEqualProbeAndHashCheckCount, 200); // 290 no GoldenRatio, 236 with GoldenRatio ~ 20% difference + Assert.Greater(m.Map.LookupGreaterProbeCheckCount, 200); // 290 no GoldenRatio, 236 with GoldenRatio ~ 20% difference #endif } From fbcd177a67bf29e8737f3bee5e9cf8d3199b67d0 Mon Sep 17 00:00:00 2001 From: dadhi Date: Thu, 31 Jul 2025 16:23:25 +0200 Subject: [PATCH 26/54] cleanup --- playground/Experiments/FEC_ImTools.cs | 36 +++---------------- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 2 +- .../Playground/FecSmallMapTests.cs | 14 ++++---- 3 files changed, 14 insertions(+), 38 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 6736290f..180a36f7 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -1303,7 +1303,7 @@ public struct SmallMap _count; /// Access to the key-value entries - public THeapEntries Entries => _heapEntries; + public THeapEntries HeapEntries => _heapEntries; /// Capacity calculates as `1 leftShift capacityBitShift` public SmallMap(byte capacityBitShift) @@ -1739,11 +1739,9 @@ internal int ResizeProbesAndHashes(int indexMask) var oldProbes = _probes; var oldHashes = _packedHashesAndIndexes; #endif - var i = 0; - var oldProbe = 0; - while (i < oldCapacity) + for (var i = 0; i < _probes.Length; ++i) { - oldProbe = oldProbes.GetSurePresentItem(i); + var oldProbe = oldProbes.GetSurePresentItem(i); if (oldProbe != 0) // for the non-empty probe and therefore hash { var oldHashAndIndex = oldHashesAndIndexes.GetSurePresentItem(i); @@ -1766,32 +1764,8 @@ internal int ResizeProbesAndHashes(int indexMask) pRef = probe; newHashes.GetSurePresentItemRef(newHashIndex) = oldHashAndIndex & ~oldCapacity; } - ++i; - } - - // Starting from the last oldProbe, if it is 0 then we done and the padding is empty - while (true) - { - oldProbe = oldProbes.GetSurePresentItem(i); - if (oldProbe == 0) - break; - - var oldHashAndIndex = oldHashesAndIndexes.GetSurePresentItem(i); - - var oldHashIndex = i - (oldProbe - 1); - var newHashIndex = (oldHashAndIndex & oldCapacity) | oldHashIndex; - - byte probe = 1; - ref var pRef = ref newProbes.GetSurePresentItemRef(newHashIndex); - while (pRef != 0) - { - ++probe; - pRef = ref newProbes.GetSurePresentItemRef(++newHashIndex); - } - - pRef = probe; - newHashes.GetSurePresentItemRef(newHashIndex) = oldHashAndIndex & ~oldCapacity; - ++i; + else if (i >= oldCapacity) + break; // No need to continue scanning the padding, because it is empty and we are done with the old hashes and probes } ++_capacityBitShift; diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index ebf53542..5ed5ca14 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -2372,7 +2372,7 @@ .NET SDK 9.0.203 */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] // [Params(10, 100, 1000)] - [Params(10, 100, 1000)] + [Params(1000)] public int Count; private Type[] _presentKeys; diff --git a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs index 38a2a68f..40f8c98d 100644 --- a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs +++ b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs @@ -1,12 +1,12 @@ - +// #define VERIFY_MAP + +namespace FastExpressionCompiler.ImTools.UnitTests; + using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using NUnit.Framework; - -namespace FastExpressionCompiler.ImTools.UnitTests; - using static SmallMap; [TestFixture] @@ -79,7 +79,9 @@ public void Benchmark_test_with_Add_and_Lookup_100_items() foreach (var type in presentTypes) map.AddOrUpdate(type, "a"); +#if VERIFY_MAP map.Verify(static (cond, msg) => Assert.IsTrue(cond, msg), presentTypes, Pass.It); +#endif var count = 0; var iters = Count / 2; @@ -95,7 +97,7 @@ public void Benchmark_test_with_Add_and_Lookup_100_items() Assert.AreEqual(0, count); #if DEBUG - Assert.Greater(m.Map.LookupGreaterProbeCheckCount, 200); // 290 no GoldenRatio, 236 with GoldenRatio ~ 20% difference + Assert.Greater(m.Map.LookupGreaterProbeCheckCount + m.Map.LookupEqualProbeCheckCount, 200); #endif } @@ -133,7 +135,7 @@ public void Benchmark_test_with_Add_and_Lookup_1000_items() Assert.AreEqual(0, count); #if DEBUG - Assert.Greater(m.Map.LookupGreaterProbeCheckCount, 200); // 290 no GoldenRatio, 236 with GoldenRatio ~ 20% difference + Assert.Greater(m.Map.LookupGreaterProbeCheckCount + m.Map.LookupEqualProbeCheckCount, 2000); #endif } From a9b9c9c2dd313684ee92959ec6e3a1a8551fce39 Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 17 Aug 2025 16:03:45 +0200 Subject: [PATCH 27/54] added StableArrayEntries and @wip with Lookup SIMD --- playground/Experiments/FEC_ImTools.cs | 332 ++++++++++++++++-- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 49 ++- .../Playground/FecSmallMapTests.cs | 20 +- 3 files changed, 349 insertions(+), 52 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 180a36f7..d3287305 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -26,9 +26,9 @@ THE SOFTWARE. // ReSharper disable once InconsistentNaming #nullable disable -// #if DEBUG +#if DEBUG // #define VERIFY_MAP -// #endif +#endif #if !NETSTANDARD2_0_OR_GREATER && !NET472 #define SUPPORTS_UNSAFE @@ -1001,6 +1001,230 @@ public ref TEntry AddKeyAndGetEntryRef(K key, int index) } } + /// Stores the entries in a single dynamically reallocated growing array + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct StableArrayEntries : IEntries + where TEntry : struct, IEntry + where TEq : struct, IEq + { + const int RegularBucketBits = 12; + const int RegularBucketSize = 1 << RegularBucketBits; // 4096 + const int RegularBucketMask = RegularBucketSize - 1; + const int UpToBucket32 = 32; + const int UpToBucket64 = UpToBucket32 + 64; + const int UpToBucket128 = UpToBucket64 + 128; + const int UpToBucket256 = UpToBucket128 + 256; + const int UpToBucket512 = UpToBucket256 + 512; + const int UpToBucket1024 = UpToBucket512 + 1024; + const int UpToBucket2048 = UpToBucket1024 + 2048; + +#if NET7_0_OR_GREATER + public static readonly Vector256 vUpToBucket = Vector256.Create( + UpToBucket32, UpToBucket64, UpToBucket128, UpToBucket256, + UpToBucket512, UpToBucket1024, UpToBucket2048, + // the last 2 things are the same for the arithmetic to work in FindBucket + UpToBucket2048); +#endif + internal TEntry[] _bucket32; + internal TEntry[] _bucket64; + internal TEntry[] _bucket128; + internal TEntry[] _bucket256; + internal TEntry[] _bucket512; + internal TEntry[] _bucket1024; + internal TEntry[] _bucket2048; + internal TEntry[][] _regularBuckets; + + /// + [MethodImpl((MethodImplOptions)256)] + private TEntry[] GetBucket(int bucketIndex) + { + Debug.Assert(bucketIndex >= 0 && bucketIndex <= 6, "Bucket index should be in the range 0..6"); +#if SUPPORTS_UNSAFE + return Unsafe.Add(ref _bucket32, bucketIndex); +#else + return bucketIndex switch + { + 0 => _bucket32, + 1 => _bucket64, + 2 => _bucket128, + 3 => _bucket256, + 4 => _bucket512, + 5 => _bucket1024, + _ => _bucket2048, + }; +#endif + } + + /// + public void Init(int capacity = 0) + { + _bucket32 = new TEntry[UpToBucket32]; // always allocate + switch (capacity) + { + case <= UpToBucket32: break; + case <= UpToBucket64: + _bucket64 = new TEntry[UpToBucket64]; + break; + case <= UpToBucket128: + _bucket64 = new TEntry[UpToBucket64]; + _bucket128 = new TEntry[UpToBucket128]; + break; + case <= UpToBucket256: + _bucket64 = new TEntry[UpToBucket64]; + _bucket128 = new TEntry[UpToBucket128]; + _bucket256 = new TEntry[UpToBucket256]; + break; + case <= UpToBucket512: + _bucket64 = new TEntry[UpToBucket64]; + _bucket128 = new TEntry[UpToBucket128]; + _bucket256 = new TEntry[UpToBucket256]; + _bucket512 = new TEntry[UpToBucket512]; + break; + case <= UpToBucket1024: + _bucket64 = new TEntry[UpToBucket64]; + _bucket128 = new TEntry[UpToBucket128]; + _bucket256 = new TEntry[UpToBucket256]; + _bucket512 = new TEntry[UpToBucket512]; + _bucket1024 = new TEntry[UpToBucket1024]; + break; + case <= UpToBucket2048: + _bucket64 = new TEntry[UpToBucket64]; + _bucket128 = new TEntry[UpToBucket128]; + _bucket256 = new TEntry[UpToBucket256]; + _bucket512 = new TEntry[UpToBucket512]; + _bucket1024 = new TEntry[UpToBucket1024]; + _bucket2048 = new TEntry[UpToBucket2048]; + break; + default: + _bucket64 = new TEntry[UpToBucket64]; + _bucket128 = new TEntry[UpToBucket128]; + _bucket256 = new TEntry[UpToBucket256]; + _bucket512 = new TEntry[UpToBucket512]; + _bucket1024 = new TEntry[UpToBucket1024]; + _bucket2048 = new TEntry[UpToBucket2048]; + + var remainingCapacity = capacity - UpToBucket2048; + var bucketCount = (remainingCapacity >>> RegularBucketBits) + 1; + _regularBuckets = new TEntry[bucketCount][]; + for (var i = 0; i < bucketCount; ++i) + _regularBuckets[i] = new TEntry[RegularBucketSize]; + break; + } + } + + /// + [MethodImpl((MethodImplOptions)256)] + public ref TEntry GetSurePresentEntryRef(int index) + { +#if NET7_0_OR_GREATER + var vIndex = Vector256.Create(index); + var vIndexThanBucket = Vector256.LessThan(vIndex, vUpToBucket); + if (vIndexThanBucket != Vector256.Zero) + { + var bucketIndex = BitOperations.TrailingZeroCount(Vector256.ExtractMostSignificantBits(vIndexThanBucket)); + var entryIndex = index - vUpToBucket.GetElement(bucketIndex); + var bucket = GetBucket(bucketIndex); + return ref bucket.GetSurePresentItemRef(entryIndex); + } + index -= UpToBucket2048; + var regularBucketIndex = index >>> RegularBucketBits; + var regularEntryIndex = index & RegularBucketMask; + var regularBucket = _regularBuckets.GetSurePresentItemRef(regularBucketIndex); + return ref regularBucket.GetSurePresentItemRef(regularEntryIndex); +#else + switch (index) + { + case < UpToBucket32: + return ref _bucket32.GetSurePresentItemRef(index - UpToBucket32); + case < UpToBucket64: + return ref _bucket64.GetSurePresentItemRef(index - UpToBucket64); + case < UpToBucket128: + return ref _bucket128.GetSurePresentItemRef(index - UpToBucket128); + case < UpToBucket256: + return ref _bucket256.GetSurePresentItemRef(index - UpToBucket256); + case < UpToBucket512: + return ref _bucket512.GetSurePresentItemRef(index - UpToBucket512); + case < UpToBucket1024: + return ref _bucket1024.GetSurePresentItemRef(index - UpToBucket1024); + case < UpToBucket2048: + return ref _bucket2048.GetSurePresentItemRef(index - UpToBucket2048); + default: + index -= UpToBucket2048; + var regularBucketIndex = index >>> RegularBucketBits; + var regularEntryIndex = index & RegularBucketMask; + return ref _regularBuckets + .GetSurePresentItemRef(regularBucketIndex) + .GetSurePresentItemRef(regularEntryIndex); + }; +#endif + } + + /// + [MethodImpl((MethodImplOptions)256)] + public ref TEntry AddKeyAndGetEntryRef(K key, int index) + { +#if NET7_0_OR_GREATER + var vIndex = Vector256.Create(index); + var vIndexThanBucket = Vector256.LessThan(vIndex, vUpToBucket); + if (vIndexThanBucket != Vector256.Zero) + { + var bucketIndex = BitOperations.TrailingZeroCount(Vector256.ExtractMostSignificantBits(vIndexThanBucket)); + var entryIndex = index - vUpToBucket.GetElement(bucketIndex); + var bucket = GetBucket(bucketIndex) ?? new TEntry[UpToBucket32 << bucketIndex]; + return ref bucket.GetSurePresentItemRef(entryIndex); + } + index -= UpToBucket2048; + var regularBucketIndex = index >>> RegularBucketBits; + var regularEntryIndex = index & RegularBucketMask; + if (_regularBuckets == null) + _regularBuckets = new TEntry[regularBucketIndex + 1][]; + else if (_regularBuckets.Length <= regularBucketIndex) + Array.Resize(ref _regularBuckets, regularBucketIndex + 1); + ref var regularBucket = ref _regularBuckets.GetSurePresentItemRef(regularBucketIndex); + if (regularBucket == null) + regularBucket = new TEntry[RegularBucketSize]; + return ref regularBucket.GetSurePresentItemRef(regularEntryIndex); +#else + switch (index) + { + case < UpToBucket32: + _bucket32 ??= new TEntry[UpToBucket32]; + return ref _bucket32.GetSurePresentItemRef(index - UpToBucket32); + case < UpToBucket64: + _bucket64 ??= new TEntry[UpToBucket64]; + return ref _bucket64.GetSurePresentItemRef(index - UpToBucket64); + case < UpToBucket128: + _bucket128 ??= new TEntry[UpToBucket128]; + return ref _bucket128.GetSurePresentItemRef(index - UpToBucket128); + case < UpToBucket256: + _bucket256 ??= new TEntry[UpToBucket256]; + return ref _bucket256.GetSurePresentItemRef(index - UpToBucket256); + case < UpToBucket512: + _bucket512 ??= new TEntry[UpToBucket512]; + return ref _bucket512.GetSurePresentItemRef(index - UpToBucket512); + case < UpToBucket1024: + _bucket1024 ??= new TEntry[UpToBucket1024]; + return ref _bucket1024.GetSurePresentItemRef(index - UpToBucket1024); + case < UpToBucket2048: + _bucket2048 ??= new TEntry[UpToBucket2048]; + return ref _bucket2048.GetSurePresentItemRef(index - UpToBucket2048); + default: + index -= UpToBucket2048; + var regularBucketIndex = index >>> RegularBucketBits; + var regularEntryIndex = index & RegularBucketMask; + if (_regularBuckets == null) + _regularBuckets = new TEntry[regularBucketIndex + 1][]; + else if (_regularBuckets.Length <= regularBucketIndex) + Array.Resize(ref _regularBuckets, regularBucketIndex + 1); + ref var regularBucket = ref _regularBuckets.GetSurePresentItemRef(regularBucketIndex); + if (regularBucket == null) + regularBucket = new TEntry[RegularBucketSize]; + return ref regularBucket.GetSurePresentItemRef(regularEntryIndex); + }; +#endif + } + } + /// Lookup for the K in the TStackEntries, first by calculating it hash with TEq and searching the hash in the TStackHashes [MethodImpl((MethodImplOptions)256)] public static int TryGetStackEntryIndex( @@ -1141,16 +1365,18 @@ public static void VerifyNoDuplicateKeys(this TMap map, ActionVerifies that the probes are consistently increasing - public static void VerifyProbesRobinHoodInvariants(this TMap map, Action assertCond, Pass _ = default) + public static void VerifyProbesRobinHoodInvariants(this TMap map, Action assertCond, Pass _ = default, + int startIndex = 0, int endIndex = -1) where TMap : struct, IMapImpl { - var prevProbe = 0; var probes = map.Probes; - for (var i = 0; i < probes.Length; i++) + var prevProbe = startIndex > 0 ? probes[startIndex - 1] : 0; + endIndex = endIndex == -1 ? probes.Length : endIndex; + for (var i = startIndex; i < endIndex; i++) { var probe = probes[i]; var invariant = prevProbe >= probe || prevProbe + 1 == probe; - assertCond(invariant, $"Probe invariant failed: prevProbe:{prevProbe}, probe:{probe}"); + assertCond(invariant, $"Probe invariant failed for probe index {i}, probe:{probe}, prevProbe:{prevProbe}"); prevProbe = probe; } } @@ -1249,11 +1475,6 @@ public struct SmallMap LookupCount == 0 ? 0 : LookupGreaterProbeCheckCount / LookupCount; - public int LookupEqualProbeCheckCount = 0; - public int LookupEqualProbeCheckCountAverage => LookupCount == 0 ? 0 : LookupEqualProbeCheckCount / LookupCount; public int AddOrGetRefInEntries_Count = 0; public int AddOrGetRefInEntries_RobinHoodCount = 0; public int AddOrGetRefInEntries_RobinHoodProbeCheckCount = 0; @@ -1353,6 +1574,10 @@ public ref TEntry GetSurePresentEntryRef(int index) return ref _stackEntries.GetSurePresentItemRef(index); } +#if NET7_0_OR_GREATER + static readonly Vector128 vProbeStep = Vector128.Create((byte)16); +#endif + /// The actual Lookup implementation in a single place [MethodImpl((MethodImplOptions)256)] private int TryGetEntryAndHashIndex(K key, int hash, int indexMask, @@ -1364,20 +1589,75 @@ private int TryGetEntryAndHashIndex(K key, int hash, int indexMask, #endif ) { -#if DEBUG - ++LookupCount; -#endif + Debug.Assert(probe == 1); +#if NET7_0_OR_GREATER // Skip over the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions + if (Vector128.IsHardwareAccelerated) + { + var vExpectedProbe = Vector128.Create( + (byte)1, (byte)2, (byte)3, (byte)4, (byte)5, (byte)6, (byte)7, (byte)8, + (byte)9, (byte)10, (byte)11, (byte)12, (byte)13, (byte)14, (byte)15, (byte)16); + + start: + // Load 16 probe bytes - safe due to padding + var vProbe = Vector128.LoadUnsafe(ref probes.GetSurePresentItemRef(hashIndex)); + + var vGreaterMask = Vector128.GreaterThan(vProbe, vExpectedProbe); + var vEqualsMask = Vector128.Equals(vProbe, vExpectedProbe); + var greaterMask = vGreaterMask.ExtractMostSignificantBits(); + var equalsMask = vEqualsMask.ExtractMostSignificantBits(); + + if (greaterMask == 0xFFFF) + { + probe += 16; + hashIndex += 16; + vExpectedProbe = Vector128.Add(vExpectedProbe, vProbeStep); + goto start; + } + + var firstEqualIndex = BitOperations.TrailingZeroCount(equalsMask); + var firstNonGreaterIndex = BitOperations.TrailingZeroCount(~greaterMask); + + // Setting the hashIndex and probe before return because they are passed by-ref + // and need to be set to the right values for the not-found case + probe += (byte)firstNonGreaterIndex; + hashIndex += firstNonGreaterIndex; + + if (firstNonGreaterIndex != firstEqualIndex) + return -1; + + compareHashes: + var equalCount = BitOperations.PopCount(equalsMask); + var hashIndexEnd = hashIndex + equalCount; + while (hashIndex < hashIndexEnd) + { + var h = hashesAndIndexes.GetSurePresentItemRef(hashIndex); + if ((h & ~indexMask) == (hash & ~indexMask)) + if (default(TEq).Equals(GetSurePresentKey(h & indexMask), key)) + return h & indexMask; + ++probe; + ++hashIndex; + } + + if (firstEqualIndex + equalCount == 16) + { + firstEqualIndex = 0; + vProbe = Vector128.LoadUnsafe(ref probes.GetSurePresentItemRef(hashIndex)); + vExpectedProbe = Vector128.Add(vExpectedProbe, vProbeStep); + equalsMask = vEqualsMask.ExtractMostSignificantBits(); + if (equalsMask != 0) + goto compareHashes; + } + + return -1; + } +#endif ref var pRef = ref probes.GetSurePresentItemRef(hashIndex); while (pRef > probe) { pRef = ref probes.GetSurePresentItemRef(++hashIndex); ++probe; -#if DEBUG - ++LookupGreaterProbeCheckCount; -#endif } - while (pRef == probe) { // Compare the hash middle part, then the indexed key @@ -1387,9 +1667,6 @@ private int TryGetEntryAndHashIndex(K key, int hash, int indexMask, return h & indexMask; pRef = ref probes.GetSurePresentItemRef(++hashIndex); ++probe; -#if DEBUG - ++LookupEqualProbeCheckCount; -#endif } return -1; } @@ -1433,6 +1710,10 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) // If the robin hooded probe is empty then we stop probe = pRobinHooded; +#if VERIFY_MAP + var startIndex = hashIndex; + var isRobinHooded = probe != 0; +#endif while (probe != 0) { pRef = ref probes.GetSurePresentItemRef(++hashIndex); @@ -1456,6 +1737,9 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) ++AddOrGetRefInEntries_RobinHoodProbeCheckCount; #endif } +#if VERIFY_MAP + this.VerifyProbesRobinHoodInvariants(static (cond, msg) => Debug.Assert(cond, msg), Pass.It, startIndex, hashIndex + 1); +#endif // @pre - keep the last slot empty so the ResizeProbesAndHashes can rely on it to Stop when scanning the padding span beyond oldCapacity _isCapacityPaddingFilled = hashIndex + 2 == _probes.Length; @@ -1732,7 +2016,7 @@ internal int ResizeProbesAndHashes(int indexMask) ref var newProbes = ref MemoryMarshal.GetArrayDataReference(newProbesArr); ref var newHashes = ref MemoryMarshal.GetArrayDataReference(newHashesAndIndexes); ref var oldProbes = ref MemoryMarshal.GetArrayDataReference(_probes); - ref var oldHashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); + ref var oldHashes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); #else var newProbes = newProbesArr; var newHashes = newHashesAndIndexes; @@ -1744,7 +2028,7 @@ internal int ResizeProbesAndHashes(int indexMask) var oldProbe = oldProbes.GetSurePresentItem(i); if (oldProbe != 0) // for the non-empty probe and therefore hash { - var oldHashAndIndex = oldHashesAndIndexes.GetSurePresentItem(i); + var oldHashAndIndex = oldHashes.GetSurePresentItem(i); // First, calculate the ideal hash index for the current probe, it should be in the range of the old capacity/index mask var oldHashIndex = i - (oldProbe - 1); @@ -1773,7 +2057,7 @@ internal int ResizeProbesAndHashes(int indexMask) _packedHashesAndIndexes = newHashesAndIndexes; #if VERIFY_MAP - this.VerifyKeyHasMetaInfo(static (x, msg) => Debug.Assert(x, msg), Use.It); + this.VerifyKeyHasMetaInfo(static (x, msg) => Debug.Assert(x, msg), Pass.It); #endif return newIndexMask; } diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 5ed5ca14..1ccd1f60 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -2358,21 +2358,40 @@ .NET SDK 9.0.203 ## After probes go to their own array + padding + wrapping - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | - |--------------------------------------------------------- |------ |------------:|----------:|----------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 108.2 ns | 1.04 ns | 0.87 ns | 0.36 | 0.01 | 1 | - | - | - | 0.00 | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 297.9 ns | 5.89 ns | 7.45 ns | 1.00 | 0.03 | 2 | 0.1707 | - | 1072 B | 1.00 | - | | | | | | | | | | | | | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,146.4 ns | 42.80 ns | 66.64 ns | 1.00 | 0.04 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,220.2 ns | 43.51 ns | 60.99 ns | 1.04 | 0.04 | 1 | 0.8659 | 0.0038 | 5440 B | 0.73 | - | | | | | | | | | | | | | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 23,454.6 ns | 455.76 ns | 695.99 ns | 1.00 | 0.04 | 1 | 9.1553 | 1.2817 | 57808 B | 1.00 | - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 47,784.8 ns | 580.37 ns | 453.11 ns | 2.04 | 0.06 | 2 | 8.9111 | 0.0610 | 55976 B | 0.97 | + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |------------:|----------:|------------:|------------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 111.6 ns | 1.16 ns | 0.97 ns | 111.8 ns | 0.38 | 0.01 | 1 | - | - | - | 0.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 205.1 ns | 3.46 ns | 3.24 ns | 204.4 ns | 0.70 | 0.02 | 2 | 0.1147 | - | 720 B | 0.67 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 292.2 ns | 5.53 ns | 5.68 ns | 291.4 ns | 1.00 | 0.03 | 3 | 0.1707 | - | 1072 B | 1.00 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,121.8 ns | 42.34 ns | 91.14 ns | 2,070.2 ns | 1.00 | 0.06 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,181.5 ns | 43.38 ns | 91.50 ns | 2,127.6 ns | 1.03 | 0.06 | 1 | 0.8659 | 0.0038 | 5440 B | 0.73 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,347.8 ns | 14.31 ns | 12.69 ns | 2,343.7 ns | 1.11 | 0.05 | 2 | 0.8507 | - | 5344 B | 0.71 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 23,640.4 ns | 280.02 ns | 248.23 ns | 23,620.0 ns | 1.00 | 0.01 | 1 | 9.1553 | 1.2817 | 57808 B | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 45,226.9 ns | 293.55 ns | 245.13 ns | 45,221.9 ns | 1.91 | 0.02 | 2 | 7.8735 | 0.3052 | 49544 B | 0.86 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 49,441.1 ns | 987.34 ns | 1,780.38 ns | 49,657.1 ns | 2.09 | 0.08 | 3 | 8.9111 | 0.0610 | 55976 B | 0.97 | + + ## Initial SIMD for Lookup - Regression in @perf + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |------------:|----------:|------------:|------------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 111.3 ns | 1.88 ns | 1.67 ns | 110.6 ns | 0.38 | 0.01 | 1 | - | - | - | 0.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 211.8 ns | 4.21 ns | 7.91 ns | 210.4 ns | 0.72 | 0.03 | 2 | 0.1147 | - | 720 B | 0.67 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 293.1 ns | 5.18 ns | 4.33 ns | 293.5 ns | 1.00 | 0.02 | 3 | 0.1707 | - | 1072 B | 1.00 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,112.8 ns | 19.17 ns | 16.00 ns | 2,108.5 ns | 1.00 | 0.01 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,433.3 ns | 47.99 ns | 95.84 ns | 2,383.6 ns | 1.15 | 0.05 | 2 | 0.8507 | - | 5344 B | 0.71 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,548.7 ns | 50.70 ns | 106.95 ns | 2,491.5 ns | 1.21 | 0.05 | 2 | 0.8659 | 0.0038 | 5440 B | 0.73 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 23,127.6 ns | 233.36 ns | 182.20 ns | 23,143.0 ns | 1.00 | 0.01 | 1 | 9.1553 | 1.2817 | 57808 B | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 45,630.3 ns | 912.10 ns | 1,735.36 ns | 44,696.8 ns | 1.97 | 0.08 | 2 | 7.8735 | 0.3052 | 49544 B | 0.86 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 49,295.9 ns | 327.44 ns | 273.43 ns | 49,364.4 ns | 2.13 | 0.02 | 3 | 8.9111 | 0.0610 | 55976 B | 0.97 | + */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] - // [Params(10, 100, 1000)] - [Params(1000)] + [Params(10, 100, 1000)] + // [Params(1000)] public int Count; private Type[] _presentKeys; @@ -2827,7 +2846,7 @@ public int DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent() return count; } - // [Benchmark] + [Benchmark] public int SmallMap_PopulateThenLookup_HalfMissed_HalfPresent() { var map = new SmallMapTypeString(); @@ -2860,10 +2879,10 @@ public int FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent() var iters = Count / 2; for (var i = 0; i < iters; ++i) { - var result = map.TryGetEntryRef(_randomPresentKeys[i], out var found); + ref var result = ref map.TryGetEntryRef(_randomPresentKeys[i], out var found); if (found) count += result.Value.Length; - _ = map.TryGetEntryRef(_missingKeys[i], out found); + _ = ref map.TryGetEntryRef(_missingKeys[i], out found); if (!found) --count; } diff --git a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs index 40f8c98d..144643ee 100644 --- a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs +++ b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs @@ -1,4 +1,4 @@ -// #define VERIFY_MAP +#define VERIFY_MAP namespace FastExpressionCompiler.ImTools.UnitTests; @@ -87,18 +87,14 @@ public void Benchmark_test_with_Add_and_Lookup_100_items() var iters = Count / 2; for (var i = 0; i < iters; ++i) { - var result = map.TryGetEntryRef(randomPresentKeys[i], out var found); + ref var result = ref map.TryGetEntryRef(randomPresentKeys[i], out var found); if (found) count += result.Value.Length; - _ = map.TryGetEntryRef(missingKeys[i], out found); + _ = ref map.TryGetEntryRef(missingKeys[i], out found); if (!found) --count; } Assert.AreEqual(0, count); - -#if DEBUG - Assert.Greater(m.Map.LookupGreaterProbeCheckCount + m.Map.LookupEqualProbeCheckCount, 200); -#endif } [Test] @@ -119,24 +115,22 @@ public void Benchmark_test_with_Add_and_Lookup_1000_items() foreach (var type in presentTypes) map.AddOrUpdate(type, "a"); +#if VERIFY_MAP map.Verify(static (cond, msg) => Assert.IsTrue(cond, msg), presentTypes, Pass.It); +#endif var count = 0; var iters = Count / 2; for (var i = 0; i < iters; ++i) { - var result = map.TryGetEntryRef(randomPresentKeys[i], out var found); + ref var result = ref map.TryGetEntryRef(randomPresentKeys[i], out var found); if (found) count += result.Value.Length; - _ = map.TryGetEntryRef(missingKeys[i], out found); + _ = ref map.TryGetEntryRef(missingKeys[i], out found); if (!found) --count; } Assert.AreEqual(0, count); - -#if DEBUG - Assert.Greater(m.Map.LookupGreaterProbeCheckCount + m.Map.LookupEqualProbeCheckCount, 2000); -#endif } // [Test] From 0ca39b2746d1a7539f755ffbde3eaba2b077599e Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 17 Aug 2025 16:24:27 +0200 Subject: [PATCH 28/54] fix ci --- playground/Experiments/FEC_ImTools.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index d3287305..3b796a70 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -905,11 +905,6 @@ public static class SmallMap internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) internal const byte MinHashesCapacityBitShift = 4; // 1 << 4 == 16 - // todo: @wip @remove if not required - // #if NET7_0_OR_GREATER - // internal static Vector128 ByteVec16OfOne = Vector128.Create((byte)1); - // #endif - /// Represent a keyed entry stored in the SmallMap. /// Its implementation struct may include the additional Value for the Map or just the Key for the Set. /// The implementation may also decide to make Value readonly or writable for the in-place update @@ -1018,7 +1013,7 @@ public struct StableArrayEntries : IEntries const int UpToBucket1024 = UpToBucket512 + 1024; const int UpToBucket2048 = UpToBucket1024 + 2048; -#if NET7_0_OR_GREATER +#if NET8_0_OR_GREATER public static readonly Vector256 vUpToBucket = Vector256.Create( UpToBucket32, UpToBucket64, UpToBucket128, UpToBucket256, UpToBucket512, UpToBucket1024, UpToBucket2048, @@ -1574,7 +1569,7 @@ public ref TEntry GetSurePresentEntryRef(int index) return ref _stackEntries.GetSurePresentItemRef(index); } -#if NET7_0_OR_GREATER +#if NET8_0_OR_GREATER static readonly Vector128 vProbeStep = Vector128.Create((byte)16); #endif From f40364bfe4985feb369f7ace5e7d347797033883 Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 17 Aug 2025 16:27:31 +0200 Subject: [PATCH 29/54] cleanup SIMD --- playground/Experiments/FEC_ImTools.cs | 66 --------------------------- 1 file changed, 66 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 3b796a70..b5fe5d45 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -1569,10 +1569,6 @@ public ref TEntry GetSurePresentEntryRef(int index) return ref _stackEntries.GetSurePresentItemRef(index); } -#if NET8_0_OR_GREATER - static readonly Vector128 vProbeStep = Vector128.Create((byte)16); -#endif - /// The actual Lookup implementation in a single place [MethodImpl((MethodImplOptions)256)] private int TryGetEntryAndHashIndex(K key, int hash, int indexMask, @@ -1585,68 +1581,6 @@ private int TryGetEntryAndHashIndex(K key, int hash, int indexMask, ) { Debug.Assert(probe == 1); -#if NET7_0_OR_GREATER - // Skip over the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - if (Vector128.IsHardwareAccelerated) - { - var vExpectedProbe = Vector128.Create( - (byte)1, (byte)2, (byte)3, (byte)4, (byte)5, (byte)6, (byte)7, (byte)8, - (byte)9, (byte)10, (byte)11, (byte)12, (byte)13, (byte)14, (byte)15, (byte)16); - - start: - // Load 16 probe bytes - safe due to padding - var vProbe = Vector128.LoadUnsafe(ref probes.GetSurePresentItemRef(hashIndex)); - - var vGreaterMask = Vector128.GreaterThan(vProbe, vExpectedProbe); - var vEqualsMask = Vector128.Equals(vProbe, vExpectedProbe); - var greaterMask = vGreaterMask.ExtractMostSignificantBits(); - var equalsMask = vEqualsMask.ExtractMostSignificantBits(); - - if (greaterMask == 0xFFFF) - { - probe += 16; - hashIndex += 16; - vExpectedProbe = Vector128.Add(vExpectedProbe, vProbeStep); - goto start; - } - - var firstEqualIndex = BitOperations.TrailingZeroCount(equalsMask); - var firstNonGreaterIndex = BitOperations.TrailingZeroCount(~greaterMask); - - // Setting the hashIndex and probe before return because they are passed by-ref - // and need to be set to the right values for the not-found case - probe += (byte)firstNonGreaterIndex; - hashIndex += firstNonGreaterIndex; - - if (firstNonGreaterIndex != firstEqualIndex) - return -1; - - compareHashes: - var equalCount = BitOperations.PopCount(equalsMask); - var hashIndexEnd = hashIndex + equalCount; - while (hashIndex < hashIndexEnd) - { - var h = hashesAndIndexes.GetSurePresentItemRef(hashIndex); - if ((h & ~indexMask) == (hash & ~indexMask)) - if (default(TEq).Equals(GetSurePresentKey(h & indexMask), key)) - return h & indexMask; - ++probe; - ++hashIndex; - } - - if (firstEqualIndex + equalCount == 16) - { - firstEqualIndex = 0; - vProbe = Vector128.LoadUnsafe(ref probes.GetSurePresentItemRef(hashIndex)); - vExpectedProbe = Vector128.Add(vExpectedProbe, vProbeStep); - equalsMask = vEqualsMask.ExtractMostSignificantBits(); - if (equalsMask != 0) - goto compareHashes; - } - - return -1; - } -#endif ref var pRef = ref probes.GetSurePresentItemRef(hashIndex); while (pRef > probe) { From b3766c13189041c99a3e6e26a6f3bc4842090bc8 Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 17 Aug 2025 23:29:20 +0200 Subject: [PATCH 30/54] using StableArrayEntries --- playground/Experiments/FEC_ImTools.cs | 258 +++++++++--------- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 17 +- 2 files changed, 146 insertions(+), 129 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index b5fe5d45..31fe1ba1 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -948,18 +948,17 @@ public struct Entry : IEntry public static string ToB(int x) => System.Convert.ToString(x, 2).PadLeft(32, '0'); /// Abstraction to configure your own entries data structure. Check the derived types for the examples - public interface IEntries + public interface IEntries where TEntry : struct, IEntry - where TEq : IEq { /// Initializes the entries storage to the specified capacity void Init(int capacityPowerOfTwoPlease); /// Returns the reference to entry by its index, index should map to the present/non-removed entry - ref TEntry GetSurePresentEntryRef(int index); + ref TEntry GetSurePresentRef(int index); /// Adds the key at the "end" of entries - so the order of addition is preserved. - ref TEntry AddKeyAndGetEntryRef(K key, int index); + ref TEntry AddDefaultAndGetRef(int index); } internal const int MinEntriesCapacity = 2; @@ -968,9 +967,8 @@ public interface IEntries public readonly struct NoValue { } /// Stores the entries in a single dynamically reallocated growing array - public struct SingleArrayEntries : IEntries + public struct SingleArrayEntries : IEntries where TEntry : struct, IEntry - where TEq : struct, IEq { internal TEntry[] _entries; @@ -980,121 +978,116 @@ public void Init(int capacityPowerOfTwoPlease) => /// [MethodImpl((MethodImplOptions)256)] - public ref TEntry GetSurePresentEntryRef(int index) => + public ref TEntry GetSurePresentRef(int index) => ref _entries.GetSurePresentItemRef(index); /// [MethodImpl((MethodImplOptions)256)] - public ref TEntry AddKeyAndGetEntryRef(K key, int index) + public ref TEntry AddDefaultAndGetRef(int index) { if (index == _entries.Length) Array.Resize(ref _entries, index << 1); - - ref var e = ref _entries.GetSurePresentItemRef(index); - e.Key = key; - return ref e; + return ref _entries.GetSurePresentItemRef(index); } } /// Stores the entries in a single dynamically reallocated growing array [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct StableArrayEntries : IEntries + public struct StableArrayEntries : IEntries where TEntry : struct, IEntry - where TEq : struct, IEq { const int RegularBucketBits = 12; const int RegularBucketSize = 1 << RegularBucketBits; // 4096 const int RegularBucketMask = RegularBucketSize - 1; - const int UpToBucket32 = 32; - const int UpToBucket64 = UpToBucket32 + 64; - const int UpToBucket128 = UpToBucket64 + 128; - const int UpToBucket256 = UpToBucket128 + 256; - const int UpToBucket512 = UpToBucket256 + 512; - const int UpToBucket1024 = UpToBucket512 + 1024; + const int UpToBucket0032 = 32; + const int UpToBucket0064 = UpToBucket0032 + 64; + const int UpToBucket0128 = UpToBucket0064 + 128; + const int UpToBucket0256 = UpToBucket0128 + 256; + const int UpToBucket0512 = UpToBucket0256 + 512; + const int UpToBucket1024 = UpToBucket0512 + 1024; const int UpToBucket2048 = UpToBucket1024 + 2048; #if NET8_0_OR_GREATER - public static readonly Vector256 vUpToBucket = Vector256.Create( - UpToBucket32, UpToBucket64, UpToBucket128, UpToBucket256, - UpToBucket512, UpToBucket1024, UpToBucket2048, - // the last 2 things are the same for the arithmetic to work in FindBucket - UpToBucket2048); + static readonly Vector256 VUpToBucket = Vector256.Create( + 0, UpToBucket0032, UpToBucket0064, UpToBucket0128, + UpToBucket0256, UpToBucket0512, UpToBucket1024, UpToBucket2048); #endif - internal TEntry[] _bucket32; - internal TEntry[] _bucket64; - internal TEntry[] _bucket128; - internal TEntry[] _bucket256; - internal TEntry[] _bucket512; + internal TEntry[] _bucket0032; + internal TEntry[] _bucket0064; + internal TEntry[] _bucket0128; + internal TEntry[] _bucket0256; + internal TEntry[] _bucket0512; internal TEntry[] _bucket1024; internal TEntry[] _bucket2048; internal TEntry[][] _regularBuckets; /// + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - private TEntry[] GetBucket(int bucketIndex) + private ref TEntry[] GetBucketRef(int bucketIndex) { - Debug.Assert(bucketIndex >= 0 && bucketIndex <= 6, "Bucket index should be in the range 0..6"); + Debug.Assert(bucketIndex >= 0 && bucketIndex < 7, "Bucket index should be in the range 0..6"); #if SUPPORTS_UNSAFE - return Unsafe.Add(ref _bucket32, bucketIndex); + return ref Unsafe.Add(ref _bucket0032, bucketIndex); #else - return bucketIndex switch + switch (bucketIndex) { - 0 => _bucket32, - 1 => _bucket64, - 2 => _bucket128, - 3 => _bucket256, - 4 => _bucket512, - 5 => _bucket1024, - _ => _bucket2048, - }; + case 0: return ref _bucket0032; + case 1: return ref _bucket0064; + case 2: return ref _bucket0128; + case 3: return ref _bucket0256; + case 4: return ref _bucket0512; + case 5: return ref _bucket1024; + default: return ref _bucket2048; + } #endif } /// public void Init(int capacity = 0) { - _bucket32 = new TEntry[UpToBucket32]; // always allocate + _bucket0032 = new TEntry[UpToBucket0032]; // always allocate switch (capacity) { - case <= UpToBucket32: break; - case <= UpToBucket64: - _bucket64 = new TEntry[UpToBucket64]; + case <= UpToBucket0032: break; + case <= UpToBucket0064: + _bucket0064 = new TEntry[UpToBucket0064]; break; - case <= UpToBucket128: - _bucket64 = new TEntry[UpToBucket64]; - _bucket128 = new TEntry[UpToBucket128]; + case <= UpToBucket0128: + _bucket0064 = new TEntry[UpToBucket0064]; + _bucket0128 = new TEntry[UpToBucket0128]; break; - case <= UpToBucket256: - _bucket64 = new TEntry[UpToBucket64]; - _bucket128 = new TEntry[UpToBucket128]; - _bucket256 = new TEntry[UpToBucket256]; + case <= UpToBucket0256: + _bucket0064 = new TEntry[UpToBucket0064]; + _bucket0128 = new TEntry[UpToBucket0128]; + _bucket0256 = new TEntry[UpToBucket0256]; break; - case <= UpToBucket512: - _bucket64 = new TEntry[UpToBucket64]; - _bucket128 = new TEntry[UpToBucket128]; - _bucket256 = new TEntry[UpToBucket256]; - _bucket512 = new TEntry[UpToBucket512]; + case <= UpToBucket0512: + _bucket0064 = new TEntry[UpToBucket0064]; + _bucket0128 = new TEntry[UpToBucket0128]; + _bucket0256 = new TEntry[UpToBucket0256]; + _bucket0512 = new TEntry[UpToBucket0512]; break; case <= UpToBucket1024: - _bucket64 = new TEntry[UpToBucket64]; - _bucket128 = new TEntry[UpToBucket128]; - _bucket256 = new TEntry[UpToBucket256]; - _bucket512 = new TEntry[UpToBucket512]; + _bucket0064 = new TEntry[UpToBucket0064]; + _bucket0128 = new TEntry[UpToBucket0128]; + _bucket0256 = new TEntry[UpToBucket0256]; + _bucket0512 = new TEntry[UpToBucket0512]; _bucket1024 = new TEntry[UpToBucket1024]; break; case <= UpToBucket2048: - _bucket64 = new TEntry[UpToBucket64]; - _bucket128 = new TEntry[UpToBucket128]; - _bucket256 = new TEntry[UpToBucket256]; - _bucket512 = new TEntry[UpToBucket512]; + _bucket0064 = new TEntry[UpToBucket0064]; + _bucket0128 = new TEntry[UpToBucket0128]; + _bucket0256 = new TEntry[UpToBucket0256]; + _bucket0512 = new TEntry[UpToBucket0512]; _bucket1024 = new TEntry[UpToBucket1024]; _bucket2048 = new TEntry[UpToBucket2048]; break; default: - _bucket64 = new TEntry[UpToBucket64]; - _bucket128 = new TEntry[UpToBucket128]; - _bucket256 = new TEntry[UpToBucket256]; - _bucket512 = new TEntry[UpToBucket512]; + _bucket0064 = new TEntry[UpToBucket0064]; + _bucket0128 = new TEntry[UpToBucket0128]; + _bucket0256 = new TEntry[UpToBucket0256]; + _bucket0512 = new TEntry[UpToBucket0512]; _bucket1024 = new TEntry[UpToBucket1024]; _bucket2048 = new TEntry[UpToBucket2048]; @@ -1109,16 +1102,17 @@ public void Init(int capacity = 0) /// [MethodImpl((MethodImplOptions)256)] - public ref TEntry GetSurePresentEntryRef(int index) + public ref TEntry GetSurePresentRef(int index) { #if NET7_0_OR_GREATER var vIndex = Vector256.Create(index); - var vIndexThanBucket = Vector256.LessThan(vIndex, vUpToBucket); - if (vIndexThanBucket != Vector256.Zero) + var vIndexLessThanBucket = Vector256.LessThan(vIndex, VUpToBucket); + if (vIndexLessThanBucket != Vector256.Zero) { - var bucketIndex = BitOperations.TrailingZeroCount(Vector256.ExtractMostSignificantBits(vIndexThanBucket)); - var entryIndex = index - vUpToBucket.GetElement(bucketIndex); - var bucket = GetBucket(bucketIndex); + var bucketIndex = BitOperations.TrailingZeroCount(Vector256.ExtractMostSignificantBits(vIndexLessThanBucket)); + --bucketIndex; // to skip a 0 bucket at the start of vector + var entryIndex = index - VUpToBucket.GetElement(bucketIndex); + ref var bucket = ref GetBucketRef(bucketIndex); return ref bucket.GetSurePresentItemRef(entryIndex); } index -= UpToBucket2048; @@ -1129,20 +1123,20 @@ public ref TEntry GetSurePresentEntryRef(int index) #else switch (index) { - case < UpToBucket32: - return ref _bucket32.GetSurePresentItemRef(index - UpToBucket32); - case < UpToBucket64: - return ref _bucket64.GetSurePresentItemRef(index - UpToBucket64); - case < UpToBucket128: - return ref _bucket128.GetSurePresentItemRef(index - UpToBucket128); - case < UpToBucket256: - return ref _bucket256.GetSurePresentItemRef(index - UpToBucket256); - case < UpToBucket512: - return ref _bucket512.GetSurePresentItemRef(index - UpToBucket512); - case < UpToBucket1024: - return ref _bucket1024.GetSurePresentItemRef(index - UpToBucket1024); - case < UpToBucket2048: - return ref _bucket2048.GetSurePresentItemRef(index - UpToBucket2048); + case < UpToBucket0032: + return ref _bucket0032.GetSurePresentItemRef(index); + case < UpToBucket0064: + return ref _bucket0064.GetSurePresentItemRef(index - UpToBucket0032); + case < UpToBucket0128: + return ref _bucket0128.GetSurePresentItemRef(index - UpToBucket0064); + case < UpToBucket0256: + return ref _bucket0256.GetSurePresentItemRef(index - UpToBucket0128); + case < UpToBucket0512: + return ref _bucket0512.GetSurePresentItemRef(index - UpToBucket0256); + case < UpToBucket1024: + return ref _bucket1024.GetSurePresentItemRef(index - UpToBucket0512); + case < UpToBucket2048: + return ref _bucket2048.GetSurePresentItemRef(index - UpToBucket1024); default: index -= UpToBucket2048; var regularBucketIndex = index >>> RegularBucketBits; @@ -1156,16 +1150,18 @@ public ref TEntry GetSurePresentEntryRef(int index) /// [MethodImpl((MethodImplOptions)256)] - public ref TEntry AddKeyAndGetEntryRef(K key, int index) + public ref TEntry AddDefaultAndGetRef(int index) { #if NET7_0_OR_GREATER var vIndex = Vector256.Create(index); - var vIndexThanBucket = Vector256.LessThan(vIndex, vUpToBucket); + var vIndexThanBucket = Vector256.LessThan(vIndex, VUpToBucket); if (vIndexThanBucket != Vector256.Zero) { var bucketIndex = BitOperations.TrailingZeroCount(Vector256.ExtractMostSignificantBits(vIndexThanBucket)); - var entryIndex = index - vUpToBucket.GetElement(bucketIndex); - var bucket = GetBucket(bucketIndex) ?? new TEntry[UpToBucket32 << bucketIndex]; + --bucketIndex; // to skip a 0 bucket at the start of vector + var entryIndex = index - VUpToBucket.GetElement(bucketIndex); + ref var bucket = ref GetBucketRef(bucketIndex); + bucket ??= new TEntry[UpToBucket0032 << bucketIndex]; return ref bucket.GetSurePresentItemRef(entryIndex); } index -= UpToBucket2048; @@ -1182,27 +1178,27 @@ public ref TEntry AddKeyAndGetEntryRef(K key, int index) #else switch (index) { - case < UpToBucket32: - _bucket32 ??= new TEntry[UpToBucket32]; - return ref _bucket32.GetSurePresentItemRef(index - UpToBucket32); - case < UpToBucket64: - _bucket64 ??= new TEntry[UpToBucket64]; - return ref _bucket64.GetSurePresentItemRef(index - UpToBucket64); - case < UpToBucket128: - _bucket128 ??= new TEntry[UpToBucket128]; - return ref _bucket128.GetSurePresentItemRef(index - UpToBucket128); - case < UpToBucket256: - _bucket256 ??= new TEntry[UpToBucket256]; - return ref _bucket256.GetSurePresentItemRef(index - UpToBucket256); - case < UpToBucket512: - _bucket512 ??= new TEntry[UpToBucket512]; - return ref _bucket512.GetSurePresentItemRef(index - UpToBucket512); + case < UpToBucket0032: + _bucket0032 ??= new TEntry[UpToBucket0032]; + return ref _bucket0032.GetSurePresentItemRef(index); + case < UpToBucket0064: + _bucket0064 ??= new TEntry[UpToBucket0064]; + return ref _bucket0064.GetSurePresentItemRef(index - UpToBucket0032); + case < UpToBucket0128: + _bucket0128 ??= new TEntry[UpToBucket0128]; + return ref _bucket0128.GetSurePresentItemRef(index - UpToBucket0064); + case < UpToBucket0256: + _bucket0256 ??= new TEntry[UpToBucket0256]; + return ref _bucket0256.GetSurePresentItemRef(index - UpToBucket0128); + case < UpToBucket0512: + _bucket0512 ??= new TEntry[UpToBucket0512]; + return ref _bucket0512.GetSurePresentItemRef(index - UpToBucket0256); case < UpToBucket1024: _bucket1024 ??= new TEntry[UpToBucket1024]; - return ref _bucket1024.GetSurePresentItemRef(index - UpToBucket1024); + return ref _bucket1024.GetSurePresentItemRef(index - UpToBucket0512); case < UpToBucket2048: _bucket2048 ??= new TEntry[UpToBucket2048]; - return ref _bucket2048.GetSurePresentItemRef(index - UpToBucket2048); + return ref _bucket2048.GetSurePresentItemRef(index - UpToBucket1024); default: index -= UpToBucket2048; var regularBucketIndex = index >>> RegularBucketBits; @@ -1233,12 +1229,13 @@ public static int TryGetStackEntryIndex= 8 & Vector256.IsHardwareAccelerated) { + if (count == 0) + return -1; + var vHash = Vector256.Create(hash); var vHashes = MemoryMarshal.Cast>(hashes.AsSpan()); var i = 0; @@ -1333,7 +1330,7 @@ public static void VerifyHashesAndKeysEq(ref this TMap map, Action where TStackEntries : struct, IStack - where THeapEntries : struct, IEntries + where THeapEntries : struct, IEntries { #if DEBUG // Diagnostic counters to measure the performance of the map operations @@ -1551,7 +1548,7 @@ public K GetSurePresentKey(int index) Debug.Assert(index >= 0); Debug.Assert(index < _count); return index >= _stackEntries.Capacity - ? _heapEntries.GetSurePresentEntryRef(index - _stackEntries.Capacity).Key + ? _heapEntries.GetSurePresentRef(index - _stackEntries.Capacity).Key : _stackEntries.GetSurePresentItemRef(index).Key; } @@ -1565,7 +1562,7 @@ public ref TEntry GetSurePresentEntryRef(int index) Debug.Assert(index >= 0); Debug.Assert(index < _count); if (index >= _stackEntries.Capacity) - return ref _heapEntries.GetSurePresentEntryRef(index - _stackEntries.Capacity); + return ref _heapEntries.GetSurePresentRef(index - _stackEntries.Capacity); return ref _stackEntries.GetSurePresentItemRef(index); } @@ -1670,10 +1667,12 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) this.VerifyProbesRobinHoodInvariants(static (cond, msg) => Debug.Assert(cond, msg), Pass.It, startIndex, hashIndex + 1); #endif - // @pre - keep the last slot empty so the ResizeProbesAndHashes can rely on it to Stop when scanning the padding span beyond oldCapacity + // todo: @wip @perf - keep the last slot empty so the ResizeProbesAndHashes can rely on it to Stop when scanning the padding span beyond oldCapacity _isCapacityPaddingFilled = hashIndex + 2 == _probes.Length; - return ref _heapEntries.AddKeyAndGetEntryRef(key, _count++ - _stackEntries.Capacity); + ref var entry = ref _heapEntries.AddDefaultAndGetRef(_count++ - _stackEntries.Capacity); + entry.Key = key; + return ref entry; } [MethodImpl((MethodImplOptions)256)] @@ -1806,7 +1805,7 @@ private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) _heapEntries.Init(stackCap); // Give the heap entries the same initial capacity as Stack, effectively doubling the capacity // Set the key for the first entry, which is the 0 index in the entries - ref var newEntry = ref _heapEntries.GetSurePresentEntryRef(0); + ref var newEntry = ref _heapEntries.GetSurePresentRef(0); newEntry.Key = key; return ref newEntry; } @@ -1849,7 +1848,9 @@ private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key, int hash) PutHashAndIndexWithoutResizing(indexMask, hash, _count); - return ref _heapEntries.AddKeyAndGetEntryRef(key, (_count++) - _stackEntries.Capacity); + ref var entry = ref _heapEntries.AddDefaultAndGetRef((_count++) - _stackEntries.Capacity); + entry.Key = key; + return ref entry; } /// @@ -1996,7 +1997,7 @@ internal int ResizeProbesAndHashes(int indexMask) public struct SmallMap4() where TEq : struct, IEq { /// Map with 4 elements on stack and entries baked by the single array - public SmallMap, TEq, Size4, Stack4, Stack4>, SmallMap.SingleArrayEntries, TEq>> Map; + public SmallMap, TEq, Size4, Stack4, Stack4>, SmallMap.SingleArrayEntries>> Map; public SmallMap4(byte capacityBitShift) : this() => Map = new(capacityBitShift); } @@ -2004,7 +2005,7 @@ public struct SmallMap4() where TEq : struct, IEq public struct SmallMap8() where TEq : struct, IEq { /// Map with 8 elements on stack and entries baked by the single array - public SmallMap, TEq, Size8, Stack8, Stack8>, SmallMap.SingleArrayEntries, TEq>> Map; + public SmallMap, TEq, Size8, Stack8, Stack8>, SmallMap.SingleArrayEntries>> Map; public SmallMap8(byte capacityBitShift) : this() => Map = new(capacityBitShift); } @@ -2012,7 +2013,8 @@ public struct SmallMap8() where TEq : struct, IEq public struct SmallMap16() where TEq : struct, IEq { /// Map with 16 elements on stack and entries baked by the single array - public SmallMap, TEq, Size16, Stack16, Stack16>, SmallMap.SingleArrayEntries, TEq>> Map; + public SmallMap, TEq, Size16, Stack16, Stack16>, + SmallMap.StableArrayEntries>> Map; public SmallMap16(byte capacityBitShift) : this() => Map = new(capacityBitShift); } @@ -2021,21 +2023,21 @@ public struct SmallMap16() where TEq : struct, IEq public struct SmallSet4() where TEq : struct, IEq { /// Set with 4 keys on stack and entries baked by the single array - public SmallMap, TEq, Size4, Stack4, Stack4>, SmallMap.SingleArrayEntries, TEq>> Set; + public SmallMap, TEq, Size4, Stack4, Stack4>, SmallMap.SingleArrayEntries>> Set; } /// Holds the Set with 8 items on stack. Minimizes the number of type arguments required to be specified public struct SmallSet8() where TEq : struct, IEq { /// Set with 8 keys on stack and entries baked by the single array - public SmallMap, TEq, Size8, Stack8, Stack8>, SmallMap.SingleArrayEntries, TEq>> Set; + public SmallMap, TEq, Size8, Stack8, Stack8>, SmallMap.SingleArrayEntries>> Set; } /// Holds the Set with 16 items on stack. Minimizes the number of type arguments required to be specified public struct SmallSet16() where TEq : struct, IEq { /// Set with 16 keys on stack and entries baked by the single array - public SmallMap, TEq, Size16, Stack16, Stack16>, SmallMap.SingleArrayEntries, TEq>> Set; + public SmallMap, TEq, Size16, Stack16, Stack16>, SmallMap.SingleArrayEntries>> Set; } -#nullable restore \ No newline at end of file +#nullable restore diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 1ccd1f60..8ff185a8 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -2373,6 +2373,7 @@ .NET SDK 9.0.203 | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 49,441.1 ns | 987.34 ns | 1,780.38 ns | 49,657.1 ns | 2.09 | 0.08 | 3 | 8.9111 | 0.0610 | 55976 B | 0.97 | ## Initial SIMD for Lookup - Regression in @perf + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | |--------------------------------------------------------- |------ |------------:|----------:|------------:|------------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 111.3 ns | 1.88 ns | 1.67 ns | 110.6 ns | 0.38 | 0.01 | 1 | - | - | - | 0.00 | @@ -2387,7 +2388,21 @@ .NET SDK 9.0.203 | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 45,630.3 ns | 912.10 ns | 1,735.36 ns | 44,696.8 ns | 1.97 | 0.08 | 2 | 7.8735 | 0.3052 | 49544 B | 0.86 | | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 49,295.9 ns | 327.44 ns | 273.43 ns | 49,364.4 ns | 2.13 | 0.02 | 3 | 8.9111 | 0.0610 | 55976 B | 0.97 | - + ## Using StableArrayEntries + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |------------:|------------:|------------:|------------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 113.3 ns | 2.32 ns | 5.42 ns | 110.1 ns | 0.37 | 0.02 | 1 | - | - | - | 0.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 204.5 ns | 2.73 ns | 2.42 ns | 204.0 ns | 0.67 | 0.03 | 2 | 0.1147 | - | 720 B | 0.67 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 305.6 ns | 6.11 ns | 14.40 ns | 299.0 ns | 1.00 | 0.06 | 3 | 0.1707 | - | 1072 B | 1.00 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,172.7 ns | 43.50 ns | 102.53 ns | 2,138.9 ns | 1.00 | 0.07 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,317.2 ns | 46.33 ns | 100.71 ns | 2,255.9 ns | 1.07 | 0.07 | 1 | 0.4921 | - | 3088 B | 0.41 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,452.2 ns | 49.04 ns | 109.69 ns | 2,512.7 ns | 1.13 | 0.07 | 2 | 0.8507 | - | 5344 B | 0.71 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 29,665.4 ns | 588.68 ns | 1,581.45 ns | 29,786.1 ns | 1.00 | 0.08 | 1 | 9.1553 | 1.2817 | 57808 B | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 55,723.9 ns | 1,098.60 ns | 1,078.97 ns | 55,599.6 ns | 1.88 | 0.12 | 2 | 7.8735 | 0.3052 | 49544 B | 0.86 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 63,373.1 ns | 1,260.87 ns | 2,106.62 ns | 63,624.5 ns | 2.14 | 0.14 | 3 | 6.2256 | 0.2441 | 39288 B | 0.68 | */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] [Params(10, 100, 1000)] From 940138dc10d8dea351aa780ac72dbedc4a0105ca Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 18 Aug 2025 12:21:01 +0200 Subject: [PATCH 31/54] fix ci --- playground/Experiments/FEC_ImTools.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 31fe1ba1..27db5725 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -1104,7 +1104,7 @@ public void Init(int capacity = 0) [MethodImpl((MethodImplOptions)256)] public ref TEntry GetSurePresentRef(int index) { -#if NET7_0_OR_GREATER +#if NET8_0_OR_GREATER var vIndex = Vector256.Create(index); var vIndexLessThanBucket = Vector256.LessThan(vIndex, VUpToBucket); if (vIndexLessThanBucket != Vector256.Zero) @@ -1152,7 +1152,7 @@ public ref TEntry GetSurePresentRef(int index) [MethodImpl((MethodImplOptions)256)] public ref TEntry AddDefaultAndGetRef(int index) { -#if NET7_0_OR_GREATER +#if NET8_0_OR_GREATER var vIndex = Vector256.Create(index); var vIndexThanBucket = Vector256.LessThan(vIndex, VUpToBucket); if (vIndexThanBucket != Vector256.Zero) From 38c2925c80a6312ebd2eeb0e8f4dbd44c60d5eb0 Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 18 Aug 2025 19:05:09 +0200 Subject: [PATCH 32/54] added GetNextPowerOfTwoFast; cleanup --- playground/Experiments/FEC_ImTools.cs | 51 +++++++++++++++------------ 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 27db5725..287ad5ad 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -911,12 +911,22 @@ public static class SmallMap public interface IEntry { /// Returns the key of the payload - K Key { get; internal set; } + K Key { get; set; } } - /// Holds a single entry consisting of key and value. - /// Value may be set or changed but the key is set in stone (by construction). - [DebuggerDisplay("{Key?.ToString()}->{Value}")] + /// The entry with just a key. + /// When used in the SmallMap it may represent a Set without wasting the space for the absent value + [DebuggerDisplay("{Key?.ToString()}")] + public struct Entry : IEntry + { + /// The readonly key + public K Key { get; set; } + /// Construct with the key and default value + public Entry(K key) => Key = key; + } + + /// Holds an entry consisting of key and value. + [DebuggerDisplay("{Key?.ToString()}:{Value}")] public struct Entry : IEntry { /// The readonly key @@ -933,20 +943,22 @@ public Entry(K key, V value) } } - /// The entry with just a key. - /// When used with the SmallMap it may represent a Set without wasting the space for the absent value - [DebuggerDisplay("{Key?.ToString()}")] - public struct Entry : IEntry + [MethodImpl((MethodImplOptions)256)] + public static uint GetNextPowerOfTwoFast(uint capacity) { - /// The readonly key - public K Key { get; set; } - /// Construct with the key and default value - public Entry(K key) => Key = key; +#if NET7_0_OR_GREATER + return BitOperations.RoundUpToPowerOf2(capacity); +#else + --capacity; + capacity |= capacity >> 1; + capacity |= capacity >> 2; + capacity |= capacity >> 4; + capacity |= capacity >> 8; + capacity |= capacity >> 16; + return capacity + 1; +#endif } - /// Binary representation of the `int` - public static string ToB(int x) => System.Convert.ToString(x, 2).PadLeft(32, '0'); - /// Abstraction to configure your own entries data structure. Check the derived types for the examples public interface IEntries where TEntry : struct, IEntry @@ -958,14 +970,9 @@ public interface IEntries ref TEntry GetSurePresentRef(int index); /// Adds the key at the "end" of entries - so the order of addition is preserved. - ref TEntry AddDefaultAndGetRef(int index); + ref TEntry AddDefaultAndGetRef(int index); // todo: @wip why do we need `index` to just prepend, because we do not track the count? } - internal const int MinEntriesCapacity = 2; - - /// For now to use in the Set as a value - public readonly struct NoValue { } - /// Stores the entries in a single dynamically reallocated growing array public struct SingleArrayEntries : IEntries where TEntry : struct, IEntry @@ -1578,6 +1585,7 @@ private int TryGetEntryAndHashIndex(K key, int hash, int indexMask, ) { Debug.Assert(probe == 1); + ref var pRef = ref probes.GetSurePresentItemRef(hashIndex); while (pRef > probe) { @@ -1586,7 +1594,6 @@ private int TryGetEntryAndHashIndex(K key, int hash, int indexMask, } while (pRef == probe) { - // Compare the hash middle part, then the indexed key var h = hashesAndIndexes.GetSurePresentItemRef(hashIndex); if ((h & ~indexMask) == (hash & ~indexMask)) if (default(TEq).Equals(GetSurePresentKey(h & indexMask), key)) From 33bc26b7f709f60613c0a3b55231709a9cfb5973 Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 18 Aug 2025 19:31:20 +0200 Subject: [PATCH 33/54] add IEntries.Count and remove index param from IEntries.AddDefaultOrGetRef --- playground/Experiments/FEC_ImTools.cs | 47 ++++++++++++++++++--------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 287ad5ad..fe28a8ea 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -963,6 +963,9 @@ public static uint GetNextPowerOfTwoFast(uint capacity) public interface IEntries where TEntry : struct, IEntry { + /// Tracks the number of store elements, enables method AddDefaultAndGetRef to work + public int Count { get; } + /// Initializes the entries storage to the specified capacity void Init(int capacityPowerOfTwoPlease); @@ -970,7 +973,7 @@ public interface IEntries ref TEntry GetSurePresentRef(int index); /// Adds the key at the "end" of entries - so the order of addition is preserved. - ref TEntry AddDefaultAndGetRef(int index); // todo: @wip why do we need `index` to just prepend, because we do not track the count? + ref TEntry AddDefaultAndGetRef(); } /// Stores the entries in a single dynamically reallocated growing array @@ -978,6 +981,10 @@ public struct SingleArrayEntries : IEntries where TEntry : struct, IEntry { internal TEntry[] _entries; + internal int _count; + + /// + public int Count => _count; /// public void Init(int capacityPowerOfTwoPlease) => @@ -985,16 +992,19 @@ public void Init(int capacityPowerOfTwoPlease) => /// [MethodImpl((MethodImplOptions)256)] - public ref TEntry GetSurePresentRef(int index) => - ref _entries.GetSurePresentItemRef(index); + public ref TEntry GetSurePresentRef(int index) + { + Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count}-1"); + return ref _entries.GetSurePresentItemRef(index); + } /// [MethodImpl((MethodImplOptions)256)] - public ref TEntry AddDefaultAndGetRef(int index) + public ref TEntry AddDefaultAndGetRef() { - if (index == _entries.Length) - Array.Resize(ref _entries, index << 1); - return ref _entries.GetSurePresentItemRef(index); + if (_count == _entries.Length) + Array.Resize(ref _entries, _count << 1); + return ref _entries.GetSurePresentItemRef(_count++); } } @@ -1028,6 +1038,11 @@ public struct StableArrayEntries : IEntries internal TEntry[] _bucket2048; internal TEntry[][] _regularBuckets; + internal int _count; + + /// + public int Count => _count; + /// [UnscopedRef] [MethodImpl((MethodImplOptions)256)] @@ -1111,6 +1126,7 @@ public void Init(int capacity = 0) [MethodImpl((MethodImplOptions)256)] public ref TEntry GetSurePresentRef(int index) { + Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count}-1"); #if NET8_0_OR_GREATER var vIndex = Vector256.Create(index); var vIndexLessThanBucket = Vector256.LessThan(vIndex, VUpToBucket); @@ -1157,8 +1173,9 @@ public ref TEntry GetSurePresentRef(int index) /// [MethodImpl((MethodImplOptions)256)] - public ref TEntry AddDefaultAndGetRef(int index) + public ref TEntry AddDefaultAndGetRef() { + var index = _count++; #if NET8_0_OR_GREATER var vIndex = Vector256.Create(index); var vIndexThanBucket = Vector256.LessThan(vIndex, VUpToBucket); @@ -1552,8 +1569,7 @@ public SmallMap(byte capacityBitShift) [MethodImpl((MethodImplOptions)256)] public K GetSurePresentKey(int index) { - Debug.Assert(index >= 0); - Debug.Assert(index < _count); + Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count}-1"); return index >= _stackEntries.Capacity ? _heapEntries.GetSurePresentRef(index - _stackEntries.Capacity).Key : _stackEntries.GetSurePresentItemRef(index).Key; @@ -1566,8 +1582,7 @@ public K GetSurePresentKey(int index) [MethodImpl((MethodImplOptions)256)] public ref TEntry GetSurePresentEntryRef(int index) { - Debug.Assert(index >= 0); - Debug.Assert(index < _count); + Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count}-1"); if (index >= _stackEntries.Capacity) return ref _heapEntries.GetSurePresentRef(index - _stackEntries.Capacity); return ref _stackEntries.GetSurePresentItemRef(index); @@ -1677,7 +1692,8 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) // todo: @wip @perf - keep the last slot empty so the ResizeProbesAndHashes can rely on it to Stop when scanning the padding span beyond oldCapacity _isCapacityPaddingFilled = hashIndex + 2 == _probes.Length; - ref var entry = ref _heapEntries.AddDefaultAndGetRef(_count++ - _stackEntries.Capacity); + ++_count; + ref var entry = ref _heapEntries.AddDefaultAndGetRef(); entry.Key = key; return ref entry; } @@ -1812,7 +1828,7 @@ private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) _heapEntries.Init(stackCap); // Give the heap entries the same initial capacity as Stack, effectively doubling the capacity // Set the key for the first entry, which is the 0 index in the entries - ref var newEntry = ref _heapEntries.GetSurePresentRef(0); + ref var newEntry = ref _heapEntries.AddDefaultAndGetRef(); newEntry.Key = key; return ref newEntry; } @@ -1855,7 +1871,8 @@ private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key, int hash) PutHashAndIndexWithoutResizing(indexMask, hash, _count); - ref var entry = ref _heapEntries.AddDefaultAndGetRef((_count++) - _stackEntries.Capacity); + ++_count; + ref var entry = ref _heapEntries.AddDefaultAndGetRef(); entry.Key = key; return ref entry; } From 49a1ef51995e513fd31f133020abc4a8e4aeccbb Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 18 Aug 2025 20:27:46 +0200 Subject: [PATCH 34/54] fix ci --- playground/Experiments/FEC_ImTools.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index fe28a8ea..88adabb5 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -946,7 +946,7 @@ public Entry(K key, V value) [MethodImpl((MethodImplOptions)256)] public static uint GetNextPowerOfTwoFast(uint capacity) { -#if NET7_0_OR_GREATER +#if NET8_0_OR_GREATER return BitOperations.RoundUpToPowerOf2(capacity); #else --capacity; From efe0519e1247958763f98a482251a427d9307b61 Mon Sep 17 00:00:00 2001 From: dadhi Date: Tue, 19 Aug 2025 16:31:40 +0200 Subject: [PATCH 35/54] using Capacity instead of CapacityBitShift --- playground/Experiments/FEC_ImTools.cs | 115 ++++++++---------- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 6 +- 2 files changed, 52 insertions(+), 69 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 88adabb5..e455b56f 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -248,12 +248,9 @@ public interface IStack : IStack public interface IStack where TStack : struct, IStack { - /// Maximum count of items hold on stack + /// Maximum count of items hold on stack. Should be Power of Two int Capacity { get; } - /// Capacity == 1 << CapacityBitShift - byte CapacityBitShift { get; } - /// Returns the item by ref to read and write the item value, /// but does not check the index bounds comparing to the `this[index]` [UnscopedRef] @@ -322,8 +319,6 @@ public struct Stack2 : IStack> { /// public int Capacity => 2; - /// - public byte CapacityBitShift => 1; internal T _it0, _it1; @@ -369,8 +364,6 @@ public struct Stack4 : IStack> { /// public int Capacity => 4; - /// - public byte CapacityBitShift => 2; internal T _it0, _it1, _it2, _it3; @@ -418,8 +411,6 @@ public struct Stack8 : IStack> { /// public int Capacity => 8; - /// - public byte CapacityBitShift => 3; internal T _it0, _it1, _it2, _it3, _it4, _it5, _it6, _it7; @@ -471,8 +462,6 @@ public struct Stack16 : IStack> { /// public int Capacity => 16; - /// - public byte CapacityBitShift => 4; internal T _it0, _it1, _it2, _it3, _it4, _it5, _it6, _it7; internal T _it8, _it9, _it10, _it11, _it12, _it13, _it14, _it15; @@ -902,8 +891,8 @@ public readonly struct Pass /// Configuration and the tools for the SmallMap and friends public static class SmallMap { + internal const uint MinCapacity = 16; internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) - internal const byte MinHashesCapacityBitShift = 4; // 1 << 4 == 16 /// Represent a keyed entry stored in the SmallMap. /// Its implementation struct may include the additional Value for the Map or just the Key for the Set. @@ -967,7 +956,7 @@ public interface IEntries public int Count { get; } /// Initializes the entries storage to the specified capacity - void Init(int capacityPowerOfTwoPlease); + void Init(uint capacity); /// Returns the reference to entry by its index, index should map to the present/non-removed entry ref TEntry GetSurePresentRef(int index); @@ -987,8 +976,8 @@ public struct SingleArrayEntries : IEntries public int Count => _count; /// - public void Init(int capacityPowerOfTwoPlease) => - _entries = new TEntry[capacityPowerOfTwoPlease]; + public void Init(uint capacity) => + _entries = new TEntry[(int)capacity]; /// [MethodImpl((MethodImplOptions)256)] @@ -1002,6 +991,7 @@ public ref TEntry GetSurePresentRef(int index) [MethodImpl((MethodImplOptions)256)] public ref TEntry AddDefaultAndGetRef() { + Debug.Assert(_count <= _entries.Length, "Entries should be initialized before adding the item"); if (_count == _entries.Length) Array.Resize(ref _entries, _count << 1); return ref _entries.GetSurePresentItemRef(_count++); @@ -1066,7 +1056,7 @@ private ref TEntry[] GetBucketRef(int bucketIndex) } /// - public void Init(int capacity = 0) + public void Init(uint capacity = 0) { _bucket0032 = new TEntry[UpToBucket0032]; // always allocate switch (capacity) @@ -1280,7 +1270,7 @@ public static int TryGetStackEntryIndex & 0b0001_1000 & 0b0001_0111 -> 0b0001_0000 matches &= matches - 1; } - if ((i += Vector256.Count) >= count) + if ((i += Vector256.Count) >= count) // todo: @perf can I remove this branch, what if the hash would be not null? return -1; } return -1; @@ -1498,7 +1488,7 @@ public struct SmallMapCapacity bits - public int CapacityBitShift => _capacityBitShift; - /// Capacity - public int Capacity => 1 << _capacityBitShift; - - /// Mask for the index part of the packedHashAndIndexes entry - public int IndexMask => (1 << _capacityBitShift) - 1; + public int Capacity => _capacityPowerOfTwo; /// Access to the probes public byte[] Probes => _probes; @@ -1543,28 +1527,30 @@ public struct SmallMap _heapEntries; /// Capacity calculates as `1 leftShift capacityBitShift` - public SmallMap(byte capacityBitShift) + public SmallMap(uint capacityPowerOfTwoPlease) { - // Keep the capacity at least 8 for SIMD Vector256, if you need less space use Stack for that - _capacityBitShift = capacityBitShift < MinHashesCapacityBitShift ? MinHashesCapacityBitShift : capacityBitShift; - var capacity = 1 << _capacityBitShift; + var capacityPowerOfTwo = capacityPowerOfTwoPlease < MinCapacity + ? MinCapacity + : GetNextPowerOfTwoFast(capacityPowerOfTwoPlease); + + _capacityPowerOfTwo = (int)capacityPowerOfTwo; // Extra padding is required to avoid index wrap around for probes and hashes, // and ensure that SIMD can read at most 16 bytes starting from the last index. // Using either 16 or 1/8 of the capacity, whichever is bigger. - var tailPadding = 1 << (_capacityBitShift - 3); - var capacityWithPadding = capacity + (tailPadding > 16 ? tailPadding : 16); + var tailPadding = capacityPowerOfTwo >>> 3; // 1/8 of the capacity + var capacityWithPadding = capacityPowerOfTwo + (tailPadding > 16 ? tailPadding : 16); _probes = new byte[capacityWithPadding]; _packedHashesAndIndexes = new int[capacityWithPadding]; - _heapEntries.Init(capacity); + _heapEntries.Init(capacityPowerOfTwo); } /// + [MethodImpl((MethodImplOptions)256)] public int GetHashCode(K key) => default(TEq).GetHashCode(key); - // todo: @perf try using StableGrowingEntries (or Xar) with O(1) random access. And make an default storage for the entries. /// [MethodImpl((MethodImplOptions)256)] public K GetSurePresentKey(int index) @@ -1590,8 +1576,7 @@ public ref TEntry GetSurePresentEntryRef(int index) /// The actual Lookup implementation in a single place [MethodImpl((MethodImplOptions)256)] - private int TryGetEntryAndHashIndex(K key, int hash, int indexMask, - ref int hashIndex, ref byte probe, + private int TryGetEntryAndHashIndex(K key, int hash, ref int hashIndex, ref byte probe, #if NET7_0_OR_GREATER ref byte probes, ref int hashesAndIndexes #else @@ -1607,6 +1592,8 @@ private int TryGetEntryAndHashIndex(K key, int hash, int indexMask, pRef = ref probes.GetSurePresentItemRef(++hashIndex); ++probe; } + + var indexMask = _capacityPowerOfTwo - 1; while (pRef == probe) { var h = hashesAndIndexes.GetSurePresentItemRef(hashIndex); @@ -1626,12 +1613,11 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) #if DEBUG ++AddOrGetRefInEntries_Count; #endif - var indexMask = (1 << _capacityBitShift) - 1; - // If the free space is less than ~1/8 of capacity (~12.5%) then Resize probes and hashes (but not the entries). - // Note: this check done here even before lookup and not later at insert, because later we rely on the Lookup hashIndex to do a first insert. - if (_isCapacityPaddingFilled || indexMask - _count <= (indexMask >>> MinFreeCapacityShift)) - indexMask = ResizeProbesAndHashes(indexMask); + // note: this check done here even before lookup and not later at insert, because later we rely on the Lookup hashIndex to do a first insert. + if (_isCapacityPaddingFilled || + _capacityPowerOfTwo - _count <= (_capacityPowerOfTwo >>> MinFreeCapacityShift)) + ResizeMetadata(); #if NET7_0_OR_GREATER ref var probes = ref MemoryMarshal.GetArrayDataReference(_probes); @@ -1641,9 +1627,10 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) ref var hashesAndIndexes = ref _packedHashesAndIndexes; #endif + var indexMask = _capacityPowerOfTwo - 1; var hashIndex = hash & indexMask; byte probe = 1; - var entryIndex = TryGetEntryAndHashIndex(key, hash, indexMask, ref hashIndex, ref probe, ref probes, ref hashesAndIndexes); + var entryIndex = TryGetEntryAndHashIndex(key, hash, ref hashIndex, ref probe, ref probes, ref hashesAndIndexes); if (found = entryIndex != -1) return ref GetSurePresentEntryRef(entryIndex); @@ -1689,7 +1676,7 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) this.VerifyProbesRobinHoodInvariants(static (cond, msg) => Debug.Assert(cond, msg), Pass.It, startIndex, hashIndex + 1); #endif - // todo: @wip @perf - keep the last slot empty so the ResizeProbesAndHashes can rely on it to Stop when scanning the padding span beyond oldCapacity + // todo: @wip @perf - keep the last slot empty so the ResizeMetadata can rely on it to Stop when scanning the padding span beyond oldCapacity _isCapacityPaddingFilled = hashIndex + 2 == _probes.Length; ++_count; @@ -1761,7 +1748,7 @@ private void PutHashAndIndexWithoutResizing(int indexMask, int hash, int index) #endif } - // @pre - keep the last slot empty so the ResizeProbesAndHashes can rely on it to Stop when scanning the padding span beyond oldCapacity + // @pre - keep the last slot empty so the ResizeMetadata can rely on it to Stop when scanning the padding span beyond oldCapacity _isCapacityPaddingFilled = hashIndex + 2 == _probes.Length; } @@ -1804,17 +1791,15 @@ private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) // because they are not copied when the Entries need to Resize (depending on the TEntries implementation). // To ensure stable entries on Entries you need to use - var newCapBitShift = Math.Max(_stackEntries.CapacityBitShift + 1, _capacityBitShift); - _capacityBitShift = (byte)newCapBitShift; - - var newCapacity = 1 << newCapBitShift; - var newTailPadding = 1 << (newCapBitShift - 3); + var newCapacity = Math.Max(_capacityPowerOfTwo, _stackEntries.Capacity << 1); + _capacityPowerOfTwo = newCapacity; + var newTailPadding = newCapacity >> 3; var newCapacityWithPadding = newCapacity + (newTailPadding > 16 ? newTailPadding : 16); - var indexMask = newCapacity - 1; _packedHashesAndIndexes = new int[newCapacityWithPadding]; _probes = new byte[newCapacityWithPadding]; + var indexMask = newCapacity - 1; var stackCap = _stackEntries.Capacity; for (var i = 0; i < stackCap; ++i) { @@ -1825,7 +1810,9 @@ private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) PutHashAndIndexWithoutResizing(indexMask, hash, stackCap); ++_count; - _heapEntries.Init(stackCap); // Give the heap entries the same initial capacity as Stack, effectively doubling the capacity + + // Give the heap entries the same initial capacity as Stack, effectively doubling the capacity + _heapEntries.Init((uint)stackCap); // Set the key for the first entry, which is the 0 index in the entries ref var newEntry = ref _heapEntries.AddDefaultAndGetRef(); @@ -1865,11 +1852,11 @@ Probe 1A 2B private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key, int hash) { // if the free space is less than 1/8 of capacity (12.5%) then Resize - var indexMask = (1 << _capacityBitShift) - 1; - if (_isCapacityPaddingFilled || indexMask - _count <= (indexMask >>> MinFreeCapacityShift)) - indexMask = ResizeProbesAndHashes(indexMask); + if (_isCapacityPaddingFilled || + _capacityPowerOfTwo - _count <= (_capacityPowerOfTwo >>> MinFreeCapacityShift)) + ResizeMetadata(); - PutHashAndIndexWithoutResizing(indexMask, hash, _count); + PutHashAndIndexWithoutResizing(_capacityPowerOfTwo - 1, hash, _count); ++_count; ref var entry = ref _heapEntries.AddDefaultAndGetRef(); @@ -1923,10 +1910,9 @@ internal int TryGetIndexInEntries(K key, int hash) var probes = _probes; var hashesAndIndexes = _packedHashesAndIndexes; #endif - var indexMask = (1 << _capacityBitShift) - 1; - var hashIndex = hash & indexMask; + var hashIndex = hash & (_capacityPowerOfTwo - 1); byte probe = 1; - return TryGetEntryAndHashIndex(key, hash, indexMask, ref hashIndex, ref probe, ref probes, ref hashesAndIndexes); + return TryGetEntryAndHashIndex(key, hash, ref hashIndex, ref probe, ref probes, ref hashesAndIndexes); } /// @@ -1950,17 +1936,15 @@ public ref TEntry TryGetEntryRef(K key, out bool found) return ref RefTools.GetNullRef(); } - internal int ResizeProbesAndHashes(int indexMask) + internal void ResizeMetadata() { #if DEBUG ++ResizeProbesAndHashes_Count; #endif - var oldCapacity = indexMask + 1; - - var newCapacity = oldCapacity << 1; - var newIndexMask = (indexMask << 1) | 1; + var oldCapacity = _capacityPowerOfTwo; + var newCapacity = _capacityPowerOfTwo << 1; - var maybeTailPadding = oldCapacity >> 2; // 1/8 of the new capacity + var maybeTailPadding = newCapacity >> 3; // 1/8 of the new capacity var newCapacityWithPadding = newCapacity + (maybeTailPadding > 16 ? maybeTailPadding : 16); var newProbesArr = new byte[newCapacityWithPadding]; @@ -2006,14 +1990,13 @@ internal int ResizeProbesAndHashes(int indexMask) break; // No need to continue scanning the padding, because it is empty and we are done with the old hashes and probes } - ++_capacityBitShift; + _capacityPowerOfTwo = newCapacity; _probes = newProbesArr; _packedHashesAndIndexes = newHashesAndIndexes; #if VERIFY_MAP this.VerifyKeyHasMetaInfo(static (x, msg) => Debug.Assert(x, msg), Pass.It); #endif - return newIndexMask; } } diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 8ff185a8..68ed58b4 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -2405,8 +2405,8 @@ .NET SDK 9.0.203 | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 63,373.1 ns | 1,260.87 ns | 2,106.62 ns | 63,624.5 ns | 2.14 | 0.14 | 3 | 6.2256 | 0.2441 | 39288 B | 0.68 | */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] - [Params(10, 100, 1000)] - // [Params(1000)] + // [Params(10, 100, 1000)] + [Params(1000)] public int Count; private Type[] _presentKeys; @@ -2861,7 +2861,7 @@ public int DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent() return count; } - [Benchmark] + // [Benchmark] public int SmallMap_PopulateThenLookup_HalfMissed_HalfPresent() { var map = new SmallMapTypeString(); From 07302c7f00d0aeaec70fa8eb4161d50cc293f3ea Mon Sep 17 00:00:00 2001 From: dadhi Date: Wed, 20 Aug 2025 17:54:49 +0200 Subject: [PATCH 36/54] @wip does not work --- playground/Experiments/FEC_ImTools.cs | 443 +++++++++++++++----------- 1 file changed, 252 insertions(+), 191 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index e455b56f..edb6731c 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -998,6 +998,8 @@ public ref TEntry AddDefaultAndGetRef() } } + // todo: @perf use power of 2 total capacity and bucket grows by keeping 2 first buckets the same size, e.g. [32, 32, 64, 128, 256, 512, 1024, ...] + // store the rest in the ever increasing buckets of power of 2 instead of the same fixed sizes??? /// Stores the entries in a single dynamically reallocated growing array [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct StableArrayEntries : IEntries @@ -1015,7 +1017,7 @@ public struct StableArrayEntries : IEntries const int UpToBucket2048 = UpToBucket1024 + 2048; #if NET8_0_OR_GREATER - static readonly Vector256 VUpToBucket = Vector256.Create( + static readonly Vector256 UpToBucketVec = Vector256.Create( 0, UpToBucket0032, UpToBucket0064, UpToBucket0128, UpToBucket0256, UpToBucket0512, UpToBucket1024, UpToBucket2048); #endif @@ -1119,20 +1121,18 @@ public ref TEntry GetSurePresentRef(int index) Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count}-1"); #if NET8_0_OR_GREATER var vIndex = Vector256.Create(index); - var vIndexLessThanBucket = Vector256.LessThan(vIndex, VUpToBucket); + var vIndexLessThanBucket = Vector256.LessThan(vIndex, UpToBucketVec); if (vIndexLessThanBucket != Vector256.Zero) { - var bucketIndex = BitOperations.TrailingZeroCount(Vector256.ExtractMostSignificantBits(vIndexLessThanBucket)); - --bucketIndex; // to skip a 0 bucket at the start of vector - var entryIndex = index - VUpToBucket.GetElement(bucketIndex); - ref var bucket = ref GetBucketRef(bucketIndex); + // -1 to skip a 0 bucket at the start of vector + var bucketIndex = BitOperations.TrailingZeroCount(Vector256.ExtractMostSignificantBits(vIndexLessThanBucket)) - 1; + var entryIndex = index - UpToBucketVec.GetElement(bucketIndex); + var bucket = GetBucketRef(bucketIndex); return ref bucket.GetSurePresentItemRef(entryIndex); } index -= UpToBucket2048; - var regularBucketIndex = index >>> RegularBucketBits; - var regularEntryIndex = index & RegularBucketMask; - var regularBucket = _regularBuckets.GetSurePresentItemRef(regularBucketIndex); - return ref regularBucket.GetSurePresentItemRef(regularEntryIndex); + var regularBucket = _regularBuckets.GetSurePresentItemRef(index >>> RegularBucketBits); + return ref regularBucket.GetSurePresentItemRef(index & RegularBucketMask); #else switch (index) { @@ -1152,11 +1152,8 @@ public ref TEntry GetSurePresentRef(int index) return ref _bucket2048.GetSurePresentItemRef(index - UpToBucket1024); default: index -= UpToBucket2048; - var regularBucketIndex = index >>> RegularBucketBits; - var regularEntryIndex = index & RegularBucketMask; - return ref _regularBuckets - .GetSurePresentItemRef(regularBucketIndex) - .GetSurePresentItemRef(regularEntryIndex); + var regularBucket = _regularBuckets.GetSurePresentItemRef(index >>> RegularBucketBits); + return ref regularBucket.GetSurePresentItemRef(index & RegularBucketMask); }; #endif } @@ -1168,12 +1165,12 @@ public ref TEntry AddDefaultAndGetRef() var index = _count++; #if NET8_0_OR_GREATER var vIndex = Vector256.Create(index); - var vIndexThanBucket = Vector256.LessThan(vIndex, VUpToBucket); + var vIndexThanBucket = Vector256.LessThan(vIndex, UpToBucketVec); if (vIndexThanBucket != Vector256.Zero) { var bucketIndex = BitOperations.TrailingZeroCount(Vector256.ExtractMostSignificantBits(vIndexThanBucket)); --bucketIndex; // to skip a 0 bucket at the start of vector - var entryIndex = index - VUpToBucket.GetElement(bucketIndex); + var entryIndex = index - UpToBucketVec.GetElement(bucketIndex); ref var bucket = ref GetBucketRef(bucketIndex); bucket ??= new TEntry[UpToBucket0032 << bucketIndex]; return ref bucket.GetSurePresentItemRef(entryIndex); @@ -1474,34 +1471,23 @@ public struct SmallMap - PutHashAndIndexWithoutResizing_Count == 0 ? 0 : PutHashAndIndexWithoutResizing_ProbeGreaterOrEqualCheckCount / PutHashAndIndexWithoutResizing_Count; - public int PutHashAndIndexWithoutResizing_RobinHoodCount = 0; - public int PutHashAndIndexWithoutResizing_RobinHoodProbeCheckCount = 0; - public int AddOrGetRefInEntries_Count = 0; - public int AddOrGetRefInEntries_RobinHoodCount = 0; - public int AddOrGetRefInEntries_RobinHoodProbeCheckCount = 0; - public int ResizeProbesAndHashes_Count = 0; #endif internal int _capacityPowerOfTwo; - // true if the last item in probes and hashes being occupied - internal bool _isCapacityPaddingFilled; internal int _count; - // todo: @wip update the description, because now we store probes separately from the hashes - // The _packedHashesAndIndexes elements are of `Int32` with the bits split as following: - // 00010|000...110|01101 - // | | |- The index into the _entries structure, 0-based. The index bit count (indexMask) is the hashes capacity - 1. - // | | | This part of the erased hash is used to get the ideal index into the hashes array, so later this part of hash may be restored from the hash index and its probes. - // | |- The remaining middle bits of the original hash - // |- 5 (MaxProbeBits) high bits of the Probe count, with the minimal value of b00001 indicating the non-empty slot. - internal byte[] _probes; - internal int[] _packedHashesAndIndexes; + // Packing hash and probe together enables single load to examine probe and hash + // and enables the fast check even without the unpacking both values. + // todo: @improve @feature Packing both values together enables an atomic update of the both values for the future concurrent version + // 00010000...110|00010 + // | |- The probe, occupies indexMask of bits always starting with 1, so the occupied item should have non 0 value + // |- The remaining part of the hash without the indexMask bits used for the item index. + internal int[] _packedHashesAndProbes; + internal int[] _entryIndexes; #pragma warning disable IDE0044 // it tries to make entries readonly but they should stay modify-able to prevent its defensive struct copying #pragma warning disable CS0649 // field is never assigned to, and will always have its default value @@ -1514,11 +1500,11 @@ public struct SmallMapCapacity public int Capacity => _capacityPowerOfTwo; - /// Access to the probes - public byte[] Probes => _probes; + /// Access to the hashes and corresponding probes + public int[] PackedHashesAndProbes => _packedHashesAndProbes; - /// Access to the hashes and indexes - public int[] PackedHashesAndIndexes => _packedHashesAndIndexes; + /// Access to the entry indexes + public int[] EntryIndexes => _entryIndexes; /// public int Count => _count; @@ -1535,14 +1521,8 @@ public SmallMap(uint capacityPowerOfTwoPlease) _capacityPowerOfTwo = (int)capacityPowerOfTwo; - // Extra padding is required to avoid index wrap around for probes and hashes, - // and ensure that SIMD can read at most 16 bytes starting from the last index. - // Using either 16 or 1/8 of the capacity, whichever is bigger. - var tailPadding = capacityPowerOfTwo >>> 3; // 1/8 of the capacity - var capacityWithPadding = capacityPowerOfTwo + (tailPadding > 16 ? tailPadding : 16); - - _probes = new byte[capacityWithPadding]; - _packedHashesAndIndexes = new int[capacityWithPadding]; + _packedHashesAndProbes = new int[capacityPowerOfTwo]; + _entryIndexes = new int[capacityPowerOfTwo]; _heapEntries.Init(capacityPowerOfTwo); } @@ -1576,109 +1556,166 @@ public ref TEntry GetSurePresentEntryRef(int index) /// The actual Lookup implementation in a single place [MethodImpl((MethodImplOptions)256)] - private int TryGetEntryAndHashIndex(K key, int hash, ref int hashIndex, ref byte probe, + private int TryGetEntryAndHashIndex(int indexMask, K key, ref int hashAndProbe, ref int hashIndex, #if NET7_0_OR_GREATER - ref byte probes, ref int hashesAndIndexes + ref int hashesAndProbes, ref int entryIndexes #else - ref byte[] probes, ref int[] hashesAndIndexes + ref int[] hashesAndProbes, ref int[] entryIndexes #endif ) { - Debug.Assert(probe == 1); + Debug.Assert((hashAndProbe & indexMask) == 1, + $"Should start from the probe 1 but found probe {hashAndProbe & indexMask} at indexMask {indexMask}"); - ref var pRef = ref probes.GetSurePresentItemRef(hashIndex); - while (pRef > probe) + while (true) { - pRef = ref probes.GetSurePresentItemRef(++hashIndex); - ++probe; + ref var hpRef = ref hashesAndProbes.GetSurePresentItemRef(hashIndex); + if ((hpRef & indexMask) < (hashAndProbe & indexMask)) + return -1; + + if (hpRef == hashAndProbe) + { + var entryIndex = _entryIndexes.GetSurePresentItemRef(hashIndex); + if (default(TEq).Equals(GetSurePresentKey(entryIndex), key)) + return entryIndex; + } + + ++hashAndProbe; + hashIndex = (hashIndex + 1) & indexMask; } + } + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] + private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) + { +#if DEBUG + ++AddOrGetRefInEntries_Count; +#endif + // todo: @perf @consistency isn't it weird when the read operartion (found == true) is changing the internal structures? + // If the free space is less than ~1/8 of capacity (~12.5%) then Resize probes and hashes (but not the entries). + // note: this check done here even before lookup and not later at insert, because later we rely on the Lookup hashIndex to do a first insert. + if (_capacityPowerOfTwo - _count <= (_capacityPowerOfTwo >>> MinFreeCapacityShift)) + ResizeMetadata(); + +#if NET7_0_OR_GREATER + ref var hashesAndProbes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndProbes); + ref var entryIndexes = ref MemoryMarshal.GetArrayDataReference(_entryIndexes); +#else + ref var hashesAndProbes = ref _packedHashesAndProbes; + ref var entryIndexes = ref _entryIndexes; +#endif + found = true; + var entryIndex = _count; var indexMask = _capacityPowerOfTwo - 1; - while (pRef == probe) + var hashAndProbe = (hash & ~indexMask) | 1; + var hashIndex = hash & indexMask; + while (true) { - var h = hashesAndIndexes.GetSurePresentItemRef(hashIndex); - if ((h & ~indexMask) == (hash & ~indexMask)) - if (default(TEq).Equals(GetSurePresentKey(h & indexMask), key)) - return h & indexMask; - pRef = ref probes.GetSurePresentItemRef(++hashIndex); - ++probe; + ref var hpRef = ref hashesAndProbes.GetSurePresentItemRef(hashIndex); + if ((hpRef & indexMask) < (hashAndProbe & indexMask)) + { + var hpRobinHooded = hpRef; + hpRef = hashAndProbe; + + ref var iRef = ref entryIndexes.GetSurePresentItemRef(hashIndex); + var iRobinHooded = iRef; + iRef = entryIndex; + + // Finish on the empty slot + if (hpRobinHooded == 0) + break; + + hashAndProbe = hpRobinHooded; + entryIndex = iRobinHooded; + } + + if (hpRef == hashAndProbe) + { + var index = _entryIndexes.GetSurePresentItemRef(hashIndex); + ref var eRef = ref GetSurePresentEntryRef(index); + if (default(TEq).Equals(eRef.Key, key)) + return ref eRef; + } + + ++hashAndProbe; + hashIndex = (hashIndex + 1) & indexMask; } - return -1; + + found = false; + ++_count; + ref var newEntry = ref _heapEntries.AddDefaultAndGetRef(); + newEntry.Key = key; + return ref newEntry; } [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) + private ref TEntry AddOrGetRefInEntries_OLD(K key, int hash, out bool found) { #if DEBUG ++AddOrGetRefInEntries_Count; #endif // If the free space is less than ~1/8 of capacity (~12.5%) then Resize probes and hashes (but not the entries). // note: this check done here even before lookup and not later at insert, because later we rely on the Lookup hashIndex to do a first insert. - if (_isCapacityPaddingFilled || - _capacityPowerOfTwo - _count <= (_capacityPowerOfTwo >>> MinFreeCapacityShift)) + if (_capacityPowerOfTwo - _count <= (_capacityPowerOfTwo >>> MinFreeCapacityShift)) ResizeMetadata(); #if NET7_0_OR_GREATER - ref var probes = ref MemoryMarshal.GetArrayDataReference(_probes); - ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); + ref var hashesAndProbes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndProbes); + ref var entryIndexes = ref MemoryMarshal.GetArrayDataReference(_entryIndexes); #else - ref var probes = ref _probes; - ref var hashesAndIndexes = ref _packedHashesAndIndexes; + ref var hashesAndProbes = ref _packedHashesAndProbes; + ref var entryIndexes = ref _entryIndexes; #endif var indexMask = _capacityPowerOfTwo - 1; + var hashAndProbe = (hash & ~indexMask) | 1; var hashIndex = hash & indexMask; - byte probe = 1; - var entryIndex = TryGetEntryAndHashIndex(key, hash, ref hashIndex, ref probe, ref probes, ref hashesAndIndexes); + var entryIndex = TryGetEntryAndHashIndex(indexMask, key, + ref hashAndProbe, ref hashIndex, ref hashesAndProbes, ref entryIndexes); + if (found = entryIndex != -1) return ref GetSurePresentEntryRef(entryIndex); - // We did not find the probe and hash and therefore the key, so insert the new probe and hash - ref var pRef = ref probes.GetSurePresentItemRef(hashIndex); - var pRobinHooded = pRef; - pRef = probe; + Debug.Assert((hashIndex & ~indexMask) == 0, + $"TryGetEntryAndHashIndex should return wrapped hashIndex {hashIndex} with indexMask {indexMask}"); - ref var hRef = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); - var hRobinHooded = hRef; - hRef = (hash & ~indexMask) | _count; + // We did not find the probe and hash and therefore the key, so insert the new hash and probe + ref var hpRef = ref hashesAndProbes.GetSurePresentItemRef(hashIndex); + var hpRobinHooded = hpRef; + hpRef = hashAndProbe; - // If the robin hooded probe is empty then we stop - probe = pRobinHooded; + ref var entryIndexRef = ref entryIndexes.GetSurePresentItemRef(hashIndex); + var eiRobinHooded = entryIndexRef; + entryIndexRef = _count; + + hashAndProbe = hpRobinHooded; #if VERIFY_MAP var startIndex = hashIndex; - var isRobinHooded = probe != 0; + var isRobinHooded = hashAndProbe != 0; #endif - while (probe != 0) + // If the robin hooded meta item is empty then stop + while (hashAndProbe != 0) { - pRef = ref probes.GetSurePresentItemRef(++hashIndex); + hpRef = ref hashesAndProbes.GetSurePresentItemRef(++hashIndex & indexMask); // Otherwise we steal the slot with the smaller probes - if (pRef < ++probe) + if ((hpRef & indexMask) < (++hashAndProbe & indexMask)) { - var pValue = pRef; - pRef = probe; - probe = pValue; - - hRef = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); - var hValue = hRef; - hRef = hRobinHooded; - hRobinHooded = hValue; -#if DEBUG - ++AddOrGetRefInEntries_RobinHoodCount; -#endif + var hpTemp = hpRef; + hpRef = hashAndProbe; + hashAndProbe = hpTemp; + + entryIndexRef = ref entryIndexes.GetSurePresentItemRef(hashIndex); + var eiTemp = entryIndexRef; + entryIndexRef = eiRobinHooded; + eiRobinHooded = eiTemp; } -#if DEBUG - ++AddOrGetRefInEntries_RobinHoodProbeCheckCount; -#endif } #if VERIFY_MAP this.VerifyProbesRobinHoodInvariants(static (cond, msg) => Debug.Assert(cond, msg), Pass.It, startIndex, hashIndex + 1); #endif - - // todo: @wip @perf - keep the last slot empty so the ResizeMetadata can rely on it to Stop when scanning the padding span beyond oldCapacity - _isCapacityPaddingFilled = hashIndex + 2 == _probes.Length; - ++_count; ref var entry = ref _heapEntries.AddDefaultAndGetRef(); entry.Key = key; @@ -1686,70 +1723,84 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) } [MethodImpl((MethodImplOptions)256)] - private void PutHashAndIndexWithoutResizing(int indexMask, int hash, int index) + private void PutMetadataWithoutResizing(int indexMask, int hash, int index) { #if DEBUG - ++PutHashAndIndexWithoutResizing_Count; + ++PutMetadataWithoutResizing_Count; #endif - var hashIndex = hash & indexMask; - var hashWithIndex = (hash & ~indexMask) | index; - #if NET7_0_OR_GREATER - ref var probes = ref MemoryMarshal.GetArrayDataReference(_probes); - ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); + ref var hashesAndProbes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndProbes); + ref var entryIndexes = ref MemoryMarshal.GetArrayDataReference(_entryIndexes); #else - var probes = _probes; - var hashesAndIndexes = _packedHashesAndIndexes; + var hashesAndProbes = _packedHashesAndProbes; + var entryIndexes = _entryIndexes; #endif - // Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - byte probe = 1; - ref var pRef = ref probes.GetSurePresentItemRef(hashIndex); - while (pRef >= probe) + var entryIndex = _count; + var hashAndProbe = (hash & ~indexMask) | 1; + var hashIndex = hash & indexMask; + while (true) { - ++probe; - pRef = ref probes.GetSurePresentItemRef(++hashIndex); // no need to wrap around because the probes have the extra capacity -#if DEBUG - ++PutHashAndIndexWithoutResizing_ProbeGreaterOrEqualCheckCount; -#endif - } - - // We did not find the hash and therefore the key, so insert the new hash - var pRobinHooded = pRef; - pRef = probe; - - ref var hRef = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); - var hRobinHooded = hRef; - hRef = hashWithIndex; + ref var hpRef = ref hashesAndProbes.GetSurePresentItemRef(hashIndex); + if ((hpRef & indexMask) < (hashAndProbe & indexMask)) + { + var hpRobinHooded = hpRef; + hpRef = hashAndProbe; - // If the robin hooded probe is empty then we stop - probe = pRobinHooded; - while (probe != 0) - { - pRef = ref probes.GetSurePresentItemRef(++hashIndex); + ref var iRef = ref entryIndexes.GetSurePresentItemRef(hashIndex); + var iRobinHooded = iRef; + iRef = entryIndex; - // Otherwise we steal the slot with the smaller probes - if (pRef < ++probe) - { - var pValue = pRef; - pRef = probe; - probe = pValue; + // Finish on the empty slot + if (hpRobinHooded == 0) + return; - hRef = ref hashesAndIndexes.GetSurePresentItemRef(hashIndex); - var hValue = hRef; - hRef = hRobinHooded; - hRobinHooded = hValue; -#if DEBUG - ++PutHashAndIndexWithoutResizing_RobinHoodCount; -#endif + hashAndProbe = hpRobinHooded; + entryIndex = iRobinHooded; } -#if DEBUG - ++PutHashAndIndexWithoutResizing_RobinHoodProbeCheckCount; -#endif + + hashIndex = (hashIndex + 1) & indexMask; + ++hashAndProbe; } - // @pre - keep the last slot empty so the ResizeMetadata can rely on it to Stop when scanning the padding span beyond oldCapacity - _isCapacityPaddingFilled = hashIndex + 2 == _probes.Length; + // // Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions + // while ((hpRef & indexMask) >= (hashAndProbe & indexMask)) + // { + // ++hashAndProbe; + // hpRef = ref hashesAndProbes.GetSurePresentItemRef(++hashIndex & indexMask); + // #if DEBUG + // ++PutHashAndIndexWithoutResizing_ProbeGreaterOrEqualCheckCount; + // #endif + // } + + // // We did not find the probe and hash and therefore the key, so insert the new hash and probe + // var hpRobinHooded = hpRef; + // hpRef = hashAndProbe; + + // ref var entryIndexRef = ref entryIndexes.GetSurePresentItemRef(hashIndex); + // var eiRobinHooded = entryIndexRef; + // entryIndexRef = _count; + + // hashAndProbe = hpRobinHooded; + + // // If the robin hooded meta item is empty then stop + // while (hashAndProbe != 0) + // { + // hpRef = ref hashesAndProbes.GetSurePresentItemRef(++hashIndex & indexMask); + + // // Otherwise we steal the slot with the smaller probes + // if ((hpRef & indexMask) < (++hashAndProbe & indexMask)) + // { + // var hpTemp = hpRef; + // hpRef = hashAndProbe; + // hashAndProbe = hpTemp; + + // entryIndexRef = ref entryIndexes.GetSurePresentItemRef(hashIndex); + // var eiTemp = entryIndexRef; + // entryIndexRef = eiRobinHooded; + // eiRobinHooded = eiTemp; + // } + // } } /// @@ -1791,28 +1842,26 @@ private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) // because they are not copied when the Entries need to Resize (depending on the TEntries implementation). // To ensure stable entries on Entries you need to use - var newCapacity = Math.Max(_capacityPowerOfTwo, _stackEntries.Capacity << 1); + var stackCapacity = _stackEntries.Capacity; + var newCapacity = Math.Max(_capacityPowerOfTwo, stackCapacity << 1); _capacityPowerOfTwo = newCapacity; - var newTailPadding = newCapacity >> 3; - var newCapacityWithPadding = newCapacity + (newTailPadding > 16 ? newTailPadding : 16); - _packedHashesAndIndexes = new int[newCapacityWithPadding]; - _probes = new byte[newCapacityWithPadding]; + _packedHashesAndProbes = new int[newCapacity]; + _entryIndexes = new int[newCapacity]; var indexMask = newCapacity - 1; - var stackCap = _stackEntries.Capacity; - for (var i = 0; i < stackCap; ++i) + for (var i = 0; i < stackCapacity; ++i) { var h = _stackHashes.GetSurePresentItemRef(i); - PutHashAndIndexWithoutResizing(indexMask, h, i); + PutMetadataWithoutResizing(indexMask, h, i); } - PutHashAndIndexWithoutResizing(indexMask, hash, stackCap); + PutMetadataWithoutResizing(indexMask, hash, stackCapacity); ++_count; // Give the heap entries the same initial capacity as Stack, effectively doubling the capacity - _heapEntries.Init((uint)stackCap); + _heapEntries.Init((uint)stackCapacity); // Set the key for the first entry, which is the 0 index in the entries ref var newEntry = ref _heapEntries.AddDefaultAndGetRef(); @@ -1852,11 +1901,10 @@ Probe 1A 2B private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key, int hash) { // if the free space is less than 1/8 of capacity (12.5%) then Resize - if (_isCapacityPaddingFilled || - _capacityPowerOfTwo - _count <= (_capacityPowerOfTwo >>> MinFreeCapacityShift)) + if (_capacityPowerOfTwo - _count <= (_capacityPowerOfTwo >>> MinFreeCapacityShift)) ResizeMetadata(); - PutHashAndIndexWithoutResizing(_capacityPowerOfTwo - 1, hash, _count); + PutMetadataWithoutResizing(_capacityPowerOfTwo - 1, hash, _count); ++_count; ref var entry = ref _heapEntries.AddDefaultAndGetRef(); @@ -1904,15 +1952,31 @@ public int TryGetIndex(K key) internal int TryGetIndexInEntries(K key, int hash) { #if NET7_0_OR_GREATER - ref var probes = ref MemoryMarshal.GetArrayDataReference(_probes); - ref var hashesAndIndexes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); + ref var hashesAndProbes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndProbes); + ref var entryIndexes = ref MemoryMarshal.GetArrayDataReference(_entryIndexes); #else - var probes = _probes; - var hashesAndIndexes = _packedHashesAndIndexes; + var hashesAndProbes = _packedHashesAndProbes; + var entryIndexes = _entryIndexes; #endif - var hashIndex = hash & (_capacityPowerOfTwo - 1); - byte probe = 1; - return TryGetEntryAndHashIndex(key, hash, ref hashIndex, ref probe, ref probes, ref hashesAndIndexes); + var indexMask = _capacityPowerOfTwo - 1; + var hashAndProbe = (hash & ~indexMask) | 1; // start with the probe 1 + var hashIndex = hash & indexMask; + while (true) + { + ref var hpRef = ref hashesAndProbes.GetSurePresentItemRef(hashIndex); + if ((hpRef & indexMask) < (hashAndProbe & indexMask)) + return -1; + + if (hpRef == hashAndProbe) + { + var entryIndex = _entryIndexes.GetSurePresentItemRef(hashIndex); + if (default(TEq).Equals(GetSurePresentKey(entryIndex), key)) + return entryIndex; + } + + ++hashAndProbe; + hashIndex = (hashIndex + 1) & indexMask; + } } /// @@ -1944,24 +2008,21 @@ internal void ResizeMetadata() var oldCapacity = _capacityPowerOfTwo; var newCapacity = _capacityPowerOfTwo << 1; - var maybeTailPadding = newCapacity >> 3; // 1/8 of the new capacity - var newCapacityWithPadding = newCapacity + (maybeTailPadding > 16 ? maybeTailPadding : 16); - - var newProbesArr = new byte[newCapacityWithPadding]; - var newHashesAndIndexes = new int[newCapacityWithPadding]; + var newHashesAndProbes = new int[newCapacity]; + var newEntryIndexes = new byte[newCapacity]; #if NET7_0_OR_GREATER - ref var newProbes = ref MemoryMarshal.GetArrayDataReference(newProbesArr); - ref var newHashes = ref MemoryMarshal.GetArrayDataReference(newHashesAndIndexes); - ref var oldProbes = ref MemoryMarshal.GetArrayDataReference(_probes); - ref var oldHashes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndIndexes); + ref var newHashes = ref MemoryMarshal.GetArrayDataReference(newHashesAndProbes); + ref var newIndexes = ref MemoryMarshal.GetArrayDataReference(newEntryIndexes); + ref var oldHashes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndProbes); + ref var oldIndexes = ref MemoryMarshal.GetArrayDataReference(_entryIndexes); #else - var newProbes = newProbesArr; - var newHashes = newHashesAndIndexes; - var oldProbes = _probes; - var oldHashes = _packedHashesAndIndexes; + var newHashes = newHashesAndProbes; + var newIndexes = newEntryIndexes; + var oldHashes = _packedHashesAndProbes; + var oldIndexes = _entryIndexes; #endif - for (var i = 0; i < _probes.Length; ++i) + for (var i = 0; i < oldCapacity; ++i) { var oldProbe = oldProbes.GetSurePresentItem(i); if (oldProbe != 0) // for the non-empty probe and therefore hash @@ -1991,8 +2052,8 @@ internal void ResizeMetadata() } _capacityPowerOfTwo = newCapacity; - _probes = newProbesArr; - _packedHashesAndIndexes = newHashesAndIndexes; + _probes = newEntryIndexes; + _packedHashesAndIndexes = newHashesAndProbes; #if VERIFY_MAP this.VerifyKeyHasMetaInfo(static (x, msg) => Debug.Assert(x, msg), Pass.It); From a58c623c4f3d1443dcfa49de9f2dc0f7feba39a7 Mon Sep 17 00:00:00 2001 From: dadhi Date: Fri, 12 Sep 2025 23:02:26 +0200 Subject: [PATCH 37/54] @wip packing --- playground/Experiments/FEC_ImTools.cs | 449 +++++++++----------------- 1 file changed, 152 insertions(+), 297 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index edb6731c..7945ecef 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -893,6 +893,8 @@ public static class SmallMap { internal const uint MinCapacity = 16; internal const byte MinFreeCapacityShift = 3; // e.g. for the capacity 16: 16 >> 3 => 2, 12.5% of the free hash slots (it does not mean the entries free slot) + internal const int EntryIndexStartAtBit = 32; // higher bits for the entry index + internal const long HashAndProbeMask = (1L << EntryIndexStartAtBit) - 1; // lower 32 bits /// Represent a keyed entry stored in the SmallMap. /// Its implementation struct may include the additional Value for the Map or just the Key for the Set. @@ -1328,20 +1330,20 @@ public static void Verify(ref this TMap map, public static void VerifyHashesAndKeysEq(ref this TMap map, Action assertCond, Pass _ = default) where TMap : struct, IMap, IMapImpl { - var probes = map.Probes; - var hashes = map.PackedHashesAndIndexes; - var indexMask = map.Capacity - 1; - for (var i = 0; i < probes.Length; i++) + var metas = map.PackedEntryIndexesHashesProbes; + var probeMask = map.Capacity - 1; + for (var i = 0; i < metas.Length; i++) { - if (probes[i] != 0) + var m = metas[i]; + if (m != 0) { - var h = hashes[i]; - var hashPart = h & ~indexMask; - var index = h & indexMask; - var key = map.GetSurePresentKey(index); - var hashFromKey = map.GetHashCode(key); - var hashPartFromKey = hashFromKey & ~indexMask; - assertCond(hashPart == hashPartFromKey, $"Hash mismatch for i:{i}, index:{index}, key:{key}, expected:{hashPartFromKey}, actual:{hashPart}"); + var metaHashPart = (int)(m & ~probeMask); + var entryIndex = (int)(m >>> EntryIndexStartAtBit); + + var key = map.GetSurePresentKey(entryIndex); + var keyHash = map.GetHashCode(key); + var keyHashPart = keyHash & ~probeMask; + assertCond(metaHashPart == keyHashPart, $"Hash mismatch for i:{i}, entryIndex:{entryIndex}, key:{key}, expected:{keyHashPart}, actual:{metaHashPart}"); } } } @@ -1351,15 +1353,18 @@ public static void VerifyNoDuplicateKeys(this TMap map, Action, IMapImpl { var uniq = new Dictionary(map.Count); - var probes = map.Probes; - var hashes = map.PackedHashesAndIndexes; - var indexMask = map.Capacity - 1; - for (var i = 0; i < probes.Length; i++) + + var metas = map.PackedEntryIndexesHashesProbes; + var probeMask = map.Capacity - 1; + for (var i = 0; i < metas.Length; i++) { - var p = probes[i]; - if (p == 0) + var m = metas[i]; + if (m == 0) continue; - var key = map.GetSurePresentKey(hashes[i] & indexMask); + + var entryIndex = (int)(m >>> EntryIndexStartAtBit); + var key = map.GetSurePresentKey(entryIndex); + if (!uniq.ContainsKey(key)) uniq.Add(key, 1); else @@ -1372,12 +1377,14 @@ public static void VerifyProbesRobinHoodInvariants(this TMap map, Actio int startIndex = 0, int endIndex = -1) where TMap : struct, IMapImpl { - var probes = map.Probes; - var prevProbe = startIndex > 0 ? probes[startIndex - 1] : 0; - endIndex = endIndex == -1 ? probes.Length : endIndex; + var metas = map.PackedEntryIndexesHashesProbes; + var probeMask = map.Capacity - 1; + var prevProbe = startIndex > 0 ? (metas[startIndex - 1] & probeMask) : 0; + + endIndex = endIndex == -1 ? metas.Length : endIndex; for (var i = startIndex; i < endIndex; i++) { - var probe = probes[i]; + var probe = metas[i] & probeMask; var invariant = prevProbe >= probe || prevProbe + 1 == probe; assertCond(invariant, $"Probe invariant failed for probe index {i}, probe:{probe}, prevProbe:{prevProbe}"); prevProbe = probe; @@ -1417,7 +1424,7 @@ public interface IMap /// Lookups for the stored key. If found true, otherwise false bool ContainsKey(K key); /// Lookups for the stored key by the index in the entries data structure - K GetSurePresentKey(int index); + K GetSurePresentKey(int entryIndex); } public interface IMap : IMap @@ -1439,9 +1446,10 @@ public interface IMap : IMap /// Implementation details public interface IMapImpl { + /// The Capacity int Capacity { get; } - byte[] Probes { get; } - int[] PackedHashesAndIndexes { get; } + /// The metadata + long[] PackedEntryIndexesHashesProbes { get; } /// Returns the hash code for the provided key using the IEq implementation used by this map int GetHashCode(K key); } @@ -1482,12 +1490,13 @@ public struct SmallMapCapacity public int Capacity => _capacityPowerOfTwo; - /// Access to the hashes and corresponding probes - public int[] PackedHashesAndProbes => _packedHashesAndProbes; - - /// Access to the entry indexes - public int[] EntryIndexes => _entryIndexes; + /// Access to the entry indexes, hashes and corresponding probes + public long[] PackedEntryIndexesHashesProbes => _packedEntryIndexesHashesProbes; /// public int Count => _count; @@ -1520,10 +1526,7 @@ public SmallMap(uint capacityPowerOfTwoPlease) : GetNextPowerOfTwoFast(capacityPowerOfTwoPlease); _capacityPowerOfTwo = (int)capacityPowerOfTwo; - - _packedHashesAndProbes = new int[capacityPowerOfTwo]; - _entryIndexes = new int[capacityPowerOfTwo]; - + _packedEntryIndexesHashesProbes = new long[capacityPowerOfTwo]; _heapEntries.Init(capacityPowerOfTwo); } @@ -1536,12 +1539,14 @@ public SmallMap(uint capacityPowerOfTwoPlease) public K GetSurePresentKey(int index) { Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count}-1"); - return index >= _stackEntries.Capacity - ? _heapEntries.GetSurePresentRef(index - _stackEntries.Capacity).Key - : _stackEntries.GetSurePresentItemRef(index).Key; + return (index >= _stackEntries.Capacity + ? _heapEntries.GetSurePresentRef(index - _stackEntries.Capacity) + : _stackEntries.GetSurePresentItemRef(index)) + .Key; } // todo: @perf try using StableGrowingEntries (or Xar) with O(1) random access. And make an default storage for the entries. + // todo: @perf make it an interface method to store the actual entry in place of the 32 bit index, e.g. store and actual int key for the int Set ///Get the value ref by the entry index. Also the index corresponds to entry adding order. /// Important: it does not check the index bounds, so you need to check that the index is from 0 to map.Count-1 [UnscopedRef] @@ -1554,37 +1559,6 @@ public ref TEntry GetSurePresentEntryRef(int index) return ref _stackEntries.GetSurePresentItemRef(index); } - /// The actual Lookup implementation in a single place - [MethodImpl((MethodImplOptions)256)] - private int TryGetEntryAndHashIndex(int indexMask, K key, ref int hashAndProbe, ref int hashIndex, -#if NET7_0_OR_GREATER - ref int hashesAndProbes, ref int entryIndexes -#else - ref int[] hashesAndProbes, ref int[] entryIndexes -#endif - ) - { - Debug.Assert((hashAndProbe & indexMask) == 1, - $"Should start from the probe 1 but found probe {hashAndProbe & indexMask} at indexMask {indexMask}"); - - while (true) - { - ref var hpRef = ref hashesAndProbes.GetSurePresentItemRef(hashIndex); - if ((hpRef & indexMask) < (hashAndProbe & indexMask)) - return -1; - - if (hpRef == hashAndProbe) - { - var entryIndex = _entryIndexes.GetSurePresentItemRef(hashIndex); - if (default(TEq).Equals(GetSurePresentKey(entryIndex), key)) - return entryIndex; - } - - ++hashAndProbe; - hashIndex = (hashIndex + 1) & indexMask; - } - } - [UnscopedRef] [MethodImpl((MethodImplOptions)256)] private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) @@ -1592,215 +1566,81 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) #if DEBUG ++AddOrGetRefInEntries_Count; #endif - // todo: @perf @consistency isn't it weird when the read operartion (found == true) is changing the internal structures? - // If the free space is less than ~1/8 of capacity (~12.5%) then Resize probes and hashes (but not the entries). - // note: this check done here even before lookup and not later at insert, because later we rely on the Lookup hashIndex to do a first insert. - if (_capacityPowerOfTwo - _count <= (_capacityPowerOfTwo >>> MinFreeCapacityShift)) - ResizeMetadata(); - #if NET7_0_OR_GREATER - ref var hashesAndProbes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndProbes); - ref var entryIndexes = ref MemoryMarshal.GetArrayDataReference(_entryIndexes); + ref var metas = ref MemoryMarshal.GetArrayDataReference(_packedEntryIndexesHashesProbes); #else - ref var hashesAndProbes = ref _packedHashesAndProbes; - ref var entryIndexes = ref _entryIndexes; + ref var metas = ref _packedEntryIndexesHashesProbes; #endif found = true; - var entryIndex = _count; - var indexMask = _capacityPowerOfTwo - 1; - var hashAndProbe = (hash & ~indexMask) | 1; - var hashIndex = hash & indexMask; + var probeMask = _capacityPowerOfTwo - 1; + var metaIndex = hash & probeMask; + var meta = (long)_count << EntryIndexStartAtBit | (long)(hash & ~probeMask) | 1L; while (true) { - ref var hpRef = ref hashesAndProbes.GetSurePresentItemRef(hashIndex); - if ((hpRef & indexMask) < (hashAndProbe & indexMask)) + ref var mRef = ref metas.GetSurePresentItemRef(metaIndex & probeMask); + if ((mRef & probeMask) < (meta & probeMask)) { - var hpRobinHooded = hpRef; - hpRef = hashAndProbe; - - ref var iRef = ref entryIndexes.GetSurePresentItemRef(hashIndex); - var iRobinHooded = iRef; - iRef = entryIndex; - - // Finish on the empty slot - if (hpRobinHooded == 0) + var mRobinHooded = mRef; + mRef = meta; + if (mRobinHooded == 0) // finish on the empty slot break; - - hashAndProbe = hpRobinHooded; - entryIndex = iRobinHooded; + meta = mRobinHooded; } - if (hpRef == hashAndProbe) + if ((mRef & HashAndProbeMask) == (meta & HashAndProbeMask)) { - var index = _entryIndexes.GetSurePresentItemRef(hashIndex); - ref var eRef = ref GetSurePresentEntryRef(index); + var entryIndex = (int)(mRef >>> EntryIndexStartAtBit); + ref var eRef = ref GetSurePresentEntryRef(entryIndex); if (default(TEq).Equals(eRef.Key, key)) return ref eRef; } - ++hashAndProbe; - hashIndex = (hashIndex + 1) & indexMask; + ++meta; + ++metaIndex; } found = false; ++_count; - ref var newEntry = ref _heapEntries.AddDefaultAndGetRef(); - newEntry.Key = key; - return ref newEntry; - } - [UnscopedRef] - [MethodImpl((MethodImplOptions)256)] - private ref TEntry AddOrGetRefInEntries_OLD(K key, int hash, out bool found) - { -#if DEBUG - ++AddOrGetRefInEntries_Count; -#endif - // If the free space is less than ~1/8 of capacity (~12.5%) then Resize probes and hashes (but not the entries). - // note: this check done here even before lookup and not later at insert, because later we rely on the Lookup hashIndex to do a first insert. + // The new metadata slot is added - this is a good place to resize the metadata. + // If the free space is less than ~1/8 of capacity (~12.5%). if (_capacityPowerOfTwo - _count <= (_capacityPowerOfTwo >>> MinFreeCapacityShift)) ResizeMetadata(); -#if NET7_0_OR_GREATER - ref var hashesAndProbes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndProbes); - ref var entryIndexes = ref MemoryMarshal.GetArrayDataReference(_entryIndexes); -#else - ref var hashesAndProbes = ref _packedHashesAndProbes; - ref var entryIndexes = ref _entryIndexes; -#endif - - var indexMask = _capacityPowerOfTwo - 1; - var hashAndProbe = (hash & ~indexMask) | 1; - var hashIndex = hash & indexMask; - var entryIndex = TryGetEntryAndHashIndex(indexMask, key, - ref hashAndProbe, ref hashIndex, ref hashesAndProbes, ref entryIndexes); - - if (found = entryIndex != -1) - return ref GetSurePresentEntryRef(entryIndex); - - Debug.Assert((hashIndex & ~indexMask) == 0, - $"TryGetEntryAndHashIndex should return wrapped hashIndex {hashIndex} with indexMask {indexMask}"); - - // We did not find the probe and hash and therefore the key, so insert the new hash and probe - ref var hpRef = ref hashesAndProbes.GetSurePresentItemRef(hashIndex); - var hpRobinHooded = hpRef; - hpRef = hashAndProbe; - - ref var entryIndexRef = ref entryIndexes.GetSurePresentItemRef(hashIndex); - var eiRobinHooded = entryIndexRef; - entryIndexRef = _count; - - hashAndProbe = hpRobinHooded; -#if VERIFY_MAP - var startIndex = hashIndex; - var isRobinHooded = hashAndProbe != 0; -#endif - // If the robin hooded meta item is empty then stop - while (hashAndProbe != 0) - { - hpRef = ref hashesAndProbes.GetSurePresentItemRef(++hashIndex & indexMask); - - // Otherwise we steal the slot with the smaller probes - if ((hpRef & indexMask) < (++hashAndProbe & indexMask)) - { - var hpTemp = hpRef; - hpRef = hashAndProbe; - hashAndProbe = hpTemp; - - entryIndexRef = ref entryIndexes.GetSurePresentItemRef(hashIndex); - var eiTemp = entryIndexRef; - entryIndexRef = eiRobinHooded; - eiRobinHooded = eiTemp; - } - } -#if VERIFY_MAP - this.VerifyProbesRobinHoodInvariants(static (cond, msg) => Debug.Assert(cond, msg), Pass.It, startIndex, hashIndex + 1); -#endif - ++_count; - ref var entry = ref _heapEntries.AddDefaultAndGetRef(); - entry.Key = key; - return ref entry; + ref var newEntry = ref _heapEntries.AddDefaultAndGetRef(); + newEntry.Key = key; + return ref newEntry; } [MethodImpl((MethodImplOptions)256)] - private void PutMetadataWithoutResizing(int indexMask, int hash, int index) + private void PutMetadataWithoutResizing(int probeMask, int hash, int index) { #if DEBUG ++PutMetadataWithoutResizing_Count; #endif #if NET7_0_OR_GREATER - ref var hashesAndProbes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndProbes); - ref var entryIndexes = ref MemoryMarshal.GetArrayDataReference(_entryIndexes); + ref var metas = ref MemoryMarshal.GetArrayDataReference(_packedEntryIndexesHashesProbes); #else - var hashesAndProbes = _packedHashesAndProbes; - var entryIndexes = _entryIndexes; + var metas = _packedHashesAndProbes; #endif - var entryIndex = _count; - var hashAndProbe = (hash & ~indexMask) | 1; - var hashIndex = hash & indexMask; + var metaIndex = hash & probeMask; + var meta = (long)_count << EntryIndexStartAtBit | (long)(hash & ~probeMask) | 1L; while (true) { - ref var hpRef = ref hashesAndProbes.GetSurePresentItemRef(hashIndex); - if ((hpRef & indexMask) < (hashAndProbe & indexMask)) + ref var mRef = ref metas.GetSurePresentItemRef(metaIndex & probeMask); + if ((mRef & probeMask) < (meta & probeMask)) { - var hpRobinHooded = hpRef; - hpRef = hashAndProbe; - - ref var iRef = ref entryIndexes.GetSurePresentItemRef(hashIndex); - var iRobinHooded = iRef; - iRef = entryIndex; - - // Finish on the empty slot - if (hpRobinHooded == 0) + var mRobinHooded = mRef; + mRef = meta; + if (mRobinHooded == 0) // finish on the empty slot return; - - hashAndProbe = hpRobinHooded; - entryIndex = iRobinHooded; + meta = mRobinHooded; } - hashIndex = (hashIndex + 1) & indexMask; - ++hashAndProbe; + ++metaIndex; + ++meta; } - - // // Skip over hashes with the bigger and equal probes. The hashes with bigger probes overlapping from the earlier ideal positions - // while ((hpRef & indexMask) >= (hashAndProbe & indexMask)) - // { - // ++hashAndProbe; - // hpRef = ref hashesAndProbes.GetSurePresentItemRef(++hashIndex & indexMask); - // #if DEBUG - // ++PutHashAndIndexWithoutResizing_ProbeGreaterOrEqualCheckCount; - // #endif - // } - - // // We did not find the probe and hash and therefore the key, so insert the new hash and probe - // var hpRobinHooded = hpRef; - // hpRef = hashAndProbe; - - // ref var entryIndexRef = ref entryIndexes.GetSurePresentItemRef(hashIndex); - // var eiRobinHooded = entryIndexRef; - // entryIndexRef = _count; - - // hashAndProbe = hpRobinHooded; - - // // If the robin hooded meta item is empty then stop - // while (hashAndProbe != 0) - // { - // hpRef = ref hashesAndProbes.GetSurePresentItemRef(++hashIndex & indexMask); - - // // Otherwise we steal the slot with the smaller probes - // if ((hpRef & indexMask) < (++hashAndProbe & indexMask)) - // { - // var hpTemp = hpRef; - // hpRef = hashAndProbe; - // hashAndProbe = hpTemp; - - // entryIndexRef = ref entryIndexes.GetSurePresentItemRef(hashIndex); - // var eiTemp = entryIndexRef; - // entryIndexRef = eiRobinHooded; - // eiRobinHooded = eiTemp; - // } - // } } /// @@ -1845,18 +1685,16 @@ private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) var stackCapacity = _stackEntries.Capacity; var newCapacity = Math.Max(_capacityPowerOfTwo, stackCapacity << 1); _capacityPowerOfTwo = newCapacity; + _packedEntryIndexesHashesProbes = new long[newCapacity]; - _packedHashesAndProbes = new int[newCapacity]; - _entryIndexes = new int[newCapacity]; - - var indexMask = newCapacity - 1; + var probeMask = newCapacity - 1; for (var i = 0; i < stackCapacity; ++i) { var h = _stackHashes.GetSurePresentItemRef(i); - PutMetadataWithoutResizing(indexMask, h, i); + PutMetadataWithoutResizing(probeMask, h, i); } - PutMetadataWithoutResizing(indexMask, hash, stackCapacity); + PutMetadataWithoutResizing(probeMask, hash, stackCapacity); ++_count; @@ -1900,7 +1738,6 @@ Probe 1A 2B [MethodImpl((MethodImplOptions)256)] private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key, int hash) { - // if the free space is less than 1/8 of capacity (12.5%) then Resize if (_capacityPowerOfTwo - _count <= (_capacityPowerOfTwo >>> MinFreeCapacityShift)) ResizeMetadata(); @@ -1952,30 +1789,28 @@ public int TryGetIndex(K key) internal int TryGetIndexInEntries(K key, int hash) { #if NET7_0_OR_GREATER - ref var hashesAndProbes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndProbes); - ref var entryIndexes = ref MemoryMarshal.GetArrayDataReference(_entryIndexes); + ref var metas = ref MemoryMarshal.GetArrayDataReference(_packedEntryIndexesHashesProbes); #else - var hashesAndProbes = _packedHashesAndProbes; - var entryIndexes = _entryIndexes; + var metas = _packedHashesAndProbes; #endif - var indexMask = _capacityPowerOfTwo - 1; - var hashAndProbe = (hash & ~indexMask) | 1; // start with the probe 1 - var hashIndex = hash & indexMask; + var probeMask = _capacityPowerOfTwo - 1; + var metaIndex = hash & probeMask; + var meta = (long)(hash & ~probeMask) | 1L; while (true) { - ref var hpRef = ref hashesAndProbes.GetSurePresentItemRef(hashIndex); - if ((hpRef & indexMask) < (hashAndProbe & indexMask)) + ref var mRef = ref metas.GetSurePresentItemRef(metaIndex & probeMask); + if ((mRef & probeMask) < (meta & probeMask)) return -1; - if (hpRef == hashAndProbe) + if ((mRef & HashAndProbeMask) == meta) { - var entryIndex = _entryIndexes.GetSurePresentItemRef(hashIndex); + var entryIndex = (int)(mRef >>> EntryIndexStartAtBit); if (default(TEq).Equals(GetSurePresentKey(entryIndex), key)) return entryIndex; } - ++hashAndProbe; - hashIndex = (hashIndex + 1) & indexMask; + ++meta; + ++metaIndex; } } @@ -2008,52 +1843,72 @@ internal void ResizeMetadata() var oldCapacity = _capacityPowerOfTwo; var newCapacity = _capacityPowerOfTwo << 1; - var newHashesAndProbes = new int[newCapacity]; - var newEntryIndexes = new byte[newCapacity]; + var newMetasArr = new long[newCapacity]; #if NET7_0_OR_GREATER - ref var newHashes = ref MemoryMarshal.GetArrayDataReference(newHashesAndProbes); - ref var newIndexes = ref MemoryMarshal.GetArrayDataReference(newEntryIndexes); - ref var oldHashes = ref MemoryMarshal.GetArrayDataReference(_packedHashesAndProbes); - ref var oldIndexes = ref MemoryMarshal.GetArrayDataReference(_entryIndexes); + ref var newMetas = ref MemoryMarshal.GetArrayDataReference(newMetasArr); + ref var metas = ref MemoryMarshal.GetArrayDataReference(_packedEntryIndexesHashesProbes); #else - var newHashes = newHashesAndProbes; - var newIndexes = newEntryIndexes; - var oldHashes = _packedHashesAndProbes; - var oldIndexes = _entryIndexes; + var newMetas = newMetasArr; + var metas = _packedEntryIndexesHashesProbes; #endif - for (var i = 0; i < oldCapacity; ++i) + var probeMask = oldCapacity - 1; + var newProbeMask = newCapacity - 1; + + // Skip the wrapped-around hashes, as they may be perfectly fit into new capacity without wrapping. + // They will be handled as the last bunch as it should be logically. + // Say it is probes 0:..3, 1:..2, 2:..0. Means the probe 3 is wrapped around from the end, + // and the next probe is 2 because its ideal position was occupied by 3. + // This algo will skip 3 and completes on 2 as it is the first non-wrapped probe. + var metaIndex = 0; + var probe = 1; + while (true) { - var oldProbe = oldProbes.GetSurePresentItem(i); - if (oldProbe != 0) // for the non-empty probe and therefore hash + var hp = metas.GetSurePresentItem(metaIndex); + if ((hp & probeMask) <= probe) + break; + ++probe; + ++metaIndex; + } + + var endBefore = metaIndex + oldCapacity; + while (true) + { + var m = metas.GetSurePresentItem(metaIndex & probeMask); + if (m != 0) { - var oldHashAndIndex = oldHashes.GetSurePresentItem(i); - - // First, calculate the ideal hash index for the current probe, it should be in the range of the old capacity/index mask - var oldHashIndex = i - (oldProbe - 1); - Debug.Assert(oldHashIndex < oldCapacity, "Old hash index should be less than old capacity"); - var newHashIndex = (oldHashAndIndex & oldCapacity) | oldHashIndex; - Debug.Assert(newHashIndex == oldHashIndex || newHashIndex == oldHashIndex + oldCapacity, "New hash index should be either the same as old or with the next bit set to the old capacity"); - - // No need for robin-hooding because we already did it for the old hashes and now just filling the hashes into the new array which are already in order - byte probe = 1; - ref var pRef = ref newProbes.GetSurePresentItemRef(newHashIndex); - while (pRef != 0) + // Сalculate the ideal hash index for the current probe + var p = (int)(m & probeMask); + var idealIndex = metaIndex - p + 1; // index - (probe - 1) because probe starts with 1 + Debug.Assert(idealIndex >= 0, $"Ideal index should be non-negative, but found {idealIndex}"); + + // Calculate the new ideal index based on the next bit after the index mask (an oldCapacity bit). + // If the bit is 0 then the new index is the same otherwise it is oldCapacity + idealIndex, or in other words... + var newMetaIndex = (int)(m & oldCapacity) | idealIndex; + var newMeta = (m & ~newProbeMask) | 1L; // starting probe is always 1 + + // Copy the hash with the probe starting from the new ideal index + // No need for robin-hooding because the robin-hood invariant holds when elements are inserted in the same order + while (true) { - ++probe; - pRef = ref newProbes.GetSurePresentItemRef(++newHashIndex); + ref var newMetaRef = ref newMetas.GetSurePresentItemRef(newMetaIndex & newProbeMask); + if (newMetaRef == 0) + { + newMetaRef = newMeta; + break; + } + ++newMeta; + ++newMetaIndex; } - - pRef = probe; - newHashes.GetSurePresentItemRef(newHashIndex) = oldHashAndIndex & ~oldCapacity; } - else if (i >= oldCapacity) - break; // No need to continue scanning the padding, because it is empty and we are done with the old hashes and probes + + // Move to the next hash or finish when all old hashes are processed + if (++metaIndex >= endBefore) + break; } _capacityPowerOfTwo = newCapacity; - _probes = newEntryIndexes; - _packedHashesAndIndexes = newHashesAndProbes; + _packedEntryIndexesHashesProbes = newMetasArr; #if VERIFY_MAP this.VerifyKeyHasMetaInfo(static (x, msg) => Debug.Assert(x, msg), Pass.It); From 0a744016918f16a26d25bfaafdba286553c1552e Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 14 Sep 2025 23:24:37 +0200 Subject: [PATCH 38/54] @wip xar --- playground/Experiments/FEC_ImTools.cs | 318 +++++++----------- .../Playground/FecSmallMapTests.cs | 100 +++--- 2 files changed, 175 insertions(+), 243 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 7945ecef..71e844ca 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -934,6 +934,7 @@ public Entry(K key, V value) } } + /// Gets the next power of two greater than or equal to the specified capacity. [MethodImpl((MethodImplOptions)256)] public static uint GetNextPowerOfTwoFast(uint capacity) { @@ -954,6 +955,8 @@ public static uint GetNextPowerOfTwoFast(uint capacity) public interface IEntries where TEntry : struct, IEntry { + public int Capacity { get; } + /// Tracks the number of store elements, enables method AddDefaultAndGetRef to work public int Count { get; } @@ -972,14 +975,20 @@ public struct SingleArrayEntries : IEntries where TEntry : struct, IEntry { internal TEntry[] _entries; + internal int _capacityPowerOfTwo; internal int _count; + /// + public int Capacity => _capacityPowerOfTwo; /// public int Count => _count; /// - public void Init(uint capacity) => - _entries = new TEntry[(int)capacity]; + public void Init(uint capacity) + { + _capacityPowerOfTwo = (int)GetNextPowerOfTwoFast(capacity); + _entries = new TEntry[_capacityPowerOfTwo]; + } /// [MethodImpl((MethodImplOptions)256)] @@ -995,125 +1004,105 @@ public ref TEntry AddDefaultAndGetRef() { Debug.Assert(_count <= _entries.Length, "Entries should be initialized before adding the item"); if (_count == _entries.Length) - Array.Resize(ref _entries, _count << 1); + { + Debug.Assert(_capacityPowerOfTwo == _entries.Length, "Capacity should be equal to the entries length"); + _capacityPowerOfTwo <<= 1; + Array.Resize(ref _entries, _capacityPowerOfTwo); + } return ref _entries.GetSurePresentItemRef(_count++); } } - // todo: @perf use power of 2 total capacity and bucket grows by keeping 2 first buckets the same size, e.g. [32, 32, 64, 128, 256, 512, 1024, ...] - // store the rest in the ever increasing buckets of power of 2 instead of the same fixed sizes??? - /// Stores the entries in a single dynamically reallocated growing array + /// Stores the data in the growing list of bucket arrays. + /// Allocating the new bucket doubles the size preserving the previous buckets intact ensuring that the item references remain stable, + /// e.g. the bucket sizes are [32, 32, 64, 128, 256, 512, 1024, ...] [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct StableArrayEntries : IEntries where TEntry : struct, IEntry { - const int RegularBucketBits = 12; - const int RegularBucketSize = 1 << RegularBucketBits; // 4096 - const int RegularBucketMask = RegularBucketSize - 1; - const int UpToBucket0032 = 32; - const int UpToBucket0064 = UpToBucket0032 + 64; - const int UpToBucket0128 = UpToBucket0064 + 128; - const int UpToBucket0256 = UpToBucket0128 + 256; - const int UpToBucket0512 = UpToBucket0256 + 512; - const int UpToBucket1024 = UpToBucket0512 + 1024; - const int UpToBucket2048 = UpToBucket1024 + 2048; - -#if NET8_0_OR_GREATER - static readonly Vector256 UpToBucketVec = Vector256.Create( - 0, UpToBucket0032, UpToBucket0064, UpToBucket0128, - UpToBucket0256, UpToBucket0512, UpToBucket1024, UpToBucket2048); -#endif - internal TEntry[] _bucket0032; - internal TEntry[] _bucket0064; - internal TEntry[] _bucket0128; - internal TEntry[] _bucket0256; - internal TEntry[] _bucket0512; - internal TEntry[] _bucket1024; - internal TEntry[] _bucket2048; - internal TEntry[][] _regularBuckets; - + internal TEntry[] _bucket00Of32; + internal TEntry[] _bucket01Of32; + internal TEntry[] _bucket02Of64; + internal TEntry[] _bucket03Of128; + internal TEntry[] _bucket04Of256; + internal TEntry[] _bucket05Of512; + internal TEntry[] _bucket06Of1024; + internal TEntry[] _bucket07Of2048; + internal TEntry[][] _bucketsOf4096AndMore; + + internal int _capacityPowerOfTwo; internal int _count; + /// + public int Capacity => _capacityPowerOfTwo; /// public int Count => _count; + // todo: @wip works only for index >= 32 + // Starting from the 0 bucket of 32 items: 0b00000000_00000000_00000000_00011111 = 31 + // 32 - leading zeros = 5, and -5 = 0 => bucket 0, e.g. 27 - leading zeros +#if NET6_0_OR_GREATER + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetBucketIndexFromGlobalIndex(uint index) => + index < 32 ? 0 : 27 - BitOperations.LeadingZeroCount(index); +#else + private static int GetBucketIndexFromGlobalIndex(uint index) + { + if (index == 0) return 0; + + var count = 0; + if (index <= 0x0000FFFF) { count += 16; index <<= 16; } + if (index <= 0x00FFFFFF) { count += 8; index <<= 8; } + if (index <= 0x0FFFFFFF) { count += 4; index <<= 4; } + if (index <= 0x3FFFFFFF) { count += 2; index <<= 2; } + if (index <= 0x7FFFFFFF) { count += 1; } + + return 27 - count; + } +#endif + /// [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - private ref TEntry[] GetBucketRef(int bucketIndex) + private ref TEntry[] GetStackBucketRef(int bucketIndex) { - Debug.Assert(bucketIndex >= 0 && bucketIndex < 7, "Bucket index should be in the range 0..6"); + Debug.Assert(bucketIndex >= 0 && bucketIndex < 8, "Bucket index should be in the range 0..7"); #if SUPPORTS_UNSAFE - return ref Unsafe.Add(ref _bucket0032, bucketIndex); + return ref Unsafe.Add(ref _bucket00Of32, bucketIndex); #else switch (bucketIndex) { - case 0: return ref _bucket0032; - case 1: return ref _bucket0064; - case 2: return ref _bucket0128; - case 3: return ref _bucket0256; - case 4: return ref _bucket0512; - case 5: return ref _bucket1024; - default: return ref _bucket2048; + case 0: return ref _bucket00Of32; + case 1: return ref _bucket01Of32; + case 2: return ref _bucket02Of64; + case 3: return ref _bucket03Of128; + case 4: return ref _bucket04Of256; + case 5: return ref _bucket05Of512; + case 6: return ref _bucket06Of1024; + default: return ref _bucket07Of2048; } #endif } - /// + /// Initializes enough buckets to hold the specified capacity. + /// Always creates at least the first bucket of 32 items. public void Init(uint capacity = 0) { - _bucket0032 = new TEntry[UpToBucket0032]; // always allocate - switch (capacity) - { - case <= UpToBucket0032: break; - case <= UpToBucket0064: - _bucket0064 = new TEntry[UpToBucket0064]; - break; - case <= UpToBucket0128: - _bucket0064 = new TEntry[UpToBucket0064]; - _bucket0128 = new TEntry[UpToBucket0128]; - break; - case <= UpToBucket0256: - _bucket0064 = new TEntry[UpToBucket0064]; - _bucket0128 = new TEntry[UpToBucket0128]; - _bucket0256 = new TEntry[UpToBucket0256]; - break; - case <= UpToBucket0512: - _bucket0064 = new TEntry[UpToBucket0064]; - _bucket0128 = new TEntry[UpToBucket0128]; - _bucket0256 = new TEntry[UpToBucket0256]; - _bucket0512 = new TEntry[UpToBucket0512]; - break; - case <= UpToBucket1024: - _bucket0064 = new TEntry[UpToBucket0064]; - _bucket0128 = new TEntry[UpToBucket0128]; - _bucket0256 = new TEntry[UpToBucket0256]; - _bucket0512 = new TEntry[UpToBucket0512]; - _bucket1024 = new TEntry[UpToBucket1024]; - break; - case <= UpToBucket2048: - _bucket0064 = new TEntry[UpToBucket0064]; - _bucket0128 = new TEntry[UpToBucket0128]; - _bucket0256 = new TEntry[UpToBucket0256]; - _bucket0512 = new TEntry[UpToBucket0512]; - _bucket1024 = new TEntry[UpToBucket1024]; - _bucket2048 = new TEntry[UpToBucket2048]; - break; - default: - _bucket0064 = new TEntry[UpToBucket0064]; - _bucket0128 = new TEntry[UpToBucket0128]; - _bucket0256 = new TEntry[UpToBucket0256]; - _bucket0512 = new TEntry[UpToBucket0512]; - _bucket1024 = new TEntry[UpToBucket1024]; - _bucket2048 = new TEntry[UpToBucket2048]; - - var remainingCapacity = capacity - UpToBucket2048; - var bucketCount = (remainingCapacity >>> RegularBucketBits) + 1; - _regularBuckets = new TEntry[bucketCount][]; - for (var i = 0; i < bucketCount; ++i) - _regularBuckets[i] = new TEntry[RegularBucketSize]; - break; - } + _bucket00Of32 = new TEntry[32]; + _capacityPowerOfTwo = 32; + if (capacity <= 32) + return; + + var lastBucketIndex = GetBucketIndexFromGlobalIndex(capacity - 1); + for (var i = 1; i <= lastBucketIndex; i++) + GetStackBucketRef(i) = new TEntry[32 << (i - 1)]; + _capacityPowerOfTwo = 32 << lastBucketIndex; + if (lastBucketIndex < 8) + return; + + _bucketsOf4096AndMore = new TEntry[lastBucketIndex - 7][]; + for (var i = 0; i < _bucketsOf4096AndMore.Length; i++) + _bucketsOf4096AndMore[i] = new TEntry[4096 << i]; } /// @@ -1121,111 +1110,52 @@ public void Init(uint capacity = 0) public ref TEntry GetSurePresentRef(int index) { Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count}-1"); -#if NET8_0_OR_GREATER - var vIndex = Vector256.Create(index); - var vIndexLessThanBucket = Vector256.LessThan(vIndex, UpToBucketVec); - if (vIndexLessThanBucket != Vector256.Zero) - { - // -1 to skip a 0 bucket at the start of vector - var bucketIndex = BitOperations.TrailingZeroCount(Vector256.ExtractMostSignificantBits(vIndexLessThanBucket)) - 1; - var entryIndex = index - UpToBucketVec.GetElement(bucketIndex); - var bucket = GetBucketRef(bucketIndex); - return ref bucket.GetSurePresentItemRef(entryIndex); - } - index -= UpToBucket2048; - var regularBucket = _regularBuckets.GetSurePresentItemRef(index >>> RegularBucketBits); - return ref regularBucket.GetSurePresentItemRef(index & RegularBucketMask); -#else - switch (index) - { - case < UpToBucket0032: - return ref _bucket0032.GetSurePresentItemRef(index); - case < UpToBucket0064: - return ref _bucket0064.GetSurePresentItemRef(index - UpToBucket0032); - case < UpToBucket0128: - return ref _bucket0128.GetSurePresentItemRef(index - UpToBucket0064); - case < UpToBucket0256: - return ref _bucket0256.GetSurePresentItemRef(index - UpToBucket0128); - case < UpToBucket0512: - return ref _bucket0512.GetSurePresentItemRef(index - UpToBucket0256); - case < UpToBucket1024: - return ref _bucket1024.GetSurePresentItemRef(index - UpToBucket0512); - case < UpToBucket2048: - return ref _bucket2048.GetSurePresentItemRef(index - UpToBucket1024); - default: - index -= UpToBucket2048; - var regularBucket = _regularBuckets.GetSurePresentItemRef(index >>> RegularBucketBits); - return ref regularBucket.GetSurePresentItemRef(index & RegularBucketMask); - }; -#endif + if (index < 32) // todo: @perf generalize GetBucketIndexFromGlobalIndex to remove this branch + return ref _bucket00Of32.GetSurePresentItemRef(index); + + var bucketIndex = GetBucketIndexFromGlobalIndex((uint)index); + var insideIndex = index - (1 << (bucketIndex + 5)); // index - (32 << bucketIndex) + var bucket = bucketIndex < 8 ? GetStackBucketRef(bucketIndex) : _bucketsOf4096AndMore[bucketIndex - 8]; + return ref bucket.GetSurePresentItemRef(insideIndex); } /// [MethodImpl((MethodImplOptions)256)] public ref TEntry AddDefaultAndGetRef() { - var index = _count++; -#if NET8_0_OR_GREATER - var vIndex = Vector256.Create(index); - var vIndexThanBucket = Vector256.LessThan(vIndex, UpToBucketVec); - if (vIndexThanBucket != Vector256.Zero) + var lastIndex = _count++; + if (lastIndex < 32) // todo: @perf generalize GetBucketIndexFromGlobalIndex to remove this branch { - var bucketIndex = BitOperations.TrailingZeroCount(Vector256.ExtractMostSignificantBits(vIndexThanBucket)); - --bucketIndex; // to skip a 0 bucket at the start of vector - var entryIndex = index - UpToBucketVec.GetElement(bucketIndex); - ref var bucket = ref GetBucketRef(bucketIndex); - bucket ??= new TEntry[UpToBucket0032 << bucketIndex]; - return ref bucket.GetSurePresentItemRef(entryIndex); + if (_bucket00Of32 == null) + _bucket00Of32 = new TEntry[32]; + return ref _bucket00Of32.GetSurePresentItemRef(lastIndex); } - index -= UpToBucket2048; - var regularBucketIndex = index >>> RegularBucketBits; - var regularEntryIndex = index & RegularBucketMask; - if (_regularBuckets == null) - _regularBuckets = new TEntry[regularBucketIndex + 1][]; - else if (_regularBuckets.Length <= regularBucketIndex) - Array.Resize(ref _regularBuckets, regularBucketIndex + 1); - ref var regularBucket = ref _regularBuckets.GetSurePresentItemRef(regularBucketIndex); - if (regularBucket == null) - regularBucket = new TEntry[RegularBucketSize]; - return ref regularBucket.GetSurePresentItemRef(regularEntryIndex); -#else - switch (index) + + var bucketIndex = GetBucketIndexFromGlobalIndex((uint)lastIndex); + var insideIndex = lastIndex - (1 << (bucketIndex + 5)); // index - (32 << bucketIndex) + + _capacityPowerOfTwo = 32 << bucketIndex; + + if (bucketIndex < 8) { - case < UpToBucket0032: - _bucket0032 ??= new TEntry[UpToBucket0032]; - return ref _bucket0032.GetSurePresentItemRef(index); - case < UpToBucket0064: - _bucket0064 ??= new TEntry[UpToBucket0064]; - return ref _bucket0064.GetSurePresentItemRef(index - UpToBucket0032); - case < UpToBucket0128: - _bucket0128 ??= new TEntry[UpToBucket0128]; - return ref _bucket0128.GetSurePresentItemRef(index - UpToBucket0064); - case < UpToBucket0256: - _bucket0256 ??= new TEntry[UpToBucket0256]; - return ref _bucket0256.GetSurePresentItemRef(index - UpToBucket0128); - case < UpToBucket0512: - _bucket0512 ??= new TEntry[UpToBucket0512]; - return ref _bucket0512.GetSurePresentItemRef(index - UpToBucket0256); - case < UpToBucket1024: - _bucket1024 ??= new TEntry[UpToBucket1024]; - return ref _bucket1024.GetSurePresentItemRef(index - UpToBucket0512); - case < UpToBucket2048: - _bucket2048 ??= new TEntry[UpToBucket2048]; - return ref _bucket2048.GetSurePresentItemRef(index - UpToBucket1024); - default: - index -= UpToBucket2048; - var regularBucketIndex = index >>> RegularBucketBits; - var regularEntryIndex = index & RegularBucketMask; - if (_regularBuckets == null) - _regularBuckets = new TEntry[regularBucketIndex + 1][]; - else if (_regularBuckets.Length <= regularBucketIndex) - Array.Resize(ref _regularBuckets, regularBucketIndex + 1); - ref var regularBucket = ref _regularBuckets.GetSurePresentItemRef(regularBucketIndex); - if (regularBucket == null) - regularBucket = new TEntry[RegularBucketSize]; - return ref regularBucket.GetSurePresentItemRef(regularEntryIndex); - }; -#endif + ref var bucket = ref GetStackBucketRef(bucketIndex); + if (bucket == null) + bucket = new TEntry[32 << (bucketIndex - 1)]; + return ref bucket.GetSurePresentItemRef(insideIndex); + } + else + { + var heapBucketCount = bucketIndex - 7; + if (_bucketsOf4096AndMore == null) + _bucketsOf4096AndMore = new TEntry[heapBucketCount][]; + else if (_bucketsOf4096AndMore.Length < heapBucketCount) + Array.Resize(ref _bucketsOf4096AndMore, heapBucketCount); + + ref var bucket = ref _bucketsOf4096AndMore[heapBucketCount - 1]; + if (bucket == null) + bucket = new TEntry[4096 << (heapBucketCount - 1)]; + return ref bucket.GetSurePresentItemRef(insideIndex); + } } } @@ -1545,7 +1475,6 @@ public K GetSurePresentKey(int index) .Key; } - // todo: @perf try using StableGrowingEntries (or Xar) with O(1) random access. And make an default storage for the entries. // todo: @perf make it an interface method to store the actual entry in place of the 32 bit index, e.g. store and actual int key for the int Set ///Get the value ref by the entry index. Also the index corresponds to entry adding order. /// Important: it does not check the index bounds, so you need to check that the index is from 0 to map.Count-1 @@ -1920,7 +1849,8 @@ internal void ResizeMetadata() public struct SmallMap4() where TEq : struct, IEq { /// Map with 4 elements on stack and entries baked by the single array - public SmallMap, TEq, Size4, Stack4, Stack4>, SmallMap.SingleArrayEntries>> Map; + public SmallMap, TEq, Size4, Stack4, Stack4>, + SmallMap.StableArrayEntries>> Map; public SmallMap4(byte capacityBitShift) : this() => Map = new(capacityBitShift); } @@ -1928,10 +1858,12 @@ public struct SmallMap4() where TEq : struct, IEq public struct SmallMap8() where TEq : struct, IEq { /// Map with 8 elements on stack and entries baked by the single array - public SmallMap, TEq, Size8, Stack8, Stack8>, SmallMap.SingleArrayEntries>> Map; + public SmallMap, TEq, Size8, Stack8, Stack8>, + SmallMap.StableArrayEntries>> Map; public SmallMap8(byte capacityBitShift) : this() => Map = new(capacityBitShift); } +// todo: @wip check that it uses stable array entries /// Holds the Map with 16 items on stack. Minimizes the number of type arguments required to be specified public struct SmallMap16() where TEq : struct, IEq { diff --git a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs index 144643ee..e60a2050 100644 --- a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs +++ b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs @@ -28,6 +28,56 @@ public void Zero_0_hash_test() Assert.True(map.Map.ContainsKey(0)); } + [Test] + public void Can_store_and_retrieve_value_from_map() + { + var m = new SmallMap4(); + ref var map = ref m.Map; + + map.AddOrUpdate(42, "1"); + map.AddOrUpdate(42 + 32, "2"); + map.AddOrUpdate(42 + 32 + 32, "3"); + + // interrupt the keys with ne key + map.AddOrUpdate(43, "a"); + map.AddOrUpdate(43 + 32, "b"); + map.AddOrUpdate(43 + 32 + 32, "c"); + + map.AddOrUpdate(42 + 32 + 32 + 32, "4"); + + // insert 3rd variety of the keys + map.AddOrUpdate(44, "*"); + + map.AddOrUpdate(42 + 32 + 32 + 32 + 32, "5"); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32, "6"); + map.AddOrUpdate(43 + 32 + 32 + 32, "d"); + + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32, "7"); + map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, "8"); + + // check for the missing key + Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32, default(string))); + + // check for the strange key + Assert.AreEqual("*", map.GetValueOrDefault(44, default(string))); + + Assert.AreEqual("1", map.GetValueOrDefault(42, default(string))); + Assert.AreEqual("2", map.GetValueOrDefault(42 + 32, default(string))); + Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32, default(string))); + Assert.AreEqual("4", map.GetValueOrDefault(42 + 32 + 32 + 32, default(string))); + Assert.AreEqual("5", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32, default(string))); + Assert.AreEqual("6", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32, default(string))); + Assert.AreEqual("7", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32, default(string))); + Assert.AreEqual("8", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, default(string))); + + Assert.AreEqual("a", map.GetValueOrDefault(43, default(string))); + Assert.AreEqual("b", map.GetValueOrDefault(43 + 32, default(string))); + Assert.AreEqual("c", map.GetValueOrDefault(43 + 32 + 32, default(string))); + Assert.AreEqual("d", map.GetValueOrDefault(43 + 32 + 32 + 32, default(string))); + + Assert.AreEqual(13, map.Count); + } + [Test] public void Benchmark_test_with_Add_and_Lookup_10_items() { @@ -222,56 +272,6 @@ public void Benchmark_test_with_Add_and_Lookup_1000_items() // Verify(map, new[] { typeof(Tuple<>), typeof(Tuple<,>) }); // } - [Test] - public void Can_store_and_retrieve_value_from_map() - { - var m = new SmallMap16(); - ref var map = ref m.Map; - - map.AddOrUpdate(42, "1"); - map.AddOrUpdate(42 + 32, "2"); - map.AddOrUpdate(42 + 32 + 32, "3"); - - // interrupt the keys with ne key - map.AddOrUpdate(43, "a"); - map.AddOrUpdate(43 + 32, "b"); - map.AddOrUpdate(43 + 32 + 32, "c"); - - map.AddOrUpdate(42 + 32 + 32 + 32, "4"); - - // insert 3rd variety of the keys - map.AddOrUpdate(44, "*"); - - map.AddOrUpdate(42 + 32 + 32 + 32 + 32, "5"); - map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32, "6"); - map.AddOrUpdate(43 + 32 + 32 + 32, "d"); - - map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32, "7"); - map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, "8"); - - // check for the missing key - Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32, default(string))); - - // check for the strange key - Assert.AreEqual("*", map.GetValueOrDefault(44, default(string))); - - Assert.AreEqual("1", map.GetValueOrDefault(42, default(string))); - Assert.AreEqual("2", map.GetValueOrDefault(42 + 32, default(string))); - Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32, default(string))); - Assert.AreEqual("4", map.GetValueOrDefault(42 + 32 + 32 + 32, default(string))); - Assert.AreEqual("5", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32, default(string))); - Assert.AreEqual("6", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32, default(string))); - Assert.AreEqual("7", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32, default(string))); - Assert.AreEqual("8", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, default(string))); - - Assert.AreEqual("a", map.GetValueOrDefault(43, default(string))); - Assert.AreEqual("b", map.GetValueOrDefault(43 + 32, default(string))); - Assert.AreEqual("c", map.GetValueOrDefault(43 + 32 + 32, default(string))); - Assert.AreEqual("d", map.GetValueOrDefault(43 + 32 + 32 + 32, default(string))); - - Assert.AreEqual(13, map.Count); - } - /* ## Debug output example From 5ef99cb4e51b0f0febf15aabe9683b2fe4d53e5c Mon Sep 17 00:00:00 2001 From: dadhi Date: Tue, 16 Sep 2025 11:30:26 +0200 Subject: [PATCH 39/54] more tests passed --- playground/Experiments/FEC_ImTools.cs | 178 ++++++++++++++++++-------- 1 file changed, 122 insertions(+), 56 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 71e844ca..d7187bbb 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -27,7 +27,7 @@ THE SOFTWARE. #nullable disable #if DEBUG -// #define VERIFY_MAP +#define VERIFY_MAP #endif #if !NETSTANDARD2_0_OR_GREATER && !NET472 @@ -181,8 +181,8 @@ public static ref T GetSurePresentItemRef(this ref T source, int index) where public static ref T Add(this ref SmallList source, int initialCapacity = DefaultInitialCapacity) => ref AddDefaultAndGetRef(ref source.Items, source.Count++, initialCapacity); - /// Appends the new item to the list // todo: @perf add the not null variant + /// Appends the new item to the list [MethodImpl((MethodImplOptions)256)] public static void Add(this ref SmallList source, in T item, int initialCapacity = DefaultInitialCapacity) => AddDefaultAndGetRef(ref source.Items, source.Count++, initialCapacity) = item; @@ -994,7 +994,7 @@ public void Init(uint capacity) [MethodImpl((MethodImplOptions)256)] public ref TEntry GetSurePresentRef(int index) { - Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count}-1"); + Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count - 1}"); return ref _entries.GetSurePresentItemRef(index); } @@ -1038,17 +1038,23 @@ public struct StableArrayEntries : IEntries /// public int Count => _count; - // todo: @wip works only for index >= 32 - // Starting from the 0 bucket of 32 items: 0b00000000_00000000_00000000_00011111 = 31 - // 32 - leading zeros = 5, and -5 = 0 => bucket 0, e.g. 27 - leading zeros #if NET6_0_OR_GREATER [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int GetBucketIndexFromGlobalIndex(uint index) => - index < 32 ? 0 : 27 - BitOperations.LeadingZeroCount(index); + // Starting from the 0 bucket of 32 items: 0b00000000_00000000_00000000_00011111 = 31 + private static int GetBucketIndexFromGlobalIndex(uint index) + { + var bucketIndex = 27 - BitOperations.LeadingZeroCount(index); + // For index < 32: LeadingZeroCount gives 27-32, so 27-lzc gives -5 to 0 + // We want to set (clamp) negative values to 0, [-5..-1] => 0 + // Arithmetic right shift (bucketIndex >> 31) will convert any negative to -1 (0xFFFFFFFF), and 0 or positive to 0 + // Then inverting ~(-1) will give 0 mask to erase any negative bucketIndex to 0. + // Inverting ~(0) will give -1 mask to keep any positive bucketIndex as is. + return bucketIndex & ~(bucketIndex >> 31); + } #else private static int GetBucketIndexFromGlobalIndex(uint index) { - if (index == 0) return 0; + if (index < 32) return 0; var count = 0; if (index <= 0x0000FFFF) { count += 16; index <<= 16; } @@ -1086,7 +1092,7 @@ private ref TEntry[] GetStackBucketRef(int bucketIndex) /// Initializes enough buckets to hold the specified capacity. /// Always creates at least the first bucket of 32 items. - public void Init(uint capacity = 0) + public void Init(uint capacity = 1) { _bucket00Of32 = new TEntry[32]; _capacityPowerOfTwo = 32; @@ -1109,12 +1115,12 @@ public void Init(uint capacity = 0) [MethodImpl((MethodImplOptions)256)] public ref TEntry GetSurePresentRef(int index) { - Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count}-1"); - if (index < 32) // todo: @perf generalize GetBucketIndexFromGlobalIndex to remove this branch + Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count - 1}"); + if (index < 32) return ref _bucket00Of32.GetSurePresentItemRef(index); var bucketIndex = GetBucketIndexFromGlobalIndex((uint)index); - var insideIndex = index - (1 << (bucketIndex + 5)); // index - (32 << bucketIndex) + var insideIndex = index - (32 << (bucketIndex - 1)); var bucket = bucketIndex < 8 ? GetStackBucketRef(bucketIndex) : _bucketsOf4096AndMore[bucketIndex - 8]; return ref bucket.GetSurePresentItemRef(insideIndex); } @@ -1124,24 +1130,23 @@ public ref TEntry GetSurePresentRef(int index) public ref TEntry AddDefaultAndGetRef() { var lastIndex = _count++; - if (lastIndex < 32) // todo: @perf generalize GetBucketIndexFromGlobalIndex to remove this branch + if (lastIndex < 32) { + _capacityPowerOfTwo = 32; if (_bucket00Of32 == null) _bucket00Of32 = new TEntry[32]; return ref _bucket00Of32.GetSurePresentItemRef(lastIndex); } var bucketIndex = GetBucketIndexFromGlobalIndex((uint)lastIndex); - var insideIndex = lastIndex - (1 << (bucketIndex + 5)); // index - (32 << bucketIndex) - - _capacityPowerOfTwo = 32 << bucketIndex; - + var fullBacketsCapacity = 32 << (bucketIndex - 1); + _capacityPowerOfTwo = fullBacketsCapacity << 1; if (bucketIndex < 8) { ref var bucket = ref GetStackBucketRef(bucketIndex); if (bucket == null) - bucket = new TEntry[32 << (bucketIndex - 1)]; - return ref bucket.GetSurePresentItemRef(insideIndex); + bucket = new TEntry[fullBacketsCapacity]; + return ref bucket.GetSurePresentItemRef(lastIndex - fullBacketsCapacity); } else { @@ -1153,8 +1158,8 @@ public ref TEntry AddDefaultAndGetRef() ref var bucket = ref _bucketsOf4096AndMore[heapBucketCount - 1]; if (bucket == null) - bucket = new TEntry[4096 << (heapBucketCount - 1)]; - return ref bucket.GetSurePresentItemRef(insideIndex); + bucket = new TEntry[fullBacketsCapacity]; + return ref bucket.GetSurePresentItemRef(lastIndex - fullBacketsCapacity); } } } @@ -1330,7 +1335,7 @@ public static void VerifyKeyHasMetaInfo(ref this TMap map, { var key = map.GetSurePresentKey(i); var foundIndex = map.TryGetIndex(key); - assertCond(foundIndex != -1, $"Not found foundIndex:-1 for i:{i}, key:{key}"); + assertCond(foundIndex != -1, $"Not found (foundIndex:-1) for entryIndex:{i}, key:{key}"); assertCond(foundIndex == i, $"Does not correspond foundIndex:{foundIndex}, i:{i}, key:{key}"); } } @@ -1384,6 +1389,23 @@ public interface IMapImpl int GetHashCode(K key); } +#if DEBUG +/// Represents unpacked metadata for debugging - matches long layout +[DebuggerDisplay("{ToString(),nq}")] +[StructLayout(LayoutKind.Explicit, Size = 8)] +public readonly struct DebugMetaUnpacked +{ + [FieldOffset(0)] + public readonly uint HashAndProbe; + + [FieldOffset(4)] + public readonly uint EntryIndex; + + public override string ToString() => + HashAndProbe == 0 ? "empty" : $"ei:{EntryIndex}, hp:0b{Convert.ToString(HashAndProbe, 2).PadLeft(32, '0')}"; +} +#endif + /// /// Fast and less-allocating hash map without thread safety nets. Please measure it in your own use case before use. /// It is configurable in regard of hash calculation/equality via `TEq` type parameter and @@ -1398,7 +1420,7 @@ public interface IMapImpl /// For instance, for the `RefEq` the tombstone is . You may redefine it in the `IEq{K}.GetTombstone()` implementation. /// /// -[DebuggerDisplay("Contains {Count} entries")] +[DebuggerDisplay("Count = {Count}, Capacity = {Capacity}")] public struct SmallMap : IMap, IMapImpl where TEntry : struct, IEntry where TEq : struct, IEq @@ -1428,6 +1450,19 @@ public struct SmallMap DebugMetaUnpacked + { + get + { + var packed = _packedEntryIndexesHashesProbes; + if (packed == null || packed.Length == 0) + return Span.Empty; + return MemoryMarshal.Cast(packed.AsSpan()); + } + } +#endif + #pragma warning disable IDE0044 // it tries to make entries readonly but they should stay modify-able to prevent its defensive struct copying #pragma warning disable CS0649 // field is never assigned to, and will always have its default value internal TStackHashes _stackHashes; @@ -1466,12 +1501,12 @@ public SmallMap(uint capacityPowerOfTwoPlease) /// [MethodImpl((MethodImplOptions)256)] - public K GetSurePresentKey(int index) + public K GetSurePresentKey(int entryIndex) { - Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count}-1"); - return (index >= _stackEntries.Capacity - ? _heapEntries.GetSurePresentRef(index - _stackEntries.Capacity) - : _stackEntries.GetSurePresentItemRef(index)) + Debug.Assert(entryIndex >= 0 & entryIndex < _count, $"Entry index {entryIndex} should be in the range 0..{_count - 1}"); + return (entryIndex >= _stackEntries.Capacity + ? _heapEntries.GetSurePresentRef(entryIndex - _stackEntries.Capacity) + : _stackEntries.GetSurePresentItemRef(entryIndex)) .Key; } @@ -1480,12 +1515,12 @@ public K GetSurePresentKey(int index) /// Important: it does not check the index bounds, so you need to check that the index is from 0 to map.Count-1 [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public ref TEntry GetSurePresentEntryRef(int index) + public ref TEntry GetSurePresentEntryRef(int entryIndex) { - Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count}-1"); - if (index >= _stackEntries.Capacity) - return ref _heapEntries.GetSurePresentRef(index - _stackEntries.Capacity); - return ref _stackEntries.GetSurePresentItemRef(index); + Debug.Assert(entryIndex >= 0 && entryIndex < _count, $"Index {entryIndex} should be in the range 0..{_count - 1}"); + if (entryIndex >= _stackEntries.Capacity) + return ref _heapEntries.GetSurePresentRef(entryIndex - _stackEntries.Capacity); + return ref _stackEntries.GetSurePresentItemRef(entryIndex); } [UnscopedRef] @@ -1502,8 +1537,8 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) #endif found = true; var probeMask = _capacityPowerOfTwo - 1; - var metaIndex = hash & probeMask; var meta = (long)_count << EntryIndexStartAtBit | (long)(hash & ~probeMask) | 1L; + var metaIndex = hash & probeMask; while (true) { ref var mRef = ref metas.GetSurePresentItemRef(metaIndex & probeMask); @@ -1529,6 +1564,8 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) } found = false; + ref var newEntry = ref _heapEntries.AddDefaultAndGetRef(); + newEntry.Key = key; ++_count; // The new metadata slot is added - this is a good place to resize the metadata. @@ -1536,13 +1573,11 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) if (_capacityPowerOfTwo - _count <= (_capacityPowerOfTwo >>> MinFreeCapacityShift)) ResizeMetadata(); - ref var newEntry = ref _heapEntries.AddDefaultAndGetRef(); - newEntry.Key = key; return ref newEntry; } [MethodImpl((MethodImplOptions)256)] - private void PutMetadataWithoutResizing(int probeMask, int hash, int index) + private void PutMetadataWithoutResizing(int probeMask, int hash, int entryIndex) { #if DEBUG ++PutMetadataWithoutResizing_Count; @@ -1553,8 +1588,8 @@ private void PutMetadataWithoutResizing(int probeMask, int hash, int index) var metas = _packedHashesAndProbes; #endif + var meta = (long)entryIndex << EntryIndexStartAtBit | (long)(hash & ~probeMask) | 1L; var metaIndex = hash & probeMask; - var meta = (long)_count << EntryIndexStartAtBit | (long)(hash & ~probeMask) | 1L; while (true) { ref var mRef = ref metas.GetSurePresentItemRef(metaIndex & probeMask); @@ -1627,7 +1662,7 @@ private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) ++_count; - // Give the heap entries the same initial capacity as Stack, effectively doubling the capacity + // Creating the heap entries in addition to the stack entries, so the stack entries remain stable and never migrate _heapEntries.Init((uint)stackCapacity); // Set the key for the first entry, which is the 0 index in the entries @@ -1723,8 +1758,8 @@ internal int TryGetIndexInEntries(K key, int hash) var metas = _packedHashesAndProbes; #endif var probeMask = _capacityPowerOfTwo - 1; - var metaIndex = hash & probeMask; var meta = (long)(hash & ~probeMask) | 1L; + var metaIndex = hash & probeMask; while (true) { ref var mRef = ref metas.GetSurePresentItemRef(metaIndex & probeMask); @@ -1734,7 +1769,8 @@ internal int TryGetIndexInEntries(K key, int hash) if ((mRef & HashAndProbeMask) == meta) { var entryIndex = (int)(mRef >>> EntryIndexStartAtBit); - if (default(TEq).Equals(GetSurePresentKey(entryIndex), key)) + ref var entry = ref GetSurePresentEntryRef(entryIndex); + if (default(TEq).Equals(entry.Key, key)) return entryIndex; } @@ -1743,6 +1779,41 @@ internal int TryGetIndexInEntries(K key, int hash) } } + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] + internal ref TEntry TryGetEntryRef(K key, int hash, out bool found) + { + found = false; +#if NET7_0_OR_GREATER + ref var metas = ref MemoryMarshal.GetArrayDataReference(_packedEntryIndexesHashesProbes); +#else + var metas = _packedHashesAndProbes; +#endif + var probeMask = _capacityPowerOfTwo - 1; + var meta = (long)(hash & ~probeMask) | 1L; + var metaIndex = hash & probeMask; + while (true) + { + ref var mRef = ref metas.GetSurePresentItemRef(metaIndex & probeMask); + if ((mRef & probeMask) < (meta & probeMask)) + return ref RefTools.GetNullRef(); + + if ((mRef & HashAndProbeMask) == meta) + { + var entryIndex = (int)(mRef >>> EntryIndexStartAtBit); + ref var entry = ref GetSurePresentEntryRef(entryIndex); + if (default(TEq).Equals(entry.Key, key)) + { + found = true; + return ref entry; + } + } + + ++meta; + ++metaIndex; + } + } + /// [UnscopedRef] [MethodImpl((MethodImplOptions)256)] @@ -1750,17 +1821,12 @@ public ref TEntry TryGetEntryRef(K key, out bool found) { var hash = default(TEq).GetHashCode(key); if (_count > _stackEntries.Capacity) - { - var i = TryGetIndexInEntries(key, hash); - if (found = i != -1) - return ref GetSurePresentEntryRef(i); // the index may refer to the stack or heap entries - } - else - { - var i = _stackEntries.TryGetStackEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); - if (found = i != -1) - return ref _stackEntries.GetSurePresentItemRef(i); - } + return ref TryGetEntryRef(key, hash, out found); + + var i = _stackEntries.TryGetStackEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); + if (found = i != -1) + return ref _stackEntries.GetSurePresentItemRef(i); + return ref RefTools.GetNullRef(); } @@ -1793,8 +1859,8 @@ internal void ResizeMetadata() var probe = 1; while (true) { - var hp = metas.GetSurePresentItem(metaIndex); - if ((hp & probeMask) <= probe) + var m = metas.GetSurePresentItem(metaIndex); + if ((m & probeMask) <= probe) break; ++probe; ++metaIndex; @@ -1808,7 +1874,7 @@ internal void ResizeMetadata() { // Сalculate the ideal hash index for the current probe var p = (int)(m & probeMask); - var idealIndex = metaIndex - p + 1; // index - (probe - 1) because probe starts with 1 + var idealIndex = metaIndex - p + 1; // probe starts with 1, so index - (1 - 1) == index Debug.Assert(idealIndex >= 0, $"Ideal index should be non-negative, but found {idealIndex}"); // Calculate the new ideal index based on the next bit after the index mask (an oldCapacity bit). From f03bad4e101ebefe3938f92410425668b6403bd6 Mon Sep 17 00:00:00 2001 From: dadhi Date: Tue, 16 Sep 2025 12:40:04 +0200 Subject: [PATCH 40/54] all tests passing --- playground/Experiments/FEC_ImTools.cs | 46 +++++++++------------------ 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index d7187bbb..f7f632fb 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -1119,7 +1119,7 @@ public ref TEntry GetSurePresentRef(int index) if (index < 32) return ref _bucket00Of32.GetSurePresentItemRef(index); - var bucketIndex = GetBucketIndexFromGlobalIndex((uint)index); + var bucketIndex = GetBucketIndexFromGlobalIndex((uint)index); Debug.Assert(bucketIndex > 0, $"Bucket index should be more than 0 here but found {bucketIndex}"); var insideIndex = index - (32 << (bucketIndex - 1)); var bucket = bucketIndex < 8 ? GetStackBucketRef(bucketIndex) : _bucketsOf4096AndMore[bucketIndex - 8]; return ref bucket.GetSurePresentItemRef(insideIndex); @@ -1314,7 +1314,7 @@ public static void VerifyProbesRobinHoodInvariants(this TMap map, Actio { var metas = map.PackedEntryIndexesHashesProbes; var probeMask = map.Capacity - 1; - var prevProbe = startIndex > 0 ? (metas[startIndex - 1] & probeMask) : 0; + var prevProbe = (startIndex == 0 ? metas[metas.Length - 1] : metas[startIndex - 1]) & probeMask; endIndex = endIndex == -1 ? metas.Length : endIndex; for (var i = startIndex; i < endIndex; i++) @@ -1850,56 +1850,40 @@ internal void ResizeMetadata() var probeMask = oldCapacity - 1; var newProbeMask = newCapacity - 1; - // Skip the wrapped-around hashes, as they may be perfectly fit into new capacity without wrapping. - // They will be handled as the last bunch as it should be logically. - // Say it is probes 0:..3, 1:..2, 2:..0. Means the probe 3 is wrapped around from the end, - // and the next probe is 2 because its ideal position was occupied by 3. - // This algo will skip 3 and completes on 2 as it is the first non-wrapped probe. - var metaIndex = 0; - var probe = 1; - while (true) + for (var metaIndex = 0; metaIndex < oldCapacity; ++metaIndex) { var m = metas.GetSurePresentItem(metaIndex); - if ((m & probeMask) <= probe) - break; - ++probe; - ++metaIndex; - } - - var endBefore = metaIndex + oldCapacity; - while (true) - { - var m = metas.GetSurePresentItem(metaIndex & probeMask); if (m != 0) { // Сalculate the ideal hash index for the current probe var p = (int)(m & probeMask); - var idealIndex = metaIndex - p + 1; // probe starts with 1, so index - (1 - 1) == index + // Add oldCapacity then clamp with probeMask to avoid negative values for the wrapped-around indexes + var idealIndex = (oldCapacity + metaIndex - p + 1) & probeMask; Debug.Assert(idealIndex >= 0, $"Ideal index should be non-negative, but found {idealIndex}"); // Calculate the new ideal index based on the next bit after the index mask (an oldCapacity bit). // If the bit is 0 then the new index is the same otherwise it is oldCapacity + idealIndex, or in other words... - var newMetaIndex = (int)(m & oldCapacity) | idealIndex; var newMeta = (m & ~newProbeMask) | 1L; // starting probe is always 1 + var newMetaIndex = (int)(m & oldCapacity) | idealIndex; - // Copy the hash with the probe starting from the new ideal index - // No need for robin-hooding because the robin-hood invariant holds when elements are inserted in the same order + // Using robin-hood resordering, even if it is required only for the wrapped-around cases. + // But this is fine because after resize it will be less frequent and comparing to just insertion it is just one check more. while (true) { ref var newMetaRef = ref newMetas.GetSurePresentItemRef(newMetaIndex & newProbeMask); - if (newMetaRef == 0) + if ((newMetaRef & newProbeMask) < (newMeta & newProbeMask)) { + var mRobinHooded = newMetaRef; newMetaRef = newMeta; - break; + if (mRobinHooded == 0) // finish on the empty slot + break; + newMeta = mRobinHooded; } - ++newMeta; + + ++newMeta; // actually incrementing the probe, it is save because probe has the space of the whole capacity constrained by the probeMask ++newMetaIndex; } } - - // Move to the next hash or finish when all old hashes are processed - if (++metaIndex >= endBefore) - break; } _capacityPowerOfTwo = newCapacity; From 01b053ba5e0e5c00a81b45e43a5a00ddd049beda Mon Sep 17 00:00:00 2001 From: dadhi Date: Tue, 16 Sep 2025 12:47:05 +0200 Subject: [PATCH 41/54] small things --- playground/Experiments/FEC_ImTools.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index f7f632fb..8ea18fed 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -1920,7 +1920,6 @@ public struct SmallMap16() where TEq : struct, IEq /// Map with 16 elements on stack and entries baked by the single array public SmallMap, TEq, Size16, Stack16, Stack16>, SmallMap.StableArrayEntries>> Map; - public SmallMap16(byte capacityBitShift) : this() => Map = new(capacityBitShift); } @@ -1928,21 +1927,24 @@ public struct SmallMap16() where TEq : struct, IEq public struct SmallSet4() where TEq : struct, IEq { /// Set with 4 keys on stack and entries baked by the single array - public SmallMap, TEq, Size4, Stack4, Stack4>, SmallMap.SingleArrayEntries>> Set; + public SmallMap, TEq, Size4, Stack4, Stack4>, + SmallMap.StableArrayEntries>> Set; } /// Holds the Set with 8 items on stack. Minimizes the number of type arguments required to be specified public struct SmallSet8() where TEq : struct, IEq { /// Set with 8 keys on stack and entries baked by the single array - public SmallMap, TEq, Size8, Stack8, Stack8>, SmallMap.SingleArrayEntries>> Set; + public SmallMap, TEq, Size8, Stack8, Stack8>, + SmallMap.StableArrayEntries>> Set; } /// Holds the Set with 16 items on stack. Minimizes the number of type arguments required to be specified public struct SmallSet16() where TEq : struct, IEq { /// Set with 16 keys on stack and entries baked by the single array - public SmallMap, TEq, Size16, Stack16, Stack16>, SmallMap.SingleArrayEntries>> Set; + public SmallMap, TEq, Size16, Stack16, Stack16>, + SmallMap.StableArrayEntries>> Set; } #nullable restore From 87adf1a1f1579087918a9de2ce134b42479e55d8 Mon Sep 17 00:00:00 2001 From: dadhi Date: Wed, 17 Sep 2025 15:06:39 +0200 Subject: [PATCH 42/54] add IEntries.Init(...,initializeCountTo=0) --- playground/Experiments/FEC_ImTools.cs | 47 ++++++++----- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 66 ++++++++++++++++++- playground/ImTools.Benchmarks/Program.cs | 9 ++- 3 files changed, 104 insertions(+), 18 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 8ea18fed..47acd76e 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -961,7 +961,7 @@ public interface IEntries public int Count { get; } /// Initializes the entries storage to the specified capacity - void Init(uint capacity); + void Init(uint capacity, uint initializeCountTo = 0); /// Returns the reference to entry by its index, index should map to the present/non-removed entry ref TEntry GetSurePresentRef(int index); @@ -984,10 +984,12 @@ public struct SingleArrayEntries : IEntries public int Count => _count; /// - public void Init(uint capacity) + public void Init(uint capacity, uint initializeCountTo = 0) { _capacityPowerOfTwo = (int)GetNextPowerOfTwoFast(capacity); + Debug.Assert(_capacityPowerOfTwo >= initializeCountTo, $"Capacity {_capacityPowerOfTwo} should be enough to hold the initialized count {initializeCountTo}"); _entries = new TEntry[_capacityPowerOfTwo]; + _count = (int)initializeCountTo; } /// @@ -1038,18 +1040,20 @@ public struct StableArrayEntries : IEntries /// public int Count => _count; -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER [MethodImpl(MethodImplOptions.AggressiveInlining)] // Starting from the 0 bucket of 32 items: 0b00000000_00000000_00000000_00011111 = 31 private static int GetBucketIndexFromGlobalIndex(uint index) { + Debug.Assert(index >= 32, "Index should be 32 or more"); // todo: @wip var bucketIndex = 27 - BitOperations.LeadingZeroCount(index); // For index < 32: LeadingZeroCount gives 27-32, so 27-lzc gives -5 to 0 // We want to set (clamp) negative values to 0, [-5..-1] => 0 // Arithmetic right shift (bucketIndex >> 31) will convert any negative to -1 (0xFFFFFFFF), and 0 or positive to 0 // Then inverting ~(-1) will give 0 mask to erase any negative bucketIndex to 0. // Inverting ~(0) will give -1 mask to keep any positive bucketIndex as is. - return bucketIndex & ~(bucketIndex >> 31); + // todo: @wip + return bucketIndex; // bucketIndex & ~(bucketIndex >> 31); } #else private static int GetBucketIndexFromGlobalIndex(uint index) @@ -1092,14 +1096,20 @@ private ref TEntry[] GetStackBucketRef(int bucketIndex) /// Initializes enough buckets to hold the specified capacity. /// Always creates at least the first bucket of 32 items. - public void Init(uint capacity = 1) + public void Init(uint capacity, uint initializeCountTo = 0) { _bucket00Of32 = new TEntry[32]; _capacityPowerOfTwo = 32; - if (capacity <= 32) + + Debug.Assert(initializeCountTo <= _capacityPowerOfTwo, $"Passed count {initializeCountTo} should be less than or equal to capacity {capacity}"); + _count = (int)initializeCountTo; + + if (capacity <= 32) // Even if user said to use capacity 0, we still allocate the first bucket of 32 items return; var lastBucketIndex = GetBucketIndexFromGlobalIndex(capacity - 1); + Debug.Assert(lastBucketIndex >= 0, $"GetBucketIndexFromGlobalIndex should return {lastBucketIndex} >= 1 for capacity-1:{capacity - 1} > 32"); + for (var i = 1; i <= lastBucketIndex; i++) GetStackBucketRef(i) = new TEntry[32 << (i - 1)]; _capacityPowerOfTwo = 32 << lastBucketIndex; @@ -1585,7 +1595,7 @@ private void PutMetadataWithoutResizing(int probeMask, int hash, int entryIndex) #if NET7_0_OR_GREATER ref var metas = ref MemoryMarshal.GetArrayDataReference(_packedEntryIndexesHashesProbes); #else - var metas = _packedHashesAndProbes; + var metas = _packedEntryIndexesHashesProbes; #endif var meta = (long)entryIndex << EntryIndexStartAtBit | (long)(hash & ~probeMask) | 1L; @@ -1659,14 +1669,13 @@ private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) } PutMetadataWithoutResizing(probeMask, hash, stackCapacity); - ++_count; // Creating the heap entries in addition to the stack entries, so the stack entries remain stable and never migrate - _heapEntries.Init((uint)stackCapacity); + _heapEntries.Init((uint)stackCapacity, initializeCountTo: 1); // Set the key for the first entry, which is the 0 index in the entries - ref var newEntry = ref _heapEntries.AddDefaultAndGetRef(); + ref var newEntry = ref _heapEntries.GetSurePresentRef(0); newEntry.Key = key; return ref newEntry; } @@ -1755,7 +1764,7 @@ internal int TryGetIndexInEntries(K key, int hash) #if NET7_0_OR_GREATER ref var metas = ref MemoryMarshal.GetArrayDataReference(_packedEntryIndexesHashesProbes); #else - var metas = _packedHashesAndProbes; + var metas = _packedEntryIndexesHashesProbes; #endif var probeMask = _capacityPowerOfTwo - 1; var meta = (long)(hash & ~probeMask) | 1L; @@ -1787,7 +1796,7 @@ internal ref TEntry TryGetEntryRef(K key, int hash, out bool found) #if NET7_0_OR_GREATER ref var metas = ref MemoryMarshal.GetArrayDataReference(_packedEntryIndexesHashesProbes); #else - var metas = _packedHashesAndProbes; + var metas = _packedEntryIndexesHashesProbes; #endif var probeMask = _capacityPowerOfTwo - 1; var meta = (long)(hash & ~probeMask) | 1L; @@ -1901,7 +1910,7 @@ public struct SmallMap4() where TEq : struct, IEq /// Map with 4 elements on stack and entries baked by the single array public SmallMap, TEq, Size4, Stack4, Stack4>, SmallMap.StableArrayEntries>> Map; - public SmallMap4(byte capacityBitShift) : this() => Map = new(capacityBitShift); + public SmallMap4(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); } /// Holds the Map with 8 items on stack. Minimizes the number of type arguments required to be specified @@ -1910,7 +1919,7 @@ public struct SmallMap8() where TEq : struct, IEq /// Map with 8 elements on stack and entries baked by the single array public SmallMap, TEq, Size8, Stack8, Stack8>, SmallMap.StableArrayEntries>> Map; - public SmallMap8(byte capacityBitShift) : this() => Map = new(capacityBitShift); + public SmallMap8(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); } // todo: @wip check that it uses stable array entries @@ -1920,7 +1929,15 @@ public struct SmallMap16() where TEq : struct, IEq /// Map with 16 elements on stack and entries baked by the single array public SmallMap, TEq, Size16, Stack16, Stack16>, SmallMap.StableArrayEntries>> Map; - public SmallMap16(byte capacityBitShift) : this() => Map = new(capacityBitShift); + public SmallMap16(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); +} + +public struct SmallMap16_SingleArrEntries() where TEq : struct, IEq +{ + /// Map with 16 elements on stack and entries baked by the single array + public SmallMap, TEq, Size16, Stack16, Stack16>, + SmallMap.SingleArrayEntries>> Map; + public SmallMap16_SingleArrEntries(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); } /// Holds the Set with 4 items on stack. Minimizes the number of type arguments required to be specified diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 68ed58b4..31268ff7 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -15,6 +15,7 @@ using FHashMap91TypeString = ImTools.Experiments.FHashMap91, ImTools.Experiments.FHashMap91.SingleArrayEntries>>; using SmallMapTypeString = ImTools.HSmallMap, ImTools.HSmallMap.SingleArrayEntries>>; using FecSmallMapTypeString = FastExpressionCompiler.ImTools.SmallMap16>; +using FecSmallMapTypeString_SingleArrEntries = FastExpressionCompiler.ImTools.SmallMap16_SingleArrEntries>; using BenchmarkDotNet.Order; using ImTools.Experiments; @@ -2403,6 +2404,21 @@ .NET SDK 9.0.203 | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 29,665.4 ns | 588.68 ns | 1,581.45 ns | 29,786.1 ns | 1.00 | 0.08 | 1 | 9.1553 | 1.2817 | 57808 B | 1.00 | | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 55,723.9 ns | 1,098.60 ns | 1,078.97 ns | 55,599.6 ns | 1.88 | 0.12 | 2 | 7.8735 | 0.3052 | 49544 B | 0.86 | | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 63,373.1 ns | 1,260.87 ns | 2,106.62 ns | 63,624.5 ns | 2.14 | 0.14 | 3 | 6.2256 | 0.2441 | 39288 B | 0.68 | + + ## Packing indexes, hashes and probes together + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|---------:|---------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 24.17 us | 0.483 us | 1.176 us | 23.48 us | 1.00 | 0.07 | 1 | 9.1553 | 1.2817 | 56.45 KB | 1.00 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 52.85 us | 1.053 us | 1.871 us | 52.14 us | 2.19 | 0.13 | 2 | 7.8125 | 0.2441 | 48.05 KB | 0.85 | + + ## Packing indexes, hashes and probes together with Resize vs no Resize + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |-------------------------------------------------------------- |------ |---------:|---------:|---------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_Init1000_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 36.08 us | 0.329 us | 0.275 us | 36.08 us | 0.66 | 0.03 | 1 | 7.9346 | 0.4883 | 48.73 KB | 1.01 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 55.17 us | 0.985 us | 2.453 us | 54.41 us | 1.00 | 0.06 | 2 | 7.8125 | 0.2441 | 48.05 KB | 1.00 | + */ // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] // [Params(10, 100, 1000)] @@ -2841,7 +2857,7 @@ public string Dictionary_TryGetValue() } // [Benchmark] - [Benchmark(Baseline = true)] + // [Benchmark(Baseline = true)] public int DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent() { var dict = new DictionarySlim(); @@ -2881,7 +2897,7 @@ public int SmallMap_PopulateThenLookup_HalfMissed_HalfPresent() return count; } - [Benchmark] + [Benchmark(Baseline = true)] public int FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent() { var m = new FecSmallMapTypeString(); @@ -2904,6 +2920,52 @@ public int FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent() return count; } + [Benchmark] + public int FecHashMap_Init1000_PopulateThenLookup_HalfMissed_HalfPresent() + { + var m = new FecSmallMapTypeString(1024); + ref var map = ref m.Map; + + foreach (var key in _presentKeys) + map.AddOrUpdate(key, "a"); + + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) + { + ref var result = ref map.TryGetEntryRef(_randomPresentKeys[i], out var found); + if (found) + count += result.Value.Length; + _ = ref map.TryGetEntryRef(_missingKeys[i], out found); + if (!found) + --count; + } + return count; + } + + // [Benchmark] + public int FecHashMap_SingleArrEntries_PopulateThenLookup_HalfMissed_HalfPresent() + { + var m = new FecSmallMapTypeString_SingleArrEntries(); + ref var map = ref m.Map; + + foreach (var key in _presentKeys) + map.AddOrUpdate(key, "a"); + + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) + { + ref var result = ref map.TryGetEntryRef(_randomPresentKeys[i], out var found); + if (found) + count += result.Value.Length; + _ = ref map.TryGetEntryRef(_missingKeys[i], out found); + if (!found) + --count; + } + return count; + } + // [Benchmark] public int FHashMap11_TryGetValue() { diff --git a/playground/ImTools.Benchmarks/Program.cs b/playground/ImTools.Benchmarks/Program.cs index ba11443b..710c7db2 100644 --- a/playground/ImTools.Benchmarks/Program.cs +++ b/playground/ImTools.Benchmarks/Program.cs @@ -23,7 +23,14 @@ static void Main() // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); - BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + + var bm = new Playground.ImHashMapBenchmarks.Lookup(); + bm.Count = 1000; + bm.Populate(); + var count = bm.FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent(); + Console.WriteLine(count); + // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); From 758fb1e46fd0f74fb06509ced4cef4ce7795fce4 Mon Sep 17 00:00:00 2001 From: dadhi Date: Thu, 18 Sep 2025 11:19:02 +0200 Subject: [PATCH 43/54] scrt commit --- playground/Experiments/FEC_ImTools.cs | 25 +- .../ImTools.Benchmarks/SecretTetrisGame.html | 577 ++++++++++++++++++ 2 files changed, 591 insertions(+), 11 deletions(-) create mode 100644 playground/ImTools.Benchmarks/SecretTetrisGame.html diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 47acd76e..8a42f078 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -27,7 +27,7 @@ THE SOFTWARE. #nullable disable #if DEBUG -#define VERIFY_MAP +// #define VERIFY_MAP #endif #if !NETSTANDARD2_0_OR_GREATER && !NET472 @@ -1040,12 +1040,12 @@ public struct StableArrayEntries : IEntries /// public int Count => _count; -#if NET8_0_OR_GREATER - [MethodImpl(MethodImplOptions.AggressiveInlining)] // Starting from the 0 bucket of 32 items: 0b00000000_00000000_00000000_00011111 = 31 +#if NET8_0_OR_GREATER + [MethodImpl((MethodImplOptions)256)] private static int GetBucketIndexFromGlobalIndex(uint index) { - Debug.Assert(index >= 32, "Index should be 32 or more"); // todo: @wip + Debug.Assert(index >= 32, $"Index {index} should be 32 or more"); // todo: @wip var bucketIndex = 27 - BitOperations.LeadingZeroCount(index); // For index < 32: LeadingZeroCount gives 27-32, so 27-lzc gives -5 to 0 // We want to set (clamp) negative values to 0, [-5..-1] => 0 @@ -1058,6 +1058,7 @@ private static int GetBucketIndexFromGlobalIndex(uint index) #else private static int GetBucketIndexFromGlobalIndex(uint index) { + Debug.Assert(index >= 32, $"Index {index} should be 32 or more"); // todo: @wip if (index < 32) return 0; var count = 0; @@ -1096,6 +1097,7 @@ private ref TEntry[] GetStackBucketRef(int bucketIndex) /// Initializes enough buckets to hold the specified capacity. /// Always creates at least the first bucket of 32 items. + [MethodImpl((MethodImplOptions)256)] public void Init(uint capacity, uint initializeCountTo = 0) { _bucket00Of32 = new TEntry[32]; @@ -1109,16 +1111,16 @@ public void Init(uint capacity, uint initializeCountTo = 0) var lastBucketIndex = GetBucketIndexFromGlobalIndex(capacity - 1); Debug.Assert(lastBucketIndex >= 0, $"GetBucketIndexFromGlobalIndex should return {lastBucketIndex} >= 1 for capacity-1:{capacity - 1} > 32"); + _capacityPowerOfTwo = 32 << lastBucketIndex; for (var i = 1; i <= lastBucketIndex; i++) GetStackBucketRef(i) = new TEntry[32 << (i - 1)]; - _capacityPowerOfTwo = 32 << lastBucketIndex; if (lastBucketIndex < 8) return; - _bucketsOf4096AndMore = new TEntry[lastBucketIndex - 7][]; - for (var i = 0; i < _bucketsOf4096AndMore.Length; i++) - _bucketsOf4096AndMore[i] = new TEntry[4096 << i]; + var buckets = _bucketsOf4096AndMore = new TEntry[lastBucketIndex - 7][]; + for (var i = 0; i < buckets.Length; i++) + buckets[i] = new TEntry[4096 << i]; } /// @@ -1129,7 +1131,9 @@ public ref TEntry GetSurePresentRef(int index) if (index < 32) return ref _bucket00Of32.GetSurePresentItemRef(index); - var bucketIndex = GetBucketIndexFromGlobalIndex((uint)index); Debug.Assert(bucketIndex > 0, $"Bucket index should be more than 0 here but found {bucketIndex}"); + var bucketIndex = GetBucketIndexFromGlobalIndex((uint)index); + Debug.Assert(bucketIndex > 0, $"Bucket index should be more than 0 here but found {bucketIndex}"); + var insideIndex = index - (32 << (bucketIndex - 1)); var bucket = bucketIndex < 8 ? GetStackBucketRef(bucketIndex) : _bucketsOf4096AndMore[bucketIndex - 8]; return ref bucket.GetSurePresentItemRef(insideIndex); @@ -1143,8 +1147,7 @@ public ref TEntry AddDefaultAndGetRef() if (lastIndex < 32) { _capacityPowerOfTwo = 32; - if (_bucket00Of32 == null) - _bucket00Of32 = new TEntry[32]; + _bucket00Of32 ??= new TEntry[32]; return ref _bucket00Of32.GetSurePresentItemRef(lastIndex); } diff --git a/playground/ImTools.Benchmarks/SecretTetrisGame.html b/playground/ImTools.Benchmarks/SecretTetrisGame.html new file mode 100644 index 00000000..0b6661b1 --- /dev/null +++ b/playground/ImTools.Benchmarks/SecretTetrisGame.html @@ -0,0 +1,577 @@ + + + + + + Tetris + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + (Alt+r)
+ + (Alt+p)
+ + (Alt+w)
+ + (Alt+q)
+ + (Alt+e)
+ + (Alt+s)
+
+ + + + + \ No newline at end of file From 23557a45af3ce613b27270081e893178b43fd868 Mon Sep 17 00:00:00 2001 From: dadhi Date: Thu, 18 Sep 2025 12:08:17 +0200 Subject: [PATCH 44/54] generalize and simplify IEntries to IGrowingArray --- playground/Experiments/FEC_ImTools.cs | 86 ++++++++++--------- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 7 +- playground/ImTools.Benchmarks/Program.cs | 12 +-- 3 files changed, 54 insertions(+), 51 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 8a42f078..10eeed61 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -952,8 +952,7 @@ public static uint GetNextPowerOfTwoFast(uint capacity) } /// Abstraction to configure your own entries data structure. Check the derived types for the examples - public interface IEntries - where TEntry : struct, IEntry + public interface IGrowingArray { public int Capacity { get; } @@ -964,17 +963,16 @@ public interface IEntries void Init(uint capacity, uint initializeCountTo = 0); /// Returns the reference to entry by its index, index should map to the present/non-removed entry - ref TEntry GetSurePresentRef(int index); + ref T GetSurePresentRef(int index); /// Adds the key at the "end" of entries - so the order of addition is preserved. - ref TEntry AddDefaultAndGetRef(); + ref T AddDefaultAndGetRef(); } /// Stores the entries in a single dynamically reallocated growing array - public struct SingleArrayEntries : IEntries - where TEntry : struct, IEntry + public struct SingleArrayEntries : IGrowingArray { - internal TEntry[] _entries; + internal T[] _entries; internal int _capacityPowerOfTwo; internal int _count; @@ -988,13 +986,13 @@ public void Init(uint capacity, uint initializeCountTo = 0) { _capacityPowerOfTwo = (int)GetNextPowerOfTwoFast(capacity); Debug.Assert(_capacityPowerOfTwo >= initializeCountTo, $"Capacity {_capacityPowerOfTwo} should be enough to hold the initialized count {initializeCountTo}"); - _entries = new TEntry[_capacityPowerOfTwo]; + _entries = new T[_capacityPowerOfTwo]; _count = (int)initializeCountTo; } /// [MethodImpl((MethodImplOptions)256)] - public ref TEntry GetSurePresentRef(int index) + public ref T GetSurePresentRef(int index) { Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count - 1}"); return ref _entries.GetSurePresentItemRef(index); @@ -1002,7 +1000,7 @@ public ref TEntry GetSurePresentRef(int index) /// [MethodImpl((MethodImplOptions)256)] - public ref TEntry AddDefaultAndGetRef() + public ref T AddDefaultAndGetRef() { Debug.Assert(_count <= _entries.Length, "Entries should be initialized before adding the item"); if (_count == _entries.Length) @@ -1019,18 +1017,17 @@ public ref TEntry AddDefaultAndGetRef() /// Allocating the new bucket doubles the size preserving the previous buckets intact ensuring that the item references remain stable, /// e.g. the bucket sizes are [32, 32, 64, 128, 256, 512, 1024, ...] [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct StableArrayEntries : IEntries - where TEntry : struct, IEntry + public struct StableArrayEntries : IGrowingArray { - internal TEntry[] _bucket00Of32; - internal TEntry[] _bucket01Of32; - internal TEntry[] _bucket02Of64; - internal TEntry[] _bucket03Of128; - internal TEntry[] _bucket04Of256; - internal TEntry[] _bucket05Of512; - internal TEntry[] _bucket06Of1024; - internal TEntry[] _bucket07Of2048; - internal TEntry[][] _bucketsOf4096AndMore; + internal T[] _bucket00Of32; + internal T[] _bucket01Of32; + internal T[] _bucket02Of64; + internal T[] _bucket03Of128; + internal T[] _bucket04Of256; + internal T[] _bucket05Of512; + internal T[] _bucket06Of1024; + internal T[] _bucket07Of2048; + internal T[][] _bucketsOf4096AndMore; internal int _capacityPowerOfTwo; internal int _count; @@ -1075,7 +1072,7 @@ private static int GetBucketIndexFromGlobalIndex(uint index) /// [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - private ref TEntry[] GetStackBucketRef(int bucketIndex) + private ref T[] GetStackBucketRef(int bucketIndex) { Debug.Assert(bucketIndex >= 0 && bucketIndex < 8, "Bucket index should be in the range 0..7"); #if SUPPORTS_UNSAFE @@ -1100,7 +1097,7 @@ private ref TEntry[] GetStackBucketRef(int bucketIndex) [MethodImpl((MethodImplOptions)256)] public void Init(uint capacity, uint initializeCountTo = 0) { - _bucket00Of32 = new TEntry[32]; + _bucket00Of32 = new T[32]; _capacityPowerOfTwo = 32; Debug.Assert(initializeCountTo <= _capacityPowerOfTwo, $"Passed count {initializeCountTo} should be less than or equal to capacity {capacity}"); @@ -1114,18 +1111,18 @@ public void Init(uint capacity, uint initializeCountTo = 0) _capacityPowerOfTwo = 32 << lastBucketIndex; for (var i = 1; i <= lastBucketIndex; i++) - GetStackBucketRef(i) = new TEntry[32 << (i - 1)]; + GetStackBucketRef(i) = new T[32 << (i - 1)]; if (lastBucketIndex < 8) return; - var buckets = _bucketsOf4096AndMore = new TEntry[lastBucketIndex - 7][]; + var buckets = _bucketsOf4096AndMore = new T[lastBucketIndex - 7][]; for (var i = 0; i < buckets.Length; i++) - buckets[i] = new TEntry[4096 << i]; + buckets[i] = new T[4096 << i]; } /// [MethodImpl((MethodImplOptions)256)] - public ref TEntry GetSurePresentRef(int index) + public ref T GetSurePresentRef(int index) { Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count - 1}"); if (index < 32) @@ -1141,13 +1138,13 @@ public ref TEntry GetSurePresentRef(int index) /// [MethodImpl((MethodImplOptions)256)] - public ref TEntry AddDefaultAndGetRef() + public ref T AddDefaultAndGetRef() { var lastIndex = _count++; if (lastIndex < 32) { _capacityPowerOfTwo = 32; - _bucket00Of32 ??= new TEntry[32]; + _bucket00Of32 ??= new T[32]; return ref _bucket00Of32.GetSurePresentItemRef(lastIndex); } @@ -1158,20 +1155,20 @@ public ref TEntry AddDefaultAndGetRef() { ref var bucket = ref GetStackBucketRef(bucketIndex); if (bucket == null) - bucket = new TEntry[fullBacketsCapacity]; + bucket = new T[fullBacketsCapacity]; return ref bucket.GetSurePresentItemRef(lastIndex - fullBacketsCapacity); } else { var heapBucketCount = bucketIndex - 7; if (_bucketsOf4096AndMore == null) - _bucketsOf4096AndMore = new TEntry[heapBucketCount][]; + _bucketsOf4096AndMore = new T[heapBucketCount][]; else if (_bucketsOf4096AndMore.Length < heapBucketCount) Array.Resize(ref _bucketsOf4096AndMore, heapBucketCount); ref var bucket = ref _bucketsOf4096AndMore[heapBucketCount - 1]; if (bucket == null) - bucket = new TEntry[fullBacketsCapacity]; + bucket = new T[fullBacketsCapacity]; return ref bucket.GetSurePresentItemRef(lastIndex - fullBacketsCapacity); } } @@ -1440,7 +1437,7 @@ public struct SmallMap where TStackEntries : struct, IStack - where THeapEntries : struct, IEntries + where THeapEntries : struct, IGrowingArray { #if DEBUG // Diagnostic counters to measure the performance of the map operations @@ -1662,7 +1659,9 @@ private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) var stackCapacity = _stackEntries.Capacity; var newCapacity = Math.Max(_capacityPowerOfTwo, stackCapacity << 1); _capacityPowerOfTwo = newCapacity; - _packedEntryIndexesHashesProbes = new long[newCapacity]; + // todo: @wip the check is here because the it may be already initialized in the constructor, find way to avoid this check + if (_packedEntryIndexesHashesProbes == null || _packedEntryIndexesHashesProbes.Length != newCapacity) + _packedEntryIndexesHashesProbes = new long[newCapacity]; var probeMask = newCapacity - 1; for (var i = 0; i < stackCapacity; ++i) @@ -1675,7 +1674,10 @@ private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) ++_count; // Creating the heap entries in addition to the stack entries, so the stack entries remain stable and never migrate - _heapEntries.Init((uint)stackCapacity, initializeCountTo: 1); + + // todo: @wip the check is here because the it may be already initialized in the constructor, find way to avoid this check + if (_heapEntries.Capacity < stackCapacity) + _heapEntries.Init((uint)stackCapacity, initializeCountTo: 1); // Set the key for the first entry, which is the 0 index in the entries ref var newEntry = ref _heapEntries.GetSurePresentRef(0); @@ -1912,7 +1914,7 @@ public struct SmallMap4() where TEq : struct, IEq { /// Map with 4 elements on stack and entries baked by the single array public SmallMap, TEq, Size4, Stack4, Stack4>, - SmallMap.StableArrayEntries>> Map; + SmallMap.StableArrayEntries>> Map; public SmallMap4(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); } @@ -1921,7 +1923,7 @@ public struct SmallMap8() where TEq : struct, IEq { /// Map with 8 elements on stack and entries baked by the single array public SmallMap, TEq, Size8, Stack8, Stack8>, - SmallMap.StableArrayEntries>> Map; + SmallMap.StableArrayEntries>> Map; public SmallMap8(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); } @@ -1931,7 +1933,7 @@ public struct SmallMap16() where TEq : struct, IEq { /// Map with 16 elements on stack and entries baked by the single array public SmallMap, TEq, Size16, Stack16, Stack16>, - SmallMap.StableArrayEntries>> Map; + SmallMap.StableArrayEntries>> Map; public SmallMap16(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); } @@ -1939,7 +1941,7 @@ public struct SmallMap16_SingleArrEntries() where TEq : struct, IEqMap with 16 elements on stack and entries baked by the single array public SmallMap, TEq, Size16, Stack16, Stack16>, - SmallMap.SingleArrayEntries>> Map; + SmallMap.SingleArrayEntries>> Map; public SmallMap16_SingleArrEntries(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); } @@ -1948,7 +1950,7 @@ public struct SmallSet4() where TEq : struct, IEq { /// Set with 4 keys on stack and entries baked by the single array public SmallMap, TEq, Size4, Stack4, Stack4>, - SmallMap.StableArrayEntries>> Set; + SmallMap.StableArrayEntries>> Set; } /// Holds the Set with 8 items on stack. Minimizes the number of type arguments required to be specified @@ -1956,7 +1958,7 @@ public struct SmallSet8() where TEq : struct, IEq { /// Set with 8 keys on stack and entries baked by the single array public SmallMap, TEq, Size8, Stack8, Stack8>, - SmallMap.StableArrayEntries>> Set; + SmallMap.StableArrayEntries>> Set; } /// Holds the Set with 16 items on stack. Minimizes the number of type arguments required to be specified @@ -1964,7 +1966,7 @@ public struct SmallSet16() where TEq : struct, IEq { /// Set with 16 keys on stack and entries baked by the single array public SmallMap, TEq, Size16, Stack16, Stack16>, - SmallMap.StableArrayEntries>> Set; + SmallMap.StableArrayEntries>> Set; } #nullable restore diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 31268ff7..48d8ae3e 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -2857,7 +2857,7 @@ public string Dictionary_TryGetValue() } // [Benchmark] - // [Benchmark(Baseline = true)] + [Benchmark(Baseline = true)] public int DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent() { var dict = new DictionarySlim(); @@ -2897,7 +2897,8 @@ public int SmallMap_PopulateThenLookup_HalfMissed_HalfPresent() return count; } - [Benchmark(Baseline = true)] + // [Benchmark(Baseline = true)] + [Benchmark] public int FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent() { var m = new FecSmallMapTypeString(); @@ -2920,7 +2921,7 @@ public int FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent() return count; } - [Benchmark] + // [Benchmark] public int FecHashMap_Init1000_PopulateThenLookup_HalfMissed_HalfPresent() { var m = new FecSmallMapTypeString(1024); diff --git a/playground/ImTools.Benchmarks/Program.cs b/playground/ImTools.Benchmarks/Program.cs index 710c7db2..bdeb3255 100644 --- a/playground/ImTools.Benchmarks/Program.cs +++ b/playground/ImTools.Benchmarks/Program.cs @@ -23,13 +23,13 @@ static void Main() // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - var bm = new Playground.ImHashMapBenchmarks.Lookup(); - bm.Count = 1000; - bm.Populate(); - var count = bm.FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent(); - Console.WriteLine(count); + BenchmarkRunner.Run(); + // var bm = new Playground.ImHashMapBenchmarks.Lookup(); + // bm.Count = 1000; + // bm.Populate(); + // var count = bm.FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent(); + // Console.WriteLine(count); // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); From d5d913c041d3e0313a82f7b57b3d5eebf328c7d5 Mon Sep 17 00:00:00 2001 From: dadhi Date: Thu, 18 Sep 2025 12:40:06 +0200 Subject: [PATCH 45/54] renaming to StableSegmentedArray --- playground/Experiments/FEC_ImTools.cs | 158 +++++++++++++------------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 10eeed61..d728383a 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -970,7 +970,7 @@ public interface IGrowingArray } /// Stores the entries in a single dynamically reallocated growing array - public struct SingleArrayEntries : IGrowingArray + public struct SingleBackingArray : IGrowingArray { internal T[] _entries; internal int _capacityPowerOfTwo; @@ -1013,21 +1013,21 @@ public ref T AddDefaultAndGetRef() } } - /// Stores the data in the growing list of bucket arrays. - /// Allocating the new bucket doubles the size preserving the previous buckets intact ensuring that the item references remain stable, - /// e.g. the bucket sizes are [32, 32, 64, 128, 256, 512, 1024, ...] + /// Stores the data in the growing "list" of segment arrays. + /// Allocating the new segment array doubles the size preserving the previous segments intact ensuring that the item references remain stable, + /// e.g. the segment sizes are [32, 32, 64, 128, 256, 512, 1024, ...] [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct StableArrayEntries : IGrowingArray + public struct StableSegmentedArray : IGrowingArray { - internal T[] _bucket00Of32; - internal T[] _bucket01Of32; - internal T[] _bucket02Of64; - internal T[] _bucket03Of128; - internal T[] _bucket04Of256; - internal T[] _bucket05Of512; - internal T[] _bucket06Of1024; - internal T[] _bucket07Of2048; - internal T[][] _bucketsOf4096AndMore; + internal T[] _segment00Of32; + internal T[] _segment01Of32; + internal T[] _segment02Of64; + internal T[] _segment03Of128; + internal T[] _segment04Of256; + internal T[] _segment05Of512; + internal T[] _segment06Of1024; + internal T[] _segment07Of2048; + internal T[][] _segmentsOf4096AndMore; internal int _capacityPowerOfTwo; internal int _count; @@ -1040,20 +1040,20 @@ public struct StableArrayEntries : IGrowingArray // Starting from the 0 bucket of 32 items: 0b00000000_00000000_00000000_00011111 = 31 #if NET8_0_OR_GREATER [MethodImpl((MethodImplOptions)256)] - private static int GetBucketIndexFromGlobalIndex(uint index) + private static int GetSegmentIndexFromGlobalIndex(uint index) { Debug.Assert(index >= 32, $"Index {index} should be 32 or more"); // todo: @wip - var bucketIndex = 27 - BitOperations.LeadingZeroCount(index); + var segmentIndex = 27 - BitOperations.LeadingZeroCount(index); // For index < 32: LeadingZeroCount gives 27-32, so 27-lzc gives -5 to 0 // We want to set (clamp) negative values to 0, [-5..-1] => 0 - // Arithmetic right shift (bucketIndex >> 31) will convert any negative to -1 (0xFFFFFFFF), and 0 or positive to 0 - // Then inverting ~(-1) will give 0 mask to erase any negative bucketIndex to 0. - // Inverting ~(0) will give -1 mask to keep any positive bucketIndex as is. + // Arithmetic right shift (segmentIndex >> 31) will convert any negative to -1 (0xFFFFFFFF), and 0 or positive to 0 + // Then inverting ~(-1) will give 0 mask to erase any negative segmentIndex to 0. + // Inverting ~(0) will give -1 mask to keep any positive segmentIndex as is. // todo: @wip - return bucketIndex; // bucketIndex & ~(bucketIndex >> 31); + return segmentIndex; // segmentIndex & ~(segmentIndex >> 31); } #else - private static int GetBucketIndexFromGlobalIndex(uint index) + private static int GetSegmentIndexFromGlobalIndex(uint index) { Debug.Assert(index >= 32, $"Index {index} should be 32 or more"); // todo: @wip if (index < 32) return 0; @@ -1072,52 +1072,52 @@ private static int GetBucketIndexFromGlobalIndex(uint index) /// [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - private ref T[] GetStackBucketRef(int bucketIndex) + private ref T[] GetStackSegmentRef(int segmentIndex) { - Debug.Assert(bucketIndex >= 0 && bucketIndex < 8, "Bucket index should be in the range 0..7"); + Debug.Assert(segmentIndex >= 0 && segmentIndex < 8, "Segment index should be in the range 0..7"); #if SUPPORTS_UNSAFE - return ref Unsafe.Add(ref _bucket00Of32, bucketIndex); + return ref Unsafe.Add(ref _segment00Of32, segmentIndex); #else - switch (bucketIndex) + switch (segmentIndex) { - case 0: return ref _bucket00Of32; - case 1: return ref _bucket01Of32; - case 2: return ref _bucket02Of64; - case 3: return ref _bucket03Of128; - case 4: return ref _bucket04Of256; - case 5: return ref _bucket05Of512; - case 6: return ref _bucket06Of1024; - default: return ref _bucket07Of2048; + case 0: return ref _segment00Of32; + case 1: return ref _segment01Of32; + case 2: return ref _segment02Of64; + case 3: return ref _segment03Of128; + case 4: return ref _segment04Of256; + case 5: return ref _segment05Of512; + case 6: return ref _segment06Of1024; + default: return ref _segment07Of2048; } #endif } - /// Initializes enough buckets to hold the specified capacity. - /// Always creates at least the first bucket of 32 items. + /// Initializes enough segments to hold the specified capacity. + /// Always creates at least the first segment of 32 items. [MethodImpl((MethodImplOptions)256)] public void Init(uint capacity, uint initializeCountTo = 0) { - _bucket00Of32 = new T[32]; + _segment00Of32 = new T[32]; _capacityPowerOfTwo = 32; Debug.Assert(initializeCountTo <= _capacityPowerOfTwo, $"Passed count {initializeCountTo} should be less than or equal to capacity {capacity}"); _count = (int)initializeCountTo; - if (capacity <= 32) // Even if user said to use capacity 0, we still allocate the first bucket of 32 items + if (capacity <= 32) // Even if user said to use capacity 0, we still allocate the first segment of 32 items return; - var lastBucketIndex = GetBucketIndexFromGlobalIndex(capacity - 1); - Debug.Assert(lastBucketIndex >= 0, $"GetBucketIndexFromGlobalIndex should return {lastBucketIndex} >= 1 for capacity-1:{capacity - 1} > 32"); - _capacityPowerOfTwo = 32 << lastBucketIndex; + var lastSegmentIndex = GetSegmentIndexFromGlobalIndex(capacity - 1); + Debug.Assert(lastSegmentIndex >= 0, $"GetSegmentIndexFromGlobalIndex should return {lastSegmentIndex} >= 1 for capacity-1:{capacity - 1} > 32"); + _capacityPowerOfTwo = 32 << lastSegmentIndex; - for (var i = 1; i <= lastBucketIndex; i++) - GetStackBucketRef(i) = new T[32 << (i - 1)]; - if (lastBucketIndex < 8) + for (var i = 1; i <= lastSegmentIndex; i++) + GetStackSegmentRef(i) = new T[32 << (i - 1)]; + if (lastSegmentIndex < 8) return; - var buckets = _bucketsOf4096AndMore = new T[lastBucketIndex - 7][]; - for (var i = 0; i < buckets.Length; i++) - buckets[i] = new T[4096 << i]; + var segments = _segmentsOf4096AndMore = new T[lastSegmentIndex - 7][]; + for (var i = 0; i < segments.Length; i++) + segments[i] = new T[4096 << i]; } /// @@ -1126,14 +1126,14 @@ public ref T GetSurePresentRef(int index) { Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count - 1}"); if (index < 32) - return ref _bucket00Of32.GetSurePresentItemRef(index); + return ref _segment00Of32.GetSurePresentItemRef(index); - var bucketIndex = GetBucketIndexFromGlobalIndex((uint)index); - Debug.Assert(bucketIndex > 0, $"Bucket index should be more than 0 here but found {bucketIndex}"); + var segmentIndex = GetSegmentIndexFromGlobalIndex((uint)index); + Debug.Assert(segmentIndex > 0, $"Segment index should be more than 0 here but found {segmentIndex}"); - var insideIndex = index - (32 << (bucketIndex - 1)); - var bucket = bucketIndex < 8 ? GetStackBucketRef(bucketIndex) : _bucketsOf4096AndMore[bucketIndex - 8]; - return ref bucket.GetSurePresentItemRef(insideIndex); + var insideIndex = index - (32 << (segmentIndex - 1)); + var segment = segmentIndex < 8 ? GetStackSegmentRef(segmentIndex) : _segmentsOf4096AndMore[segmentIndex - 8]; + return ref segment.GetSurePresentItemRef(insideIndex); } /// @@ -1144,32 +1144,32 @@ public ref T AddDefaultAndGetRef() if (lastIndex < 32) { _capacityPowerOfTwo = 32; - _bucket00Of32 ??= new T[32]; - return ref _bucket00Of32.GetSurePresentItemRef(lastIndex); + _segment00Of32 ??= new T[32]; + return ref _segment00Of32.GetSurePresentItemRef(lastIndex); } - var bucketIndex = GetBucketIndexFromGlobalIndex((uint)lastIndex); - var fullBacketsCapacity = 32 << (bucketIndex - 1); - _capacityPowerOfTwo = fullBacketsCapacity << 1; - if (bucketIndex < 8) + var segmentIndex = GetSegmentIndexFromGlobalIndex((uint)lastIndex); + var fullSegmentsCapacity = 32 << (segmentIndex - 1); + _capacityPowerOfTwo = fullSegmentsCapacity << 1; + if (segmentIndex < 8) { - ref var bucket = ref GetStackBucketRef(bucketIndex); - if (bucket == null) - bucket = new T[fullBacketsCapacity]; - return ref bucket.GetSurePresentItemRef(lastIndex - fullBacketsCapacity); + ref var segment = ref GetStackSegmentRef(segmentIndex); + if (segment == null) + segment = new T[fullSegmentsCapacity]; + return ref segment.GetSurePresentItemRef(lastIndex - fullSegmentsCapacity); } else { - var heapBucketCount = bucketIndex - 7; - if (_bucketsOf4096AndMore == null) - _bucketsOf4096AndMore = new T[heapBucketCount][]; - else if (_bucketsOf4096AndMore.Length < heapBucketCount) - Array.Resize(ref _bucketsOf4096AndMore, heapBucketCount); - - ref var bucket = ref _bucketsOf4096AndMore[heapBucketCount - 1]; - if (bucket == null) - bucket = new T[fullBacketsCapacity]; - return ref bucket.GetSurePresentItemRef(lastIndex - fullBacketsCapacity); + var heapSegmentCount = segmentIndex - 7; + if (_segmentsOf4096AndMore == null) + _segmentsOf4096AndMore = new T[heapSegmentCount][]; + else if (_segmentsOf4096AndMore.Length < heapSegmentCount) + Array.Resize(ref _segmentsOf4096AndMore, heapSegmentCount); + + ref var segment = ref _segmentsOf4096AndMore[heapSegmentCount - 1]; + if (segment == null) + segment = new T[fullSegmentsCapacity]; + return ref segment.GetSurePresentItemRef(lastIndex - fullSegmentsCapacity); } } } @@ -1914,7 +1914,7 @@ public struct SmallMap4() where TEq : struct, IEq { /// Map with 4 elements on stack and entries baked by the single array public SmallMap, TEq, Size4, Stack4, Stack4>, - SmallMap.StableArrayEntries>> Map; + SmallMap.StableSegmentedArray>> Map; public SmallMap4(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); } @@ -1923,7 +1923,7 @@ public struct SmallMap8() where TEq : struct, IEq { /// Map with 8 elements on stack and entries baked by the single array public SmallMap, TEq, Size8, Stack8, Stack8>, - SmallMap.StableArrayEntries>> Map; + SmallMap.StableSegmentedArray>> Map; public SmallMap8(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); } @@ -1933,7 +1933,7 @@ public struct SmallMap16() where TEq : struct, IEq { /// Map with 16 elements on stack and entries baked by the single array public SmallMap, TEq, Size16, Stack16, Stack16>, - SmallMap.StableArrayEntries>> Map; + SmallMap.StableSegmentedArray>> Map; public SmallMap16(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); } @@ -1941,7 +1941,7 @@ public struct SmallMap16_SingleArrEntries() where TEq : struct, IEqMap with 16 elements on stack and entries baked by the single array public SmallMap, TEq, Size16, Stack16, Stack16>, - SmallMap.SingleArrayEntries>> Map; + SmallMap.SingleBackingArray>> Map; public SmallMap16_SingleArrEntries(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); } @@ -1950,7 +1950,7 @@ public struct SmallSet4() where TEq : struct, IEq { /// Set with 4 keys on stack and entries baked by the single array public SmallMap, TEq, Size4, Stack4, Stack4>, - SmallMap.StableArrayEntries>> Set; + SmallMap.StableSegmentedArray>> Set; } /// Holds the Set with 8 items on stack. Minimizes the number of type arguments required to be specified @@ -1958,7 +1958,7 @@ public struct SmallSet8() where TEq : struct, IEq { /// Set with 8 keys on stack and entries baked by the single array public SmallMap, TEq, Size8, Stack8, Stack8>, - SmallMap.StableArrayEntries>> Set; + SmallMap.StableSegmentedArray>> Set; } /// Holds the Set with 16 items on stack. Minimizes the number of type arguments required to be specified @@ -1966,7 +1966,7 @@ public struct SmallSet16() where TEq : struct, IEq { /// Set with 16 keys on stack and entries baked by the single array public SmallMap, TEq, Size16, Stack16, Stack16>, - SmallMap.StableArrayEntries>> Set; + SmallMap.StableSegmentedArray>> Set; } #nullable restore From e37f35af0a6f94ab08704dc663f234cb95ad6f3e Mon Sep 17 00:00:00 2001 From: dadhi Date: Thu, 18 Sep 2025 12:50:17 +0200 Subject: [PATCH 46/54] move IGrowingArray and impls out of the SmallMap --- playground/Experiments/FEC_ImTools.cs | 506 +++++++++++++------------- 1 file changed, 251 insertions(+), 255 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index d728383a..81d68a65 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -704,6 +704,249 @@ public bool MoveNext() public void Dispose() { } } +internal static class BitOps +{ + /// Gets the next power of two greater than or equal to the specified capacity. + [MethodImpl((MethodImplOptions)256)] + public static uint GetNextPowerOfTwoFast(uint capacity) + { +#if NET8_0_OR_GREATER + return BitOperations.RoundUpToPowerOf2(capacity); +#else + --capacity; + capacity |= capacity >> 1; + capacity |= capacity >> 2; + capacity |= capacity >> 4; + capacity |= capacity >> 8; + capacity |= capacity >> 16; + return capacity + 1; +#endif + } +} + +/// Abstraction to configure your own entries data structure. Check the derived types for the examples +public interface IGrowingArray +{ + public int Capacity { get; } + + /// Tracks the number of store elements, enables method AddDefaultAndGetRef to work + public int Count { get; } + + /// Initializes the entries storage to the specified capacity + void Init(uint capacity, uint initializeCountTo = 0); + + /// Returns the reference to entry by its index, index should map to the present/non-removed entry + ref T GetSurePresentRef(int index); + + /// Adds the key at the "end" of entries - so the order of addition is preserved. + ref T AddDefaultAndGetRef(); +} + +/// Stores the entries in a single dynamically reallocated growing array +public struct SingleBackingArray : IGrowingArray +{ + internal T[] _entries; + internal int _capacityPowerOfTwo; + internal int _count; + + /// + public int Capacity => _capacityPowerOfTwo; + /// + public int Count => _count; + + /// + public void Init(uint capacity, uint initializeCountTo = 0) + { + _capacityPowerOfTwo = (int)BitOps.GetNextPowerOfTwoFast(capacity); + Debug.Assert(_capacityPowerOfTwo >= initializeCountTo, $"Capacity {_capacityPowerOfTwo} should be enough to hold the initialized count {initializeCountTo}"); + _entries = new T[_capacityPowerOfTwo]; + _count = (int)initializeCountTo; + } + + /// + [MethodImpl((MethodImplOptions)256)] + public ref T GetSurePresentRef(int index) + { + Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count - 1}"); + return ref _entries.GetSurePresentItemRef(index); + } + + /// + [MethodImpl((MethodImplOptions)256)] + public ref T AddDefaultAndGetRef() + { + Debug.Assert(_count <= _entries.Length, "Entries should be initialized before adding the item"); + if (_count == _entries.Length) + { + Debug.Assert(_capacityPowerOfTwo == _entries.Length, "Capacity should be equal to the entries length"); + _capacityPowerOfTwo <<= 1; + Array.Resize(ref _entries, _capacityPowerOfTwo); + } + return ref _entries.GetSurePresentItemRef(_count++); + } +} + +/// Stores the data in the growing "list" of segment arrays. +/// Allocating the new segment array doubles the size preserving the previous segments intact ensuring that the item references remain stable, +/// e.g. the segment sizes are [32, 32, 64, 128, 256, 512, 1024, ...] +[StructLayout(LayoutKind.Sequential, Pack = 1)] +public struct StableSegmentedArray : IGrowingArray +{ + internal T[] _segment00Of32; + internal T[] _segment01Of32; + internal T[] _segment02Of64; + internal T[] _segment03Of128; + internal T[] _segment04Of256; + internal T[] _segment05Of512; + internal T[] _segment06Of1024; + internal T[] _segment07Of2048; + + internal T[][] _segmentsOf4096AndMore; + internal int _capacityPowerOfTwo; + internal int _count; + + /// + public int Capacity => _capacityPowerOfTwo; + /// + public int Count => _count; + + // Starting from the 0 bucket of 32 items: 0b00000000_00000000_00000000_00011111 = 31 +#if NET8_0_OR_GREATER + [MethodImpl((MethodImplOptions)256)] + private static int GetSegmentIndexFromGlobalIndex(uint index) + { + Debug.Assert(index >= 32, $"Index {index} should be 32 or more"); // todo: @wip + var segmentIndex = 27 - BitOperations.LeadingZeroCount(index); + // For index < 32: LeadingZeroCount gives 27-32, so 27-lzc gives -5 to 0 + // We want to set (clamp) negative values to 0, [-5..-1] => 0 + // Arithmetic right shift (segmentIndex >> 31) will convert any negative to -1 (0xFFFFFFFF), and 0 or positive to 0 + // Then inverting ~(-1) will give 0 mask to erase any negative segmentIndex to 0. + // Inverting ~(0) will give -1 mask to keep any positive segmentIndex as is. + // todo: @wip + return segmentIndex; // segmentIndex & ~(segmentIndex >> 31); + } +#else + private static int GetSegmentIndexFromGlobalIndex(uint index) + { + Debug.Assert(index >= 32, $"Index {index} should be 32 or more"); // todo: @wip + if (index < 32) return 0; + + var count = 0; + if (index <= 0x0000FFFF) { count += 16; index <<= 16; } + if (index <= 0x00FFFFFF) { count += 8; index <<= 8; } + if (index <= 0x0FFFFFFF) { count += 4; index <<= 4; } + if (index <= 0x3FFFFFFF) { count += 2; index <<= 2; } + if (index <= 0x7FFFFFFF) { count += 1; } + + return 27 - count; + } +#endif + + /// + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] + private ref T[] GetStackSegmentRef(int segmentIndex) + { + Debug.Assert(segmentIndex >= 0 && segmentIndex < 8, "Segment index should be in the range 0..7"); +#if SUPPORTS_UNSAFE + return ref Unsafe.Add(ref _segment00Of32, segmentIndex); +#else + switch (segmentIndex) + { + case 0: return ref _segment00Of32; + case 1: return ref _segment01Of32; + case 2: return ref _segment02Of64; + case 3: return ref _segment03Of128; + case 4: return ref _segment04Of256; + case 5: return ref _segment05Of512; + case 6: return ref _segment06Of1024; + default: return ref _segment07Of2048; + } +#endif + } + + /// Initializes enough segments to hold the specified capacity. + /// Always creates at least the first segment of 32 items. + [MethodImpl((MethodImplOptions)256)] + public void Init(uint capacity, uint initializeCountTo = 0) + { + _segment00Of32 = new T[32]; + _capacityPowerOfTwo = 32; + + Debug.Assert(initializeCountTo <= _capacityPowerOfTwo, $"Passed count {initializeCountTo} should be less than or equal to capacity {capacity}"); + _count = (int)initializeCountTo; + + if (capacity <= 32) // Even if user said to use capacity 0, we still allocate the first segment of 32 items + return; + + var lastSegmentIndex = GetSegmentIndexFromGlobalIndex(capacity - 1); + Debug.Assert(lastSegmentIndex >= 0, $"GetSegmentIndexFromGlobalIndex should return {lastSegmentIndex} >= 1 for capacity-1:{capacity - 1} > 32"); + _capacityPowerOfTwo = 32 << lastSegmentIndex; + + for (var i = 1; i <= lastSegmentIndex; i++) + GetStackSegmentRef(i) = new T[32 << (i - 1)]; + if (lastSegmentIndex < 8) + return; + + var segments = _segmentsOf4096AndMore = new T[lastSegmentIndex - 7][]; + for (var i = 0; i < segments.Length; i++) + segments[i] = new T[4096 << i]; + } + + /// + [MethodImpl((MethodImplOptions)256)] + public ref T GetSurePresentRef(int index) + { + Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count - 1}"); + if (index < 32) + return ref _segment00Of32.GetSurePresentItemRef(index); + + var segmentIndex = GetSegmentIndexFromGlobalIndex((uint)index); + Debug.Assert(segmentIndex > 0, $"Segment index should be more than 0 here but found {segmentIndex}"); + + var insideIndex = index - (32 << (segmentIndex - 1)); + var segment = segmentIndex < 8 ? GetStackSegmentRef(segmentIndex) : _segmentsOf4096AndMore[segmentIndex - 8]; + return ref segment.GetSurePresentItemRef(insideIndex); + } + + /// + [MethodImpl((MethodImplOptions)256)] + public ref T AddDefaultAndGetRef() + { + var lastIndex = _count++; + if (lastIndex < 32) + { + _capacityPowerOfTwo = 32; + _segment00Of32 ??= new T[32]; + return ref _segment00Of32.GetSurePresentItemRef(lastIndex); + } + + var segmentIndex = GetSegmentIndexFromGlobalIndex((uint)lastIndex); + var fullSegmentsCapacity = 32 << (segmentIndex - 1); + _capacityPowerOfTwo = fullSegmentsCapacity << 1; + if (segmentIndex < 8) + { + ref var segment = ref GetStackSegmentRef(segmentIndex); + if (segment == null) + segment = new T[fullSegmentsCapacity]; + return ref segment.GetSurePresentItemRef(lastIndex - fullSegmentsCapacity); + } + else + { + var heapSegmentCount = segmentIndex - 7; + if (_segmentsOf4096AndMore == null) + _segmentsOf4096AndMore = new T[heapSegmentCount][]; + else if (_segmentsOf4096AndMore.Length < heapSegmentCount) + Array.Resize(ref _segmentsOf4096AndMore, heapSegmentCount); + + ref var segment = ref _segmentsOf4096AndMore[heapSegmentCount - 1]; + if (segment == null) + segment = new T[fullSegmentsCapacity]; + return ref segment.GetSurePresentItemRef(lastIndex - fullSegmentsCapacity); + } + } +} + /// Printable thing via provided printer public interface IPrintable { @@ -934,246 +1177,6 @@ public Entry(K key, V value) } } - /// Gets the next power of two greater than or equal to the specified capacity. - [MethodImpl((MethodImplOptions)256)] - public static uint GetNextPowerOfTwoFast(uint capacity) - { -#if NET8_0_OR_GREATER - return BitOperations.RoundUpToPowerOf2(capacity); -#else - --capacity; - capacity |= capacity >> 1; - capacity |= capacity >> 2; - capacity |= capacity >> 4; - capacity |= capacity >> 8; - capacity |= capacity >> 16; - return capacity + 1; -#endif - } - - /// Abstraction to configure your own entries data structure. Check the derived types for the examples - public interface IGrowingArray - { - public int Capacity { get; } - - /// Tracks the number of store elements, enables method AddDefaultAndGetRef to work - public int Count { get; } - - /// Initializes the entries storage to the specified capacity - void Init(uint capacity, uint initializeCountTo = 0); - - /// Returns the reference to entry by its index, index should map to the present/non-removed entry - ref T GetSurePresentRef(int index); - - /// Adds the key at the "end" of entries - so the order of addition is preserved. - ref T AddDefaultAndGetRef(); - } - - /// Stores the entries in a single dynamically reallocated growing array - public struct SingleBackingArray : IGrowingArray - { - internal T[] _entries; - internal int _capacityPowerOfTwo; - internal int _count; - - /// - public int Capacity => _capacityPowerOfTwo; - /// - public int Count => _count; - - /// - public void Init(uint capacity, uint initializeCountTo = 0) - { - _capacityPowerOfTwo = (int)GetNextPowerOfTwoFast(capacity); - Debug.Assert(_capacityPowerOfTwo >= initializeCountTo, $"Capacity {_capacityPowerOfTwo} should be enough to hold the initialized count {initializeCountTo}"); - _entries = new T[_capacityPowerOfTwo]; - _count = (int)initializeCountTo; - } - - /// - [MethodImpl((MethodImplOptions)256)] - public ref T GetSurePresentRef(int index) - { - Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count - 1}"); - return ref _entries.GetSurePresentItemRef(index); - } - - /// - [MethodImpl((MethodImplOptions)256)] - public ref T AddDefaultAndGetRef() - { - Debug.Assert(_count <= _entries.Length, "Entries should be initialized before adding the item"); - if (_count == _entries.Length) - { - Debug.Assert(_capacityPowerOfTwo == _entries.Length, "Capacity should be equal to the entries length"); - _capacityPowerOfTwo <<= 1; - Array.Resize(ref _entries, _capacityPowerOfTwo); - } - return ref _entries.GetSurePresentItemRef(_count++); - } - } - - /// Stores the data in the growing "list" of segment arrays. - /// Allocating the new segment array doubles the size preserving the previous segments intact ensuring that the item references remain stable, - /// e.g. the segment sizes are [32, 32, 64, 128, 256, 512, 1024, ...] - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct StableSegmentedArray : IGrowingArray - { - internal T[] _segment00Of32; - internal T[] _segment01Of32; - internal T[] _segment02Of64; - internal T[] _segment03Of128; - internal T[] _segment04Of256; - internal T[] _segment05Of512; - internal T[] _segment06Of1024; - internal T[] _segment07Of2048; - internal T[][] _segmentsOf4096AndMore; - - internal int _capacityPowerOfTwo; - internal int _count; - - /// - public int Capacity => _capacityPowerOfTwo; - /// - public int Count => _count; - - // Starting from the 0 bucket of 32 items: 0b00000000_00000000_00000000_00011111 = 31 -#if NET8_0_OR_GREATER - [MethodImpl((MethodImplOptions)256)] - private static int GetSegmentIndexFromGlobalIndex(uint index) - { - Debug.Assert(index >= 32, $"Index {index} should be 32 or more"); // todo: @wip - var segmentIndex = 27 - BitOperations.LeadingZeroCount(index); - // For index < 32: LeadingZeroCount gives 27-32, so 27-lzc gives -5 to 0 - // We want to set (clamp) negative values to 0, [-5..-1] => 0 - // Arithmetic right shift (segmentIndex >> 31) will convert any negative to -1 (0xFFFFFFFF), and 0 or positive to 0 - // Then inverting ~(-1) will give 0 mask to erase any negative segmentIndex to 0. - // Inverting ~(0) will give -1 mask to keep any positive segmentIndex as is. - // todo: @wip - return segmentIndex; // segmentIndex & ~(segmentIndex >> 31); - } -#else - private static int GetSegmentIndexFromGlobalIndex(uint index) - { - Debug.Assert(index >= 32, $"Index {index} should be 32 or more"); // todo: @wip - if (index < 32) return 0; - - var count = 0; - if (index <= 0x0000FFFF) { count += 16; index <<= 16; } - if (index <= 0x00FFFFFF) { count += 8; index <<= 8; } - if (index <= 0x0FFFFFFF) { count += 4; index <<= 4; } - if (index <= 0x3FFFFFFF) { count += 2; index <<= 2; } - if (index <= 0x7FFFFFFF) { count += 1; } - - return 27 - count; - } -#endif - - /// - [UnscopedRef] - [MethodImpl((MethodImplOptions)256)] - private ref T[] GetStackSegmentRef(int segmentIndex) - { - Debug.Assert(segmentIndex >= 0 && segmentIndex < 8, "Segment index should be in the range 0..7"); -#if SUPPORTS_UNSAFE - return ref Unsafe.Add(ref _segment00Of32, segmentIndex); -#else - switch (segmentIndex) - { - case 0: return ref _segment00Of32; - case 1: return ref _segment01Of32; - case 2: return ref _segment02Of64; - case 3: return ref _segment03Of128; - case 4: return ref _segment04Of256; - case 5: return ref _segment05Of512; - case 6: return ref _segment06Of1024; - default: return ref _segment07Of2048; - } -#endif - } - - /// Initializes enough segments to hold the specified capacity. - /// Always creates at least the first segment of 32 items. - [MethodImpl((MethodImplOptions)256)] - public void Init(uint capacity, uint initializeCountTo = 0) - { - _segment00Of32 = new T[32]; - _capacityPowerOfTwo = 32; - - Debug.Assert(initializeCountTo <= _capacityPowerOfTwo, $"Passed count {initializeCountTo} should be less than or equal to capacity {capacity}"); - _count = (int)initializeCountTo; - - if (capacity <= 32) // Even if user said to use capacity 0, we still allocate the first segment of 32 items - return; - - var lastSegmentIndex = GetSegmentIndexFromGlobalIndex(capacity - 1); - Debug.Assert(lastSegmentIndex >= 0, $"GetSegmentIndexFromGlobalIndex should return {lastSegmentIndex} >= 1 for capacity-1:{capacity - 1} > 32"); - _capacityPowerOfTwo = 32 << lastSegmentIndex; - - for (var i = 1; i <= lastSegmentIndex; i++) - GetStackSegmentRef(i) = new T[32 << (i - 1)]; - if (lastSegmentIndex < 8) - return; - - var segments = _segmentsOf4096AndMore = new T[lastSegmentIndex - 7][]; - for (var i = 0; i < segments.Length; i++) - segments[i] = new T[4096 << i]; - } - - /// - [MethodImpl((MethodImplOptions)256)] - public ref T GetSurePresentRef(int index) - { - Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count - 1}"); - if (index < 32) - return ref _segment00Of32.GetSurePresentItemRef(index); - - var segmentIndex = GetSegmentIndexFromGlobalIndex((uint)index); - Debug.Assert(segmentIndex > 0, $"Segment index should be more than 0 here but found {segmentIndex}"); - - var insideIndex = index - (32 << (segmentIndex - 1)); - var segment = segmentIndex < 8 ? GetStackSegmentRef(segmentIndex) : _segmentsOf4096AndMore[segmentIndex - 8]; - return ref segment.GetSurePresentItemRef(insideIndex); - } - - /// - [MethodImpl((MethodImplOptions)256)] - public ref T AddDefaultAndGetRef() - { - var lastIndex = _count++; - if (lastIndex < 32) - { - _capacityPowerOfTwo = 32; - _segment00Of32 ??= new T[32]; - return ref _segment00Of32.GetSurePresentItemRef(lastIndex); - } - - var segmentIndex = GetSegmentIndexFromGlobalIndex((uint)lastIndex); - var fullSegmentsCapacity = 32 << (segmentIndex - 1); - _capacityPowerOfTwo = fullSegmentsCapacity << 1; - if (segmentIndex < 8) - { - ref var segment = ref GetStackSegmentRef(segmentIndex); - if (segment == null) - segment = new T[fullSegmentsCapacity]; - return ref segment.GetSurePresentItemRef(lastIndex - fullSegmentsCapacity); - } - else - { - var heapSegmentCount = segmentIndex - 7; - if (_segmentsOf4096AndMore == null) - _segmentsOf4096AndMore = new T[heapSegmentCount][]; - else if (_segmentsOf4096AndMore.Length < heapSegmentCount) - Array.Resize(ref _segmentsOf4096AndMore, heapSegmentCount); - - ref var segment = ref _segmentsOf4096AndMore[heapSegmentCount - 1]; - if (segment == null) - segment = new T[fullSegmentsCapacity]; - return ref segment.GetSurePresentItemRef(lastIndex - fullSegmentsCapacity); - } - } - } - /// Lookup for the K in the TStackEntries, first by calculating it hash with TEq and searching the hash in the TStackHashes [MethodImpl((MethodImplOptions)256)] public static int TryGetStackEntryIndex( @@ -1498,7 +1501,7 @@ public SmallMap(uint capacityPowerOfTwoPlease) { var capacityPowerOfTwo = capacityPowerOfTwoPlease < MinCapacity ? MinCapacity - : GetNextPowerOfTwoFast(capacityPowerOfTwoPlease); + : BitOps.GetNextPowerOfTwoFast(capacityPowerOfTwoPlease); _capacityPowerOfTwo = (int)capacityPowerOfTwo; _packedEntryIndexesHashesProbes = new long[capacityPowerOfTwo]; @@ -1913,8 +1916,7 @@ internal void ResizeMetadata() public struct SmallMap4() where TEq : struct, IEq { /// Map with 4 elements on stack and entries baked by the single array - public SmallMap, TEq, Size4, Stack4, Stack4>, - SmallMap.StableSegmentedArray>> Map; + public SmallMap, TEq, Size4, Stack4, Stack4>, StableSegmentedArray>> Map; public SmallMap4(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); } @@ -1922,8 +1924,7 @@ public struct SmallMap4() where TEq : struct, IEq public struct SmallMap8() where TEq : struct, IEq { /// Map with 8 elements on stack and entries baked by the single array - public SmallMap, TEq, Size8, Stack8, Stack8>, - SmallMap.StableSegmentedArray>> Map; + public SmallMap, TEq, Size8, Stack8, Stack8>, StableSegmentedArray>> Map; public SmallMap8(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); } @@ -1932,16 +1933,14 @@ public struct SmallMap8() where TEq : struct, IEq public struct SmallMap16() where TEq : struct, IEq { /// Map with 16 elements on stack and entries baked by the single array - public SmallMap, TEq, Size16, Stack16, Stack16>, - SmallMap.StableSegmentedArray>> Map; + public SmallMap, TEq, Size16, Stack16, Stack16>, StableSegmentedArray>> Map; public SmallMap16(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); } public struct SmallMap16_SingleArrEntries() where TEq : struct, IEq { /// Map with 16 elements on stack and entries baked by the single array - public SmallMap, TEq, Size16, Stack16, Stack16>, - SmallMap.SingleBackingArray>> Map; + public SmallMap, TEq, Size16, Stack16, Stack16>, SingleBackingArray>> Map; public SmallMap16_SingleArrEntries(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); } @@ -1949,24 +1948,21 @@ public struct SmallMap16_SingleArrEntries() where TEq : struct, IEq() where TEq : struct, IEq { /// Set with 4 keys on stack and entries baked by the single array - public SmallMap, TEq, Size4, Stack4, Stack4>, - SmallMap.StableSegmentedArray>> Set; + public SmallMap, TEq, Size4, Stack4, Stack4>, StableSegmentedArray>> Set; } /// Holds the Set with 8 items on stack. Minimizes the number of type arguments required to be specified public struct SmallSet8() where TEq : struct, IEq { /// Set with 8 keys on stack and entries baked by the single array - public SmallMap, TEq, Size8, Stack8, Stack8>, - SmallMap.StableSegmentedArray>> Set; + public SmallMap, TEq, Size8, Stack8, Stack8>, StableSegmentedArray>> Set; } /// Holds the Set with 16 items on stack. Minimizes the number of type arguments required to be specified public struct SmallSet16() where TEq : struct, IEq { /// Set with 16 keys on stack and entries baked by the single array - public SmallMap, TEq, Size16, Stack16, Stack16>, - SmallMap.StableSegmentedArray>> Set; + public SmallMap, TEq, Size16, Stack16, Stack16>, StableSegmentedArray>> Set; } #nullable restore From 541c565d452c5f63924085faa6a4c8efaaea0c87 Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 4 Oct 2026 21:37:42 +0200 Subject: [PATCH 47/54] new optimized resize for map; new tests for the map --- playground/Experiments/FEC_ImTools.cs | 996 +++++++++++------- .../ImTools.Benchmarks.csproj | 2 +- playground/ImTools.Benchmarks/Program.cs | 84 +- .../SmallGrrVsSmallListBenchmarks.cs | 65 ++ playground/ImTools.UnionPlayground/Program.cs | 30 +- playground/ImTools.UnionPlayground/Union.cs | 240 ++--- test/ImTools.UnitTests/GrrTests.cs | 270 +++++ .../Playground/FecSmallMapTests.cs | 630 +++++------ 8 files changed, 1433 insertions(+), 884 deletions(-) create mode 100644 playground/ImTools.Benchmarks/SmallGrrVsSmallListBenchmarks.cs create mode 100644 test/ImTools.UnitTests/GrrTests.cs diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 81d68a65..5afd0349 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -27,7 +27,7 @@ THE SOFTWARE. #nullable disable #if DEBUG -// #define VERIFY_MAP +#define VERIFY_MAP #endif #if !NETSTANDARD2_0_OR_GREATER && !NET472 @@ -153,7 +153,7 @@ public static ref T GetSurePresentItemRef(this ref SmallList source, int i /// Returns surely present item ref by its index without boundary checks [MethodImpl((MethodImplOptions)256)] - public static ref T GetSurePresentItemRef(this T[] source, int index) => + public static ref T GetSurePresentRef(this T[] source, int index) => #if SUPPORTS_UNSAFE ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(source), index); #else @@ -243,6 +243,7 @@ public interface IStack : IStack { } +// todo: @wip possibly separate intoduce IStackCap interface for the StackCap, because nothing in this interface requires the stack. /// Abstracts over collection of the items on stack of the fixed Capacity, /// to be used as a part of the hybrid data structures which grow from stack to heap public interface IStack @@ -254,7 +255,7 @@ public interface IStack /// Returns the item by ref to read and write the item value, /// but does not check the index bounds comparing to the `this[index]` [UnscopedRef] - ref T GetSurePresentItemRef(int index); + ref T GetSurePresentRef(int index); /// Indexer returning the item by ref to read and write the item value [UnscopedRef] @@ -325,7 +326,7 @@ public struct Stack2 : IStack> /// [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public ref T GetSurePresentItemRef(int index) + public ref T GetSurePresentRef(int index) { #if SUPPORTS_UNSAFE return ref Unsafe.Add(ref _it0, index); @@ -346,7 +347,7 @@ public ref T this[int index] get { if (index >= 0 & index < Capacity) - return ref GetSurePresentItemRef(index); + return ref GetSurePresentRef(index); return ref Stack.ThrowIndexOutOfBounds(index, Capacity); } } @@ -370,7 +371,7 @@ public struct Stack4 : IStack> /// [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public ref T GetSurePresentItemRef(int index) + public ref T GetSurePresentRef(int index) { #if SUPPORTS_UNSAFE return ref Unsafe.Add(ref _it0, index); @@ -393,7 +394,7 @@ public ref T this[int index] get { if (index >= 0 & index < Capacity) - return ref GetSurePresentItemRef(index); + return ref GetSurePresentRef(index); return ref Stack.ThrowIndexOutOfBounds(index, Capacity); } } @@ -417,7 +418,7 @@ public struct Stack8 : IStack> /// [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public ref T GetSurePresentItemRef(int index) + public ref T GetSurePresentRef(int index) { #if SUPPORTS_UNSAFE return ref Unsafe.Add(ref _it0, index); @@ -444,7 +445,7 @@ public ref T this[int index] get { if (index >= 0 & index < Capacity) - return ref GetSurePresentItemRef(index); + return ref GetSurePresentRef(index); return ref Stack.ThrowIndexOutOfBounds(index, Capacity); } } @@ -469,7 +470,7 @@ public struct Stack16 : IStack> /// [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public ref T GetSurePresentItemRef(int index) + public ref T GetSurePresentRef(int index) { #if SUPPORTS_UNSAFE return ref Unsafe.Add(ref _it0, index); @@ -504,7 +505,7 @@ public ref T this[int index] get { if (index >= 0 & index < Capacity) - return ref GetSurePresentItemRef(index); + return ref GetSurePresentRef(index); return ref Stack.ThrowIndexOutOfBounds(index, Capacity); } } @@ -557,24 +558,24 @@ public ref T this[int index] var stackCap = Stack.Capacity; if (index < stackCap) - return ref Stack.GetSurePresentItemRef(index); + return ref Stack.GetSurePresentRef(index); Debug.Assert(Rest != null, "Expecting deeper items are already existing on heap"); - return ref Rest.GetSurePresentItemRef(index - stackCap); + return ref Rest.GetSurePresentRef(index - stackCap); } } /// Returns a surely present item ref by its index [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - public ref T GetSurePresentItemRef(int index) + public ref T GetSurePresentRef(int index) { Debug.Assert(Count != 0, "SmallList.GetSurePresentItemRef: list should not be empty"); Debug.Assert(index >= 0 & index < Count, $"SmallList.GetSurePresentItemRef: index {index} should be less than Count {Count}"); var stackCap = Stack.Capacity; if (index < stackCap) - return ref Stack.GetSurePresentItemRef(index); + return ref Stack.GetSurePresentRef(index); Debug.Assert(Rest != null); return ref Rest[index - stackCap]; @@ -588,7 +589,7 @@ public ref T AddDefaultAndGetRef() var index = Count++; var stackCap = Stack.Capacity; if (index < stackCap) - return ref Stack.GetSurePresentItemRef(index); + return ref Stack.GetSurePresentRef(index); return ref SmallList.AddDefaultAndGetRef(ref Rest, index - stackCap); } @@ -599,7 +600,7 @@ public int Add(in T item) var index = Count++; var stackCap = Stack.Capacity; if (index < stackCap) - Stack.GetSurePresentItemRef(index) = item; + Stack.GetSurePresentRef(index) = item; else SmallList.AddDefaultAndGetRef(ref Rest, index - stackCap) = item; return index; @@ -650,7 +651,7 @@ public void Clear() public ref T GetLastSurePresentItem() { Debug.Assert(Count != 0, "Expecting that the list is not empty"); - return ref GetSurePresentItemRef(Count - 1); + return ref GetSurePresentRef(Count - 1); } /// Removes the last item from the list aka the Stack Pop. Assumes that the list is not empty! @@ -658,7 +659,7 @@ public ref T GetLastSurePresentItem() public void RemoveLastSurePresentItem() { Debug.Assert(Count != 0, "SmallList.RemoveLastSurePresentItem: Expecting that the list is not empty"); - GetSurePresentItemRef(Count - 1) = default; + GetSurePresentRef(Count - 1) = default; --Count; } @@ -692,7 +693,7 @@ public bool MoveNext() if (index < _list.Count) { _current = index < _list.Stack.Capacity - ? _list.Stack.GetSurePresentItemRef(index) + ? _list.Stack.GetSurePresentRef(index) : _list.Rest[index - _list.Stack.Capacity]; return true; } @@ -708,243 +709,472 @@ internal static class BitOps { /// Gets the next power of two greater than or equal to the specified capacity. [MethodImpl((MethodImplOptions)256)] - public static uint GetNextPowerOfTwoFast(uint capacity) + public static uint NextPowerOfTwo(uint value) { #if NET8_0_OR_GREATER - return BitOperations.RoundUpToPowerOf2(capacity); + return BitOperations.RoundUpToPowerOf2(value); #else - --capacity; - capacity |= capacity >> 1; - capacity |= capacity >> 2; - capacity |= capacity >> 4; - capacity |= capacity >> 8; - capacity |= capacity >> 16; - return capacity + 1; + --value; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + return value + 1; +#endif + } + + /// Counts the number of leading zero bits in the specified value. + /// Useful to get Log2(value) and the number of bits needed to represent the value. + [MethodImpl((MethodImplOptions)256)] + public static int LeadingZeroCount(uint value) + { +#if NET8_0_OR_GREATER + return BitOperations.LeadingZeroCount(value); +#else + if (value == 0) return 32; + var count = 0; + if (value <= 0x0000FFFF) { count += 16; value <<= 16; } + if (value <= 0x00FFFFFF) { count += 8; value <<= 8; } + if (value <= 0x0FFFFFFF) { count += 4; value <<= 4; } + if (value <= 0x3FFFFFFF) { count += 2; value <<= 2; } + if (value <= 0x7FFFFFFF) { count += 1; } + return count; #endif } } -/// Abstraction to configure your own entries data structure. Check the derived types for the examples -public interface IGrowingArray +public interface ICapIndex { + /// The capacity public int Capacity { get; } - /// Tracks the number of store elements, enables method AddDefaultAndGetRef to work - public int Count { get; } + /// Returns the item by ref to read and write the item value, + /// but does not check the index bounds comparing to the `this[index]` + [UnscopedRef] + ref T GetSurePresentRef(int index); +} +/// Abstraction to configure your own entries data structure. Check the derived types for the examples +public interface IGrowingArray : ICapIndex +{ /// Initializes the entries storage to the specified capacity - void Init(uint capacity, uint initializeCountTo = 0); + void Init(uint capacity); - /// Returns the reference to entry by its index, index should map to the present/non-removed entry - ref T GetSurePresentRef(int index); + /// Doubles the current capacity of the array preserving the existing elements and count. + void DoubleCapacity(); - /// Adds the key at the "end" of entries - so the order of addition is preserved. - ref T AddDefaultAndGetRef(); + /// Doubles the capacity until the specified value is inside + void GrowCapacity(uint capacity); } /// Stores the entries in a single dynamically reallocated growing array public struct SingleBackingArray : IGrowingArray { - internal T[] _entries; internal int _capacityPowerOfTwo; - internal int _count; + internal T[] _entries; /// public int Capacity => _capacityPowerOfTwo; - /// - public int Count => _count; /// - public void Init(uint capacity, uint initializeCountTo = 0) + [MethodImpl((MethodImplOptions)256)] + public void Init(uint capacity) { - _capacityPowerOfTwo = (int)BitOps.GetNextPowerOfTwoFast(capacity); - Debug.Assert(_capacityPowerOfTwo >= initializeCountTo, $"Capacity {_capacityPowerOfTwo} should be enough to hold the initialized count {initializeCountTo}"); + _capacityPowerOfTwo = capacity == 0 ? 1 : (int)BitOps.NextPowerOfTwo(capacity); _entries = new T[_capacityPowerOfTwo]; - _count = (int)initializeCountTo; } /// [MethodImpl((MethodImplOptions)256)] - public ref T GetSurePresentRef(int index) + public void DoubleCapacity() { - Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count - 1}"); - return ref _entries.GetSurePresentItemRef(index); + Debug.Assert(_entries != null, "Entries should be initialized before doubling the capacity"); + Debug.Assert(_capacityPowerOfTwo != 0, "Capacity should be not 0"); + Array.Resize(ref _entries, _capacityPowerOfTwo <<= 1); } /// - [MethodImpl((MethodImplOptions)256)] - public ref T AddDefaultAndGetRef() + public void GrowCapacity(uint capacity) { - Debug.Assert(_count <= _entries.Length, "Entries should be initialized before adding the item"); - if (_count == _entries.Length) + if (_capacityPowerOfTwo == 0) { - Debug.Assert(_capacityPowerOfTwo == _entries.Length, "Capacity should be equal to the entries length"); - _capacityPowerOfTwo <<= 1; - Array.Resize(ref _entries, _capacityPowerOfTwo); + Init(capacity); + return; } - return ref _entries.GetSurePresentItemRef(_count++); + while (_capacityPowerOfTwo < capacity) + DoubleCapacity(); + } + + /// + [MethodImpl((MethodImplOptions)256)] + public ref T GetSurePresentRef(int index) + { + Debug.Assert(index >= 0 && index < _capacityPowerOfTwo, $"Index {index} should be in the range of 0 to capacity:{_capacityPowerOfTwo}"); + return ref _entries.GetSurePresentRef(index); } } -/// Stores the data in the growing "list" of segment arrays. -/// Allocating the new segment array doubles the size preserving the previous segments intact ensuring that the item references remain stable, -/// e.g. the segment sizes are [32, 32, 64, 128, 256, 512, 1024, ...] -[StructLayout(LayoutKind.Sequential, Pack = 1)] -public struct StableSegmentedArray : IGrowingArray +/// A growing array that grows by appending a new segment doubling the total capacity. +/// Existing segments are never resized/copied, so item references stay valid (stable). +/// Random access is still O(1) via power-of-two segment sizes. +/// Segment sizes starting from first segment F: F, F, 2F, 4F, 8F, ... (first two equal so total doubles each add). +/// +public struct Grr : IGrowingArray, IEnumerable + where TStack : struct, IStack { - internal T[] _segment00Of32; - internal T[] _segment01Of32; - internal T[] _segment02Of64; - internal T[] _segment03Of128; - internal T[] _segment04Of256; - internal T[] _segment05Of512; - internal T[] _segment06Of1024; - internal T[] _segment07Of2048; - - internal T[][] _segmentsOf4096AndMore; - internal int _capacityPowerOfTwo; - internal int _count; + public const int MinCapacityAndFirstSegmentSize = 16; + + internal int _totalCapacityPowerOfTwo; + /// Cached first segment length (power of two). Avoids recomputing 1 << bitIndex on the hot path. + internal int _firstSegmentCapacity; + /// log2(_firstSegmentCapacity). Used for segment index math after the first segment. + internal byte _firstSegmentCapacityBitIndex; + internal SmallList _segments; + + /// The number of segments. + public int SegmentCount => _segments.Count; /// - public int Capacity => _capacityPowerOfTwo; - /// - public int Count => _count; + public int Capacity => _totalCapacityPowerOfTwo; - // Starting from the 0 bucket of 32 items: 0b00000000_00000000_00000000_00011111 = 31 -#if NET8_0_OR_GREATER + /// Maps a global index that is already known to be past the first segment into a segment index. + /// Layout: [0 .. F) seg0, [F .. 2F) seg1, [2F .. 4F) seg2, ... so segment = log2(index) - log2(F) + 1 for index >= F. + /// Equivalently: segmentIndex = (32 - firstBit) - LZC(index). [MethodImpl((MethodImplOptions)256)] - private static int GetSegmentIndexFromGlobalIndex(uint index) + private int GetSegmentIndexPastFirst(uint index) { - Debug.Assert(index >= 32, $"Index {index} should be 32 or more"); // todo: @wip - var segmentIndex = 27 - BitOperations.LeadingZeroCount(index); - // For index < 32: LeadingZeroCount gives 27-32, so 27-lzc gives -5 to 0 - // We want to set (clamp) negative values to 0, [-5..-1] => 0 - // Arithmetic right shift (segmentIndex >> 31) will convert any negative to -1 (0xFFFFFFFF), and 0 or positive to 0 - // Then inverting ~(-1) will give 0 mask to erase any negative segmentIndex to 0. - // Inverting ~(0) will give -1 mask to keep any positive segmentIndex as is. - // todo: @wip - return segmentIndex; // segmentIndex & ~(segmentIndex >> 31); + Debug.Assert(index >= (uint)_firstSegmentCapacity, + $"Index {index} should be >= firstSegmentCapacity:{_firstSegmentCapacity}"); + // index >= F = 2^firstBit => LZC(index) <= 31 - firstBit => segmentIndex >= 1 + return 32 - _firstSegmentCapacityBitIndex - BitOps.LeadingZeroCount(index); } -#else - private static int GetSegmentIndexFromGlobalIndex(uint index) + + /// Creates the first segment with capacity rounded up to the next power-of-two (min 16). + /// Safe to call again to re-initialize (discards previous segments). + [MethodImpl((MethodImplOptions)256)] + public void Init(uint capacity) { - Debug.Assert(index >= 32, $"Index {index} should be 32 or more"); // todo: @wip - if (index < 32) return 0; + var firstSegmentCap = capacity <= MinCapacityAndFirstSegmentSize + ? MinCapacityAndFirstSegmentSize + : (int)BitOps.NextPowerOfTwo(capacity); + _totalCapacityPowerOfTwo = firstSegmentCap; + _firstSegmentCapacity = firstSegmentCap; + _firstSegmentCapacityBitIndex = (byte)(31 - BitOps.LeadingZeroCount((uint)firstSegmentCap)); + // Reset so re-Init works (SmallList.InitCount asserts Count == 0) + _segments = default; + _segments.InitCount(1); + _segments.GetSurePresentRef(0) = new T[firstSegmentCap]; + } - var count = 0; - if (index <= 0x0000FFFF) { count += 16; index <<= 16; } - if (index <= 0x00FFFFFF) { count += 8; index <<= 8; } - if (index <= 0x0FFFFFFF) { count += 4; index <<= 4; } - if (index <= 0x3FFFFFFF) { count += 2; index <<= 2; } - if (index <= 0x7FFFFFFF) { count += 1; } + /// + [MethodImpl((MethodImplOptions)256)] + public void DoubleCapacity() + { + Debug.Assert(_segments.Count != 0, + $"Segments should be initialized before doubling the capacity but found {_segments.Count} segments"); + Debug.Assert(_totalCapacityPowerOfTwo != 0, "Capacity should be non-zero before doubling"); + // New segment size == current total capacity, so total doubles and old segments stay untouched. + _segments.AddDefaultAndGetRef() = new T[_totalCapacityPowerOfTwo]; + _totalCapacityPowerOfTwo <<= 1; + } - return 27 - count; + /// + [MethodImpl((MethodImplOptions)256)] + public void GrowCapacity(uint capacity) + { + if (_totalCapacityPowerOfTwo == 0) + { + Init(capacity); + return; + } + while ((uint)_totalCapacityPowerOfTwo < capacity) + DoubleCapacity(); } -#endif /// [UnscopedRef] [MethodImpl((MethodImplOptions)256)] - private ref T[] GetStackSegmentRef(int segmentIndex) + public ref T GetSurePresentRef(int index) { - Debug.Assert(segmentIndex >= 0 && segmentIndex < 8, "Segment index should be in the range 0..7"); -#if SUPPORTS_UNSAFE - return ref Unsafe.Add(ref _segment00Of32, segmentIndex); -#else - switch (segmentIndex) + Debug.Assert(_totalCapacityPowerOfTwo != 0, "Grr should be initialized before accessing items"); + Debug.Assert(index >= 0 && index < _totalCapacityPowerOfTwo, + $"Index {index} should be in the range of 0 to capacity:{_totalCapacityPowerOfTwo}"); + + var firstCap = _firstSegmentCapacity; + // Hot path: first segment (often the only one for small maps) + if (index < firstCap) + return ref _segments.GetSurePresentRef(0).GetSurePresentRef(index); + + var segmentIndex = GetSegmentIndexPastFirst((uint)index); + Debug.Assert(segmentIndex >= 1 && segmentIndex < _segments.Count, + $"segmentIndex {segmentIndex} out of range for index:{index}, segmentCount:{_segments.Count}"); + + // Prefix capacity before segment S (S >= 1) is firstCap << (S - 1) == total capacity of segments 0..S-1 + var insideIndex = index - (firstCap << (segmentIndex - 1)); + return ref _segments.GetSurePresentRef(segmentIndex).GetSurePresentRef(insideIndex); + } + + /// Returns an enumerator over all allocated slots (Capacity items, not a separate Count). + [MethodImpl((MethodImplOptions)256)] + public GrrEnumerator GetEnumerator() => new GrrEnumerator(_segments); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} + +/// Enumerator over Grr segment slots (full segment arrays, including unused capacity tails). +public struct GrrEnumerator : IEnumerator, IEnumerator + where TStack : struct, IStack +{ + private T _current; + private T[] _currentSegment; + private int _segmentIndex; + private int _insideIndex; + private SmallList _segments; + + internal GrrEnumerator(SmallList segments) + { + _current = default; + _segmentIndex = 0; + _insideIndex = -1; + _segments = segments; + _currentSegment = segments.Count == 0 ? null : segments.GetSurePresentRef(0); + } + + /// + public T Current => _current; + object IEnumerator.Current => _current; + + /// + [MethodImpl((MethodImplOptions)256)] + public bool MoveNext() + { + if (_currentSegment == null) + return false; + + if (++_insideIndex >= _currentSegment.Length) { - case 0: return ref _segment00Of32; - case 1: return ref _segment01Of32; - case 2: return ref _segment02Of64; - case 3: return ref _segment03Of128; - case 4: return ref _segment04Of256; - case 5: return ref _segment05Of512; - case 6: return ref _segment06Of1024; - default: return ref _segment07Of2048; + if (++_segmentIndex >= _segments.Count) + { + _currentSegment = null; + return false; + } + _currentSegment = _segments.GetSurePresentRef(_segmentIndex); + Debug.Assert(_currentSegment != null, + $"New current segment should not be null for segmentIndex:{_segmentIndex} < _segments.Count:{_segments.Count}"); + _insideIndex = 0; } -#endif + + _current = _currentSegment.GetSurePresentRef(_insideIndex); + return true; } - /// Initializes enough segments to hold the specified capacity. - /// Always creates at least the first segment of 32 items. - [MethodImpl((MethodImplOptions)256)] - public void Init(uint capacity, uint initializeCountTo = 0) + /// + public void Reset() { - _segment00Of32 = new T[32]; - _capacityPowerOfTwo = 32; + _current = default; + _segmentIndex = 0; + _insideIndex = -1; + // Keep _segments so Reset+MoveNext can restart over the same data + _currentSegment = _segments.Count == 0 ? null : _segments.GetSurePresentRef(0); + } - Debug.Assert(initializeCountTo <= _capacityPowerOfTwo, $"Passed count {initializeCountTo} should be less than or equal to capacity {capacity}"); - _count = (int)initializeCountTo; + /// + public void Dispose() { } +} - if (capacity <= 32) // Even if user said to use capacity 0, we still allocate the first segment of 32 items - return; +/// +/// Hybrid growable list: first items live on a fixed , +/// overflow lives in a stable segmented (no resize/copy of old heap items). +/// API mirrors but heap growth keeps references into already-added heap items stable. +/// +public struct SmallGrr : IEnumerable + where TStack : struct, IStack + where TSegStack : struct, IStack +{ + /// Number of used items. May decrement to pop if you clear the slot yourself when needed. + public int Count; - var lastSegmentIndex = GetSegmentIndexFromGlobalIndex(capacity - 1); - Debug.Assert(lastSegmentIndex >= 0, $"GetSegmentIndexFromGlobalIndex should return {lastSegmentIndex} >= 1 for capacity-1:{capacity - 1} > 32"); - _capacityPowerOfTwo = 32 << lastSegmentIndex; +#pragma warning disable CS0649 + /// Inline/stack portion of the list. + public TStack Stack; +#pragma warning restore CS0649 - for (var i = 1; i <= lastSegmentIndex; i++) - GetStackSegmentRef(i) = new T[32 << (i - 1)]; - if (lastSegmentIndex < 8) - return; + /// Heap overflow stored as stable segments. Valid indexes are 0 .. Count - Stack.Capacity - 1. + public Grr Rest; - var segments = _segmentsOf4096AndMore = new T[lastSegmentIndex - 7][]; - for (var i = 0; i < segments.Length; i++) - segments[i] = new T[4096 << i]; + /// Indexer with bounds checks. + [UnscopedRef] + public ref T this[int index] + { + [MethodImpl((MethodImplOptions)256)] + get + { + if (index < 0 | index >= Count) + return ref SmallList.ThrowIndexOutOfBounds(index, Count); + return ref GetSurePresentRef(index); + } } - /// + /// Returns a surely present item by index (no bounds check beyond asserts). + [UnscopedRef] [MethodImpl((MethodImplOptions)256)] public ref T GetSurePresentRef(int index) { - Debug.Assert(index >= 0 && index < _count, $"Index {index} should be in the range 0..{_count - 1}"); - if (index < 32) - return ref _segment00Of32.GetSurePresentItemRef(index); + Debug.Assert(index >= 0 & index < Count, $"SmallGrr index {index} should be in [0, {Count})"); + var stackCap = Stack.Capacity; + if (index < stackCap) + return ref Stack.GetSurePresentRef(index); + return ref Rest.GetSurePresentRef(index - stackCap); + } - var segmentIndex = GetSegmentIndexFromGlobalIndex((uint)index); - Debug.Assert(segmentIndex > 0, $"Segment index should be more than 0 here but found {segmentIndex}"); + /// Appends a default item and returns a ref to it. + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] + public ref T AddDefaultAndGetRef() + { + var index = Count++; + var stackCap = Stack.Capacity; + if (index < stackCap) + return ref Stack.GetSurePresentRef(index); - var insideIndex = index - (32 << (segmentIndex - 1)); - var segment = segmentIndex < 8 ? GetStackSegmentRef(segmentIndex) : _segmentsOf4096AndMore[segmentIndex - 8]; - return ref segment.GetSurePresentItemRef(insideIndex); + var restIndex = index - stackCap; + if (restIndex >= Rest.Capacity) + Rest.GrowCapacity((uint)restIndex + 1); + return ref Rest.GetSurePresentRef(restIndex); } - /// + /// Appends an item. Returns the index of the added item. [MethodImpl((MethodImplOptions)256)] - public ref T AddDefaultAndGetRef() + public int Add(in T item) + { + AddDefaultAndGetRef() = item; + return Count - 1; + } + + /// Ensures capacity for at least items (stack + rest). + [MethodImpl((MethodImplOptions)256)] + public void EnsureCapacity(int count) + { + Debug.Assert(count >= 0); + var stackCap = Stack.Capacity; + if (count <= stackCap) + return; + Rest.GrowCapacity((uint)(count - stackCap)); + } + + /// Pre-sizes Count and backing storage (like ). + [MethodImpl((MethodImplOptions)256)] + public void InitCount(int count) + { + Debug.Assert(count > 0, "Count should be more than 0"); + Debug.Assert(Count == 0, "Initial the count should be 0"); + EnsureCapacity(count); + Count = count; + } + + /// Looks for the item and returns its index, or -1. + [MethodImpl((MethodImplOptions)256)] + public int TryGetIndex(in T item, TEq eq = default) where TEq : struct, IEq { - var lastIndex = _count++; - if (lastIndex < 32) + for (var i = 0; i < Count; ++i) { - _capacityPowerOfTwo = 32; - _segment00Of32 ??= new T[32]; - return ref _segment00Of32.GetSurePresentItemRef(lastIndex); + if (eq.Equals(item, GetSurePresentRef(i))) + return i; } + return -1; + } - var segmentIndex = GetSegmentIndexFromGlobalIndex((uint)lastIndex); - var fullSegmentsCapacity = 32 << (segmentIndex - 1); - _capacityPowerOfTwo = fullSegmentsCapacity << 1; - if (segmentIndex < 8) + /// Returns index of existing item or appends and returns the new index. + [MethodImpl((MethodImplOptions)256)] + public int GetIndexOrAdd(in T item, TEq eq = default) where TEq : struct, IEq + { + var i = TryGetIndex(in item, eq); + return i != -1 ? i : Add(in item); + } + + /// Clears count; does not free heap segments (they can be reused). + [MethodImpl((MethodImplOptions)256)] + public void Clear() + { + var stackCap = Stack.Capacity; + var n = Count < stackCap ? Count : stackCap; + for (var i = 0; i < n; ++i) + Stack.GetSurePresentRef(i) = default; + // Leave Rest segments allocated for reuse; clear used rest slots + var restCount = Count - stackCap; + if (restCount > 0) { - ref var segment = ref GetStackSegmentRef(segmentIndex); - if (segment == null) - segment = new T[fullSegmentsCapacity]; - return ref segment.GetSurePresentItemRef(lastIndex - fullSegmentsCapacity); + for (var i = 0; i < restCount; ++i) + Rest.GetSurePresentRef(i) = default; } - else + Count = 0; + } + + /// Last item ref; list must be non-empty. + [UnscopedRef] + [MethodImpl((MethodImplOptions)256)] + public ref T GetLastSurePresentItem() + { + Debug.Assert(Count != 0, "Expecting that the list is not empty"); + return ref GetSurePresentRef(Count - 1); + } + + /// Removes the last item. List must be non-empty. + [MethodImpl((MethodImplOptions)256)] + public void RemoveLastSurePresentItem() + { + Debug.Assert(Count != 0, "SmallGrr.RemoveLastSurePresentItem: list should not be empty"); + GetSurePresentRef(Count - 1) = default; + --Count; + } + + /// Struct enumerator over used items only (Count). + [MethodImpl((MethodImplOptions)256)] + public SmallGrrEnumerator GetEnumerator() => + new SmallGrrEnumerator(this); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} + +/// Enumerator for over used items. +public struct SmallGrrEnumerator : IEnumerator, IEnumerator + where TStack : struct, IStack + where TSegStack : struct, IStack +{ + private SmallGrr _list; + private int _index; + private T _current; + + internal SmallGrrEnumerator(SmallGrr list) + { + _list = list; + _index = -1; + _current = default; + } + + /// + public T Current => _current; + object IEnumerator.Current => _current; + + /// + [MethodImpl((MethodImplOptions)256)] + public bool MoveNext() + { + var index = ++_index; + if (index < _list.Count) { - var heapSegmentCount = segmentIndex - 7; - if (_segmentsOf4096AndMore == null) - _segmentsOf4096AndMore = new T[heapSegmentCount][]; - else if (_segmentsOf4096AndMore.Length < heapSegmentCount) - Array.Resize(ref _segmentsOf4096AndMore, heapSegmentCount); - - ref var segment = ref _segmentsOf4096AndMore[heapSegmentCount - 1]; - if (segment == null) - segment = new T[fullSegmentsCapacity]; - return ref segment.GetSurePresentItemRef(lastIndex - fullSegmentsCapacity); + _current = _list.GetSurePresentRef(index); + return true; } + return false; } + + /// + public void Reset() => _index = -1; + + /// + public void Dispose() { } } /// Printable thing via provided printer @@ -1210,7 +1440,7 @@ public static int TryGetStackEntryIndex= count) // todo: @perf can I remove this branch? return -1; - if (default(TEq).Equals(entries.GetSurePresentItemRef(matchIndex).Key, key)) + if (default(TEq).Equals(entries.GetSurePresentRef(matchIndex).Key, key)) return matchIndex; // Clear lower bits up to and including the first set bit, afaik it can be hw accelerated @@ -1225,9 +1455,9 @@ public static int TryGetStackEntryIndex(ref this TMap map, public static void VerifyHashesAndKeysEq(ref this TMap map, Action assertCond, Pass _ = default) where TMap : struct, IMap, IMapImpl { + if (map.Capacity <= 0) return; var metas = map.PackedEntryIndexesHashesProbes; var probeMask = map.Capacity - 1; - for (var i = 0; i < metas.Length; i++) + for (var i = 0; i < metas.Capacity; i++) { - var m = metas[i]; + var m = metas.GetSurePresentRef(i); if (m != 0) { var metaHashPart = (int)(m & ~probeMask); @@ -1300,13 +1531,14 @@ public static void VerifyHashesAndKeysEq(ref this TMap map, Action(this TMap map, Action assertCond, Pass _ = default) where TMap : struct, IMap, IMapImpl { + if (map.Capacity <= 0) return; var uniq = new Dictionary(map.Count); var metas = map.PackedEntryIndexesHashesProbes; var probeMask = map.Capacity - 1; - for (var i = 0; i < metas.Length; i++) + for (var i = 0; i < metas.Capacity; i++) { - var m = metas[i]; + var m = metas.GetSurePresentRef(i); if (m == 0) continue; @@ -1322,19 +1554,23 @@ public static void VerifyNoDuplicateKeys(this TMap map, ActionVerifies that the probes are consistently increasing public static void VerifyProbesRobinHoodInvariants(this TMap map, Action assertCond, Pass _ = default, - int startIndex = 0, int endIndex = -1) + int startIndex = 0, int endBefore = -1) where TMap : struct, IMapImpl { + if (map.Capacity <= 0) + return; // default or stack-only map: no metadata table yet var metas = map.PackedEntryIndexesHashesProbes; var probeMask = map.Capacity - 1; - var prevProbe = (startIndex == 0 ? metas[metas.Length - 1] : metas[startIndex - 1]) & probeMask; + var prevIndex = startIndex == 0 ? probeMask : startIndex - 1; + var prevProbe = metas.GetSurePresentRef(prevIndex) & probeMask; - endIndex = endIndex == -1 ? metas.Length : endIndex; - for (var i = startIndex; i < endIndex; i++) + endBefore = endBefore == -1 ? map.Capacity : endBefore; + for (var i = startIndex; i < endBefore; i++) { - var probe = metas[i] & probeMask; + var probe = metas.GetSurePresentRef(i) & probeMask; var invariant = prevProbe >= probe || prevProbe + 1 == probe; - assertCond(invariant, $"Probe invariant failed for probe index {i}, probe:{probe}, prevProbe:{prevProbe}"); + assertCond(invariant, $"Probe invariant failed for probe:{probe} at {i}, prevProbe:{prevProbe} at {prevIndex}"); + prevIndex = i; prevProbe = probe; } } @@ -1397,7 +1633,7 @@ public interface IMapImpl /// The Capacity int Capacity { get; } /// The metadata - long[] PackedEntryIndexesHashesProbes { get; } + IGrowingArray PackedEntryIndexesHashesProbes { get; } /// Returns the hash code for the provided key using the IEq implementation used by this map int GetHashCode(K key); } @@ -1421,17 +1657,18 @@ public override string ToString() => /// /// Fast and less-allocating hash map without thread safety nets. Please measure it in your own use case before use. -/// It is configurable in regard of hash calculation/equality via `TEq` type parameter and -/// in regard of key-value storage via `TEntries` type parameter. -/// +/// Configurable via (hash/equality) and (entry storage). +/// /// Details: -/// - Implemented as a struct so that the empty/default map does not allocate on heap -/// - Hashes and key-values are the separate collections enabling better cash locality and faster performance (data-oriented design) -/// - No SIMD for now to avoid complexity and costs for the smaller maps, so the map is more fit for the smaller sizes. -/// - Provides the "stable" enumeration of the entries in the added order -/// - The TryRemove method removes the hash but replaces the key-value entry with the tombstone key and the default value. -/// For instance, for the `RefEq` the tombstone is . You may redefine it in the `IEq{K}.GetTombstone()` implementation. -/// +/// - Struct so the empty/default map allocates nothing on the heap +/// - First N entries live on stack (); overflow goes to +/// - Metadata (packed entry-index | hash | probe) is stored in a stable and grows independently +/// - Robin-Hood open addressing on the metadata table +/// - Entry indexes preserve insertion order (stable enumeration by index 0..Count-1) +/// - Prefer for heap entries when you need stable refs across growth; +/// use when raw random-access speed matters more than ref stability +/// +/// Not implemented yet: removal / tombstones ( is reserved for that). /// [DebuggerDisplay("Count = {Count}, Capacity = {Capacity}")] public struct SmallMap : IMap, IMapImpl @@ -1453,7 +1690,7 @@ public struct SmallMap> _packedEntryIndexesHashesProbes; #if DEBUG && SUPPORTS_UNSAFE + /// Debug view of the first metadata segment only (not full multi-segment capacity). public Span DebugMetaUnpacked { get { var packed = _packedEntryIndexesHashesProbes; - if (packed == null || packed.Length == 0) + if (packed.SegmentCount == 0) return Span.Empty; - return MemoryMarshal.Cast(packed.AsSpan()); + return MemoryMarshal.Cast(packed._segments.GetSurePresentRef(0).AsSpan()); } } #endif @@ -1487,8 +1725,9 @@ public Span DebugMetaUnpacked /// Capacity public int Capacity => _capacityPowerOfTwo; - /// Access to the entry indexes, hashes and corresponding probes - public long[] PackedEntryIndexesHashesProbes => _packedEntryIndexesHashesProbes; + /// Access to the entry indexes, hashes and corresponding probes. + /// Note: the result value may be boxed + public IGrowingArray PackedEntryIndexesHashesProbes => _packedEntryIndexesHashesProbes; /// public int Count => _count; @@ -1501,10 +1740,10 @@ public SmallMap(uint capacityPowerOfTwoPlease) { var capacityPowerOfTwo = capacityPowerOfTwoPlease < MinCapacity ? MinCapacity - : BitOps.GetNextPowerOfTwoFast(capacityPowerOfTwoPlease); + : BitOps.NextPowerOfTwo(capacityPowerOfTwoPlease); _capacityPowerOfTwo = (int)capacityPowerOfTwo; - _packedEntryIndexesHashesProbes = new long[capacityPowerOfTwo]; + _packedEntryIndexesHashesProbes.Init(capacityPowerOfTwo); _heapEntries.Init(capacityPowerOfTwo); } @@ -1519,11 +1758,10 @@ public K GetSurePresentKey(int entryIndex) Debug.Assert(entryIndex >= 0 & entryIndex < _count, $"Entry index {entryIndex} should be in the range 0..{_count - 1}"); return (entryIndex >= _stackEntries.Capacity ? _heapEntries.GetSurePresentRef(entryIndex - _stackEntries.Capacity) - : _stackEntries.GetSurePresentItemRef(entryIndex)) + : _stackEntries.GetSurePresentRef(entryIndex)) .Key; } - // todo: @perf make it an interface method to store the actual entry in place of the 32 bit index, e.g. store and actual int key for the int Set ///Get the value ref by the entry index. Also the index corresponds to entry adding order. /// Important: it does not check the index bounds, so you need to check that the index is from 0 to map.Count-1 [UnscopedRef] @@ -1533,7 +1771,7 @@ public ref TEntry GetSurePresentEntryRef(int entryIndex) Debug.Assert(entryIndex >= 0 && entryIndex < _count, $"Index {entryIndex} should be in the range 0..{_count - 1}"); if (entryIndex >= _stackEntries.Capacity) return ref _heapEntries.GetSurePresentRef(entryIndex - _stackEntries.Capacity); - return ref _stackEntries.GetSurePresentItemRef(entryIndex); + return ref _stackEntries.GetSurePresentRef(entryIndex); } [UnscopedRef] @@ -1542,11 +1780,6 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) { #if DEBUG ++AddOrGetRefInEntries_Count; -#endif -#if NET7_0_OR_GREATER - ref var metas = ref MemoryMarshal.GetArrayDataReference(_packedEntryIndexesHashesProbes); -#else - ref var metas = ref _packedEntryIndexesHashesProbes; #endif found = true; var probeMask = _capacityPowerOfTwo - 1; @@ -1554,7 +1787,7 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) var metaIndex = hash & probeMask; while (true) { - ref var mRef = ref metas.GetSurePresentItemRef(metaIndex & probeMask); + ref var mRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(metaIndex & probeMask); if ((mRef & probeMask) < (meta & probeMask)) { var mRobinHooded = mRef; @@ -1577,12 +1810,15 @@ private ref TEntry AddOrGetRefInEntries(K key, int hash, out bool found) } found = false; - ref var newEntry = ref _heapEntries.AddDefaultAndGetRef(); + var newEntryIndex = _count - _stackEntries.Capacity; + if (newEntryIndex >= _heapEntries.Capacity) + _heapEntries.DoubleCapacity(); + + ref var newEntry = ref _heapEntries.GetSurePresentRef(newEntryIndex); newEntry.Key = key; ++_count; - // The new metadata slot is added - this is a good place to resize the metadata. - // If the free space is less than ~1/8 of capacity (~12.5%). + // If free metadata slots are less than ~1/8 of capacity (~12.5%), grow the table. if (_capacityPowerOfTwo - _count <= (_capacityPowerOfTwo >>> MinFreeCapacityShift)) ResizeMetadata(); @@ -1595,17 +1831,11 @@ private void PutMetadataWithoutResizing(int probeMask, int hash, int entryIndex) #if DEBUG ++PutMetadataWithoutResizing_Count; #endif -#if NET7_0_OR_GREATER - ref var metas = ref MemoryMarshal.GetArrayDataReference(_packedEntryIndexesHashesProbes); -#else - var metas = _packedEntryIndexesHashesProbes; -#endif - var meta = (long)entryIndex << EntryIndexStartAtBit | (long)(hash & ~probeMask) | 1L; var metaIndex = hash & probeMask; while (true) { - ref var mRef = ref metas.GetSurePresentItemRef(metaIndex & probeMask); + ref var mRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(metaIndex & probeMask); if ((mRef & probeMask) < (meta & probeMask)) { var mRobinHooded = mRef; @@ -1632,14 +1862,14 @@ public ref TEntry AddOrGetEntryRef(K key, out bool found) var i = _stackEntries.TryGetStackEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); if (found = i != -1) - return ref _stackEntries.GetSurePresentItemRef(i); + return ref _stackEntries.GetSurePresentRef(i); // Add the new entry to the stack if there is still space in the stack if (_count < _stackEntries.Capacity) { var addedIndex = _count++; - _stackHashes.GetSurePresentItemRef(addedIndex) = hash; - ref var newStackEntry = ref _stackEntries.GetSurePresentItemRef(addedIndex); + _stackHashes.GetSurePresentRef(addedIndex) = hash; + ref var newStackEntry = ref _stackEntries.GetSurePresentRef(addedIndex); newStackEntry.Key = key; return ref newStackEntry; } @@ -1647,73 +1877,54 @@ public ref TEntry AddOrGetEntryRef(K key, out bool found) return ref MigrateToHeapAndAddEntry(key, hash); } + [UnscopedRef] private ref TEntry MigrateToHeapAndAddEntry(K key, int hash) { - // Now all capacity of the stack is used. - // To avoid double work always going linearly through the Stack with the comparison, - // let's calculate the hash of the keys stored on stack and put them - // to the usual HashMap packed hashes and indexes array for the promised O(1) lookup. - // But the key-value entries will remain on the Stack, and for the found index of the entry we use the GetSurePresentItemRef(index) - // to get the entry reference either from the Stack or the TEntries. - // So the entries on stack are guarantied to be stable from the beginning of the map creation, - // because they are not copied when the Entries need to Resize (depending on the TEntries implementation). - // To ensure stable entries on Entries you need to use - + // Stack is full. Switch lookups from linear stack scan to the Robin-Hood metadata table. + // Stack entries themselves stay put (never copied). Heap overflow uses THeapEntries; + // with Grr those heap entry refs also stay stable across later growth. var stackCapacity = _stackEntries.Capacity; var newCapacity = Math.Max(_capacityPowerOfTwo, stackCapacity << 1); - _capacityPowerOfTwo = newCapacity; - // todo: @wip the check is here because the it may be already initialized in the constructor, find way to avoid this check - if (_packedEntryIndexesHashesProbes == null || _packedEntryIndexesHashesProbes.Length != newCapacity) - _packedEntryIndexesHashesProbes = new long[newCapacity]; + // Keep the larger of constructor-provided capacity vs. stack-driven capacity. + if (_packedEntryIndexesHashesProbes.Capacity != newCapacity) + _packedEntryIndexesHashesProbes.GrowCapacity((uint)newCapacity); + + _capacityPowerOfTwo = newCapacity = _packedEntryIndexesHashesProbes.Capacity; var probeMask = newCapacity - 1; for (var i = 0; i < stackCapacity; ++i) { - var h = _stackHashes.GetSurePresentItemRef(i); + var h = _stackHashes.GetSurePresentRef(i); PutMetadataWithoutResizing(probeMask, h, i); } PutMetadataWithoutResizing(probeMask, hash, stackCapacity); - ++_count; - - // Creating the heap entries in addition to the stack entries, so the stack entries remain stable and never migrate - // todo: @wip the check is here because the it may be already initialized in the constructor, find way to avoid this check - if (_heapEntries.Capacity < stackCapacity) - _heapEntries.Init((uint)stackCapacity, initializeCountTo: 1); - - // Set the key for the first entry, which is the 0 index in the entries + // First heap entry slot is index 0 in THeapEntries (global entry index == stackCapacity). + _heapEntries.GrowCapacity((uint)stackCapacity); ref var newEntry = ref _heapEntries.GetSurePresentRef(0); newEntry.Key = key; + ++_count; + +#if VERIFY_MAP + this.VerifyProbesRobinHoodInvariants(static (x, msg) => Debug.Assert(x, msg), Pass.It); + this.VerifyKeyHasMetaInfo(static (x, msg) => Debug.Assert(x, msg), Pass.It); +#endif + return ref newEntry; } /* - Insertion step by step: + Robin Hood open addressing example (capacity mask 7): - 1. Initially the map is empty. Its capacity mask is 7: + 1. Empty map Index: 0 1 2 3 4 5 6 7 Hash: [0] [0] [0] [0] [0] [0] [0] [0] - 2. Insert the key A with the hash 13, which is 0b0011_0101. 13 & 7 Mask = 5, so the index is 5. - Index: 0 1 2 3 4 5 6 7 - Hash: [0] [0] [0] [0] [0] [13] [0] [0] - Probe: 1A - - 3. Insert the key B with the hash 5, which is 0b0000_1011. 5 & 7 Mask = 5, so the index is again 5. - Index: 0 1 2 3 4 5 6 7 - Hash: [0] [0] [0] [0] [0] [13] [5] [0] - Probe 1A 2B - - 4. Insert the key C with the hash 7, which is 0b0010_0101. 7 & 7 Mask = 7, so the index is 7. - Index: 0 1 2 3 4 5 6 7 - Hash: [0] [0] [0] [0] [0] [13] [5] [7] - Probe: 1A 2B 1C - - 5. Insert the key D with the hash 21, which is 0b0101_0101. 21 & 7 Mask = 5, so the index is again again 5. - Index: 0 1 2 3 4 5 6 7 - Hash: [7] [0] [0] [0] [0] [13] [5] [21] - Probe: 2C 1A 2B 3D + 2. Insert A hash 13 -> index 5, probe 1 + 3. Insert B hash 5 -> index 6, probe 2 (displaced from 5) + 4. Insert C hash 7 -> index 7, probe 1 + 5. Insert D hash 21 -> index 7 probe 3, C wraps to index 0 probe 2 */ [UnscopedRef] [MethodImpl((MethodImplOptions)256)] @@ -1724,9 +1935,15 @@ private ref TEntry AddSureAbsentDefaultAndGetRefInEntries(K key, int hash) PutMetadataWithoutResizing(_capacityPowerOfTwo - 1, hash, _count); - ++_count; - ref var entry = ref _heapEntries.AddDefaultAndGetRef(); + var newEntryIndex = _count - _stackEntries.Capacity; + if (newEntryIndex >= _heapEntries.Capacity) + _heapEntries.DoubleCapacity(); + + ref var entry = ref _heapEntries.GetSurePresentRef(newEntryIndex); entry.Key = key; + + ++_count; + return ref entry; } @@ -1744,8 +1961,8 @@ public ref TEntry AddSureAbsentDefaultEntryAndGetRef(K key) if (_count < _stackEntries.Capacity) { var newIndex = _count++; - _stackHashes.GetSurePresentItemRef(newIndex) = hash; - ref var newStackEntry = ref _stackEntries.GetSurePresentItemRef(newIndex); + _stackHashes.GetSurePresentRef(newIndex) = hash; + ref var newStackEntry = ref _stackEntries.GetSurePresentRef(newIndex); newStackEntry.Key = key; return ref newStackEntry; } @@ -1769,17 +1986,12 @@ public int TryGetIndex(K key) [MethodImpl((MethodImplOptions)256)] internal int TryGetIndexInEntries(K key, int hash) { -#if NET7_0_OR_GREATER - ref var metas = ref MemoryMarshal.GetArrayDataReference(_packedEntryIndexesHashesProbes); -#else - var metas = _packedEntryIndexesHashesProbes; -#endif var probeMask = _capacityPowerOfTwo - 1; var meta = (long)(hash & ~probeMask) | 1L; var metaIndex = hash & probeMask; while (true) { - ref var mRef = ref metas.GetSurePresentItemRef(metaIndex & probeMask); + ref var mRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(metaIndex & probeMask); if ((mRef & probeMask) < (meta & probeMask)) return -1; @@ -1801,17 +2013,12 @@ internal int TryGetIndexInEntries(K key, int hash) internal ref TEntry TryGetEntryRef(K key, int hash, out bool found) { found = false; -#if NET7_0_OR_GREATER - ref var metas = ref MemoryMarshal.GetArrayDataReference(_packedEntryIndexesHashesProbes); -#else - var metas = _packedEntryIndexesHashesProbes; -#endif var probeMask = _capacityPowerOfTwo - 1; var meta = (long)(hash & ~probeMask) | 1L; var metaIndex = hash & probeMask; while (true) { - ref var mRef = ref metas.GetSurePresentItemRef(metaIndex & probeMask); + ref var mRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(metaIndex & probeMask); if ((mRef & probeMask) < (meta & probeMask)) return ref RefTools.GetNullRef(); @@ -1842,127 +2049,190 @@ public ref TEntry TryGetEntryRef(K key, out bool found) var i = _stackEntries.TryGetStackEntryIndex(ref _stackHashes, _count, key, hash, Pass.It); if (found = i != -1) - return ref _stackEntries.GetSurePresentItemRef(i); + return ref _stackEntries.GetSurePresentRef(i); return ref RefTools.GetNullRef(); } + internal void ResizeMetadataWithoutWrapArounds(int startAt, int oldCapacity, int probeMask) + { + var newCapacity = _capacityPowerOfTwo << 1; + var newProbeMask = newCapacity - 1; + + _capacityPowerOfTwo = newCapacity; + _packedEntryIndexesHashesProbes.DoubleCapacity(); + + // From the current index i reinsert the metas into the grown metadata array. + // The meta either stays if it is already in ideal position (probe == 1) + // or depending on the next recovered hash bit either tries to go closer to its ideal position or move to the initially empty second half of the doubled meta list. + Debug.Assert(startAt >= 0 && startAt < oldCapacity, $"Start index should be within the bounds of the old capacity."); + for (var i = startAt; i < oldCapacity; ++i) + { + ref var metaRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(i); + if (metaRef == 0) // empty slot, nothing to reinsert + continue; + + var probe = (int)(metaRef & probeMask); + var nextHashBit = metaRef & oldCapacity; // recover the next hash bit + if (probe == 1 & nextHashBit == 0) // already in ideal position, skip + continue; + + var newMeta = metaRef & ~newProbeMask; + var newProbe = 1; + + metaRef = 0; // erase the current meta slot, so we starting from ideal position we may find better/closer to ideal position slot or return to the previous erased slot. + + var idealIndex = i - (probe - 1) + (int)nextHashBit; + Debug.Assert(idealIndex >= 0, $"After skipping all wrapped-around metas, the new ideal index should be non-negative."); + while (idealIndex < newCapacity) // defensive check, should be prevented by wrapping with newProbeMask + { + ref var newMetaRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(idealIndex); + if (newMetaRef == 0) // empty -> place the new meta here + { + newMetaRef = newMeta | (long)newProbe; + break; + } + ++newProbe; + idealIndex = (idealIndex + 1) & newProbeMask; // defensive wrap on the new doubled capacity boundary + } + } + } + internal void ResizeMetadata() { #if DEBUG ++ResizeProbesAndHashes_Count; #endif var oldCapacity = _capacityPowerOfTwo; - var newCapacity = _capacityPowerOfTwo << 1; + var probeMask = oldCapacity - 1; - var newMetasArr = new long[newCapacity]; + // If no wrapped-around entries exist, we can resize without worrying about them. + ref var metaRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(0); + var probe = (int)(metaRef & probeMask); + if (probe <= 1) + { + ResizeMetadataWithoutWrapArounds(0, oldCapacity, probeMask); + return; + } -#if NET7_0_OR_GREATER - ref var newMetas = ref MemoryMarshal.GetArrayDataReference(newMetasArr); - ref var metas = ref MemoryMarshal.GetArrayDataReference(_packedEntryIndexesHashesProbes); -#else - var newMetas = newMetasArr; - var metas = _packedEntryIndexesHashesProbes; -#endif - var probeMask = oldCapacity - 1; + var newCapacity = _capacityPowerOfTwo << 1; var newProbeMask = newCapacity - 1; - for (var metaIndex = 0; metaIndex < oldCapacity; ++metaIndex) + // Move the wrapped-around metadata to the aside small-list hoping that the small amount of the items will keep them on stack. + // We need this to avoid wrapped-around items to be in a ways of simplistic linear probing re-insertions of the rest of the metadata. + SmallList> asideWrappedAroundMetas = default; + var i = 0; + while (i < oldCapacity) // defensive check - in reality metas should not contain only wrapped-around entries, there is no such thing without normal entries. { - var m = metas.GetSurePresentItem(metaIndex); - if (m != 0) + metaRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(i); + probe = (int)(metaRef & probeMask); + if (probe - 1 <= i) // default probe is 1, when at the index i:0 the probe is 2 and more means the item ideal index was before the 0 - so it goes from the end + break; + asideWrappedAroundMetas.Add(in metaRef); + metaRef = 0; // don't forget to clear the original slot + ++i; + } + Debug.Assert(i < oldCapacity, $"All entries are wrapped-around, which should not happen."); + + // Go reinsert the non-wrapped-around entries into the resized metadata list. + ResizeMetadataWithoutWrapArounds(i, oldCapacity, probeMask); + + // Reinsert the wrapped around entries from the set aside list to the grown metas via normal robin-hood insertion. + var wrappedCount = asideWrappedAroundMetas.Count; + for (var wrappedIndex = 0; wrappedIndex < wrappedCount; ++wrappedIndex) + { + var wrappedMeta = asideWrappedAroundMetas.GetSurePresentRef(wrappedIndex); + probe = (int)(wrappedMeta & probeMask); // oldCapacity + wrappedIndex - (probe - 1) gives the original position before wrap around, which in turn is the hash part we used + var nextHashBit = wrappedMeta & oldCapacity; + var newMetaIndex = (oldCapacity + wrappedIndex - (probe - 1) + (int)nextHashBit) & newProbeMask; // wrappedIndex in the aside list is the same as the original in metas which is funny/useful invariant + var newMeta = wrappedMeta & ~newProbeMask; + var newProbe = 1; + + // Reinsert using Robin Hood swaps, advancing the probe in each occupied slot and when the smaller probe found, switch to this new RH candidate and advance it instead. + while (true) { - // Сalculate the ideal hash index for the current probe - var p = (int)(m & probeMask); - // Add oldCapacity then clamp with probeMask to avoid negative values for the wrapped-around indexes - var idealIndex = (oldCapacity + metaIndex - p + 1) & probeMask; - Debug.Assert(idealIndex >= 0, $"Ideal index should be non-negative, but found {idealIndex}"); - - // Calculate the new ideal index based on the next bit after the index mask (an oldCapacity bit). - // If the bit is 0 then the new index is the same otherwise it is oldCapacity + idealIndex, or in other words... - var newMeta = (m & ~newProbeMask) | 1L; // starting probe is always 1 - var newMetaIndex = (int)(m & oldCapacity) | idealIndex; - - // Using robin-hood resordering, even if it is required only for the wrapped-around cases. - // But this is fine because after resize it will be less frequent and comparing to just insertion it is just one check more. - while (true) + ref var newMetaRef = ref _packedEntryIndexesHashesProbes.GetSurePresentRef(newMetaIndex); // here we operate on the new metas already spread into the new position + if ((newMetaRef & newProbeMask) < newProbe) // iterate until we found empty slot (probe 0) or RH candidate with smaller probe { - ref var newMetaRef = ref newMetas.GetSurePresentItemRef(newMetaIndex & newProbeMask); - if ((newMetaRef & newProbeMask) < (newMeta & newProbeMask)) - { - var mRobinHooded = newMetaRef; - newMetaRef = newMeta; - if (mRobinHooded == 0) // finish on the empty slot - break; - newMeta = mRobinHooded; - } - - ++newMeta; // actually incrementing the probe, it is save because probe has the space of the whole capacity constrained by the probeMask - ++newMetaIndex; + var candidate = newMetaRef; + newMetaRef = newMeta | (long)newProbe; + if (candidate == 0) // if candidate slot was empty we are done with this insertion + break; + newMeta = candidate & ~newProbeMask; + newProbe = (int)(candidate & newProbeMask); } + + ++newProbe; + newMetaIndex = (newMetaIndex + 1) & newProbeMask; } } - _capacityPowerOfTwo = newCapacity; - _packedEntryIndexesHashesProbes = newMetasArr; - #if VERIFY_MAP + this.VerifyProbesRobinHoodInvariants(static (x, msg) => Debug.Assert(x, msg), Pass.It); this.VerifyKeyHasMetaInfo(static (x, msg) => Debug.Assert(x, msg), Pass.It); #endif } } -/// Holds the Map with 4 items on stack. Minimizes the number of type arguments required to be specified -public struct SmallMap4() where TEq : struct, IEq +/// Map with 4 stack entries; heap overflow in stable . +public struct SmallMap4 where TEq : struct, IEq { - /// Map with 4 elements on stack and entries baked by the single array - public SmallMap, TEq, Size4, Stack4, Stack4>, StableSegmentedArray>> Map; - public SmallMap4(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); + public SmallMap, TEq, Size4, Stack4, Stack4>, + Grr, Stack8[]>>> Map; + public SmallMap4() => Map = new(MinCapacity); + public SmallMap4(uint capacityPowerOfTwo) => Map = new(capacityPowerOfTwo); } -/// Holds the Map with 8 items on stack. Minimizes the number of type arguments required to be specified -public struct SmallMap8() where TEq : struct, IEq +/// Map with 8 stack entries; heap overflow in stable . +public struct SmallMap8 where TEq : struct, IEq { - /// Map with 8 elements on stack and entries baked by the single array - public SmallMap, TEq, Size8, Stack8, Stack8>, StableSegmentedArray>> Map; - public SmallMap8(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); + public SmallMap, TEq, Size8, Stack8, Stack8>, + Grr, Stack8[]>>> Map; + public SmallMap8() => Map = new(MinCapacity); + public SmallMap8(uint capacityPowerOfTwo) => Map = new(capacityPowerOfTwo); } -// todo: @wip check that it uses stable array entries -/// Holds the Map with 16 items on stack. Minimizes the number of type arguments required to be specified -public struct SmallMap16() where TEq : struct, IEq +/// Map with 16 stack entries; heap overflow in stable (refs stay valid across growth). +public struct SmallMap16 where TEq : struct, IEq { - /// Map with 16 elements on stack and entries baked by the single array - public SmallMap, TEq, Size16, Stack16, Stack16>, StableSegmentedArray>> Map; - public SmallMap16(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); + public SmallMap, TEq, Size16, Stack16, Stack16>, + Grr, Stack8[]>>> Map; + public SmallMap16() => Map = new(MinCapacity); + public SmallMap16(uint capacityPowerOfTwo) => Map = new(capacityPowerOfTwo); } -public struct SmallMap16_SingleArrEntries() where TEq : struct, IEq +/// Map with 16 stack entries; heap overflow in a single growing array (faster access, refs invalidate on resize). +public struct SmallMap16_SingleArrEntries where TEq : struct, IEq { - /// Map with 16 elements on stack and entries baked by the single array - public SmallMap, TEq, Size16, Stack16, Stack16>, SingleBackingArray>> Map; - public SmallMap16_SingleArrEntries(uint capacityPowerOfTwo) : this() => Map = new(capacityPowerOfTwo); + public SmallMap, TEq, Size16, Stack16, Stack16>, + SingleBackingArray>> Map; + public SmallMap16_SingleArrEntries() => Map = new(MinCapacity); + public SmallMap16_SingleArrEntries(uint capacityPowerOfTwo) => Map = new(capacityPowerOfTwo); } -/// Holds the Set with 4 items on stack. Minimizes the number of type arguments required to be specified -public struct SmallSet4() where TEq : struct, IEq +/// Set with 4 stack keys; heap overflow in stable . +public struct SmallSet4 where TEq : struct, IEq { - /// Set with 4 keys on stack and entries baked by the single array - public SmallMap, TEq, Size4, Stack4, Stack4>, StableSegmentedArray>> Set; + public SmallMap, TEq, Size4, Stack4, Stack4>, + Grr, Stack8[]>>> Set; + public SmallSet4() => Set = new(MinCapacity); + public SmallSet4(uint capacityPowerOfTwo) => Set = new(capacityPowerOfTwo); } -/// Holds the Set with 8 items on stack. Minimizes the number of type arguments required to be specified -public struct SmallSet8() where TEq : struct, IEq +/// Set with 8 stack keys; heap overflow in stable . +public struct SmallSet8 where TEq : struct, IEq { - /// Set with 8 keys on stack and entries baked by the single array - public SmallMap, TEq, Size8, Stack8, Stack8>, StableSegmentedArray>> Set; + public SmallMap, TEq, Size8, Stack8, Stack8>, + Grr, Stack8[]>>> Set; + public SmallSet8() => Set = new(MinCapacity); + public SmallSet8(uint capacityPowerOfTwo) => Set = new(capacityPowerOfTwo); } -/// Holds the Set with 16 items on stack. Minimizes the number of type arguments required to be specified -public struct SmallSet16() where TEq : struct, IEq +/// Set with 16 stack keys; heap overflow in stable . +public struct SmallSet16 where TEq : struct, IEq { - /// Set with 16 keys on stack and entries baked by the single array - public SmallMap, TEq, Size16, Stack16, Stack16>, StableSegmentedArray>> Set; + public SmallMap, TEq, Size16, Stack16, Stack16>, + Grr, Stack8[]>>> Set; + public SmallSet16() => Set = new(MinCapacity); + public SmallSet16(uint capacityPowerOfTwo) => Set = new(capacityPowerOfTwo); } - -#nullable restore diff --git a/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj b/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj index 7b7df3c2..f6b1829f 100644 --- a/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj +++ b/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net9.0;net10.0 x64 diff --git a/playground/ImTools.Benchmarks/Program.cs b/playground/ImTools.Benchmarks/Program.cs index bdeb3255..68f93de0 100644 --- a/playground/ImTools.Benchmarks/Program.cs +++ b/playground/ImTools.Benchmarks/Program.cs @@ -1,57 +1,67 @@ -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Running; -namespace ImTools.Benchmarks +namespace ImTools.Benchmarks; + +class Program { - class Program + static void Main(string[] args) { - static void Main() + // With args: full BenchmarkSwitcher (filters, jobs, etc.) + // dotnet run -c Release -f net10.0 -- --filter *SmallGrrVsSmallList* + // Without args: default baseline benchmark for the hybrid list work. + if (args is { Length: > 0 }) { - // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + return; + } - // new SIO.Program().Run(); + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); - //var x = new ImMapBenchmarks.Populate { Count = 10 }; - //x.ImMapArray_AddOrUpdate(); + // new SIO.Program().Run(); - //var b = new ImMapBenchmarks.Populate { Count = 10 }; - //b.ImMap_FixedData4(); + //var x = new ImMapBenchmarks.Populate { Count = 10 }; + //x.ImMapArray_AddOrUpdate(); - // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); + //var b = new ImMapBenchmarks.Populate { Count = 10 }; + //b.ImMap_FixedData4(); - // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); - BenchmarkRunner.Run(); - // var bm = new Playground.ImHashMapBenchmarks.Lookup(); - // bm.Count = 1000; - // bm.Populate(); - // var count = bm.FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent(); - // Console.WriteLine(count); + // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); + BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + // var bm = new Playground.ImHashMapBenchmarks.Lookup(); + // bm.Count = 1000; + // bm.Populate(); + // var count = bm.FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent(); + // Console.WriteLine(count); - //BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - //BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); - //BenchmarkRunner.Run(); + //BenchmarkRunner.Run(); - //BenchmarkRunner.Run(); - //BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + //BenchmarkRunner.Run(); - //BenchmarkRunner.Run(); + //BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); - } + //BenchmarkRunner.Run(); + //BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + + //BenchmarkRunner.Run(); + + // BenchmarkRunner.Run(); } } diff --git a/playground/ImTools.Benchmarks/SmallGrrVsSmallListBenchmarks.cs b/playground/ImTools.Benchmarks/SmallGrrVsSmallListBenchmarks.cs new file mode 100644 index 00000000..38c57826 --- /dev/null +++ b/playground/ImTools.Benchmarks/SmallGrrVsSmallListBenchmarks.cs @@ -0,0 +1,65 @@ +using System; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; +using FastExpressionCompiler.ImTools; + +namespace ImTools.Benchmarks; + +/// +/// Random-access benchmark: SmallList (stack + single growing array) +/// vs SmallGrr (stack + stable segmented Grr) once enough items spill to heap. +/// Run under net10.0 host (project TFM). Heap spill starts after Stack8 capacity (8). +/// +[MemoryDiagnoser] +[SimpleJob(RunStrategy.Throughput, launchCount: 1, warmupCount: 3, iterationCount: 8)] +public class SmallGrrVsSmallListBenchmarks +{ + // Sizes where Rest/Grr is on the hot path (well past stack capacity). + [Params(256, 1024, 4096)] + public int Count; + + private int[] _indices = null!; + private SmallList> _smallList; + private SmallGrr, Stack8> _smallGrr; + + [GlobalSetup] + public void Setup() + { + _smallList = default; + _smallGrr = default; + + for (var i = 0; i < Count; i++) + { + _smallList.Add(i); + _smallGrr.Add(i); + } + + // Deterministic pseudo-random access pattern (mostly heap indices) + _indices = new int[Count * 4]; + var rng = new Random(42); + for (var i = 0; i < _indices.Length; i++) + _indices[i] = rng.Next(Count); + } + + [Benchmark(Baseline = true)] + public int SmallList_RandomAccess() + { + var sum = 0; + var idxs = _indices; + ref var list = ref _smallList; + for (var i = 0; i < idxs.Length; i++) + sum += list.GetSurePresentRef(idxs[i]); + return sum; + } + + [Benchmark] + public int SmallGrr_RandomAccess() + { + var sum = 0; + var idxs = _indices; + ref var list = ref _smallGrr; + for (var i = 0; i < idxs.Length; i++) + sum += list.GetSurePresentRef(idxs[i]); + return sum; + } +} diff --git a/playground/ImTools.UnionPlayground/Program.cs b/playground/ImTools.UnionPlayground/Program.cs index 26730758..9fb36077 100644 --- a/playground/ImTools.UnionPlayground/Program.cs +++ b/playground/ImTools.UnionPlayground/Program.cs @@ -10,18 +10,18 @@ class Program { static void Main() { - // Unnamed (anonymous) union is fast to declare and use + // Unnamed (anonymous) un is fast to declare and use var i = U.Of(42); var s = U.Of("hey"); WriteLine(i); WriteLine(s); - // You may create the union case directly via constructor, helpful for the cases like `U` or `U` + // You may create the un case directly via constructor, helpful for the cases like `U` or `U` var s2 = new U.case1("hsss"); WriteLine(s2); - // Typed union (check the definition below) is different from the unnamed `U`, e.g. `s = name;` won't compile + // Typed un (check the definition below) is different from the unnamed `U`, e.g. `s = name;` won't compile var name = BoolOrString.Of("Bob"); var flag = BoolOrString.Of(false); @@ -29,7 +29,7 @@ static void Main() WriteLine(SwitchOnCases(flag)); WriteLine(name.Match(f => "" + f, n => n)); - // Typed union with the Typed cases so you can pattern match on `case I name` or `I flag` + // Typed un with the Typed cases so you can pattern match on `case I name` or `I flag` var name2 = FlagOrName.Of(Name.Of("Alice")); var flag2 = FlagOrName.Of(Flag.Of(true)); @@ -48,13 +48,13 @@ static void Main() } } - // One-liner named union definition + // One-liner named un definition public sealed class BoolOrString : Union { } // A different type from the NamedBoolOrString public sealed class OtherBoolOrString : Union { } - // Typed union with the typed cases - now you can pattern match via `I` and `I` + // Typed un with the typed cases - now you can pattern match via `I` and `I` public sealed class FlagOrName : Union { } public sealed class Flag : Item { } public sealed class Name : Item { } @@ -62,7 +62,7 @@ public sealed class Name : Item { } public static class Usage { // note: Using T with constraint instead of FlagOrName.I interface improves the performance by avoiding boxing. - public static string SwitchOnCases(T x) where T : BoolOrString.union + public static string SwitchOnCases(T x) where T : BoolOrString.un { switch (x) { @@ -72,7 +72,7 @@ public static string SwitchOnCases(T x) where T : BoolOrString.union } } - public static string SwitchOnTypedItems(FlagOrName.union x) + public static string SwitchOnTypedItems(FlagOrName.un x) { // Refactoring friendly Named cases, with some performance price due the boxing - likely is not important for your case, // except you are designing a performance oriented data structure or being used in performance sensitive spot context. @@ -125,34 +125,34 @@ public sealed class Decrement : Item { } public sealed class MyList : Union, Unit, MyList.NonEmptyList> { public static readonly case1 Empty = new case1(Unit.unit); - public static case2 NonEmpty(T head, union tail) => new case2(new NonEmptyList(head, tail)); + public static case2 NonEmpty(T head, un tail) => new case2(new NonEmptyList(head, tail)); public readonly struct NonEmptyList { public readonly T Head; - public readonly union Tail; - public NonEmptyList(T head, union tail) => (Head, Tail) = (head, tail); + public readonly un Tail; + public NonEmptyList(T head, un tail) => (Head, Tail) = (head, tail); public override string ToString() => Head + "::" + Tail; } } public static class MyList { - public static MyList.case2 Push(this MyList.union list, T head) => MyList.NonEmpty(head, list); + public static MyList.case2 Push(this MyList.un list, T head) => MyList.NonEmpty(head, list); } // Less efficient, but less boilerplate recursive type - requires one heap reference per recursive type usage. public sealed class MyTree : Union, Unit, MyTree.NonEmptyTree> { - public sealed class NonEmptyTree : Box { } + public sealed class NonEmptyTree : Box { } public static readonly case1 Empty = new case1(Unit.unit); } public static class MyTree { - public static MyTree.union Of(MyTree.union left, T leaf, MyTree.union right) => + public static MyTree.un Of(MyTree.un left, T leaf, MyTree.un right) => MyTree.Of(MyTree.NonEmptyTree.Of((left, leaf, right))); - public static MyTree.union Leaf(T leaf) => Of(MyTree.Empty, leaf, MyTree.Empty); + public static MyTree.un Leaf(T leaf) => Of(MyTree.Empty, leaf, MyTree.Empty); } } diff --git a/playground/ImTools.UnionPlayground/Union.cs b/playground/ImTools.UnionPlayground/Union.cs index 0984a2d7..fc0f2d2d 100644 --- a/playground/ImTools.UnionPlayground/Union.cs +++ b/playground/ImTools.UnionPlayground/Union.cs @@ -162,10 +162,10 @@ public bool Equals(Box other) => public override string ToString() => UnionTools.ToString(Value, "box("); } -/// Unnamed discriminated union (with Empty name), shorter name for simplified inline usage +/// Unnamed discriminated un (with Empty name), shorter name for simplified inline usage public class U : Union { } -/// Discriminated union +/// Discriminated un public abstract class Union { /// To tag the cases with enum value for efficient pattern matching of required - @@ -182,23 +182,23 @@ public enum Tag : byte /// The naming is selected to start from the lower letter, cause we need to use the nested type. /// It is an unusual case, that's why using the __union__ will be fine to highlight this. // ReSharper disable once InconsistentNaming - public interface union + public interface un { /// The tag Tag Tag { get; } - /// Matches the union cases to the R value + /// Matches the un cases to the R value R Match(Func map1, Func map2); } /// Creates the respective case - public static union Of(T1 x) => new case1(x); + public static un Of(T1 x) => new case1(x); /// Creates the respective case - public static union Of(T2 x) => new case2(x); + public static un Of(T2 x) => new case2(x); /// Wraps the respective case - public readonly struct case1 : union, IEquatable, I + public readonly struct case1 : un, IEquatable, I { /// Implicit conversion public static implicit operator case1(T1 x) => new case1(x); @@ -232,7 +232,7 @@ public interface union } /// Wraps the respective case - public readonly struct case2 : union, IEquatable, I + public readonly struct case2 : un, IEquatable, I { /// Conversion public static implicit operator case2(T2 x) => new case2(x); @@ -273,17 +273,17 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -303,7 +303,7 @@ public struct case1 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -321,7 +321,7 @@ public struct case2 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -345,18 +345,18 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3, Case4 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3, Func map4); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); - public static union Of(T4 x) => new case4(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); + public static un Of(T4 x) => new case4(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -376,7 +376,7 @@ public struct case1 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -394,7 +394,7 @@ public struct case2 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -412,7 +412,7 @@ public struct case3 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case4 : union, IEquatable, I + public struct case4 : un, IEquatable, I { public static implicit operator case4(T4 x) => new case4(x); @@ -436,19 +436,19 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3, Case4, Case5 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3, Func map4, Func map5); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); - public static union Of(T4 x) => new case4(x); - public static union Of(T5 x) => new case5(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); + public static un Of(T4 x) => new case4(x); + public static un Of(T5 x) => new case5(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -468,7 +468,7 @@ public struct case1 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -486,7 +486,7 @@ public struct case2 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -504,7 +504,7 @@ public struct case3 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case4 : union, IEquatable, I + public struct case4 : un, IEquatable, I { public static implicit operator case4(T4 x) => new case4(x); @@ -522,7 +522,7 @@ public struct case4 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case5 : union, IEquatable, I + public struct case5 : un, IEquatable, I { public static implicit operator case5(T5 x) => new case5(x); @@ -546,20 +546,20 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3, Case4, Case5, Case6 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3, Func map4, Func map5, Func map6); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); - public static union Of(T4 x) => new case4(x); - public static union Of(T5 x) => new case5(x); - public static union Of(T6 x) => new case6(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); + public static un Of(T4 x) => new case4(x); + public static un Of(T5 x) => new case5(x); + public static un Of(T6 x) => new case6(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -579,7 +579,7 @@ public struct case1 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -597,7 +597,7 @@ public struct case2 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -615,7 +615,7 @@ public struct case3 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case4 : union, IEquatable, I + public struct case4 : un, IEquatable, I { public static implicit operator case4(T4 x) => new case4(x); @@ -633,7 +633,7 @@ public struct case4 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case5 : union, IEquatable, I + public struct case5 : un, IEquatable, I { public static implicit operator case5(T5 x) => new case5(x); @@ -651,7 +651,7 @@ public struct case5 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case6 : union, IEquatable, I + public struct case6 : un, IEquatable, I { public static implicit operator case6(T6 x) => new case6(x); @@ -675,21 +675,21 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3, Case4, Case5, Case6, Case7 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3, Func map4, Func map5, Func map6, Func map7); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); - public static union Of(T4 x) => new case4(x); - public static union Of(T5 x) => new case5(x); - public static union Of(T6 x) => new case6(x); - public static union Of(T7 x) => new case7(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); + public static un Of(T4 x) => new case4(x); + public static un Of(T5 x) => new case5(x); + public static un Of(T6 x) => new case6(x); + public static un Of(T7 x) => new case7(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -709,7 +709,7 @@ public struct case1 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -727,7 +727,7 @@ public struct case2 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -745,7 +745,7 @@ public struct case3 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case4 : union, IEquatable, I + public struct case4 : un, IEquatable, I { public static implicit operator case4(T4 x) => new case4(x); @@ -763,7 +763,7 @@ public struct case4 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case5 : union, IEquatable, I + public struct case5 : un, IEquatable, I { public static implicit operator case5(T5 x) => new case5(x); @@ -781,7 +781,7 @@ public struct case5 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case6 : union, IEquatable, I + public struct case6 : un, IEquatable, I { public static implicit operator case6(T6 x) => new case6(x); @@ -799,7 +799,7 @@ public struct case6 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case7 : union, IEquatable, I + public struct case7 : un, IEquatable, I { public static implicit operator case7(T7 x) => new case7(x); @@ -822,22 +822,22 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3, Case4, Case5, Case6, Case7, Case8 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3, Func map4, Func map5, Func map6, Func map7, Func map8); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); - public static union Of(T4 x) => new case4(x); - public static union Of(T5 x) => new case5(x); - public static union Of(T6 x) => new case6(x); - public static union Of(T7 x) => new case7(x); - public static union Of(T8 x) => new case8(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); + public static un Of(T4 x) => new case4(x); + public static un Of(T5 x) => new case5(x); + public static un Of(T6 x) => new case6(x); + public static un Of(T7 x) => new case7(x); + public static un Of(T8 x) => new case8(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -857,7 +857,7 @@ public struct case1 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -875,7 +875,7 @@ public struct case2 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -893,7 +893,7 @@ public struct case3 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case4 : union, IEquatable, I + public struct case4 : un, IEquatable, I { public static implicit operator case4(T4 x) => new case4(x); @@ -911,7 +911,7 @@ public struct case4 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case5 : union, IEquatable, I + public struct case5 : un, IEquatable, I { public static implicit operator case5(T5 x) => new case5(x); @@ -929,7 +929,7 @@ public struct case5 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case6 : union, IEquatable, I + public struct case6 : un, IEquatable, I { public static implicit operator case6(T6 x) => new case6(x); @@ -947,7 +947,7 @@ public struct case6 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case7 : union, IEquatable, I + public struct case7 : un, IEquatable, I { public static implicit operator case7(T7 x) => new case7(x); @@ -965,7 +965,7 @@ public struct case7 : union, IEquatable, I public override string ToString() => UnionTools.ToString(Case); } - public struct case8 : union, IEquatable, I + public struct case8 : un, IEquatable, I { public static implicit operator case8(T8 x) => new case8(x); @@ -988,24 +988,24 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3, Case4, Case5, Case6, Case7, Case8, Case9 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3, Func map4, Func map5, Func map6, Func map7, Func map8, Func map9); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); - public static union Of(T4 x) => new case4(x); - public static union Of(T5 x) => new case5(x); - public static union Of(T6 x) => new case6(x); - public static union Of(T7 x) => new case7(x); - public static union Of(T8 x) => new case8(x); - public static union Of(T9 x) => new case9(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); + public static un Of(T4 x) => new case4(x); + public static un Of(T5 x) => new case5(x); + public static un Of(T6 x) => new case6(x); + public static un Of(T7 x) => new case7(x); + public static un Of(T8 x) => new case8(x); + public static un Of(T9 x) => new case9(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -1026,7 +1026,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -1045,7 +1045,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -1064,7 +1064,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case4 : union, IEquatable, I + public struct case4 : un, IEquatable, I { public static implicit operator case4(T4 x) => new case4(x); @@ -1083,7 +1083,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case5 : union, IEquatable, I + public struct case5 : un, IEquatable, I { public static implicit operator case5(T5 x) => new case5(x); @@ -1102,7 +1102,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case6 : union, IEquatable, I + public struct case6 : un, IEquatable, I { public static implicit operator case6(T6 x) => new case6(x); @@ -1121,7 +1121,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case7 : union, IEquatable, I + public struct case7 : un, IEquatable, I { public static implicit operator case7(T7 x) => new case7(x); @@ -1140,7 +1140,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case8 : union, IEquatable, I + public struct case8 : un, IEquatable, I { public static implicit operator case8(T8 x) => new case8(x); @@ -1159,7 +1159,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case9 : union, IEquatable, I + public struct case9 : un, IEquatable, I { public static implicit operator case9(T9 x) => new case9(x); @@ -1183,25 +1183,25 @@ public abstract class Union { public enum Tag : byte { Case1, Case2, Case3, Case4, Case5, Case6, Case7, Case8, Case9, Case10 } - public interface union + public interface un { Tag Tag { get; } R Match(Func map1, Func map2, Func map3, Func map4, Func map5, Func map6, Func map7, Func map8, Func map9, Func map10); } - public static union Of(T1 x) => new case1(x); - public static union Of(T2 x) => new case2(x); - public static union Of(T3 x) => new case3(x); - public static union Of(T4 x) => new case4(x); - public static union Of(T5 x) => new case5(x); - public static union Of(T6 x) => new case6(x); - public static union Of(T7 x) => new case7(x); - public static union Of(T8 x) => new case8(x); - public static union Of(T9 x) => new case9(x); - public static union Of(T10 x) => new case10(x); + public static un Of(T1 x) => new case1(x); + public static un Of(T2 x) => new case2(x); + public static un Of(T3 x) => new case3(x); + public static un Of(T4 x) => new case4(x); + public static un Of(T5 x) => new case5(x); + public static un Of(T6 x) => new case6(x); + public static un Of(T7 x) => new case7(x); + public static un Of(T8 x) => new case8(x); + public static un Of(T9 x) => new case9(x); + public static un Of(T10 x) => new case10(x); - public struct case1 : union, IEquatable, I + public struct case1 : un, IEquatable, I { public static implicit operator case1(T1 x) => new case1(x); @@ -1222,7 +1222,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case2 : union, IEquatable, I + public struct case2 : un, IEquatable, I { public static implicit operator case2(T2 x) => new case2(x); @@ -1241,7 +1241,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case3 : union, IEquatable, I + public struct case3 : un, IEquatable, I { public static implicit operator case3(T3 x) => new case3(x); @@ -1260,7 +1260,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case4 : union, IEquatable, I + public struct case4 : un, IEquatable, I { public static implicit operator case4(T4 x) => new case4(x); @@ -1279,7 +1279,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case5 : union, IEquatable, I + public struct case5 : un, IEquatable, I { public static implicit operator case5(T5 x) => new case5(x); @@ -1298,7 +1298,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case6 : union, IEquatable, I + public struct case6 : un, IEquatable, I { public static implicit operator case6(T6 x) => new case6(x); @@ -1317,7 +1317,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case7 : union, IEquatable, I + public struct case7 : un, IEquatable, I { public static implicit operator case7(T7 x) => new case7(x); @@ -1336,7 +1336,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case8 : union, IEquatable, I + public struct case8 : un, IEquatable, I { public static implicit operator case8(T8 x) => new case8(x); @@ -1355,7 +1355,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case9 : union, IEquatable, I + public struct case9 : un, IEquatable, I { public static implicit operator case9(T9 x) => new case9(x); @@ -1374,7 +1374,7 @@ public R Match(Func map1, Func map2, Func map3, Func UnionTools.ToString(Case); } - public struct case10 : union, IEquatable, I + public struct case10 : un, IEquatable, I { public static implicit operator case10(T10 x) => new case10(x); diff --git a/test/ImTools.UnitTests/GrrTests.cs b/test/ImTools.UnitTests/GrrTests.cs new file mode 100644 index 00000000..9e49bd89 --- /dev/null +++ b/test/ImTools.UnitTests/GrrTests.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; + +#if LIGHT_EXPRESSION +using FastExpressionCompiler.LightExpression.ImTools; +#else +using FastExpressionCompiler.ImTools; +#endif + +namespace ImTools.Experiments.UnitTests; + +[TestFixture] +public class GrrTests +{ + private static Grr> NewGrr(uint capacity = 0) + { + var g = default(Grr>); + g.Init(capacity); + return g; + } + + [Test] + public void Init_uses_min_capacity_16() + { + var g = NewGrr(0); + Assert.AreEqual(16, g.Capacity); + Assert.AreEqual(1, g.SegmentCount); + g.GetSurePresentRef(0) = 42; + Assert.AreEqual(42, g.GetSurePresentRef(0)); + } + + [Test] + public void Init_rounds_up_to_power_of_two() + { + var g = NewGrr(20); + Assert.AreEqual(32, g.Capacity); + } + + [Test] + public void DoubleCapacity_keeps_existing_values_and_refs_stable() + { + var g = NewGrr(16); + for (var i = 0; i < 16; i++) + g.GetSurePresentRef(i) = i + 1; + + ref var first = ref g.GetSurePresentRef(0); + var before = first; + + g.DoubleCapacity(); + Assert.AreEqual(32, g.Capacity); + Assert.AreEqual(2, g.SegmentCount); + + for (var i = 0; i < 16; i++) + Assert.AreEqual(i + 1, g.GetSurePresentRef(i)); + + Assert.AreEqual(before, first); + first = 99; + Assert.AreEqual(99, g.GetSurePresentRef(0)); + } + + [Test] + public void GrowCapacity_across_multiple_segments_and_random_access() + { + var g = NewGrr(16); + g.GrowCapacity(1000); + Assert.GreaterOrEqual(g.Capacity, 1000); + Assert.Greater(g.SegmentCount, 1); + + for (var i = 0; i < 1000; i++) + g.GetSurePresentRef(i) = i * 3; + + for (var i = 999; i >= 0; i -= 7) + Assert.AreEqual(i * 3, g.GetSurePresentRef(i)); + } + + [Test] + public void Indexing_matches_segment_layout_for_various_first_sizes() + { + foreach (var firstReq in new uint[] { 1, 16, 17, 32, 64, 100 }) + { + var g = NewGrr(firstReq); + var first = g.Capacity; + Assert.GreaterOrEqual(first, 16); + Assert.AreEqual(0, first & (first - 1)); // power of two + + while (g.Capacity < first * 16) + g.DoubleCapacity(); + + var n = g.Capacity; + for (var i = 0; i < n; i++) + g.GetSurePresentRef(i) = i; + + for (var i = 0; i < n; i++) + Assert.AreEqual(i, g.GetSurePresentRef(i), $"firstReq={firstReq}, i={i}, cap={n}"); + } + } + + [Test] + public void Re_Init_resets_segments() + { + var g = NewGrr(16); + g.DoubleCapacity(); + g.DoubleCapacity(); + Assert.Greater(g.SegmentCount, 1); + + g.Init(16); + Assert.AreEqual(16, g.Capacity); + Assert.AreEqual(1, g.SegmentCount); + } + + [Test] + public void Enumerator_visits_all_capacity_slots() + { + var g = NewGrr(16); + g.DoubleCapacity(); // capacity 32 + for (var i = 0; i < 32; i++) + g.GetSurePresentRef(i) = i; + + var list = new List(); + foreach (var x in g) + list.Add(x); + + Assert.AreEqual(32, list.Count); + for (var i = 0; i < 32; i++) + Assert.AreEqual(i, list[i]); + } + + [Test] + public void Enumerator_Reset_restarts() + { + var g = NewGrr(16); + for (var i = 0; i < 16; i++) + g.GetSurePresentRef(i) = i + 10; + + var e = g.GetEnumerator(); + Assert.IsTrue(e.MoveNext()); + Assert.AreEqual(10, e.Current); + e.Reset(); + Assert.IsTrue(e.MoveNext()); + Assert.AreEqual(10, e.Current); + } +} + +[TestFixture] +public class SmallGrrTests +{ + private static SmallGrr, Stack8> New() => default; + + [Test] + public void Add_stays_on_stack_until_capacity() + { + var s = New(); + for (var i = 0; i < 8; i++) + Assert.AreEqual(i, s.Add(i * 10)); + + Assert.AreEqual(8, s.Count); + Assert.AreEqual(0, s.Rest.Capacity); // never grew rest + + for (var i = 0; i < 8; i++) + Assert.AreEqual(i * 10, s.GetSurePresentRef(i)); + } + + [Test] + public void Add_overflow_goes_to_Grr_and_keeps_stack_stable() + { + var s = New(); + for (var i = 0; i < 8; i++) + s.Add(i); + + ref var stack0 = ref s.GetSurePresentRef(0); + for (var i = 8; i < 100; i++) + s.Add(i); + + Assert.AreEqual(100, s.Count); + Assert.GreaterOrEqual(s.Rest.Capacity, 100 - 8); + + for (var i = 0; i < 100; i++) + Assert.AreEqual(i, s[i]); + + // stack item ref still valid after heap growth + Assert.AreEqual(0, stack0); + stack0 = 777; + Assert.AreEqual(777, s.GetSurePresentRef(0)); + } + + [Test] + public void Heap_item_refs_remain_stable_across_Rest_growth() + { + var s = New(); + for (var i = 0; i < 20; i++) + s.Add(i * 2); + + ref var heapItem = ref s.GetSurePresentRef(10); // past stack + Assert.AreEqual(20, heapItem); + + // force more rest growth + for (var i = 20; i < 500; i++) + s.Add(i); + + Assert.AreEqual(20, heapItem); + heapItem = -1; + Assert.AreEqual(-1, s.GetSurePresentRef(10)); + } + + [Test] + public void Random_access_matches_SmallList_for_heap_heavy_counts() + { + var grr = New(); + var list = default(SmallList>); + + const int n = 1024; + for (var i = 0; i < n; i++) + { + grr.Add(i ^ 0x5a5a); + list.Add(i ^ 0x5a5a); + } + + var rng = new Random(123); + for (var t = 0; t < 5000; t++) + { + var i = rng.Next(n); + Assert.AreEqual(list.GetSurePresentRef(i), grr.GetSurePresentRef(i)); + } + } + + [Test] + public void Clear_and_reuse() + { + var s = New(); + for (var i = 0; i < 40; i++) + s.Add(i); + var restCap = s.Rest.Capacity; + s.Clear(); + Assert.AreEqual(0, s.Count); + Assert.AreEqual(restCap, s.Rest.Capacity); // segments kept + + s.Add(5); + Assert.AreEqual(1, s.Count); + Assert.AreEqual(5, s[0]); + } + + [Test] + public void Enumerator_and_TryGetIndex() + { + var s = New(); + for (var i = 0; i < 30; i++) + s.Add(i * 3); + + var sum = 0; + foreach (var x in s) + sum += x; + Assert.AreEqual(3 * 29 * 30 / 2, sum); + + Assert.AreEqual(10, s.TryGetIndex(30, default(FastExpressionCompiler.ImTools.IntEq))); + Assert.AreEqual(-1, s.TryGetIndex(1, default(FastExpressionCompiler.ImTools.IntEq))); + Assert.AreEqual(30, s.GetIndexOrAdd(999, default(FastExpressionCompiler.ImTools.IntEq))); + } + + [Test] + public void RemoveLast() + { + var s = New(); + s.Add(1); + s.Add(2); + s.RemoveLastSurePresentItem(); + Assert.AreEqual(1, s.Count); + Assert.AreEqual(1, s.GetLastSurePresentItem()); + } +} diff --git a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs index e60a2050..f886da3b 100644 --- a/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs +++ b/test/ImTools.UnitTests/Playground/FecSmallMapTests.cs @@ -1,31 +1,49 @@ -#define VERIFY_MAP +#define VERIFY_MAP +#if NET6_0_OR_GREATER +#define CS_CHECK +#endif namespace FastExpressionCompiler.ImTools.UnitTests; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using NUnit.Framework; using static SmallMap; +#if CS_CHECK +using CsCheck; +#endif +/// +/// Baseline tests for the hybrid SmallMap (stack entries + Grr/SingleArray heap + Grr metadata). +/// Removal/TryRemove is intentionally not covered yet - not implemented in this baseline. +/// [TestFixture] public class FecSmallMapTests { - private static readonly Type[] _allKeys = typeof(List<>).Assembly.GetTypes().Take(2000).ToArray(); + private static readonly Type[] _allKeys = typeof(Dictionary<,>).Assembly.GetTypes().Take(2000).ToArray(); + + private static void AssertVerifyKeys(ref TMap map, IEnumerable expectedKeys = null) + where TMap : struct, IMap, IMapImpl + { +#if VERIFY_MAP + map.Verify(static (cond, msg) => Assert.IsTrue(cond, msg), expectedKeys, Pass.It); +#endif + } [Test] public void Zero_0_hash_test() { - var map = new SmallMap16(); - // Make non-empty map - map.Map.AddOrUpdate(1, 1); - Assert.AreEqual(1, map.Map.Count); - - Assert.False(map.Map.ContainsKey(0)); - map.Map.AddOrUpdate(0, 0); - Assert.AreEqual(2, map.Map.Count); - Assert.True(map.Map.ContainsKey(0)); + var m = new SmallMap16(); + ref var map = ref m.Map; + map.AddOrUpdate(1, 1); + Assert.AreEqual(1, map.Count); + + Assert.False(map.ContainsKey(0)); + map.AddOrUpdate(0, 0); + Assert.AreEqual(2, map.Count); + Assert.True(map.ContainsKey(0)); + Assert.AreEqual(0, map.GetValueOrDefault(0, 0)); } [Test] @@ -38,14 +56,11 @@ public void Can_store_and_retrieve_value_from_map() map.AddOrUpdate(42 + 32, "2"); map.AddOrUpdate(42 + 32 + 32, "3"); - // interrupt the keys with ne key map.AddOrUpdate(43, "a"); map.AddOrUpdate(43 + 32, "b"); map.AddOrUpdate(43 + 32 + 32, "c"); map.AddOrUpdate(42 + 32 + 32 + 32, "4"); - - // insert 3rd variety of the keys map.AddOrUpdate(44, "*"); map.AddOrUpdate(42 + 32 + 32 + 32 + 32, "5"); @@ -55,10 +70,9 @@ public void Can_store_and_retrieve_value_from_map() map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32, "7"); map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, "8"); - // check for the missing key - Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32, default(string))); + AssertVerifyKeys(ref map, (IEnumerable)null); - // check for the strange key + Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32, default(string))); Assert.AreEqual("*", map.GetValueOrDefault(44, default(string))); Assert.AreEqual("1", map.GetValueOrDefault(42, default(string))); @@ -78,18 +92,162 @@ public void Can_store_and_retrieve_value_from_map() Assert.AreEqual(13, map.Count); } + [Test] + public void Update_existing_key_keeps_count() + { + var m = new SmallMap8(); + ref var map = ref m.Map; + + Assert.IsFalse(map.AddOrUpdate(7, "a")); + Assert.IsTrue(map.AddOrUpdate(7, "b")); + Assert.AreEqual(1, map.Count); + Assert.AreEqual("b", map.GetValueOrDefault(7, default(string))); + AssertVerifyKeys(ref map, new[] { 7 }); + } + + [Test] + public void Default_map_lookups_are_safe() + { + // Truly default (uninitialized) map: Count/Capacity 0, no heap metadata yet. + var m = default(SmallMap16); + ref var map = ref m.Map; + Assert.AreEqual(0, map.Count); + Assert.IsFalse(map.ContainsKey(42)); + Assert.AreEqual(null, map.GetValueOrDefault(42, default(string))); + map.TryGetEntryRef(42, out var found); + Assert.IsFalse(found); + // Do not dereference the returned ref when found is false (null ref). + } + + [Test] + public void Stack_to_heap_migration_and_entry_order() + { + var m = new SmallMap4(); + ref var map = ref m.Map; + + const int n = 40; + for (var i = 0; i < n; i++) + map.AddOrUpdate(i, i * 10); + + Assert.AreEqual(n, map.Count); + Assert.Greater(map.Capacity, 0); + Assert.Greater(map.HeapEntries.Capacity, 0); + + for (var i = 0; i < n; i++) + { + Assert.AreEqual(i, map.GetSurePresentKey(i)); + Assert.AreEqual(i * 10, map.GetSurePresentEntryRef(i).Value); + Assert.AreEqual(i * 10, map.GetValueOrDefault(i, 0)); + } + + AssertVerifyKeys(ref map, Enumerable.Range(0, n)); + } + + [Test] + public void Heap_entry_refs_remain_stable_with_Grr_backing() + { + var m = new SmallMap4(); + ref var map = ref m.Map; + + for (var i = 0; i < 8; i++) + map.AddOrUpdate(i, i); + + ref var heapEntry = ref map.GetSurePresentEntryRef(4); + Assert.AreEqual(4, heapEntry.Key); + Assert.AreEqual(4, heapEntry.Value); + + for (var i = 8; i < 200; i++) + map.AddOrUpdate(i, i); + + Assert.AreEqual(4, heapEntry.Key); + Assert.AreEqual(4, heapEntry.Value); + heapEntry.Value = -1; + Assert.AreEqual(-1, map.GetSurePresentEntryRef(4).Value); + Assert.AreEqual(-1, map.GetValueOrDefault(4, 0)); + + AssertVerifyKeys(ref map, (IEnumerable)null); + } + + [Test] + public void SingleBackingArray_map_behaves_the_same_for_lookup() + { + var m = new SmallMap16_SingleArrEntries(); + ref var map = ref m.Map; + + for (var i = 0; i < 100; i++) + map.AddOrUpdate(i, "v" + i); + + for (var i = 0; i < 100; i++) + Assert.AreEqual("v" + i, map.GetValueOrDefault(i, default(string))); + + Assert.IsFalse(map.ContainsKey(1000)); + AssertVerifyKeys(ref map, Enumerable.Range(0, 100)); + } + + [Test] + public void AddSureAbsent_and_AddOrGet_paths() + { + var m = new SmallMap8(); + ref var map = ref m.Map; + + map.AddSureAbsentDefaultEntryAndGetRef(1).Value = 10; + map.AddSureAbsentDefaultEntryAndGetRef(2).Value = 20; + Assert.AreEqual(2, map.Count); + + ref var existing = ref map.AddOrGetEntryRef(1, out var found); + Assert.IsTrue(found); + Assert.AreEqual(10, existing.Value); + existing.Value = 11; + + ref var added = ref map.AddOrGetEntryRef(3, out found); + Assert.IsFalse(found); + added.Value = 30; + + Assert.AreEqual(3, map.Count); + Assert.AreEqual(11, map.GetValueOrDefault(1, 0)); + Assert.AreEqual(30, map.GetValueOrDefault(3, 0)); + AssertVerifyKeys(ref map, new[] { 1, 2, 3 }); + } + + [Test] + public void Constructor_capacity_is_honored_as_power_of_two_floor() + { + var m = new SmallMap16(100); + ref var map = ref m.Map; + Assert.GreaterOrEqual(map.Capacity, 16); + Assert.AreEqual(0, map.Capacity & (map.Capacity - 1)); + + for (var i = 0; i < 50; i++) + map.AddOrUpdate(i, i); + + Assert.AreEqual(50, map.Count); + AssertVerifyKeys(ref map, Enumerable.Range(0, 50)); + } + + [Test] + public void Set_stores_keys_only() + { + var s = new SmallSet8(); + ref var set = ref s.Set; + + set.AddOrGetEntryRef(5, out var found); + Assert.IsFalse(found); + set.AddOrGetEntryRef(5, out found); + Assert.IsTrue(found); + Assert.IsTrue(set.ContainsKey(5)); + Assert.IsFalse(set.ContainsKey(6)); + Assert.AreEqual(1, set.Count); + } + [Test] public void Benchmark_test_with_Add_and_Lookup_10_items() { const int Count = 10; - Debug.Assert(Count <= 1000, "Count should be less than or equal to 1000 for this test to work correctly."); - var m = new SmallMap16>(); ref var map = ref m.Map; var presentKeys = _allKeys.Take(Count).ToArray(); var missingKeys = _allKeys.Skip(1000).Take(Count).ToArray(); - var seed = new Random(42); var randomPresentKeys = presentKeys.OrderBy(_ => seed.Next()).ToArray(); @@ -100,7 +258,7 @@ public void Benchmark_test_with_Add_and_Lookup_10_items() var iters = Count / 2; for (var i = 0; i < iters; ++i) { - var entry = map.TryGetEntryRef(randomPresentKeys[i], out var found); + ref var entry = ref map.TryGetEntryRef(randomPresentKeys[i], out var found); if (found) count += entry.Value.Length; _ = map.TryGetEntryRef(missingKeys[i], out found); @@ -109,29 +267,25 @@ public void Benchmark_test_with_Add_and_Lookup_10_items() } Assert.AreEqual(0, count); + AssertVerifyKeys(ref map, presentKeys); } [Test] public void Benchmark_test_with_Add_and_Lookup_100_items() { const int Count = 100; - Debug.Assert(Count <= 1000, "Count should be less than or equal to 1000 for this test to work correctly."); - var m = new SmallMap16>(); ref var map = ref m.Map; var presentTypes = _allKeys.Take(Count).ToArray(); var missingKeys = _allKeys.Skip(1000).Take(Count).ToArray(); - var seed = new Random(42); var randomPresentKeys = presentTypes.OrderBy(_ => seed.Next()).ToArray(); foreach (var type in presentTypes) map.AddOrUpdate(type, "a"); -#if VERIFY_MAP - map.Verify(static (cond, msg) => Assert.IsTrue(cond, msg), presentTypes, Pass.It); -#endif + AssertVerifyKeys(ref map, presentTypes); var count = 0; var iters = Count / 2; @@ -151,23 +305,18 @@ public void Benchmark_test_with_Add_and_Lookup_100_items() public void Benchmark_test_with_Add_and_Lookup_1000_items() { const int Count = 1000; - Debug.Assert(Count <= 1000, "Count should be less than or equal to 1000 for this test to work correctly."); - var m = new SmallMap16>(); ref var map = ref m.Map; var presentTypes = _allKeys.Take(Count).ToArray(); var missingKeys = _allKeys.Skip(1000).Take(Count).ToArray(); - var seed = new Random(42); var randomPresentKeys = presentTypes.OrderBy(_ => seed.Next()).ToArray(); foreach (var type in presentTypes) map.AddOrUpdate(type, "a"); -#if VERIFY_MAP - map.Verify(static (cond, msg) => Assert.IsTrue(cond, msg), presentTypes, Pass.It); -#endif + AssertVerifyKeys(ref map, presentTypes); var count = 0; var iters = Count / 2; @@ -183,325 +332,110 @@ public void Benchmark_test_with_Add_and_Lookup_1000_items() Assert.AreEqual(0, count); } - // [Test] - // public void Real_world_test_AddOrUpdate_NO_Resize() - // { - // var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(100).ToArray(); - - // var map = new SmallMap16>(8); - - // foreach (var key in types) - // map.AddOrUpdate(key, "a"); - - // map.AddOrUpdate(typeof(FHashMap11Tests), "!"); - - // Assert.AreEqual(101, map.Count); - - // Verify(map, types); - // } - - // [Test] - // public void Real_world_test_with_TryRemove_from_1000_items() - // { - // var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(1000).ToArray(); - - // var map = new SmallMap16>(); - - // foreach (var key in types) - // map.AddOrUpdate(key, "a"); - - // map.AddOrUpdate(typeof(FHashMap11Tests), "!"); - // Assert.AreEqual(1001, map.Count); - - // Assert.IsTrue(map.TryRemove(typeof(FHashMap11Tests))); - // Assert.AreEqual(1000, map.Count); - - // Verify(map, types); - // } - - // [Test] - // public void Real_world_test_with_Enumerator_and_TryRemove_the_entries() - // { - // var count = 1000; - // var types = typeof(Dictionary<,>).Assembly.GetTypes().Take(count).ToList(); - // Assert.AreEqual(count, types.Count); - - // var map = new SmallMap16>(); - - // foreach (var key in types) - // map.AddOrUpdate(key, "a"); - - // var keys = map.Select(kv => kv.Key).ToList(); - // CollectionAssert.AreEquivalent(types, keys); - - // Assert.IsTrue(map.TryRemove(types[0])); - // Assert.IsTrue(map.TryRemove(types[999])); - // Assert.IsTrue(map.TryRemove(types[377])); - // Assert.IsTrue(map.TryRemove(types[733])); - // Assert.AreEqual(count - 4, map.Count); - - // // remove in the reverse order to keep the correct index in regard to map - // types.RemoveAt(999); - // types.RemoveAt(733); - // types.RemoveAt(377); - // types.RemoveAt(0); - // Assert.AreEqual(count - 4, types.Count); - - // // Check the second enumeration is working - // var keys2 = map.Select(kv => kv.Key).ToList(); - // CollectionAssert.AreEquivalent(types, keys2); - - // Verify(map, types); - // } - - // [Test] - // public void Simplified_test_with_equal_hashes_RefEq() - // { - // var map = new SmallMap16>(); - - // var keys = new[] { typeof(Tuple<>), typeof(Tuple<,>), typeof(Tuple<,,>) }; - // var i = 1; - // foreach (var k in keys) - // map.AddOrUpdate(k, "" + i++); - - // Assert.AreEqual(3, map.Count); - - // Assert.IsTrue(map.TryRemove(typeof(Tuple<,,>))); - // Assert.AreEqual(2, map.Count); - - // Verify(map, new[] { typeof(Tuple<>), typeof(Tuple<,>) }); - // } - - /* - ## Debug output example + [Test] + public void Colliding_hashes_still_roundtrip() + { + var m = new SmallMap8(); + ref var map = ref m.Map; - ### IntEq + var keys = new[] { 1, 1 + 16, 1 + 32, 1 + 48, 1 + 64, 1 + 80, 1 + 96, 1 + 112, 1 + 128, 1 + 144 }; + for (var i = 0; i < keys.Length; i++) + map.AddOrUpdate(keys[i], "v" + i); - [AddOrUpdate] Probes abs max=2, max=2, all=[1: 1, 2: 1]; first 4 probes are 2 out of 2 - [AddOrUpdate] Probes abs max=3, max=3, all=[1: 1, 2: 1, 3: 1]; first 4 probes are 3 out of 3 - [AllocateEntries] Resize entries: 2 -> 4 - [ResizeHashes] 4 -> 8 - [ResizeHashes] Probes abs max=3, max=3, all=[1: 1, 2: 1, 3: 1]; first 4 probes are 3 out of 3 - [AddOrUpdate] Probes abs max=4, max=4, all=[1: 1, 2: 1, 3: 2, 4: 1]; first 4 probes are 5 out of 5 - [AllocateEntries] Resize entries: 4 -> 8 - [AddOrUpdate] Probes abs max=5, max=5, all=[1: 1, 2: 1, 3: 2, 4: 1, 5: 1]; first 4 probes are 5 out of 6 - [AddOrUpdate-RH] Probes abs max=6, max=6, all=[1: 1, 2: 1, 3: 2, 4: 2, 5: 1, 6: 1]; first 4 probes are 6 out of 8 - [ResizeHashes] 8 -> 16 - [ResizeHashes] Probes abs max=6, max=6, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 1, 6: 1]; first 4 probes are 5 out of 7 - [AllocateEntries] Resize entries: 8 -> 16 - [AddOrUpdate-RH] Probes abs max=7, max=7, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 1, 7: 1]; first 4 probes are 5 out of 9 - [AddOrUpdate-RH] Probes abs max=8, max=8, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 1, 8: 1]; first 4 probes are 5 out of 11 - [AddOrUpdate] Probes abs max=9, max=9, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 1, 8: 2, 9: 1]; first 4 probes are 5 out of 13 - [AddOrUpdate-RH] Probes abs max=10, max=10, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 2, 8: 2, 9: 1, 10: 1]; first 4 probes are 5 out of 15 - [AddOrUpdate-RH] Probes abs max=11, max=11, all=[1: 1, 2: 1, 3: 1, 4: 2, 5: 2, 6: 2, 7: 2, 8: 3, 9: 1, 10: 1, 11: 1]; first 4 probes are 5 out of 17 + for (var i = 0; i < keys.Length; i++) + Assert.AreEqual("v" + i, map.GetValueOrDefault(keys[i], default(string))); - ### GoldenIntEq - - [AddOrUpdate] Probes abs max=2, max=2, all=[1: 1, 2: 1]; first 4 probes are 2 out of 2 - [AllocateEntries] Resize entries: 2 -> 4 - [ResizeHashes] 4 -> 8 - [ResizeHashes] Probes abs max=2, max=1, all=[1: 3]; first 4 probes are 3 out of 3 - [AllocateEntries] Resize entries: 4 -> 8 - [AddOrUpdate] Probes abs max=2, max=2, all=[1: 5, 2: 1]; first 4 probes are 6 out of 6 - [ResizeHashes] 8 -> 16 - [ResizeHashes] Probes abs max=2, max=2, all=[1: 6, 2: 1]; first 4 probes are 7 out of 7 - [AllocateEntries] Resize entries: 8 -> 16 - - */ - - // [Test] - // public void Can_store_and_retrieve_value_from_map_Golden() - // { - // var map = new SmallMap16(2); - // // var map = new SmallMap16(2); - - // map.AddOrUpdate(42, "1"); - // map.AddOrUpdate(42 + 32, "2"); - // map.AddOrUpdate(42 + 32 + 32, "3"); - - // // interrupt the keys with new key - // map.AddOrUpdate(43, "a"); - // map.AddOrUpdate(43 + 32, "b"); - // map.AddOrUpdate(43 + 32 + 32, "c"); - - // map.AddOrUpdate(42 + 32 + 32 + 32, "4"); - - // // insert 3rd variety of the keys - // map.AddOrUpdate(44, "*"); - - // map.AddOrUpdate(42 + 32 + 32 + 32 + 32, "5"); - // map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32, "6"); - // map.AddOrUpdate(43 + 32 + 32 + 32, "d"); - - // map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32, "7"); - // map.AddOrUpdate(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32, "8"); - - // // check for the missing key - // Assert.AreEqual(null, map.GetValueOrDefault(43 + 32 + 32 + 32 + 32)); - - // // check for the strange key - // Assert.AreEqual("*", map.GetValueOrDefault(44)); - - // Assert.AreEqual("1", map.GetValueOrDefault(42)); - // Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); - // Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); - // Assert.AreEqual("4", map.GetValueOrDefault(42 + 32 + 32 + 32)); - // Assert.AreEqual("5", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32)); - // Assert.AreEqual("6", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32)); - // Assert.AreEqual("7", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32)); - // Assert.AreEqual("8", map.GetValueOrDefault(42 + 32 + 32 + 32 + 32 + 32 + 32 + 32)); - - // Assert.AreEqual("a", map.GetValueOrDefault(43)); - // Assert.AreEqual("b", map.GetValueOrDefault(43 + 32)); - // Assert.AreEqual("c", map.GetValueOrDefault(43 + 32 + 32)); - // Assert.AreEqual("d", map.GetValueOrDefault(43 + 32 + 32 + 32)); - - // Assert.AreEqual(13, map.Count); - - // Verify(map, null); - // } - - // [Test] - // public void Can_lookup_the_default_map_without_error() - // { - // SmallMap16 map = default; - - // Assert.IsFalse(map.TryGetValue(42, out _)); - // } - - // [Test] - // public void Can_store_and_retrieve_value_from_map_with_Expand_in_the_middle() - // { - // var map = new SmallMap16(1); - - // Assert.IsFalse(map.TryGetValue(42, out _)); - - // map.AddOrUpdate(42, "1"); - // map.AddOrUpdate(42 + 32, "2"); - - // // interrupt the keys with new key - // map.AddOrUpdate(43, "a"); - // map.AddOrUpdate(43 + 32, "b"); - - // map.AddOrUpdate(42 + 32 + 32, "3"); - - // Assert.AreEqual("1", map.GetValueOrDefault(42)); - // Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); - // Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); - // Assert.AreEqual(null, map.GetValueOrDefault(42 + 32 + 32 + 32)); - // Assert.AreEqual("a", map.GetValueOrDefault(43)); - - // map.AddOrUpdate(43, "a!"); - // Assert.AreEqual("a!", map.GetValueOrDefault(43)); - - // map.AddOrUpdate(47, "x"); - // map.AddOrUpdate(53, "y"); - // Assert.AreEqual("x", map.GetValueOrDefault(47)); - // Assert.AreEqual("y", map.GetValueOrDefault(53)); - - // map.AddOrUpdate(47 + 16, "x!"); - // map.AddOrUpdate(53 + 16, "y!"); - // Assert.AreEqual("x!", map.GetValueOrDefault(47 + 16)); - // Assert.AreEqual("y!", map.GetValueOrDefault(53 + 16)); - - // Verify(map, null); - // } + Assert.AreEqual(keys.Length, map.Count); + AssertVerifyKeys(ref map, keys); + } - // [Test] - // public void Can_resize_without_moving() - // { - // var map = new SmallMap16(2); - - // map.AddOrUpdate(0, "0"); - // map.AddOrUpdate(1, "1"); - // map.AddOrUpdate(9, "9"); - - // // resize goes here - // map.AddOrUpdate(3, "3"); + [Test] + public void Multiple_resizes_keep_all_keys_findable() + { + // Force several metadata resizes (threshold ~7/8 full) past stack migration. + var m = new SmallMap4(); + ref var map = ref m.Map; + const int n = 5000; + for (var i = 0; i < n; i++) + map.AddOrUpdate(i, i); + + Assert.AreEqual(n, map.Count); + Assert.Greater(map.Capacity, 16); + for (var i = 0; i < n; i++) + Assert.AreEqual(i, map.GetValueOrDefault(i, -1)); + Assert.AreEqual(-1, map.GetValueOrDefault(n, -1)); + AssertVerifyKeys(ref map, Enumerable.Range(0, n)); + } - // map.AddOrUpdate(5, "5"); - - // Verify(map, new[] { 0, 1, 3, 5, 9 }); - // } +#if CS_CHECK + [Test] + public void Check_AddOrUpdate_random_items_and_verify_all_added() + { + const int upperBound = 100_000; + Gen.Int[0, upperBound].Array.Sample(items => + { + var m = new SmallMap16(); + ref var map = ref m.Map; + foreach (var n in items) + { + map.AddOrUpdate(n, n); + Assert.AreEqual(n, map.GetValueOrDefault(n, -1)); + } + + foreach (var n in items) + Assert.AreEqual(n, map.GetValueOrDefault(n, -1)); + + Assert.AreEqual(-1, map.GetValueOrDefault(upperBound + 1, -1)); + Assert.AreEqual(-1, map.GetValueOrDefault(-1, -1)); + AssertVerifyKeys(ref map, items.Distinct()); + }, + iter: 2000); + } - // [Test] - // public void Can_store_and_get_stored_item_count() - // { - // var map = new SmallMap16(); - - // map.AddOrUpdate(42, "1"); - // map.AddOrUpdate(42 + 32 + 32, "3"); - - // Assert.AreEqual(2, map.Count); - // Verify(map, new[] { 42, 42 + 32 + 32 }); - // } - - // [Test] - // public void Can_update_a_stored_item_with_new_value() - // { - // var map = new SmallMap16(); - - // map.AddOrUpdate(42, "1"); - // map.AddOrUpdate(42, "3"); - - // Assert.AreEqual("3", map.GetValueOrDefault(42)); - // Assert.AreEqual(1, map.Count); - // Verify(map, new[] { 42 }); - // } - - // [Test] - // public void Can_add_key_with_0_hash_code() - // { - // var map = new SmallMap16(); + [Test] + public void Check_AddOrUpdate_unique_keys_count_and_lookup() + { + Gen.Int[0, 50_000].ArrayUnique.Sample(keys => + { + var m = new SmallMap8(); + ref var map = ref m.Map; + for (var i = 0; i < keys.Length; i++) + map.AddOrUpdate(keys[i], i); + + Assert.AreEqual(keys.Length, map.Count); + for (var i = 0; i < keys.Length; i++) + Assert.AreEqual(i, map.GetValueOrDefault(keys[i], -1)); + + AssertVerifyKeys(ref map, keys); + }, + iter: 1500); + } - // map.AddOrUpdate(0, "aaa"); - // map.AddOrUpdate(0 + 32, "2"); - // map.AddOrUpdate(0 + 32 + 32, "3"); - // Verify(map, new[] { 0, 0 + 32, 0 + 32 + 32 }); - - // string value; - // Assert.IsTrue(map.TryGetValue(0, out value)); - - // Assert.AreEqual("aaa", value); - // } - - // [Test] - // public void Can_quickly_find_the_scattered_items_with_the_same_cache() - // { - // var map = new SmallMap16(); - - // map.AddOrUpdate(42, "1"); - // map.AddOrUpdate(43, "a"); - // map.AddOrUpdate(42 + 32, "2"); - // map.AddOrUpdate(45, "b"); - // map.AddOrUpdate(46, "c"); - // map.AddOrUpdate(42 + 32 + 32, "3"); - // Verify(map, new[] { 42, 43, 42 + 32, 45, 46, 42 + 32 + 32 }); - - // string value; - // Assert.IsTrue(map.TryGetValue(42 + 32, out value)); - // Assert.AreEqual("2", value); - - // Assert.IsTrue(map.TryGetValue(42 + 32 + 32, out value)); - // Assert.AreEqual("3", value); - // } - - // [Test] - // public void Can_remove_the_stored_item() - // { - // var map = new SmallMap16(2); - - // map.AddOrUpdate(42, "1"); - // map.AddOrUpdate(42 + 32, "2"); - // map.AddOrUpdate(42 + 32 + 32, "3"); - - // Assert.AreEqual("2", map.GetValueOrDefault(42 + 32)); - // var r = map.TryRemove(42 + 32); - // Assert.IsTrue(r); - - // Assert.AreEqual(2, map.Count); - // Assert.AreEqual("1", map.GetValueOrDefault(42)); - // Assert.AreEqual("3", map.GetValueOrDefault(42 + 32 + 32)); - // Verify(map, null); - // } + [Test] + public void Check_colliding_int_keys_across_resizes() + { + // Same low bits (ideal index) force long RH chains and wrap cases before/after resize. + Gen.Int[1, 64].SelectMany(baseKey => + Gen.Int[8, 400].Select(n => (baseKey, n))) + .Sample(t => + { + var (baseKey, n) = t; + var m = new SmallMap4(); + ref var map = ref m.Map; + var keys = new int[n]; + for (var i = 0; i < n; i++) + { + // Step by growing powers of two so ideals collide under successive masks. + keys[i] = baseKey + i * 16; + map.AddOrUpdate(keys[i], i); + } + + Assert.AreEqual(n, map.Count); + for (var i = 0; i < n; i++) + Assert.AreEqual(i, map.GetValueOrDefault(keys[i], -1)); + AssertVerifyKeys(ref map, keys); + }, + iter: 1000); + } +#endif } From 2b79559fd390fb07ce2b18c51561342246d42f4a Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 5 Oct 2026 18:35:19 +0200 Subject: [PATCH 48/54] update .net targets and slnx --- .github/workflows/ci.yml | 29 +++++ .vscode/settings.json | 3 +- ImTools.sln | 107 ------------------ ImTools.slnx | 32 ++++++ README.md | 2 +- appveyor.yml | 25 ---- b.bat | 2 +- bd.bat | 4 +- bm.bat | 2 +- build.bat | 2 +- build_with_packaging.bat | 2 +- playground/Experiments/Experiments.csproj | 5 +- .../ImTools.Benchmarks.csproj | 2 +- .../ImTools.SimpleDIPlayground.csproj | 3 +- .../ImTools.UnionCsVsFsBenchmarks.fsproj | 3 +- .../ImTools.UnionPlayground.csproj | 4 +- src/ImTools/ImTools.csproj | 5 +- .../ImTools.UnitTests.csproj | 5 +- 18 files changed, 83 insertions(+), 154 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 ImTools.sln create mode 100644 ImTools.slnx delete mode 100644 appveyor.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..c0f35c9e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [ master, main ] + pull_request: + +jobs: + build-test: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 10.0.x + 11.0.x + dotnet-quality: preview + - name: Build + run: dotnet build ImTools.slnx -c Release -p:DevMode=false + - name: Test + run: dotnet test --no-build -c Release -p:DevMode=false test/ImTools.UnitTests/ImTools.UnitTests.csproj + - name: Pack + run: BuildScripts\NugetPack.bat + - uses: actions/upload-artifact@v4 + with: + name: packages + path: .dist/packages/*.nupkg diff --git a/.vscode/settings.json b/.vscode/settings.json index 051e436f..60a55d7b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,5 +11,6 @@ ], "files.watcherExclude": { "**/target": true - } + }, + "dotnet.defaultSolution": "ImTools.slnx" } \ No newline at end of file diff --git a/ImTools.sln b/ImTools.sln deleted file mode 100644 index 73d333c3..00000000 --- a/ImTools.sln +++ /dev/null @@ -1,107 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29424.173 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImTools", "src\ImTools\ImTools.csproj", "{6B4B1E6B-6C7B-4FE4-8620-D4ABEE67C592}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A6F5B847-FF68-4EA1-98D4-B7517296AC16}" - ProjectSection(SolutionItems) = preProject - src\Directory.Build.props = src\Directory.Build.props - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F779D683-7F9C-4B65-95C5-ED8E75642362}" - ProjectSection(SolutionItems) = preProject - test\Directory.Build.props = test\Directory.Build.props - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "playground", "playground", "{D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3}" - ProjectSection(SolutionItems) = preProject - playground\Directory.Build.props = playground\Directory.Build.props - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F6D087C9-3B9A-413A-86DD-14BD1F6BF013}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - .travis.yml = .travis.yml - appveyor.yml = appveyor.yml - build.bat = build.bat - build_no_legacy.bat = build_no_legacy.bat - Directory.Build.props = Directory.Build.props - README.md = README.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImTools.UnitTests", "test\ImTools.UnitTests\ImTools.UnitTests.csproj", "{D95E6E09-2052-4F08-A4D4-C73244720920}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImTools.Benchmarks", "playground\ImTools.Benchmarks\ImTools.Benchmarks.csproj", "{113D7A58-BA48-4C82-9E3C-0862BE1186A5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{CD913EEB-7AF4-47E3-8B7C-02235E5DCDE4}" - ProjectSection(SolutionItems) = preProject - build\NuGetPack.bat = build\NuGetPack.bat - build\NuGetPublish.bat = build\NuGetPublish.bat - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nuspecs", "nuspecs", "{29BC7B76-D817-425F-98F1-A92454D23857}" - ProjectSection(SolutionItems) = preProject - nuspecs\ImTools.nuspec = nuspecs\ImTools.nuspec - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImTools.UnionPlayground", "playground\ImTools.UnionPlayground\ImTools.UnionPlayground.csproj", "{EFBD055A-3C8A-45C7-8718-D9C53A66521D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImTools.SimpleDIPlayground", "playground\ImTools.SimpleDIPlayground\ImTools.SimpleDIPlayground.csproj", "{60B58862-31B3-48DC-BFF3-42A9B0A194A5}" -EndProject -Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "ImTools.UnionCsVsFsBenchmarks", "playground\ImTools.UnionCsVsFsBenchmarks\ImTools.UnionCsVsFsBenchmarks.fsproj", "{239B6CA0-2860-4A67-859F-FD4EF6DAA547}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Experiments", "playground\Experiments\Experiments.csproj", "{88D8A585-1936-41DC-9A6E-74589EB6EA97}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6B4B1E6B-6C7B-4FE4-8620-D4ABEE67C592}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6B4B1E6B-6C7B-4FE4-8620-D4ABEE67C592}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6B4B1E6B-6C7B-4FE4-8620-D4ABEE67C592}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6B4B1E6B-6C7B-4FE4-8620-D4ABEE67C592}.Release|Any CPU.Build.0 = Release|Any CPU - {D95E6E09-2052-4F08-A4D4-C73244720920}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D95E6E09-2052-4F08-A4D4-C73244720920}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D95E6E09-2052-4F08-A4D4-C73244720920}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D95E6E09-2052-4F08-A4D4-C73244720920}.Release|Any CPU.Build.0 = Release|Any CPU - {113D7A58-BA48-4C82-9E3C-0862BE1186A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {113D7A58-BA48-4C82-9E3C-0862BE1186A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {113D7A58-BA48-4C82-9E3C-0862BE1186A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {113D7A58-BA48-4C82-9E3C-0862BE1186A5}.Release|Any CPU.Build.0 = Release|Any CPU - {EFBD055A-3C8A-45C7-8718-D9C53A66521D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EFBD055A-3C8A-45C7-8718-D9C53A66521D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EFBD055A-3C8A-45C7-8718-D9C53A66521D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EFBD055A-3C8A-45C7-8718-D9C53A66521D}.Release|Any CPU.Build.0 = Release|Any CPU - {60B58862-31B3-48DC-BFF3-42A9B0A194A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {60B58862-31B3-48DC-BFF3-42A9B0A194A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {60B58862-31B3-48DC-BFF3-42A9B0A194A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {60B58862-31B3-48DC-BFF3-42A9B0A194A5}.Release|Any CPU.Build.0 = Release|Any CPU - {239B6CA0-2860-4A67-859F-FD4EF6DAA547}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {239B6CA0-2860-4A67-859F-FD4EF6DAA547}.Debug|Any CPU.Build.0 = Debug|Any CPU - {239B6CA0-2860-4A67-859F-FD4EF6DAA547}.Release|Any CPU.ActiveCfg = Release|Any CPU - {239B6CA0-2860-4A67-859F-FD4EF6DAA547}.Release|Any CPU.Build.0 = Release|Any CPU - {88D8A585-1936-41DC-9A6E-74589EB6EA97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {88D8A585-1936-41DC-9A6E-74589EB6EA97}.Debug|Any CPU.Build.0 = Debug|Any CPU - {88D8A585-1936-41DC-9A6E-74589EB6EA97}.Release|Any CPU.ActiveCfg = Release|Any CPU - {88D8A585-1936-41DC-9A6E-74589EB6EA97}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {6B4B1E6B-6C7B-4FE4-8620-D4ABEE67C592} = {A6F5B847-FF68-4EA1-98D4-B7517296AC16} - {D95E6E09-2052-4F08-A4D4-C73244720920} = {F779D683-7F9C-4B65-95C5-ED8E75642362} - {113D7A58-BA48-4C82-9E3C-0862BE1186A5} = {D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3} - {EFBD055A-3C8A-45C7-8718-D9C53A66521D} = {D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3} - {60B58862-31B3-48DC-BFF3-42A9B0A194A5} = {D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3} - {239B6CA0-2860-4A67-859F-FD4EF6DAA547} = {D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3} - {88D8A585-1936-41DC-9A6E-74589EB6EA97} = {D109DF34-CB08-42E5-8F8F-CD2D4D4A8DB3} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {B8B8E41A-6F08-46C3-828D-C74B91428793} - EndGlobalSection -EndGlobal diff --git a/ImTools.slnx b/ImTools.slnx new file mode 100644 index 00000000..3424ff77 --- /dev/null +++ b/ImTools.slnx @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 32097dd6..d1b7e5b9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Initially split from the [DryIoc](https://github.com/dadhi/dryioc), and now used [![license](https://img.shields.io/github/license/dadhi/ImTools.svg)](http://opensource.org/licenses/MIT) -- Windows, Linux, MacOS [![CI build](https://ci.appveyor.com/api/projects/status/el9echuqfnl86u53?svg=true)](https://ci.appveyor.com/project/MaksimVolkau/imtools/branch/master) +- Windows, Linux, MacOS [![CI build](https://github.com/dadhi/ImTools/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/dadhi/ImTools/actions/workflows/ci.yml) - Lib package [![NuGet Version](https://img.shields.io/nuget/v/ImTools.dll)](https://www.nuget.org/packages/ImTools.dll)![NuGet Downloads](https://img.shields.io/nuget/dt/ImTools.dll) - Code package [![NuGet Version](https://img.shields.io/nuget/v/ImTools)](https://www.nuget.org/packages/ImTools)![NuGet Downloads](https://img.shields.io/nuget/dt/ImTools) - Latest release [![latest release](https://img.shields.io/badge/latest%20release-v4.0.0-green)](https://github.com/dadhi/ImTools/releases/tag/v4.0.0) diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 48872932..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,25 +0,0 @@ -image: - - Visual Studio 2022 - # - Ubuntu - # - macOS - -version: 5.0.0-preview-01-b{build} - -test: false - -skip_commits: - message: /noci .*/ - -build_script: - - dotnet test -p:CILinux=true -c:Release test/ImTools.UnitTests/ImTools.UnitTests.csproj - -for: - - matrix: - only: - - image: Visual Studio 2022 - - build_script: - - build_with_packaging.bat - - artifacts: - - path: .\.dist\packages\*.nupkg diff --git a/b.bat b/b.bat index 5b85acb5..5dad065e 100644 --- a/b.bat +++ b/b.bat @@ -5,7 +5,7 @@ echo:## Starting: RESTORE and BUILD... echo: dotnet clean -v:m -dotnet build -c:Release -v:m +dotnet build ImTools.slnx -c:Release -v:m if %ERRORLEVEL% neq 0 goto :error echo: diff --git a/bd.bat b/bd.bat index 5b85acb5..5ed2022e 100644 --- a/bd.bat +++ b/bd.bat @@ -5,7 +5,7 @@ echo:## Starting: RESTORE and BUILD... echo: dotnet clean -v:m -dotnet build -c:Release -v:m +dotnet build ImTools.slnx -c:Release -v:m -p:DevMode=true if %ERRORLEVEL% neq 0 goto :error echo: @@ -15,7 +15,7 @@ echo: echo:## Starting: TESTS... echo: -dotnet test --no-build -c Release test/ImTools.UnitTests/ImTools.UnitTests.csproj +dotnet test --no-build -c Release -p:DevMode=true test/ImTools.UnitTests/ImTools.UnitTests.csproj if %ERRORLEVEL% neq 0 goto :error echo: diff --git a/bm.bat b/bm.bat index a6a1a547..b7dce89c 100644 --- a/bm.bat +++ b/bm.bat @@ -1 +1 @@ -dotnet run -f net9.0 -c Release --project playground\ImTools.Benchmarks \ No newline at end of file +dotnet run -f net11.0 -c Release --project playground\ImTools.Benchmarks \ No newline at end of file diff --git a/build.bat b/build.bat index 896c2de9..84b1a802 100644 --- a/build.bat +++ b/build.bat @@ -5,7 +5,7 @@ echo:## Starting: RESTORE and BUILD... echo: dotnet clean -v:m -dotnet build -c:Release -v:m -p:DevMode=false +dotnet build ImTools.slnx -c:Release -v:m -p:DevMode=false if %ERRORLEVEL% neq 0 goto :error echo: diff --git a/build_with_packaging.bat b/build_with_packaging.bat index 2d44a746..27a2457f 100644 --- a/build_with_packaging.bat +++ b/build_with_packaging.bat @@ -5,7 +5,7 @@ echo:## Starting: RESTORE and BUILD... echo: dotnet clean -v:m -dotnet build -c:Release -p:DevMode=false -v:m +dotnet build ImTools.slnx -c:Release -p:DevMode=false -v:m if %ERRORLEVEL% neq 0 goto :error echo: diff --git a/playground/Experiments/Experiments.csproj b/playground/Experiments/Experiments.csproj index 25e087ab..1f2e8fb7 100644 --- a/playground/Experiments/Experiments.csproj +++ b/playground/Experiments/Experiments.csproj @@ -1,9 +1,8 @@ - net9.0 - net9.0;net8.0;net6.0;net472 - net7.0 + net11.0;net10.0;net8.0 + net11.0;net10.0;net8.0;net472 1701;1702;AD0001;NU1608 diff --git a/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj b/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj index f6b1829f..2aab43ac 100644 --- a/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj +++ b/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj @@ -2,7 +2,7 @@ Exe - net9.0;net10.0 + net11.0;net10.0;net8.0 x64 diff --git a/playground/ImTools.SimpleDIPlayground/ImTools.SimpleDIPlayground.csproj b/playground/ImTools.SimpleDIPlayground/ImTools.SimpleDIPlayground.csproj index 6d99787a..84b004a3 100644 --- a/playground/ImTools.SimpleDIPlayground/ImTools.SimpleDIPlayground.csproj +++ b/playground/ImTools.SimpleDIPlayground/ImTools.SimpleDIPlayground.csproj @@ -2,7 +2,8 @@ Exe - net8.0 + net11.0;net10.0;net8.0 + net11.0;net10.0;net8.0;net472 diff --git a/playground/ImTools.UnionCsVsFsBenchmarks/ImTools.UnionCsVsFsBenchmarks.fsproj b/playground/ImTools.UnionCsVsFsBenchmarks/ImTools.UnionCsVsFsBenchmarks.fsproj index 6782ec12..9cdc1311 100644 --- a/playground/ImTools.UnionCsVsFsBenchmarks/ImTools.UnionCsVsFsBenchmarks.fsproj +++ b/playground/ImTools.UnionCsVsFsBenchmarks/ImTools.UnionCsVsFsBenchmarks.fsproj @@ -2,7 +2,8 @@ Exe - net6.0 + net11.0;net10.0;net8.0 + net11.0;net10.0;net8.0;net472 diff --git a/playground/ImTools.UnionPlayground/ImTools.UnionPlayground.csproj b/playground/ImTools.UnionPlayground/ImTools.UnionPlayground.csproj index ffa088bc..c6c41996 100644 --- a/playground/ImTools.UnionPlayground/ImTools.UnionPlayground.csproj +++ b/playground/ImTools.UnionPlayground/ImTools.UnionPlayground.csproj @@ -2,8 +2,8 @@ Exe - net6.0 - net7.0 + net11.0;net10.0;net8.0 + net11.0;net10.0;net8.0;net472 diff --git a/src/ImTools/ImTools.csproj b/src/ImTools/ImTools.csproj index e7916e94..e5631430 100644 --- a/src/ImTools/ImTools.csproj +++ b/src/ImTools/ImTools.csproj @@ -1,8 +1,7 @@ - net9.0;net8.0;net6.0 - net9.0;net8.0;net6.0;net45;netstandard2.0 - net7.0 + net11.0;net10.0;net8.0 + net11.0;net10.0;net8.0;net472;netstandard2.1 ImTools 5.0.0 diff --git a/test/ImTools.UnitTests/ImTools.UnitTests.csproj b/test/ImTools.UnitTests/ImTools.UnitTests.csproj index 501fae37..40024d40 100644 --- a/test/ImTools.UnitTests/ImTools.UnitTests.csproj +++ b/test/ImTools.UnitTests/ImTools.UnitTests.csproj @@ -1,9 +1,8 @@ - net9.0 - net9.0;net8.0;net6.0;net472 - net7.0 + net11.0;net10.0;net8.0 + net11.0;net10.0;net8.0;net472 From 5abb1e81538399810a0fb3be2440498ea1e68dff Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 5 Oct 2026 21:43:40 +0200 Subject: [PATCH 49/54] bmed the SmallMap vs HSmallMap - replacing the Grr with SingleArrayEntries, because we do not need a stable metadata. --- playground/Experiments/FEC_ImTools.cs | 8 +- .../SmallMapVsHSmallMapBenchmarks.cs | 246 ++++++++++++++++++ 2 files changed, 250 insertions(+), 4 deletions(-) create mode 100644 playground/ImTools.Benchmarks/SmallMapVsHSmallMapBenchmarks.cs diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 5afd0349..145b92b8 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -1662,7 +1662,7 @@ public override string ToString() => /// Details: /// - Struct so the empty/default map allocates nothing on the heap /// - First N entries live on stack (); overflow goes to -/// - Metadata (packed entry-index | hash | probe) is stored in a stable and grows independently +/// - Metadata (packed entry-index | hash | probe) is stored in a single array (nothing references it, so no need for stable segments) and grows independently /// - Robin-Hood open addressing on the metadata table /// - Entry indexes preserve insertion order (stable enumeration by index 0..Count-1) /// - Prefer for heap entries when you need stable refs across growth; @@ -1698,7 +1698,7 @@ public struct SmallMap> _packedEntryIndexesHashesProbes; + internal SingleBackingArray _packedEntryIndexesHashesProbes; #if DEBUG && SUPPORTS_UNSAFE /// Debug view of the first metadata segment only (not full multi-segment capacity). @@ -1707,9 +1707,9 @@ public Span DebugMetaUnpacked get { var packed = _packedEntryIndexesHashesProbes; - if (packed.SegmentCount == 0) + if (packed._entries == null) return Span.Empty; - return MemoryMarshal.Cast(packed._segments.GetSurePresentRef(0).AsSpan()); + return MemoryMarshal.Cast(packed._entries.AsSpan()); } } #endif diff --git a/playground/ImTools.Benchmarks/SmallMapVsHSmallMapBenchmarks.cs b/playground/ImTools.Benchmarks/SmallMapVsHSmallMapBenchmarks.cs new file mode 100644 index 00000000..81c28462 --- /dev/null +++ b/playground/ImTools.Benchmarks/SmallMapVsHSmallMapBenchmarks.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; +using Fec = FastExpressionCompiler.ImTools; +using Im = ImTools; + +namespace ImTools.Benchmarks; + +/// +/// FEC SmallMap (stack entries + Grr or single-array heap overflow) vs ImTools HSmallMap and Dictionary. +/// Run: dotnet run -c Release -f net10.0 -- --filter *SmallMapVsHSmallMap* [--runtimes net10.0 net11.0 net8.0] +/// +[MemoryDiagnoser] +[SimpleJob(RunStrategy.Throughput, launchCount: 1, warmupCount: 3, iterationCount: 10)] +[GroupBenchmarksBy(BenchmarkDotNet.Configs.BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class SmallMapVsHSmallMapBenchmarks +{ + // 4/8/16 are the stack-only zone of SmallMap16, 32+ spill to the heap and the metadata table. + [Params(4, 8, 16, 32, 128, 1024)] + public int Count; + + private int[] _keys = null!; + private int[] _missingKeys = null!; + + private Im.HSmallMap> _hMap; + private Fec.SmallMap16 _grr16; + private Fec.SmallMap16_SingleArrEntries _single16; + private Fec.SmallMap8 _grr8; + private Dictionary _dict = null!; + + [GlobalSetup] + public void Setup() + { + var rng = new Random(42); + var seen = new HashSet(); + _keys = new int[Count]; + for (var i = 0; i < Count;) + if (seen.Add(_keys[i] = rng.Next(1, int.MaxValue))) + ++i; + + _missingKeys = new int[Count]; + for (var i = 0; i < Count;) + { + var k = rng.Next(1, int.MaxValue); + if (seen.Add(k)) + _missingKeys[i++] = k; + } + + _hMap = Im.HSmallMap.New(); + _grr16 = new Fec.SmallMap16(); + _single16 = new Fec.SmallMap16_SingleArrEntries(); + _grr8 = new Fec.SmallMap8(); + _dict = new Dictionary(); + foreach (var k in _keys) + { + _hMap.AddOrUpdate(k, k); + Fec.SmallMap.AddOrUpdate(ref _grr16.Map, k, k); + Fec.SmallMap.AddOrUpdate(ref _single16.Map, k, k); + Fec.SmallMap.AddOrUpdate(ref _grr8.Map, k, k); + _dict[k] = k; + } + } + + #region Populate + + [BenchmarkCategory("Populate"), Benchmark(Baseline = true)] + public int Populate_HSmallMap() + { + var m = Im.HSmallMap.New(); + foreach (var k in _keys) + m.AddOrUpdate(k, k); + return m.Count; + } + + [BenchmarkCategory("Populate"), Benchmark] + public int Populate_SmallMap16_Grr() + { + var m = new Fec.SmallMap16(); + foreach (var k in _keys) + Fec.SmallMap.AddOrUpdate(ref m.Map, k, k); + return m.Map.Count; + } + + [BenchmarkCategory("Populate"), Benchmark] + public int Populate_SmallMap16_SingleArray() + { + var m = new Fec.SmallMap16_SingleArrEntries(); + foreach (var k in _keys) + Fec.SmallMap.AddOrUpdate(ref m.Map, k, k); + return m.Map.Count; + } + + [BenchmarkCategory("Populate"), Benchmark] + public int Populate_SmallMap8_Grr() + { + var m = new Fec.SmallMap8(); + foreach (var k in _keys) + Fec.SmallMap.AddOrUpdate(ref m.Map, k, k); + return m.Map.Count; + } + + [BenchmarkCategory("Populate"), Benchmark] + public int Populate_Dictionary() + { + var m = new Dictionary(); + foreach (var k in _keys) + m[k] = k; + return m.Count; + } + + #endregion + + #region Lookup hit + + [BenchmarkCategory("LookupHit"), Benchmark(Baseline = true)] + public int LookupHit_HSmallMap() + { + var sum = 0; + foreach (var k in _keys) + if (_hMap.TryGetValue(k, out var v)) sum += v; + return sum; + } + + [BenchmarkCategory("LookupHit"), Benchmark] + public int LookupHit_SmallMap16_Grr() + { + var sum = 0; + foreach (var k in _keys) + sum += Fec.SmallMap.GetValueOrDefault(ref _grr16.Map, k, -1); + return sum; + } + + [BenchmarkCategory("LookupHit"), Benchmark] + public int LookupHit_SmallMap16_SingleArray() + { + var sum = 0; + foreach (var k in _keys) + sum += Fec.SmallMap.GetValueOrDefault(ref _single16.Map, k, -1); + return sum; + } + + [BenchmarkCategory("LookupHit"), Benchmark] + public int LookupHit_SmallMap8_Grr() + { + var sum = 0; + foreach (var k in _keys) + sum += Fec.SmallMap.GetValueOrDefault(ref _grr8.Map, k, -1); + return sum; + } + + [BenchmarkCategory("LookupHit"), Benchmark] + public int LookupHit_Dictionary() + { + var sum = 0; + foreach (var k in _keys) + if (_dict.TryGetValue(k, out var v)) sum += v; + return sum; + } + + #endregion + + #region Lookup miss + + [BenchmarkCategory("LookupMiss"), Benchmark(Baseline = true)] + public int LookupMiss_HSmallMap() + { + var sum = 0; + foreach (var k in _missingKeys) + if (_hMap.TryGetValue(k, out var v)) sum += v; + return sum; + } + + [BenchmarkCategory("LookupMiss"), Benchmark] + public int LookupMiss_SmallMap16_Grr() + { + var sum = 0; + foreach (var k in _missingKeys) + sum += Fec.SmallMap.GetValueOrDefault(ref _grr16.Map, k, -1); + return sum; + } + + [BenchmarkCategory("LookupMiss"), Benchmark] + public int LookupMiss_SmallMap16_SingleArray() + { + var sum = 0; + foreach (var k in _missingKeys) + sum += Fec.SmallMap.GetValueOrDefault(ref _single16.Map, k, -1); + return sum; + } + + [BenchmarkCategory("LookupMiss"), Benchmark] + public int LookupMiss_SmallMap8_Grr() + { + var sum = 0; + foreach (var k in _missingKeys) + sum += Fec.SmallMap.GetValueOrDefault(ref _grr8.Map, k, -1); + return sum; + } + + [BenchmarkCategory("LookupMiss"), Benchmark] + public int LookupMiss_Dictionary() + { + var sum = 0; + foreach (var k in _missingKeys) + if (_dict.TryGetValue(k, out var v)) sum += v; + return sum; + } + + #endregion + + #region Enumerate by insertion order + + [BenchmarkCategory("Enumerate"), Benchmark(Baseline = true)] + public int Enumerate_HSmallMap() + { + var sum = 0; + var e = _hMap.Entries; + for (var i = 0; i < e.GetCount(); ++i) + sum += e.GetSurePresentEntryRef(i).Value; + return sum; + } + + [BenchmarkCategory("Enumerate"), Benchmark] + public int Enumerate_SmallMap16_Grr() + { + var sum = 0; + ref var m = ref _grr16.Map; + for (var i = 0; i < m.Count; ++i) + sum += m.GetSurePresentEntryRef(i).Value; + return sum; + } + + [BenchmarkCategory("Enumerate"), Benchmark] + public int Enumerate_SmallMap16_SingleArray() + { + var sum = 0; + ref var m = ref _single16.Map; + for (var i = 0; i < m.Count; ++i) + sum += m.GetSurePresentEntryRef(i).Value; + return sum; + } + + #endregion +} From fe9ee6b6e46e8054aadbf26e20c54c8b5a18bc4b Mon Sep 17 00:00:00 2001 From: dadhi Date: Mon, 5 Oct 2026 23:32:34 +0200 Subject: [PATCH 50/54] updating the bm for hsmallmap vs smallmap -> tbd use ints meta singlearrayentries with 5 upper bits for probe and resize modified --- bm.bat | 2 +- .../ImTools.Benchmarks/ImHashMapBenchmarks.cs | 5322 +++++++++-------- .../ImTools.Benchmarks.csproj | 10 +- playground/ImTools.Benchmarks/Program.cs | 17 +- 4 files changed, 2683 insertions(+), 2668 deletions(-) diff --git a/bm.bat b/bm.bat index b7dce89c..8f9bcc99 100644 --- a/bm.bat +++ b/bm.bat @@ -1 +1 @@ -dotnet run -f net11.0 -c Release --project playground\ImTools.Benchmarks \ No newline at end of file +dotnet run -f net10.0 -c Release --project playground\ImTools.Benchmarks \ No newline at end of file diff --git a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs index 48d8ae3e..d1545abc 100644 --- a/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs +++ b/playground/ImTools.Benchmarks/ImHashMapBenchmarks.cs @@ -23,256 +23,256 @@ #pragma warning disable CS0649, CS0169 -namespace Playground +namespace Playground; + +public class ImHashMapBenchmarks { - public class ImHashMapBenchmarks - { - private static readonly Type[] _allKeys = typeof(List<>).Assembly.GetTypes().Take(2000).ToArray(); - private static readonly Type[] _keys = _allKeys.Take(1000).ToArray(); - private static readonly Type[] _missingKeys = _allKeys.TakeLast(1000).ToArray(); + private static readonly Type[] _allKeys = typeof(List<>).Assembly.GetTypes().Take(2000).ToArray(); + private static readonly Type[] _keys = _allKeys.Take(1000).ToArray(); + private static readonly Type[] _missingKeys = _allKeys.TakeLast(1000).ToArray(); - // get half of the random _keys for the lookup, by selecting half of the random indexes from keys - private static readonly Random _seed = new Random(42); + // get half of the random _keys for the lookup, by selecting half of the random indexes from keys + private static readonly Random _seed = new Random(42); - public struct TypeVal : IEquatable - { - public static implicit operator TypeVal(Type t) => new TypeVal(t); + public struct TypeVal : IEquatable + { + public static implicit operator TypeVal(Type t) => new TypeVal(t); - public readonly Type Type; - public TypeVal(Type type) => Type = type; - public bool Equals(TypeVal other) => Type == other.Type; - public override bool Equals(object obj) => !ReferenceEquals(null, obj) && obj is TypeVal other && Equals(other); - public override int GetHashCode() => Type.GetHashCode(); - } + public readonly Type Type; + public TypeVal(Type type) => Type = type; + public bool Equals(TypeVal other) => Type == other.Type; + public override bool Equals(object obj) => !ReferenceEquals(null, obj) && obj is TypeVal other && Equals(other); + public override int GetHashCode() => Type.GetHashCode(); + } - [HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchMispredictions, HardwareCounter.BranchInstructions)] - [MemoryDiagnoser] - public class Populate - { - /* - ## 15.01.2019: + [HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchMispredictions, HardwareCounter.BranchInstructions)] + [MemoryDiagnoser] + public class Populate + { + /* + ## 15.01.2019: - Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------- |---------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate | 15.84 us | 0.1065 us | 0.0944 us | 1.00 | 0.00 | 7.3242 | - | - | 33.87 KB | - AddOrUpdate_v1 | 27.00 us | 0.1792 us | 0.1588 us | 1.71 | 0.02 | 7.7515 | - | - | 35.77 KB | + Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------- |---------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + AddOrUpdate | 15.84 us | 0.1065 us | 0.0944 us | 1.00 | 0.00 | 7.3242 | - | - | 33.87 KB | + AddOrUpdate_v1 | 27.00 us | 0.1792 us | 0.1588 us | 1.71 | 0.02 | 7.7515 | - | - | 35.77 KB | - - ## 16.01.2019: Total test against ImHashMap V1, System ImmutableDictionary and ConcurrentDictionary + + ## 16.01.2019: Total test against ImHashMap V1, System ImmutableDictionary and ConcurrentDictionary BenchmarkDotNet=v0.11.3, OS=Windows 10.0.17134.523 (1803/April2018Update/Redstone4) Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores Frequency=2156249 Hz, Resolution=463.7683 ns, Timer=TSC .NET Core SDK=2.2.100 - [Host] : .NET Core 2.1.6 (CoreCLR 4.6.27019.06, CoreFX 4.6.27019.05), 64bit RyuJIT - DefaultJob : .NET Core 2.1.6 (CoreCLR 4.6.27019.06, CoreFX 4.6.27019.05), 64bit RyuJIT +[Host] : .NET Core 2.1.6 (CoreCLR 4.6.27019.06, CoreFX 4.6.27019.05), 64bit RyuJIT +DefaultJob : .NET Core 2.1.6 (CoreCLR 4.6.27019.06, CoreFX 4.6.27019.05), 64bit RyuJIT - Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |---------------:|--------------:|--------------:|---------------:|------:|--------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v1 | 10 | 987.6 ns | 36.53 ns | 99.99 ns | 934.3 ns | 0.94 | 0.17 | 0.5589 | - | - | 2.58 KB | - AddOrUpdate | 10 | 1,067.5 ns | 50.43 ns | 136.33 ns | 1,073.5 ns | 1.00 | 0.00 | 0.4044 | - | - | 1.87 KB | - ConcurrentDict | 10 | 1,943.9 ns | 92.31 ns | 81.83 ns | 1,921.3 ns | 1.85 | 0.25 | 0.6371 | - | - | 2.95 KB | - AddOrUpdate_v2 | 10 | 2,688.3 ns | 229.79 ns | 677.55 ns | 2,342.9 ns | 2.50 | 0.77 | 0.4349 | - | - | 2.02 KB | - ImmutableDict | 10 | 5,903.1 ns | 749.28 ns | 801.72 ns | 5,607.8 ns | 5.66 | 1.06 | 0.5875 | - | - | 2.73 KB | - | | | | | | | | | | | | - ConcurrentDict | 100 | 14,476.1 ns | 1,184.05 ns | 1,215.93 ns | 14,193.3 ns | 0.48 | 0.21 | 3.6011 | 0.0305 | - | 16.66 KB | - AddOrUpdate_v1 | 100 | 16,999.4 ns | 1,522.75 ns | 4,441.93 ns | 14,281.8 ns | 0.59 | 0.25 | 8.4686 | - | - | 39.05 KB | - AddOrUpdate_v2 | 100 | 28,695.4 ns | 41.78 ns | 32.62 ns | 28,697.6 ns | 0.94 | 0.33 | 7.7515 | - | - | 35.81 KB | - AddOrUpdate | 100 | 31,854.4 ns | 2,882.03 ns | 8,497.72 ns | 36,547.9 ns | 1.00 | 0.00 | 7.3242 | - | - | 33.91 KB | - ImmutableDict | 100 | 89,602.2 ns | 1,873.19 ns | 2,229.89 ns | 88,767.5 ns | 2.98 | 1.05 | 9.3994 | - | - | 43.68 KB | - | | | | | | | | | | | | - ConcurrentDict | 1000 | 219,064.7 ns | 559.14 ns | 466.91 ns | 218,894.7 ns | 0.69 | 0.01 | 49.3164 | 17.8223 | - | 254.29 KB | - AddOrUpdate_v1 | 1000 | 297,651.6 ns | 1,073.37 ns | 838.02 ns | 297,421.1 ns | 0.93 | 0.01 | 120.6055 | 3.4180 | - | 556.41 KB | - AddOrUpdate | 1000 | 319,478.3 ns | 2,768.11 ns | 2,161.16 ns | 319,079.8 ns | 1.00 | 0.00 | 113.2813 | 0.9766 | - | 526.48 KB | - AddOrUpdate_v2 | 1000 | 615,321.8 ns | 72,410.43 ns | 213,503.80 ns | 467,207.0 ns | 1.97 | 0.66 | 118.6523 | 0.4883 | - | 547.3 KB | - ImmutableDict | 1000 | 1,516,613.7 ns | 107,376.35 ns | 290,298.20 ns | 1,387,325.2 ns | 4.95 | 1.19 | 140.6250 | 1.9531 | - | 648.02 KB | +AddOrUpdate_v1 | 10 | 987.6 ns | 36.53 ns | 99.99 ns | 934.3 ns | 0.94 | 0.17 | 0.5589 | - | - | 2.58 KB | +AddOrUpdate | 10 | 1,067.5 ns | 50.43 ns | 136.33 ns | 1,073.5 ns | 1.00 | 0.00 | 0.4044 | - | - | 1.87 KB | +ConcurrentDict | 10 | 1,943.9 ns | 92.31 ns | 81.83 ns | 1,921.3 ns | 1.85 | 0.25 | 0.6371 | - | - | 2.95 KB | +AddOrUpdate_v2 | 10 | 2,688.3 ns | 229.79 ns | 677.55 ns | 2,342.9 ns | 2.50 | 0.77 | 0.4349 | - | - | 2.02 KB | +ImmutableDict | 10 | 5,903.1 ns | 749.28 ns | 801.72 ns | 5,607.8 ns | 5.66 | 1.06 | 0.5875 | - | - | 2.73 KB | + | | | | | | | | | | | | +ConcurrentDict | 100 | 14,476.1 ns | 1,184.05 ns | 1,215.93 ns | 14,193.3 ns | 0.48 | 0.21 | 3.6011 | 0.0305 | - | 16.66 KB | +AddOrUpdate_v1 | 100 | 16,999.4 ns | 1,522.75 ns | 4,441.93 ns | 14,281.8 ns | 0.59 | 0.25 | 8.4686 | - | - | 39.05 KB | +AddOrUpdate_v2 | 100 | 28,695.4 ns | 41.78 ns | 32.62 ns | 28,697.6 ns | 0.94 | 0.33 | 7.7515 | - | - | 35.81 KB | +AddOrUpdate | 100 | 31,854.4 ns | 2,882.03 ns | 8,497.72 ns | 36,547.9 ns | 1.00 | 0.00 | 7.3242 | - | - | 33.91 KB | +ImmutableDict | 100 | 89,602.2 ns | 1,873.19 ns | 2,229.89 ns | 88,767.5 ns | 2.98 | 1.05 | 9.3994 | - | - | 43.68 KB | + | | | | | | | | | | | | +ConcurrentDict | 1000 | 219,064.7 ns | 559.14 ns | 466.91 ns | 218,894.7 ns | 0.69 | 0.01 | 49.3164 | 17.8223 | - | 254.29 KB | +AddOrUpdate_v1 | 1000 | 297,651.6 ns | 1,073.37 ns | 838.02 ns | 297,421.1 ns | 0.93 | 0.01 | 120.6055 | 3.4180 | - | 556.41 KB | +AddOrUpdate | 1000 | 319,478.3 ns | 2,768.11 ns | 2,161.16 ns | 319,079.8 ns | 1.00 | 0.00 | 113.2813 | 0.9766 | - | 526.48 KB | +AddOrUpdate_v2 | 1000 | 615,321.8 ns | 72,410.43 ns | 213,503.80 ns | 467,207.0 ns | 1.97 | 0.66 | 118.6523 | 0.4883 | - | 547.3 KB | +ImmutableDict | 1000 | 1,516,613.7 ns | 107,376.35 ns | 290,298.20 ns | 1,387,325.2 ns | 4.95 | 1.19 | 140.6250 | 1.9531 | - | 648.02 KB | ## 21.01.2019 - all versions compared - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v1 | 30 | 3.326 us | 0.0098 us | 0.0092 us | 0.84 | 0.01 | 2.0409 | - | - | 9.42 KB | - AddOrUpdate_v2 | 30 | 3.515 us | 0.0700 us | 0.0688 us | 0.89 | 0.02 | 1.9302 | - | - | 8.91 KB | - AddOrUpdate | 30 | 3.945 us | 0.0261 us | 0.0231 us | 1.00 | 0.00 | 2.1591 | - | - | 9.98 KB | - AddOrUpdate_v3 | 30 | 4.607 us | 0.0875 us | 0.0819 us | 1.17 | 0.02 | 1.7624 | - | - | 8.13 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 150 | 24.883 us | 0.4840 us | 0.4527 us | 0.86 | 0.02 | 13.5193 | - | - | 62.39 KB | - AddOrUpdate_v2 | 150 | 26.862 us | 0.5042 us | 0.4717 us | 0.92 | 0.02 | 13.7024 | - | - | 63.2 KB | - AddOrUpdate | 150 | 29.054 us | 0.1205 us | 0.1127 us | 1.00 | 0.00 | 15.1978 | - | - | 70.17 KB | - AddOrUpdate_v3 | 150 | 35.585 us | 0.3740 us | 0.3498 us | 1.22 | 0.01 | 12.5732 | - | - | 58.05 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 500 | 124.728 us | 0.7225 us | 0.6759 us | 0.91 | 0.01 | 54.4434 | - | - | 251.95 KB | - AddOrUpdate_v2 | 500 | 128.650 us | 1.4938 us | 1.3973 us | 0.94 | 0.01 | 58.1055 | 0.2441 | - | 267.97 KB | - AddOrUpdate | 500 | 137.325 us | 1.9010 us | 1.7782 us | 1.00 | 0.00 | 63.2324 | 0.2441 | - | 291.42 KB | - AddOrUpdate_v3 | 500 | 166.994 us | 1.7109 us | 1.6004 us | 1.22 | 0.02 | 52.7344 | - | - | 243.81 KB | +AddOrUpdate_v1 | 30 | 3.326 us | 0.0098 us | 0.0092 us | 0.84 | 0.01 | 2.0409 | - | - | 9.42 KB | +AddOrUpdate_v2 | 30 | 3.515 us | 0.0700 us | 0.0688 us | 0.89 | 0.02 | 1.9302 | - | - | 8.91 KB | +AddOrUpdate | 30 | 3.945 us | 0.0261 us | 0.0231 us | 1.00 | 0.00 | 2.1591 | - | - | 9.98 KB | +AddOrUpdate_v3 | 30 | 4.607 us | 0.0875 us | 0.0819 us | 1.17 | 0.02 | 1.7624 | - | - | 8.13 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 150 | 24.883 us | 0.4840 us | 0.4527 us | 0.86 | 0.02 | 13.5193 | - | - | 62.39 KB | +AddOrUpdate_v2 | 150 | 26.862 us | 0.5042 us | 0.4717 us | 0.92 | 0.02 | 13.7024 | - | - | 63.2 KB | +AddOrUpdate | 150 | 29.054 us | 0.1205 us | 0.1127 us | 1.00 | 0.00 | 15.1978 | - | - | 70.17 KB | +AddOrUpdate_v3 | 150 | 35.585 us | 0.3740 us | 0.3498 us | 1.22 | 0.01 | 12.5732 | - | - | 58.05 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 500 | 124.728 us | 0.7225 us | 0.6759 us | 0.91 | 0.01 | 54.4434 | - | - | 251.95 KB | +AddOrUpdate_v2 | 500 | 128.650 us | 1.4938 us | 1.3973 us | 0.94 | 0.01 | 58.1055 | 0.2441 | - | 267.97 KB | +AddOrUpdate | 500 | 137.325 us | 1.9010 us | 1.7782 us | 1.00 | 0.00 | 63.2324 | 0.2441 | - | 291.42 KB | +AddOrUpdate_v3 | 500 | 166.994 us | 1.7109 us | 1.6004 us | 1.22 | 0.02 | 52.7344 | - | - | 243.81 KB | ## Inlining With and removing not necessary call to Balance in case of Update. - - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v1 | 30 | 3.323 us | 0.0121 us | 0.0107 us | 0.91 | 0.00 | 2.0409 | - | - | 9.42 KB | - AddOrUpdate | 30 | 3.647 us | 0.0111 us | 0.0093 us | 1.00 | 0.00 | 2.0409 | - | - | 9.42 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 150 | 24.605 us | 0.2023 us | 0.1690 us | 0.92 | 0.02 | 13.5193 | - | - | 62.39 KB | - AddOrUpdate | 150 | 26.832 us | 0.5300 us | 0.5443 us | 1.00 | 0.00 | 13.5193 | - | - | 62.39 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 500 | 121.799 us | 0.9309 us | 0.8252 us | 0.92 | 0.01 | 54.4434 | - | - | 251.95 KB | - AddOrUpdate | 500 | 132.886 us | 2.3470 us | 2.0805 us | 1.00 | 0.00 | 54.4434 | - | - | 251.95 KB | +AddOrUpdate_v1 | 30 | 3.323 us | 0.0121 us | 0.0107 us | 0.91 | 0.00 | 2.0409 | - | - | 9.42 KB | +AddOrUpdate | 30 | 3.647 us | 0.0111 us | 0.0093 us | 1.00 | 0.00 | 2.0409 | - | - | 9.42 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 150 | 24.605 us | 0.2023 us | 0.1690 us | 0.92 | 0.02 | 13.5193 | - | - | 62.39 KB | +AddOrUpdate | 150 | 26.832 us | 0.5300 us | 0.5443 us | 1.00 | 0.00 | 13.5193 | - | - | 62.39 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 500 | 121.799 us | 0.9309 us | 0.8252 us | 0.92 | 0.01 | 54.4434 | - | - | 251.95 KB | +AddOrUpdate | 500 | 132.886 us | 2.3470 us | 2.0805 us | 1.00 | 0.00 | 54.4434 | - | - | 251.95 KB | ## Special fast logic for adding to empty branch. - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v1 | 30 | 3.369 us | 0.0102 us | 0.0090 us | 0.94 | 0.01 | 2.0409 | - | - | 9.42 KB | - AddOrUpdate | 30 | 3.587 us | 0.0244 us | 0.0228 us | 1.00 | 0.00 | 2.0409 | - | - | 9.42 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 150 | 25.025 us | 0.4927 us | 0.4609 us | 0.95 | 0.02 | 13.5193 | - | - | 62.39 KB | - AddOrUpdate | 150 | 26.235 us | 0.1058 us | 0.0989 us | 1.00 | 0.00 | 13.5193 | - | - | 62.39 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 500 | 126.106 us | 1.0204 us | 0.9545 us | 1.00 | 0.01 | 54.4434 | - | - | 251.95 KB | - AddOrUpdate | 500 | 126.844 us | 0.8788 us | 0.7339 us | 1.00 | 0.00 | 54.4434 | - | - | 251.95 KB | +AddOrUpdate_v1 | 30 | 3.369 us | 0.0102 us | 0.0090 us | 0.94 | 0.01 | 2.0409 | - | - | 9.42 KB | +AddOrUpdate | 30 | 3.587 us | 0.0244 us | 0.0228 us | 1.00 | 0.00 | 2.0409 | - | - | 9.42 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 150 | 25.025 us | 0.4927 us | 0.4609 us | 0.95 | 0.02 | 13.5193 | - | - | 62.39 KB | +AddOrUpdate | 150 | 26.235 us | 0.1058 us | 0.0989 us | 1.00 | 0.00 | 13.5193 | - | - | 62.39 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 500 | 126.106 us | 1.0204 us | 0.9545 us | 1.00 | 0.01 | 54.4434 | - | - | 251.95 KB | +AddOrUpdate | 500 | 126.844 us | 0.8788 us | 0.7339 us | 1.00 | 0.00 | 54.4434 | - | - | 251.95 KB | ## Removing not necessary imbalanced tree creation before balance - first memory win. - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate | 30 | 3.357 us | 0.0163 us | 0.0145 us | 1.00 | 1.9417 | - | - | 8.95 KB | - AddOrUpdate_v1 | 30 | 3.359 us | 0.0124 us | 0.0116 us | 1.00 | 2.0409 | - | - | 9.42 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 150 | 24.513 us | 0.0420 us | 0.0393 us | 0.98 | 13.5193 | - | - | 62.39 KB | - AddOrUpdate | 150 | 25.074 us | 0.1160 us | 0.1085 us | 1.00 | 12.9395 | 0.0305 | - | 59.72 KB | - | | | | | | | | | | - AddOrUpdate | 500 | 122.624 us | 0.6553 us | 0.6130 us | 1.00 | 52.4902 | 0.2441 | - | 242.06 KB | - AddOrUpdate_v1 | 500 | 122.656 us | 0.8018 us | 0.7500 us | 1.00 | 54.4434 | - | - | 251.95 KB | +AddOrUpdate | 30 | 3.357 us | 0.0163 us | 0.0145 us | 1.00 | 1.9417 | - | - | 8.95 KB | +AddOrUpdate_v1 | 30 | 3.359 us | 0.0124 us | 0.0116 us | 1.00 | 2.0409 | - | - | 9.42 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 150 | 24.513 us | 0.0420 us | 0.0393 us | 0.98 | 13.5193 | - | - | 62.39 KB | +AddOrUpdate | 150 | 25.074 us | 0.1160 us | 0.1085 us | 1.00 | 12.9395 | 0.0305 | - | 59.72 KB | + | | | | | | | | | | +AddOrUpdate | 500 | 122.624 us | 0.6553 us | 0.6130 us | 1.00 | 52.4902 | 0.2441 | - | 242.06 KB | +AddOrUpdate_v1 | 500 | 122.656 us | 0.8018 us | 0.7500 us | 1.00 | 54.4434 | - | - | 251.95 KB | ## More variety to benchmark input - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-------------:|-------------:|-------------:|------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate | 5 | 472.4 ns | 2.139 ns | 1.786 ns | 1.00 | 0.2537 | - | - | 1.17 KB | - AddOrUpdate_v1 | 5 | 488.1 ns | 2.258 ns | 2.112 ns | 1.03 | 0.2737 | - | - | 1.27 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 40 | 4,855.4 ns | 19.395 ns | 17.194 ns | 0.98 | 2.9678 | - | - | 13.69 KB | - AddOrUpdate | 40 | 4,974.1 ns | 17.098 ns | 15.157 ns | 1.00 | 2.8000 | - | - | 12.94 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 200 | 35,267.4 ns | 100.767 ns | 84.145 ns | 0.98 | 18.7378 | 0.0610 | - | 86.53 KB | - AddOrUpdate | 200 | 35,874.3 ns | 173.786 ns | 162.560 ns | 1.00 | 18.0054 | - | - | 83.02 KB | - | | | | | | | | | | - AddOrUpdate | 1000 | 302,900.4 ns | 1,116.252 ns | 1,044.143 ns | 1.00 | 115.7227 | 2.4414 | - | 535.08 KB | - AddOrUpdate_v1 | 1000 | 303,552.8 ns | 906.769 ns | 803.827 ns | 1.00 | 120.6055 | 3.4180 | - | 556.41 KB | +AddOrUpdate | 5 | 472.4 ns | 2.139 ns | 1.786 ns | 1.00 | 0.2537 | - | - | 1.17 KB | +AddOrUpdate_v1 | 5 | 488.1 ns | 2.258 ns | 2.112 ns | 1.03 | 0.2737 | - | - | 1.27 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 40 | 4,855.4 ns | 19.395 ns | 17.194 ns | 0.98 | 2.9678 | - | - | 13.69 KB | +AddOrUpdate | 40 | 4,974.1 ns | 17.098 ns | 15.157 ns | 1.00 | 2.8000 | - | - | 12.94 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 200 | 35,267.4 ns | 100.767 ns | 84.145 ns | 0.98 | 18.7378 | 0.0610 | - | 86.53 KB | +AddOrUpdate | 200 | 35,874.3 ns | 173.786 ns | 162.560 ns | 1.00 | 18.0054 | - | - | 83.02 KB | + | | | | | | | | | | +AddOrUpdate | 1000 | 302,900.4 ns | 1,116.252 ns | 1,044.143 ns | 1.00 | 115.7227 | 2.4414 | - | 535.08 KB | +AddOrUpdate_v1 | 1000 | 303,552.8 ns | 906.769 ns | 803.827 ns | 1.00 | 120.6055 | 3.4180 | - | 556.41 KB | ## Remove unnecessary temporary left leaf(y) branch creation before balancing - memory win - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-------------:|-------------:|-------------:|------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v1 | 5 | 491.4 ns | 2.121 ns | 1.984 ns | 1.00 | 0.2737 | - | - | 1.27 KB | - AddOrUpdate | 5 | 493.4 ns | 2.199 ns | 2.057 ns | 1.00 | 0.2537 | - | - | 1.17 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 40 | 4,871.0 ns | 26.907 ns | 23.852 ns | 0.98 | 2.9678 | - | - | 13.69 KB | - AddOrUpdate | 40 | 4,949.1 ns | 10.957 ns | 9.713 ns | 1.00 | 2.7542 | - | - | 12.7 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 200 | 35,540.5 ns | 271.146 ns | 240.364 ns | 0.95 | 18.7378 | 0.0610 | - | 86.53 KB | - AddOrUpdate | 200 | 37,344.9 ns | 127.081 ns | 118.871 ns | 1.00 | 17.6392 | - | - | 81.38 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 1000 | 308,033.2 ns | 1,643.015 ns | 1,536.877 ns | 0.98 | 120.6055 | 3.4180 | - | 556.41 KB | - AddOrUpdate | 1000 | 314,370.2 ns | 1,781.632 ns | 1,666.540 ns | 1.00 | 113.7695 | 0.4883 | - | 525.09 KB | +AddOrUpdate_v1 | 5 | 491.4 ns | 2.121 ns | 1.984 ns | 1.00 | 0.2737 | - | - | 1.27 KB | +AddOrUpdate | 5 | 493.4 ns | 2.199 ns | 2.057 ns | 1.00 | 0.2537 | - | - | 1.17 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 40 | 4,871.0 ns | 26.907 ns | 23.852 ns | 0.98 | 2.9678 | - | - | 13.69 KB | +AddOrUpdate | 40 | 4,949.1 ns | 10.957 ns | 9.713 ns | 1.00 | 2.7542 | - | - | 12.7 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 200 | 35,540.5 ns | 271.146 ns | 240.364 ns | 0.95 | 18.7378 | 0.0610 | - | 86.53 KB | +AddOrUpdate | 200 | 37,344.9 ns | 127.081 ns | 118.871 ns | 1.00 | 17.6392 | - | - | 81.38 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 1000 | 308,033.2 ns | 1,643.015 ns | 1,536.877 ns | 0.98 | 120.6055 | 3.4180 | - | 556.41 KB | +AddOrUpdate | 1000 | 314,370.2 ns | 1,781.632 ns | 1,666.540 ns | 1.00 | 113.7695 | 0.4883 | - | 525.09 KB | ## Remove unnecessary temporary right leaf(y) branch creation before balancing - memory win - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-------------:|-------------:|-------------:|------:|--------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v1 | 5 | 489.0 ns | 2.395 ns | 2.241 ns | 0.99 | 0.01 | 0.2737 | - | - | 1.27 KB | - AddOrUpdate | 5 | 496.0 ns | 3.869 ns | 3.619 ns | 1.00 | 0.00 | 0.2432 | - | - | 1.13 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 40 | 4,873.1 ns | 35.313 ns | 33.032 ns | 0.91 | 0.01 | 2.9678 | - | - | 13.69 KB | - AddOrUpdate | 40 | 5,346.9 ns | 18.590 ns | 16.480 ns | 1.00 | 0.00 | 2.6779 | - | - | 12.38 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 200 | 36,484.4 ns | 309.583 ns | 274.437 ns | 0.90 | 0.01 | 18.7378 | 0.0610 | - | 86.53 KB | - AddOrUpdate | 200 | 40,641.9 ns | 628.973 ns | 588.342 ns | 1.00 | 0.00 | 17.1509 | 0.0610 | - | 79.08 KB | - | | | | | | | | | | | - AddOrUpdate_v1 | 1000 | 316,678.8 ns | 5,722.780 ns | 5,353.092 ns | 0.98 | 0.02 | 120.6055 | 3.4180 | - | 556.41 KB | - AddOrUpdate | 1000 | 323,403.6 ns | 1,483.940 ns | 1,315.474 ns | 1.00 | 0.00 | 111.8164 | 32.7148 | - | 515.39 KB | +AddOrUpdate_v1 | 5 | 489.0 ns | 2.395 ns | 2.241 ns | 0.99 | 0.01 | 0.2737 | - | - | 1.27 KB | +AddOrUpdate | 5 | 496.0 ns | 3.869 ns | 3.619 ns | 1.00 | 0.00 | 0.2432 | - | - | 1.13 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 40 | 4,873.1 ns | 35.313 ns | 33.032 ns | 0.91 | 0.01 | 2.9678 | - | - | 13.69 KB | +AddOrUpdate | 40 | 5,346.9 ns | 18.590 ns | 16.480 ns | 1.00 | 0.00 | 2.6779 | - | - | 12.38 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 200 | 36,484.4 ns | 309.583 ns | 274.437 ns | 0.90 | 0.01 | 18.7378 | 0.0610 | - | 86.53 KB | +AddOrUpdate | 200 | 40,641.9 ns | 628.973 ns | 588.342 ns | 1.00 | 0.00 | 17.1509 | 0.0610 | - | 79.08 KB | + | | | | | | | | | | | +AddOrUpdate_v1 | 1000 | 316,678.8 ns | 5,722.780 ns | 5,353.092 ns | 0.98 | 0.02 | 120.6055 | 3.4180 | - | 556.41 KB | +AddOrUpdate | 1000 | 323,403.6 ns | 1,483.940 ns | 1,315.474 ns | 1.00 | 0.00 | 111.8164 | 32.7148 | - | 515.39 KB | ## Parity with v1 via handling one more special case - where `Height == 1` - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-------------:|-------------:|-------------:|------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate | 5 | 480.3 ns | 3.645 ns | 3.410 ns | 1.00 | 0.2432 | - | - | 1.13 KB | - AddOrUpdate_v1 | 5 | 487.1 ns | 2.547 ns | 2.382 ns | 1.01 | 0.2737 | - | - | 1.27 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 40 | 4,844.9 ns | 23.494 ns | 21.976 ns | 0.99 | 2.9678 | - | - | 13.69 KB | - AddOrUpdate | 40 | 4,908.8 ns | 29.374 ns | 27.477 ns | 1.00 | 2.6779 | - | - | 12.38 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 200 | 35,727.8 ns | 84.717 ns | 75.099 ns | 0.99 | 18.7378 | 0.0610 | - | 86.53 KB | - AddOrUpdate | 200 | 36,231.4 ns | 136.671 ns | 121.156 ns | 1.00 | 17.1509 | 0.0610 | - | 79.08 KB | - | | | | | | | | | | - AddOrUpdate | 1000 | 304,238.0 ns | 1,636.331 ns | 1,366.410 ns | 1.00 | 111.8164 | 32.7148 | - | 515.39 KB | - AddOrUpdate_v1 | 1000 | 307,233.3 ns | 1,487.373 ns | 1,391.290 ns | 1.01 | 120.6055 | 3.4180 | - | 556.41 KB | +AddOrUpdate | 5 | 480.3 ns | 3.645 ns | 3.410 ns | 1.00 | 0.2432 | - | - | 1.13 KB | +AddOrUpdate_v1 | 5 | 487.1 ns | 2.547 ns | 2.382 ns | 1.01 | 0.2737 | - | - | 1.27 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 40 | 4,844.9 ns | 23.494 ns | 21.976 ns | 0.99 | 2.9678 | - | - | 13.69 KB | +AddOrUpdate | 40 | 4,908.8 ns | 29.374 ns | 27.477 ns | 1.00 | 2.6779 | - | - | 12.38 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 200 | 35,727.8 ns | 84.717 ns | 75.099 ns | 0.99 | 18.7378 | 0.0610 | - | 86.53 KB | +AddOrUpdate | 200 | 36,231.4 ns | 136.671 ns | 121.156 ns | 1.00 | 17.1509 | 0.0610 | - | 79.08 KB | + | | | | | | | | | | +AddOrUpdate | 1000 | 304,238.0 ns | 1,636.331 ns | 1,366.410 ns | 1.00 | 111.8164 | 32.7148 | - | 515.39 KB | +AddOrUpdate_v1 | 1000 | 307,233.3 ns | 1,487.373 ns | 1,391.290 ns | 1.01 | 120.6055 | 3.4180 | - | 556.41 KB | ## Some fixes for updating things - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-------------:|-------------:|-------------:|------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate | 5 | 469.8 ns | 4.613 ns | 3.602 ns | 1.00 | 0.2432 | - | - | 1.13 KB | - AddOrUpdate_v1 | 5 | 480.6 ns | 2.648 ns | 2.477 ns | 1.02 | 0.2737 | - | - | 1.27 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 40 | 4,731.4 ns | 22.512 ns | 21.058 ns | 0.96 | 2.9678 | - | - | 13.69 KB | - AddOrUpdate | 40 | 4,945.0 ns | 12.489 ns | 10.429 ns | 1.00 | 2.6779 | - | - | 12.38 KB | - | | | | | | | | | | - AddOrUpdate_v1 | 200 | 35,094.6 ns | 70.945 ns | 66.362 ns | 0.98 | 18.7378 | 0.0610 | - | 86.53 KB | - AddOrUpdate | 200 | 35,821.9 ns | 129.417 ns | 121.057 ns | 1.00 | 17.1509 | 0.0610 | - | 79.08 KB | - | | | | | | | | | | - AddOrUpdate | 1000 | 303,448.5 ns | 2,017.835 ns | 1,887.484 ns | 1.00 | 111.8164 | 32.7148 | - | 515.39 KB | - AddOrUpdate_v1 | 1000 | 304,471.0 ns | 1,549.817 ns | 1,449.700 ns | 1.00 | 120.6055 | 3.4180 | - | 556.41 KB | +AddOrUpdate | 5 | 469.8 ns | 4.613 ns | 3.602 ns | 1.00 | 0.2432 | - | - | 1.13 KB | +AddOrUpdate_v1 | 5 | 480.6 ns | 2.648 ns | 2.477 ns | 1.02 | 0.2737 | - | - | 1.27 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 40 | 4,731.4 ns | 22.512 ns | 21.058 ns | 0.96 | 2.9678 | - | - | 13.69 KB | +AddOrUpdate | 40 | 4,945.0 ns | 12.489 ns | 10.429 ns | 1.00 | 2.6779 | - | - | 12.38 KB | + | | | | | | | | | | +AddOrUpdate_v1 | 200 | 35,094.6 ns | 70.945 ns | 66.362 ns | 0.98 | 18.7378 | 0.0610 | - | 86.53 KB | +AddOrUpdate | 200 | 35,821.9 ns | 129.417 ns | 121.057 ns | 1.00 | 17.1509 | 0.0610 | - | 79.08 KB | + | | | | | | | | | | +AddOrUpdate | 1000 | 303,448.5 ns | 2,017.835 ns | 1,887.484 ns | 1.00 | 111.8164 | 32.7148 | - | 515.39 KB | +AddOrUpdate_v1 | 1000 | 304,471.0 ns | 1,549.817 ns | 1,449.700 ns | 1.00 | 120.6055 | 3.4180 | - | 556.41 KB | ## Inlining the Balance and removing its not used if-branches - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-------------:|-------------:|-------------:|------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v2 | 5 | 418.2 ns | 2.540 ns | 2.376 ns | 0.90 | 0.2170 | - | - | 1024 B | - AddOrUpdate | 5 | 464.6 ns | 2.301 ns | 2.039 ns | 1.00 | 0.2437 | - | - | 1152 B | - AddOrUpdate_v1 | 5 | 487.7 ns | 1.769 ns | 1.655 ns | 1.05 | 0.2737 | - | - | 1296 B | - AddOrUpdate_v3 | 5 | 516.5 ns | 2.289 ns | 2.141 ns | 1.11 | 0.1993 | - | - | 944 B | - | | | | | | | | | | - AddOrUpdate | 40 | 4,621.6 ns | 19.899 ns | 18.614 ns | 1.00 | 2.6779 | - | - | 12672 B | - AddOrUpdate_v1 | 40 | 4,910.9 ns | 19.805 ns | 18.525 ns | 1.06 | 2.9678 | - | - | 14016 B | - AddOrUpdate_v2 | 40 | 5,115.0 ns | 26.410 ns | 23.411 ns | 1.11 | 2.8458 | - | - | 13456 B | - AddOrUpdate_v3 | 40 | 6,953.3 ns | 24.391 ns | 22.815 ns | 1.50 | 2.6016 | - | - | 12304 B | - | | | | | | | | | | - AddOrUpdate | 200 | 34,250.7 ns | 87.221 ns | 81.586 ns | 1.00 | 17.1509 | 0.0610 | - | 80976 B | - AddOrUpdate_v1 | 200 | 35,425.8 ns | 137.918 ns | 129.008 ns | 1.03 | 18.7378 | 0.0610 | - | 88608 B | - AddOrUpdate_v2 | 200 | 37,942.8 ns | 188.112 ns | 175.960 ns | 1.11 | 19.2261 | - | - | 91008 B | - AddOrUpdate_v3 | 200 | 50,640.8 ns | 66.485 ns | 55.518 ns | 1.48 | 17.6392 | 0.0610 | - | 83352 B | - | | | | | | | | | | - AddOrUpdate | 1000 | 290,830.8 ns | 915.770 ns | 811.806 ns | 1.00 | 111.8164 | 32.7148 | - | 527760 B | - AddOrUpdate_v2 | 1000 | 308,260.3 ns | 2,278.822 ns | 2,020.116 ns | 1.06 | 130.8594 | 0.9766 | - | 619056 B | - AddOrUpdate_v1 | 1000 | 308,355.6 ns | 2,046.461 ns | 1,914.261 ns | 1.06 | 120.6055 | 3.4180 | - | 569760 B | - AddOrUpdate_v3 | 1000 | 375,880.3 ns | 1,710.716 ns | 1,600.205 ns | 1.29 | 118.6523 | 0.4883 | - | 560440 B | - - - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | +AddOrUpdate_v2 | 5 | 418.2 ns | 2.540 ns | 2.376 ns | 0.90 | 0.2170 | - | - | 1024 B | +AddOrUpdate | 5 | 464.6 ns | 2.301 ns | 2.039 ns | 1.00 | 0.2437 | - | - | 1152 B | +AddOrUpdate_v1 | 5 | 487.7 ns | 1.769 ns | 1.655 ns | 1.05 | 0.2737 | - | - | 1296 B | +AddOrUpdate_v3 | 5 | 516.5 ns | 2.289 ns | 2.141 ns | 1.11 | 0.1993 | - | - | 944 B | + | | | | | | | | | | +AddOrUpdate | 40 | 4,621.6 ns | 19.899 ns | 18.614 ns | 1.00 | 2.6779 | - | - | 12672 B | +AddOrUpdate_v1 | 40 | 4,910.9 ns | 19.805 ns | 18.525 ns | 1.06 | 2.9678 | - | - | 14016 B | +AddOrUpdate_v2 | 40 | 5,115.0 ns | 26.410 ns | 23.411 ns | 1.11 | 2.8458 | - | - | 13456 B | +AddOrUpdate_v3 | 40 | 6,953.3 ns | 24.391 ns | 22.815 ns | 1.50 | 2.6016 | - | - | 12304 B | + | | | | | | | | | | +AddOrUpdate | 200 | 34,250.7 ns | 87.221 ns | 81.586 ns | 1.00 | 17.1509 | 0.0610 | - | 80976 B | +AddOrUpdate_v1 | 200 | 35,425.8 ns | 137.918 ns | 129.008 ns | 1.03 | 18.7378 | 0.0610 | - | 88608 B | +AddOrUpdate_v2 | 200 | 37,942.8 ns | 188.112 ns | 175.960 ns | 1.11 | 19.2261 | - | - | 91008 B | +AddOrUpdate_v3 | 200 | 50,640.8 ns | 66.485 ns | 55.518 ns | 1.48 | 17.6392 | 0.0610 | - | 83352 B | + | | | | | | | | | | +AddOrUpdate | 1000 | 290,830.8 ns | 915.770 ns | 811.806 ns | 1.00 | 111.8164 | 32.7148 | - | 527760 B | +AddOrUpdate_v2 | 1000 | 308,260.3 ns | 2,278.822 ns | 2,020.116 ns | 1.06 | 130.8594 | 0.9766 | - | 619056 B | +AddOrUpdate_v1 | 1000 | 308,355.6 ns | 2,046.461 ns | 1,914.261 ns | 1.06 | 120.6055 | 3.4180 | - | 569760 B | +AddOrUpdate_v3 | 1000 | 375,880.3 ns | 1,710.716 ns | 1,600.205 ns | 1.29 | 118.6523 | 0.4883 | - | 560440 B | + + + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | --------------- |------ |-------------:|-------------:|--------------:|------:|--------:|------------:|------------:|------------:|--------------------:| - AddOrUpdate_v2 | 5 | 404.8 ns | 2.504 ns | 2.0907 ns | 0.91 | 0.01 | 0.2170 | - | - | 1 KB | - AddOrUpdate | 5 | 447.1 ns | 4.673 ns | 4.1429 ns | 1.00 | 0.00 | 0.2437 | - | - | 1.13 KB | - AddOrUpdate_v1 | 5 | 481.5 ns | 1.112 ns | 0.9854 ns | 1.08 | 0.01 | 0.2737 | - | - | 1.27 KB | - | | | | | | | | | | | - AddOrUpdate | 40 | 4,563.0 ns | 91.302 ns | 89.6712 ns | 1.00 | 0.00 | 2.6779 | - | - | 12.38 KB | - AddOrUpdate_v1 | 40 | 4,712.5 ns | 27.017 ns | 25.2716 ns | 1.03 | 0.02 | 2.9678 | - | - | 13.69 KB | - AddOrUpdate_v2 | 40 | 4,943.3 ns | 22.715 ns | 21.2474 ns | 1.08 | 0.02 | 2.8458 | - | - | 13.14 KB | - | | | | | | | | | | | - AddOrUpdate | 200 | 33,629.4 ns | 647.198 ns | 635.6353 ns | 1.00 | 0.00 | 17.1509 | 0.0610 | - | 79.08 KB | - AddOrUpdate_v1 | 200 | 34,643.5 ns | 155.549 ns | 145.5005 ns | 1.03 | 0.02 | 18.7378 | 0.0610 | - | 86.53 KB | - AddOrUpdate_v2 | 200 | 36,726.4 ns | 419.812 ns | 372.1521 ns | 1.10 | 0.01 | 19.2261 | - | - | 88.88 KB | - | | | | | | | | | | | - AddOrUpdate | 1000 | 291,376.3 ns | 3,419.191 ns | 3,198.3136 ns | 1.00 | 0.00 | 111.8164 | 32.7148 | - | 515.39 KB | - AddOrUpdate_v2 | 1000 | 302,027.2 ns | 4,981.373 ns | 4,659.5798 ns | 1.04 | 0.02 | 130.8594 | 0.9766 | - | 604.55 KB | - AddOrUpdate_v1 | 1000 | 304,899.6 ns | 4,634.673 ns | 4,335.2763 ns | 1.05 | 0.02 | 120.6055 | 3.4180 | - | 556.41 KB | +AddOrUpdate_v2 | 5 | 404.8 ns | 2.504 ns | 2.0907 ns | 0.91 | 0.01 | 0.2170 | - | - | 1 KB | +AddOrUpdate | 5 | 447.1 ns | 4.673 ns | 4.1429 ns | 1.00 | 0.00 | 0.2437 | - | - | 1.13 KB | +AddOrUpdate_v1 | 5 | 481.5 ns | 1.112 ns | 0.9854 ns | 1.08 | 0.01 | 0.2737 | - | - | 1.27 KB | + | | | | | | | | | | | +AddOrUpdate | 40 | 4,563.0 ns | 91.302 ns | 89.6712 ns | 1.00 | 0.00 | 2.6779 | - | - | 12.38 KB | +AddOrUpdate_v1 | 40 | 4,712.5 ns | 27.017 ns | 25.2716 ns | 1.03 | 0.02 | 2.9678 | - | - | 13.69 KB | +AddOrUpdate_v2 | 40 | 4,943.3 ns | 22.715 ns | 21.2474 ns | 1.08 | 0.02 | 2.8458 | - | - | 13.14 KB | + | | | | | | | | | | | +AddOrUpdate | 200 | 33,629.4 ns | 647.198 ns | 635.6353 ns | 1.00 | 0.00 | 17.1509 | 0.0610 | - | 79.08 KB | +AddOrUpdate_v1 | 200 | 34,643.5 ns | 155.549 ns | 145.5005 ns | 1.03 | 0.02 | 18.7378 | 0.0610 | - | 86.53 KB | +AddOrUpdate_v2 | 200 | 36,726.4 ns | 419.812 ns | 372.1521 ns | 1.10 | 0.01 | 19.2261 | - | - | 88.88 KB | + | | | | | | | | | | | +AddOrUpdate | 1000 | 291,376.3 ns | 3,419.191 ns | 3,198.3136 ns | 1.00 | 0.00 | 111.8164 | 32.7148 | - | 515.39 KB | +AddOrUpdate_v2 | 1000 | 302,027.2 ns | 4,981.373 ns | 4,659.5798 ns | 1.04 | 0.02 | 130.8594 | 0.9766 | - | 604.55 KB | +AddOrUpdate_v1 | 1000 | 304,899.6 ns | 4,634.673 ns | 4,335.2763 ns | 1.05 | 0.02 | 120.6055 | 3.4180 | - | 556.41 KB | ## Some base line results @@ -304,8 +304,8 @@ public class Populate BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores .NET Core SDK=3.1.100 - [Host] : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT - DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT +[Host] : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT +DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | @@ -363,8 +363,8 @@ public class Populate BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.572 (2004/?/20H1) Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores .NET Core SDK=3.1.403 - [Host] : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT - DefaultJob : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT +[Host] : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT +DefaultJob : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | |----------------------------------- |------ |-------------:|------------:|------------:|------:|--------:|---------:|--------:|------:|----------:| @@ -389,8 +389,8 @@ public class Populate BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores .NET Core SDK=5.0.201 - [Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT - DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT +[Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT +DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | @@ -436,8 +436,8 @@ public class Populate BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores .NET Core SDK=5.0.201 - [Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT - DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT +[Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT +DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | |---------------------------------- |------ |---------:|---------:|---------:|------:|--------:|-------:|-------:|------:|----------:| @@ -469,8 +469,8 @@ public class Populate BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores .NET Core SDK=6.0.202 - [Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT - DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT +[Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT +DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | @@ -518,8 +518,8 @@ public class Populate Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores .NET Core SDK=6.0.202 - [Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT - DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT +[Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT +DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | |---------------------------------- |------ |--------------:|-------------:|-------------:|------:|--------:|---------:|--------:|------:|----------:| @@ -590,8 +590,8 @@ public class Populate BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19042.928/20H2/October2020Update) Intel Core i5-8350U CPU 1.70GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores .NET SDK=7.0.100 - [Host] : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 +[Host] : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 +DefaultJob : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | @@ -641,8 +641,8 @@ public class Populate BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19042.928/20H2/October2020Update) Intel Core i5-8350U CPU 1.70GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores .NET SDK=7.0.100 - [Host] : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 +[Host] : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 +DefaultJob : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 ``` | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | @@ -797,8 +797,8 @@ public class Populate BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1702/22H2/2022Update/SunValley2) 11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores .NET SDK=7.0.304 - [Host] : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 +[Host] : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 +DefaultJob : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Gen0 | Gen1 | Allocated | Alloc Ratio | |----------------------- |------ |---------:|----------:|----------:|------:|--------:|----------------------:|---------------:|------------------------:|-------:|-------:|----------:|------------:| @@ -982,2713 +982,2727 @@ public class Populate | FHashMap91_GetOrAddValueRef | 100 | 2.399 us | 0.1387 us | 0.3630 us | 2.264 us | 0.72 | 0.16 | 0.8469 | 4,366 | 26 | 8 | 0.0076 | 5.22 KB | 0.71 | */ - // [Params(1, 10, 100, 1000)] - [Params(10)]//, 1000)] - // [Params(1000)] - public int Count; + // [Params(1, 10, 100, 1000)] + [Params(10)]//, 1000)] + // [Params(1000)] + public int Count; - private Type[] _types; + private Type[] _types; - [GlobalSetup] - public void Setup() - { - _types = _keys.Take(Count).ToArray(); - } + [GlobalSetup] + public void Setup() + { + _types = _keys.Take(Count).ToArray(); + } - // [Benchmark(Baseline = true)] - // [Benchmark] - public ImTools.ImHashMap V4_ImHashMap_AddOrUpdate() - { - var map = ImTools.ImHashMap.Empty; + // [Benchmark(Baseline = true)] + // [Benchmark] + public ImTools.ImHashMap V4_ImHashMap_AddOrUpdate() + { + var map = ImTools.ImHashMap.Empty; - foreach (var key in _types) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + foreach (var key in _types) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - } + return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + } - // [Benchmark] - public ImTools.ImHashMap V4_ImMap_Add() - { - var map = ImTools.ImHashMap.Empty; + // [Benchmark] + public ImTools.ImHashMap V4_ImMap_Add() + { + var map = ImTools.ImHashMap.Empty; - foreach (var key in _types) - map = map.AddSureNotPresent(key.GetHashCode(), key, "a"); + foreach (var key in _types) + map = map.AddSureNotPresent(key.GetHashCode(), key, "a"); - return map.AddSureNotPresent(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - } + return map.AddSureNotPresent(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + } - // [Benchmark] - public ImTools.ImHashMap> V3_ImMap_AddOrUpdate() - { - var map = ImTools.ImHashMap>.Empty; + // [Benchmark] + public ImTools.ImHashMap> V3_ImMap_AddOrUpdate() + { + var map = ImTools.ImHashMap>.Empty; - foreach (var key in _types) - map = map.AddOrUpdate(key.GetHashCode(), new KeyValuePair(key, "a")); + foreach (var key in _types) + map = map.AddOrUpdate(key.GetHashCode(), new KeyValuePair(key, "a")); - return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new KeyValuePair(typeof(ImHashMapBenchmarks), "!")); - } + return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new KeyValuePair(typeof(ImHashMapBenchmarks), "!")); + } - // [Benchmark] - public ImTools.ImHashMap[] V4_PartitionedHashMap_AddOrUpdate() - { - var map = ImTools.PartitionedHashMap.CreateEmpty(); + // [Benchmark] + public ImTools.ImHashMap[] V4_PartitionedHashMap_AddOrUpdate() + { + var map = ImTools.PartitionedHashMap.CreateEmpty(); - foreach (var key in _types) - map.AddOrUpdate(key, "a"); + foreach (var key in _types) + map.AddOrUpdate(key, "a"); - map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; + } - // // [Benchmark] - // public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() - // { - // var map = ImToolsV3.ImHashMap.Empty; + // // [Benchmark] + // public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() + // { + // var map = ImToolsV3.ImHashMap.Empty; - // foreach (var key in _types) - // map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + // foreach (var key in _types) + // map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - // return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - // } + // return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + // } - // // [Benchmark] - // public ImToolsV3.ImHashMap[] V3_PartitionedHashMap_AddOrUpdate() - // { - // var map = ImToolsV3.PartitionedHashMap.CreateEmpty(); + // // [Benchmark] + // public ImToolsV3.ImHashMap[] V3_PartitionedHashMap_AddOrUpdate() + // { + // var map = ImToolsV3.PartitionedHashMap.CreateEmpty(); - // foreach (var key in _types) - // map.AddOrUpdate(key, "a"); + // foreach (var key in _types) + // map.AddOrUpdate(key, "a"); - // map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - // return map; - // } + // map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + // return map; + // } - // // [Benchmark] - // public ImTools.V2.ImHashMap V2_ImHashMap_AddOrUpdate() - // { - // var map = ImTools.V2.ImHashMap.Empty; + // // [Benchmark] + // public ImTools.V2.ImHashMap V2_ImHashMap_AddOrUpdate() + // { + // var map = ImTools.V2.ImHashMap.Empty; - // foreach (var key in _types) - // map = map.AddOrUpdate(key, "a"); + // foreach (var key in _types) + // map = map.AddOrUpdate(key, "a"); - // return map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - // } + // return map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + // } - // // [Benchmark] - // public ImTools.V2.ImHashMap[] ImHashMapSlots32_AddOrUpdate() - // { - // var map = ImTools.V2.ImHashMapSlots.CreateWithEmpty(); + // // [Benchmark] + // public ImTools.V2.ImHashMap[] ImHashMapSlots32_AddOrUpdate() + // { + // var map = ImTools.V2.ImHashMapSlots.CreateWithEmpty(); - // foreach (var key in _types) - // map.AddOrUpdate(key, "a"); + // foreach (var key in _types) + // map.AddOrUpdate(key, "a"); - // map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - // return map; - // } + // map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + // return map; + // } - // // [Benchmark] - // public ImTools.V2.Experimental.ImMap> V2_ImHashMap_AVLOptimizedForAdd_AddOrUpdate() - // { - // var map = ImTools.V2.Experimental.ImMap>.Empty; + // // [Benchmark] + // public ImTools.V2.Experimental.ImMap> V2_ImHashMap_AVLOptimizedForAdd_AddOrUpdate() + // { + // var map = ImTools.V2.Experimental.ImMap>.Empty; - // foreach (var key in _types) - // map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + // foreach (var key in _types) + // map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - // return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - // } + // return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + // } - // // [Benchmark] - // public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots32_AddOrUpdate() - // { - // var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(); + // // [Benchmark] + // public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots32_AddOrUpdate() + // { + // var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(); - // foreach (var key in _types) - // map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a")); + // foreach (var key in _types) + // map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a")); - // map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!")); - // return map; - // } + // map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!")); + // return map; + // } - // // [Benchmark] - // public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots64_AddOrUpdate() - // { - // var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(64); + // // [Benchmark] + // public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots64_AddOrUpdate() + // { + // var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(64); - // foreach (var key in _types) - // map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a"), 63); + // foreach (var key in _types) + // map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a"), 63); - // map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!"), 63); - // return map; - // } + // map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!"), 63); + // return map; + // } - // [Benchmark(Baseline = true)] - public ImTools.Experiments.RefEqHashMap RefEqHashMap_AddOrUpdate() - { - var map = new ImTools.Experiments.RefEqHashMap(5); + // [Benchmark(Baseline = true)] + public ImTools.Experiments.RefEqHashMap RefEqHashMap_AddOrUpdate() + { + var map = new ImTools.Experiments.RefEqHashMap(5); - foreach (var key in _types) - map.AddOrUpdate(key, "a"); + foreach (var key in _types) + map.AddOrUpdate(key, "a"); - map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; + } - // [Benchmark] - public ImTools.Experiments.HashMapLeapfrog HashMapLeapfrog_AddOrUpdate() - { - var map = new ImTools.Experiments.HashMapLeapfrog(); + // [Benchmark] + public ImTools.Experiments.HashMapLeapfrog HashMapLeapfrog_AddOrUpdate() + { + var map = new ImTools.Experiments.HashMapLeapfrog(); - foreach (var key in _types) - map.AddOrUpdate(key, "a"); + foreach (var key in _types) + map.AddOrUpdate(key, "a"); - map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; + } - [Benchmark(Baseline = true)] - public DictionarySlim DictSlim_TryAdd() - { - var map = new DictionarySlim(); + [Benchmark(Baseline = true)] + public DictionarySlim DictSlim_TryAdd() + { + var map = new DictionarySlim(); - foreach (var key in _types) - map.GetOrAddValueRef(key) = "a"; + foreach (var key in _types) + map.GetOrAddValueRef(key) = "a"; - map.GetOrAddValueRef(typeof(ImHashMapBenchmarks)) = "!"; - return map; - } + map.GetOrAddValueRef(typeof(ImHashMapBenchmarks)) = "!"; + return map; + } - // [Benchmark] - public ImTools.Experiments.FHashMap7> FHashMap7_AddOrUpdate() - { - var map = new ImTools.Experiments.FHashMap7>(); + // [Benchmark] + public ImTools.Experiments.FHashMap7> FHashMap7_AddOrUpdate() + { + var map = new ImTools.Experiments.FHashMap7>(); - foreach (var key in _types) - map.AddOrUpdate(key, "a"); + foreach (var key in _types) + map.AddOrUpdate(key, "a"); - map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; + } - // [Benchmark(Baseline = true)] - [Benchmark] - public FHashMap91TypeString FHashMap91_GetOrAddValueRef() - { - var map = new FHashMap91TypeString(); + // [Benchmark(Baseline = true)] + [Benchmark] + public FHashMap91TypeString FHashMap91_GetOrAddValueRef() + { + var map = new FHashMap91TypeString(); - foreach (var key in _types) - map.GetOrAddValueRef(key) = "a"; + foreach (var key in _types) + map.GetOrAddValueRef(key) = "a"; - map.GetOrAddValueRef(typeof(ImHashMapBenchmarks)) = "!"; - return map; - } + map.GetOrAddValueRef(typeof(ImHashMapBenchmarks)) = "!"; + return map; + } - // [Benchmark] - public ImTools.Experiments.FHashMap8> FHashMap8_AddOrUpdate() - { - var map = new ImTools.Experiments.FHashMap8>(); + // [Benchmark] + public ImTools.Experiments.FHashMap8> FHashMap8_AddOrUpdate() + { + var map = new ImTools.Experiments.FHashMap8>(); - foreach (var key in _types) - map.AddOrUpdate(key, "a"); + foreach (var key in _types) + map.AddOrUpdate(key, "a"); - map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; + } - // [Benchmark] - public Dictionary Dict_TryAdd() - { - var map = new Dictionary(); + // [Benchmark] + public Dictionary Dict_TryAdd() + { + var map = new Dictionary(); - foreach (var key in _types) - map.TryAdd(key, "a"); + foreach (var key in _types) + map.TryAdd(key, "a"); - map.TryAdd(typeof(ImHashMapBenchmarks), "!"); - return map; - } + map.TryAdd(typeof(ImHashMapBenchmarks), "!"); + return map; + } - // [Benchmark] - public ConcurrentDictionary ConcurrentDictionary_TryAdd() - { - var map = new ConcurrentDictionary(); + // [Benchmark] + public ConcurrentDictionary ConcurrentDictionary_TryAdd() + { + var map = new ConcurrentDictionary(); - foreach (var key in _types) - map.TryAdd(key, "a"); + foreach (var key in _types) + map.TryAdd(key, "a"); - map.TryAdd(typeof(ImHashMapBenchmarks), "!"); - return map; - } + map.TryAdd(typeof(ImHashMapBenchmarks), "!"); + return map; + } - // [Benchmark] - public ImmutableDictionary ImmutableDict_Builder_Add() - { - var builder = ImmutableDictionary.CreateBuilder(); + // [Benchmark] + public ImmutableDictionary ImmutableDict_Builder_Add() + { + var builder = ImmutableDictionary.CreateBuilder(); - foreach (var key in _types) - builder.Add(key, "a"); - builder.Add(typeof(ImHashMapBenchmarks), "!"); - return builder.ToImmutable(); - } + foreach (var key in _types) + builder.Add(key, "a"); + builder.Add(typeof(ImHashMapBenchmarks), "!"); + return builder.ToImmutable(); + } - // [Benchmark] - public ImmutableDictionary ImmutableDict_Add() - { - var map = ImmutableDictionary.Empty; + // [Benchmark] + public ImmutableDictionary ImmutableDict_Add() + { + var map = ImmutableDictionary.Empty; - foreach (var key in _types) - map = map.Add(key, "a"); + foreach (var key in _types) + map = map.Add(key, "a"); - return map.Add(typeof(ImHashMapBenchmarks), "!"); - } + return map.Add(typeof(ImHashMapBenchmarks), "!"); + } + + // [Benchmark] + public TypeDictionary TypeDictionary_Add() + { + var map = new TypeDictionary(); + + map.Add("a"); + map.Add("a"); + map.Add("a"); + map.Add("a"); + map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + + map.Add("!"); + + return map; + } + } + + class A1 { } + class A2 { } + class A3 { } + class A4 { } + class A5 { } + class A6 { } + class A7 { } + class A8 { } + class A9 { } + class A10 { } + class B1 { } + class B2 { } + class B3 { } + class B4 { } + class B5 { } + class B6 { } + class B7 { } + class B8 { } + class B9 { } + class B10 { } + + public class TypeDictionary + { + // ReSharper disable once StaticMemberInGenericType + private static int typeIndex; + private readonly object _lockObject = new object(); - // [Benchmark] - public TypeDictionary TypeDictionary_Add() + private TValue[] _values = new TValue[100]; + + public void Add(TValue value) + { + lock (_lockObject) { - var map = new TypeDictionary(); - - map.Add("a"); - map.Add("a"); - map.Add("a"); - map.Add("a"); - map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - - map.Add("!"); - - return map; + var id = TypeKey.Id; + if (id >= _values.Length) + Array.Resize(ref _values, id * 2); + _values[id] = value; } } - class A1 { } - class A2 { } - class A3 { } - class A4 { } - class A5 { } - class A6 { } - class A7 { } - class A8 { } - class A9 { } - class A10 { } - class B1 { } - class B2 { } - class B3 { } - class B4 { } - class B5 { } - class B6 { } - class B7 { } - class B8 { } - class B9 { } - class B10 { } - - public class TypeDictionary + public TValue Get() + { + var id = TypeKey.Id; + return id >= _values.Length ? default : _values[id]; + } + + // ReSharper disable once UnusedTypeParameter + private static class TypeKey { // ReSharper disable once StaticMemberInGenericType - private static int typeIndex; - private readonly object _lockObject = new object(); + internal static readonly int Id = Interlocked.Increment(ref typeIndex); + } + } - private TValue[] _values = new TValue[100]; + [MemoryDiagnoser, RankColumn, Orderer(SummaryOrderPolicy.FastestToSlowest)] + // [HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchMispredictions, HardwareCounter.BranchInstructions)] + public class Lookup + { + /* + ## 21.01.2019: All versions. + + Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |---------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault_v1 | 13.74 ns | 0.0686 ns | 0.0642 ns | 0.79 | - | - | - | - | + GetValueOrDefault | 17.43 ns | 0.0924 ns | 0.0864 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v2 | 19.15 ns | 0.0786 ns | 0.0656 ns | 1.10 | - | - | - | - | + GetValueOrDefault_v3 | 25.73 ns | 0.0711 ns | 0.0665 ns | 1.48 | - | - | - | - | + + ## For some reason dropping lookup speed with only changes to AddOrUpdate + + Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |---------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault_v1 | 13.89 ns | 0.0938 ns | 0.0877 ns | 0.80 | - | - | - | - | + GetValueOrDefault | 17.40 ns | 0.0888 ns | 0.0831 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v2 | 19.04 ns | 0.0712 ns | 0.0666 ns | 1.09 | - | - | - | - | + GetValueOrDefault_v3 | 25.93 ns | 0.0474 ns | 0.0420 ns | 1.49 | - | - | - | - | + + ## Got back some perf by moving GetValueOrDefault to static method and specializing for Type + + Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |---------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault_v1 | 13.87 ns | 0.0400 ns | 0.0355 ns | 0.85 | - | - | - | - | + GetValueOrDefault | 16.34 ns | 0.0932 ns | 0.0826 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v2 | 19.18 ns | 0.0460 ns | 0.0430 ns | 1.17 | - | - | - | - | + GetValueOrDefault_v3 | 25.96 ns | 0.0756 ns | 0.0707 ns | 1.59 | - | - | - | - | + + ## Benchmark against variety of inputs on par with Populate benchmark + + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault_v1 | 5 | 6.155 ns | 0.0321 ns | 0.0301 ns | 0.98 | 0.01 | - | - | - | - | + GetValueOrDefault | 5 | 6.267 ns | 0.0510 ns | 0.0452 ns | 1.00 | 0.00 | - | - | - | - | + GetValueOrDefault_v2 | 5 | 7.439 ns | 0.0763 ns | 0.0676 ns | 1.19 | 0.02 | - | - | - | - | + GetValueOrDefault_v3 | 5 | 9.558 ns | 0.0409 ns | 0.0383 ns | 1.52 | 0.01 | - | - | - | - | + | | | | | | | | | | | + GetValueOrDefault_v1 | 40 | 10.897 ns | 0.0673 ns | 0.0629 ns | 0.95 | 0.01 | - | - | - | - | + GetValueOrDefault | 40 | 11.467 ns | 0.0325 ns | 0.0304 ns | 1.00 | 0.00 | - | - | - | - | + GetValueOrDefault_v2 | 40 | 14.012 ns | 0.1092 ns | 0.1022 ns | 1.22 | 0.01 | - | - | - | - | + GetValueOrDefault_v3 | 40 | 19.945 ns | 0.1032 ns | 0.0965 ns | 1.74 | 0.01 | - | - | - | - | + | | | | | | | | | | | + GetValueOrDefault_v1 | 200 | 13.664 ns | 0.0291 ns | 0.0258 ns | 0.97 | 0.00 | - | - | - | - | + GetValueOrDefault | 200 | 14.051 ns | 0.0524 ns | 0.0491 ns | 1.00 | 0.00 | - | - | - | - | + GetValueOrDefault_v2 | 200 | 16.722 ns | 0.0568 ns | 0.0531 ns | 1.19 | 0.01 | - | - | - | - | + GetValueOrDefault_v3 | 200 | 24.473 ns | 0.0792 ns | 0.0702 ns | 1.74 | 0.01 | - | - | - | - | + | | | | | | | | | | | + GetValueOrDefault_v1 | 1000 | 14.213 ns | 0.1528 ns | 0.1354 ns | 0.96 | 0.01 | - | - | - | - | + GetValueOrDefault | 1000 | 14.805 ns | 0.0518 ns | 0.0485 ns | 1.00 | 0.00 | - | - | - | - | + GetValueOrDefault_v2 | 1000 | 16.645 ns | 0.0447 ns | 0.0419 ns | 1.12 | 0.01 | - | - | - | - | + GetValueOrDefault_v3 | 1000 | 27.489 ns | 0.0890 ns | 0.0832 ns | 1.86 | 0.01 | - | - | - | - | + + ## Adding aggressive inlining to the Data { Hash, Key, Value } properties + + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault | 5 | 5.853 ns | 0.0685 ns | 0.0607 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v1 | 5 | 5.913 ns | 0.0373 ns | 0.0349 ns | 1.01 | - | - | - | - | + | | | | | | | | | | + GetValueOrDefault | 40 | 9.649 ns | 0.0235 ns | 0.0220 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v1 | 40 | 10.266 ns | 0.0236 ns | 0.0221 ns | 1.06 | - | - | - | - | + | | | | | | | | | | + GetValueOrDefault | 200 | 11.613 ns | 0.0554 ns | 0.0491 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v1 | 200 | 12.052 ns | 0.0555 ns | 0.0520 ns | 1.04 | - | - | - | - | + + ## Using `!= Empty` instead of `.Height != 0` drops some perf + + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault_v1 | 5 | 5.933 ns | 0.0310 ns | 0.0290 ns | 0.93 | 0.03 | - | - | - | - | + GetValueOrDefault | 5 | 6.386 ns | 0.1807 ns | 0.1602 ns | 1.00 | 0.00 | - | - | - | - | + | | | | | | | | | | | + GetValueOrDefault | 40 | 9.820 ns | 0.0521 ns | 0.0488 ns | 1.00 | 0.00 | - | - | - | - | + GetValueOrDefault_v1 | 40 | 10.257 ns | 0.0300 ns | 0.0266 ns | 1.05 | 0.01 | - | - | - | - | + | | | | | | | | | | | + GetValueOrDefault | 200 | 11.717 ns | 0.0689 ns | 0.0644 ns | 1.00 | 0.00 | - | - | - | - | + GetValueOrDefault_v1 | 200 | 12.104 ns | 0.0548 ns | 0.0486 ns | 1.03 | 0.01 | - | - | - | - | + + ## Removing `.Height != 0` check completely did not change much, but let it stay cause less code is better + + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault | 5 | 5.903 ns | 0.0612 ns | 0.0573 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v1 | 5 | 5.931 ns | 0.0503 ns | 0.0470 ns | 1.00 | - | - | - | - | + | | | | | | | | | | + GetValueOrDefault | 40 | 9.636 ns | 0.0419 ns | 0.0392 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v1 | 40 | 10.231 ns | 0.0333 ns | 0.0312 ns | 1.06 | - | - | - | - | + | | | | | | | | | | + GetValueOrDefault | 200 | 11.637 ns | 0.0721 ns | 0.0602 ns | 1.00 | - | - | - | - | + GetValueOrDefault_v1 | 200 | 12.042 ns | 0.0607 ns | 0.0568 ns | 1.03 | - | - | - | - | + + ## TryFind + + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + ----------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + TryFind_v1 | 5 | 5.229 ns | 0.0307 ns | 0.0257 ns | 1.00 | - | - | - | - | + TryFind | 5 | 6.766 ns | 0.0695 ns | 0.0650 ns | 1.30 | - | - | - | - | + | | | | | | | | | | + TryFind_v1 | 40 | 9.268 ns | 0.0116 ns | 0.0108 ns | 1.00 | - | - | - | - | + TryFind | 40 | 9.755 ns | 0.0219 ns | 0.0205 ns | 1.05 | - | - | - | - | + | | | | | | | | | | + TryFind_v1 | 200 | 11.773 ns | 0.0558 ns | 0.0494 ns | 1.00 | - | - | - | - | + TryFind | 200 | 12.212 ns | 0.0456 ns | 0.0380 ns | 1.04 | - | - | - | - | + + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + ----------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + TryFind | 5 | 5.906 ns | 0.0191 ns | 0.0178 ns | 0.97 | - | - | - | - | + TryFind_v1 | 5 | 6.079 ns | 0.0947 ns | 0.0839 ns | 1.00 | - | - | - | - | + | | | | | | | | | | + TryFind | 40 | 9.211 ns | 0.0214 ns | 0.0200 ns | 0.87 | - | - | - | - | + TryFind_v1 | 40 | 10.566 ns | 0.0149 ns | 0.0132 ns | 1.00 | - | - | - | - | + | | | | | | | | | | + TryFind | 200 | 11.400 ns | 0.1152 ns | 0.1078 ns | 0.88 | - | - | - | - | + TryFind_v1 | 200 | 12.929 ns | 0.0712 ns | 0.0666 ns | 1.00 | - | - | - | - | + + ## GetOrDefault a bit optimized + + Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| + GetValueOrDefault | 5 | 6.782 ns | 0.0449 ns | 0.0398 ns | 0.96 | - | - | - | - | + GetValueOrDefault_v1 | 5 | 7.042 ns | 0.0659 ns | 0.0616 ns | 1.00 | - | - | - | - | + | | | | | | | | | | + GetValueOrDefault | 40 | 10.962 ns | 0.0866 ns | 0.0768 ns | 0.99 | - | - | - | - | + GetValueOrDefault_v1 | 40 | 11.094 ns | 0.0973 ns | 0.0813 ns | 1.00 | - | - | - | - | + | | | | | | | | | | + GetValueOrDefault | 200 | 13.329 ns | 0.0338 ns | 0.0299 ns | 0.97 | - | - | - | - | + GetValueOrDefault_v1 | 200 | 13.722 ns | 0.0537 ns | 0.0448 ns | 1.00 | - | - | - | - | + + ## The whole result for the docs + + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + ---------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + TryFind_v1 | 10 | 7.274 ns | 0.0410 ns | 0.0384 ns | 0.98 | 0.01 | - | - | - | - | + TryFind | 10 | 7.422 ns | 0.0237 ns | 0.0222 ns | 1.00 | 0.00 | - | - | - | - | + ConcurrentDict_TryGet | 10 | 21.664 ns | 0.0213 ns | 0.0189 ns | 2.92 | 0.01 | - | - | - | - | + ImmutableDict_TryGet | 10 | 71.199 ns | 0.1312 ns | 0.1228 ns | 9.59 | 0.03 | - | - | - | - | + | | | | | | | | | | | + TryFind_v1 | 100 | 8.426 ns | 0.0236 ns | 0.0221 ns | 0.91 | 0.00 | - | - | - | - | + TryFind | 100 | 9.304 ns | 0.0305 ns | 0.0270 ns | 1.00 | 0.00 | - | - | - | - | + ConcurrentDict_TryGet | 100 | 21.791 ns | 0.1072 ns | 0.0951 ns | 2.34 | 0.01 | - | - | - | - | + ImmutableDict_TryGet | 100 | 74.985 ns | 0.1053 ns | 0.0879 ns | 8.06 | 0.03 | - | - | - | - | + | | | | | | | | | | | + TryFind | 1000 | 13.837 ns | 0.0291 ns | 0.0272 ns | 1.00 | 0.00 | - | - | - | - | + TryFind_v1 | 1000 | 16.108 ns | 0.0415 ns | 0.0367 ns | 1.16 | 0.00 | - | - | - | - | + ConcurrentDict_TryGet | 1000 | 21.876 ns | 0.0325 ns | 0.0288 ns | 1.58 | 0.00 | - | - | - | - | + ImmutableDict_TryGet | 1000 | 83.563 ns | 0.1046 ns | 0.0873 ns | 6.04 | 0.01 | - | - | - | - | + + + ## 2019-03-28: Comparing vs `Dictionary`: + + Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + TryFind | 10 | 7.722 ns | 0.0451 ns | 0.0422 ns | 7.718 ns | 1.00 | 0.00 | - | - | - | - | + Dictionary_TryGetValue | 10 | 18.475 ns | 0.0502 ns | 0.0470 ns | 18.470 ns | 2.39 | 0.01 | - | - | - | - | + ConcurrentDictionary_TryGetValue | 10 | 22.661 ns | 0.0463 ns | 0.0433 ns | 22.653 ns | 2.93 | 0.02 | - | - | - | - | + ImmutableDict_TryGet | 10 | 72.911 ns | 1.5234 ns | 2.1355 ns | 74.134 ns | 9.25 | 0.23 | - | - | - | - | + | | | | | | | | | | | | + TryFind | 100 | 9.987 ns | 0.0543 ns | 0.0508 ns | 9.978 ns | 1.00 | 0.00 | - | - | - | - | + Dictionary_TryGetValue | 100 | 18.110 ns | 0.0644 ns | 0.0602 ns | 18.088 ns | 1.81 | 0.01 | - | - | - | - | + ConcurrentDictionary_TryGetValue | 100 | 24.402 ns | 0.0978 ns | 0.0915 ns | 24.435 ns | 2.44 | 0.02 | - | - | - | - | + ImmutableDict_TryGet | 100 | 76.689 ns | 0.3632 ns | 0.3397 ns | 76.704 ns | 7.68 | 0.04 | - | - | - | - | + | | | | | | | | | | | | + TryFind | 1000 | 12.600 ns | 0.1506 ns | 0.1335 ns | 12.551 ns | 1.00 | 0.00 | - | - | - | - | + Dictionary_TryGetValue | 1000 | 19.023 ns | 0.0575 ns | 0.0538 ns | 19.036 ns | 1.51 | 0.02 | - | - | - | - | + ConcurrentDictionary_TryGetValue | 1000 | 22.651 ns | 0.1238 ns | 0.1097 ns | 22.613 ns | 1.80 | 0.02 | - | - | - | - | + ImmutableDict_TryGet | 1000 | 83.608 ns | 0.3105 ns | 0.2904 ns | 83.612 ns | 6.64 | 0.07 | - | - | - | - | + + ## 2019-03-29: Comparing vs `DictionarySlim`: + + Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + --------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + DictionarySlim_TryGetValue | 10 | 8.228 ns | 0.0682 ns | 0.0604 ns | 1.00 | 0.01 | - | - | - | - | + TryFind | 10 | 8.257 ns | 0.0796 ns | 0.0706 ns | 1.00 | 0.00 | - | - | - | - | + Dictionary_TryGetValue | 10 | 19.615 ns | 0.0251 ns | 0.0209 ns | 2.38 | 0.02 | - | - | - | - | + ConcurrentDictionary_TryGetValue | 10 | 22.339 ns | 0.0922 ns | 0.0863 ns | 2.71 | 0.03 | - | - | - | - | + ImmutableDict_TryGet | 10 | 69.872 ns | 0.2699 ns | 0.2524 ns | 8.46 | 0.07 | - | - | - | - | + | | | | | | | | | | | + DictionarySlim_TryGetValue | 100 | 8.351 ns | 0.1613 ns | 0.1508 ns | 0.69 | 0.01 | - | - | - | - | + TryFind | 100 | 12.144 ns | 0.0570 ns | 0.0533 ns | 1.00 | 0.00 | - | - | - | - | + Dictionary_TryGetValue | 100 | 17.985 ns | 0.0880 ns | 0.0823 ns | 1.48 | 0.01 | - | - | - | - | + ConcurrentDictionary_TryGetValue | 100 | 22.312 ns | 0.0564 ns | 0.0471 ns | 1.84 | 0.01 | - | - | - | - | + ImmutableDict_TryGet | 100 | 75.374 ns | 0.3042 ns | 0.2846 ns | 6.21 | 0.04 | - | - | - | - | + | | | | | | | | | | | + DictionarySlim_TryGetValue | 1000 | 8.202 ns | 0.0713 ns | 0.0667 ns | 0.55 | 0.01 | - | - | - | - | + TryFind | 1000 | 14.919 ns | 0.1101 ns | 0.0919 ns | 1.00 | 0.00 | - | - | - | - | + Dictionary_TryGetValue | 1000 | 18.073 ns | 0.2415 ns | 0.2141 ns | 1.21 | 0.02 | - | - | - | - | + ConcurrentDictionary_TryGetValue | 1000 | 22.406 ns | 0.1039 ns | 0.0921 ns | 1.50 | 0.01 | - | - | - | - | + ImmutableDict_TryGet | 1000 | 84.215 ns | 0.2835 ns | 0.2513 ns | 5.65 | 0.04 | - | - | - | - | + + ## 2019-04-08: Full test + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + | ImHashMap_TryFind | 10 | 9.072 ns | 0.0301 ns | 0.0282 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 10 | 8.405 ns | 0.0124 ns | 0.0116 ns | 0.93 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 10 | 8.199 ns | 0.0118 ns | 0.0105 ns | 0.90 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 18.151 ns | 0.0724 ns | 0.0677 ns | 2.00 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 22.281 ns | 0.1872 ns | 0.1462 ns | 2.45 | 0.02 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 70.143 ns | 0.2833 ns | 0.2650 ns | 7.73 | 0.04 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 100 | 12.698 ns | 0.0545 ns | 0.0510 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 100 | 12.440 ns | 0.0145 ns | 0.0129 ns | 0.98 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 100 | 8.197 ns | 0.0157 ns | 0.0139 ns | 0.65 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 100 | 18.108 ns | 0.0263 ns | 0.0205 ns | 1.43 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 22.834 ns | 0.0627 ns | 0.0524 ns | 1.80 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 76.253 ns | 0.2767 ns | 0.2311 ns | 6.00 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 1000 | 14.960 ns | 0.0457 ns | 0.0427 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 1000 | 14.614 ns | 0.0508 ns | 0.0451 ns | 0.98 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 1000 | 8.209 ns | 0.0534 ns | 0.0499 ns | 0.55 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 1000 | 18.256 ns | 0.0383 ns | 0.0320 ns | 1.22 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 22.261 ns | 0.1509 ns | 0.1411 ns | 1.49 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 83.095 ns | 0.3395 ns | 0.3176 ns | 5.55 | 0.03 | - | - | - | - | + + ## 2019-04-08: Different variants tested + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + | ImHashMap_TryFind2 | 10 | 7.979 ns | 0.0446 ns | 0.0417 ns | 0.93 | 0.01 | - | - | - | - | + | ImHashMap_TryFind3 | 10 | 7.908 ns | 0.0295 ns | 0.0262 ns | 0.92 | 0.00 | - | - | - | - | + | ImHashMap_TryFind | 10 | 8.608 ns | 0.0186 ns | 0.0174 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 10 | 8.248 ns | 0.0262 ns | 0.0232 ns | 0.96 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 10 | 7.212 ns | 0.0194 ns | 0.0181 ns | 0.84 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 17.707 ns | 0.0631 ns | 0.0590 ns | 2.06 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 22.210 ns | 0.1074 ns | 0.1004 ns | 2.58 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 72.093 ns | 0.4162 ns | 0.3893 ns | 8.37 | 0.05 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind2 | 100 | 11.245 ns | 0.0174 ns | 0.0163 ns | 0.92 | 0.00 | - | - | - | - | + | ImHashMap_TryFind3 | 100 | 11.488 ns | 0.0801 ns | 0.0710 ns | 0.94 | 0.01 | - | - | - | - | + | ImHashMap_TryFind | 100 | 12.165 ns | 0.0141 ns | 0.0118 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 100 | 12.272 ns | 0.0367 ns | 0.0343 ns | 1.01 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 100 | 7.019 ns | 0.0516 ns | 0.0458 ns | 0.58 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 100 | 17.825 ns | 0.1278 ns | 0.1196 ns | 1.47 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 22.189 ns | 0.1034 ns | 0.0968 ns | 1.82 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 75.564 ns | 0.3778 ns | 0.3534 ns | 6.21 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind2 | 1000 | 15.909 ns | 0.0924 ns | 0.0864 ns | 1.07 | 0.01 | - | - | - | - | + | ImHashMap_TryFind3 | 1000 | 13.643 ns | 0.0715 ns | 0.0669 ns | 0.92 | 0.01 | - | - | - | - | + | ImHashMap_TryFind | 1000 | 14.819 ns | 0.0465 ns | 0.0412 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 1000 | 14.541 ns | 0.0555 ns | 0.0520 ns | 0.98 | 0.01 | - | - | - | - | + | DictionarySlim_TryGetValue | 1000 | 7.678 ns | 0.0341 ns | 0.0302 ns | 0.52 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 1000 | 17.664 ns | 0.0403 ns | 0.0377 ns | 1.19 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 22.010 ns | 0.0497 ns | 0.0465 ns | 1.48 | 0.00 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 83.661 ns | 0.4033 ns | 0.3772 ns | 5.65 | 0.03 | - | - | - | - | + + ## Selecting the 3rd variant: + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| + | ImHashMap_TryFind | 10 | 8.078 ns | 0.0405 ns | 0.0379 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 10 | 8.224 ns | 0.0114 ns | 0.0101 ns | 1.02 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 10 | 7.387 ns | 0.0406 ns | 0.0380 ns | 0.91 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 17.917 ns | 0.0429 ns | 0.0401 ns | 2.22 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 22.256 ns | 0.0726 ns | 0.0643 ns | 2.75 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 70.638 ns | 0.6266 ns | 0.5861 ns | 8.74 | 0.09 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 100 | 11.577 ns | 0.0168 ns | 0.0141 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 100 | 12.329 ns | 0.0295 ns | 0.0276 ns | 1.07 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 100 | 7.410 ns | 0.0398 ns | 0.0353 ns | 0.64 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 100 | 17.890 ns | 0.0425 ns | 0.0377 ns | 1.55 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 22.240 ns | 0.0654 ns | 0.0580 ns | 1.92 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 78.697 ns | 1.1242 ns | 1.0516 ns | 6.78 | 0.08 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 1000 | 13.731 ns | 0.0292 ns | 0.0258 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 1000 | 14.553 ns | 0.0370 ns | 0.0346 ns | 1.06 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 1000 | 7.345 ns | 0.0208 ns | 0.0194 ns | 0.53 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 1000 | 18.672 ns | 0.0483 ns | 0.0451 ns | 1.36 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 22.150 ns | 0.1141 ns | 0.1068 ns | 1.61 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 82.402 ns | 0.9798 ns | 0.9165 ns | 6.01 | 0.07 | - | - | - | - | + + ## V2: + + BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 + Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=3.1.100 + [Host] : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT + DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT + + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | ImHashMap_TryFind | 1 | 4.755 ns | 0.1690 ns | 0.1878 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 1 | 3.657 ns | 0.0458 ns | 0.0406 ns | 0.78 | 0.04 | - | - | - | - | + | ImHashMapSlots_TryFind | 1 | 2.518 ns | 0.0149 ns | 0.0132 ns | 0.53 | 0.02 | - | - | - | - | + | DictionarySlim_TryGetValue | 1 | 6.804 ns | 0.0272 ns | 0.0254 ns | 1.44 | 0.07 | - | - | - | - | + | Dictionary_TryGetValue | 1 | 16.495 ns | 0.3948 ns | 0.4993 ns | 3.49 | 0.16 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1 | 15.812 ns | 0.1016 ns | 0.0951 ns | 3.35 | 0.15 | - | - | - | - | + | ImmutableDict_TryGet | 1 | 24.346 ns | 0.1253 ns | 0.1172 ns | 5.16 | 0.23 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 10 | 6.080 ns | 0.0235 ns | 0.0208 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 10 | 6.091 ns | 0.0707 ns | 0.0590 ns | 1.00 | 0.01 | - | - | - | - | + | ImHashMapSlots_TryFind | 10 | 2.517 ns | 0.0206 ns | 0.0193 ns | 0.41 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 10 | 6.670 ns | 0.0278 ns | 0.0260 ns | 1.10 | 0.01 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 16.202 ns | 0.0634 ns | 0.0562 ns | 2.66 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 15.764 ns | 0.0659 ns | 0.0617 ns | 2.59 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 26.282 ns | 0.2232 ns | 0.2088 ns | 4.32 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 100 | 9.350 ns | 0.0315 ns | 0.0295 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 100 | 10.752 ns | 0.0199 ns | 0.0166 ns | 1.15 | 0.00 | - | - | - | - | + | ImHashMapSlots_TryFind | 100 | 5.664 ns | 0.0391 ns | 0.0366 ns | 0.61 | 0.01 | - | - | - | - | + | DictionarySlim_TryGetValue | 100 | 6.665 ns | 0.0287 ns | 0.0254 ns | 0.71 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 100 | 17.007 ns | 0.0615 ns | 0.0576 ns | 1.82 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 16.024 ns | 0.3340 ns | 0.3124 ns | 1.71 | 0.03 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 30.667 ns | 0.1026 ns | 0.0960 ns | 3.28 | 0.02 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 1000 | 12.729 ns | 0.0393 ns | 0.0368 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 1000 | 13.352 ns | 0.0638 ns | 0.0597 ns | 1.05 | 0.00 | - | - | - | - | + | ImHashMapSlots_TryFind | 1000 | 7.131 ns | 0.0246 ns | 0.0230 ns | 0.56 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 1000 | 6.686 ns | 0.0317 ns | 0.0297 ns | 0.53 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 1000 | 16.848 ns | 0.0593 ns | 0.0526 ns | 1.32 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 15.684 ns | 0.0695 ns | 0.0650 ns | 1.23 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 33.579 ns | 0.1077 ns | 0.0955 ns | 2.64 | 0.01 | - | - | - | - | + + + BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 + Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=3.1.100 + [Host] : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT + DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT + + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |-------------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | ImHashMap_TryFind | 1 | 4.365 ns | 0.0198 ns | 0.0185 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMapSlots32_TryFind | 1 | 2.400 ns | 0.0229 ns | 0.0214 ns | 0.55 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 1 | 3.385 ns | 0.0071 ns | 0.0063 ns | 0.78 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1 | 4.892 ns | 0.0134 ns | 0.0118 ns | 1.12 | 0.01 | - | - | - | - | + | Experimental_ImHashMapSlots32_TryFind | 1 | 6.268 ns | 0.0455 ns | 0.0425 ns | 1.44 | 0.01 | - | - | - | - | + | Experimental_ImHashMapSlots64_TryFind | 1 | 6.175 ns | 0.0335 ns | 0.0313 ns | 1.41 | 0.01 | - | - | - | - | + | DictionarySlim_TryGetValue | 1 | 6.190 ns | 0.0524 ns | 0.0490 ns | 1.42 | 0.02 | - | - | - | - | + | Dictionary_TryGetValue | 1 | 15.425 ns | 0.0636 ns | 0.0595 ns | 3.53 | 0.02 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1 | 14.754 ns | 0.0807 ns | 0.0716 ns | 3.38 | 0.02 | - | - | - | - | + | ImmutableDict_TryGet | 1 | 23.449 ns | 0.1027 ns | 0.0960 ns | 5.37 | 0.04 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 10 | 5.449 ns | 0.0216 ns | 0.0202 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMapSlots32_TryFind | 10 | 2.911 ns | 0.0089 ns | 0.0083 ns | 0.53 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 10 | 6.019 ns | 0.0129 ns | 0.0115 ns | 1.10 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 10 | 8.060 ns | 0.0239 ns | 0.0224 ns | 1.48 | 0.01 | - | - | - | - | + | Experimental_ImHashMapSlots32_TryFind | 10 | 5.605 ns | 0.0261 ns | 0.0244 ns | 1.03 | 0.00 | - | - | - | - | + | Experimental_ImHashMapSlots64_TryFind | 10 | 5.504 ns | 0.0456 ns | 0.0404 ns | 1.01 | 0.01 | - | - | - | - | + | DictionarySlim_TryGetValue | 10 | 5.216 ns | 0.0252 ns | 0.0236 ns | 0.96 | 0.01 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 15.346 ns | 0.0732 ns | 0.0685 ns | 2.82 | 0.02 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 14.870 ns | 0.1094 ns | 0.1023 ns | 2.73 | 0.02 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 24.888 ns | 0.0798 ns | 0.0746 ns | 4.57 | 0.02 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 100 | 7.665 ns | 0.0208 ns | 0.0184 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMapSlots32_TryFind | 100 | 5.240 ns | 0.0238 ns | 0.0223 ns | 0.68 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 100 | 9.222 ns | 0.0223 ns | 0.0208 ns | 1.20 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 100 | 12.248 ns | 0.0584 ns | 0.0546 ns | 1.60 | 0.01 | - | - | - | - | + | Experimental_ImHashMapSlots32_TryFind | 100 | 7.546 ns | 0.0966 ns | 0.0903 ns | 0.98 | 0.01 | - | - | - | - | + | Experimental_ImHashMapSlots64_TryFind | 100 | 5.780 ns | 0.0250 ns | 0.0234 ns | 0.75 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 100 | 6.452 ns | 0.0538 ns | 0.0503 ns | 0.84 | 0.01 | - | - | - | - | + | Dictionary_TryGetValue | 100 | 15.705 ns | 0.0586 ns | 0.0549 ns | 2.05 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 15.321 ns | 0.0413 ns | 0.0386 ns | 2.00 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 27.937 ns | 0.1050 ns | 0.0982 ns | 3.64 | 0.02 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 1000 | 11.459 ns | 0.0306 ns | 0.0286 ns | 1.00 | 0.00 | - | - | - | - | + | ImHashMapSlots32_TryFind | 1000 | 8.633 ns | 0.0154 ns | 0.0144 ns | 0.75 | 0.00 | - | - | - | - | + | ImHashMap_TryFind_V1 | 1000 | 12.486 ns | 0.0527 ns | 0.0493 ns | 1.09 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1000 | 16.002 ns | 0.0378 ns | 0.0353 ns | 1.40 | 0.00 | - | - | - | - | + | Experimental_ImHashMapSlots32_TryFind | 1000 | 11.673 ns | 0.0638 ns | 0.0597 ns | 1.02 | 0.00 | - | - | - | - | + | Experimental_ImHashMapSlots64_TryFind | 1000 | 9.817 ns | 0.0123 ns | 0.0115 ns | 0.86 | 0.00 | - | - | - | - | + | DictionarySlim_TryGetValue | 1000 | 6.469 ns | 0.0428 ns | 0.0401 ns | 0.56 | 0.00 | - | - | - | - | + | Dictionary_TryGetValue | 1000 | 19.170 ns | 0.0708 ns | 0.0628 ns | 1.67 | 0.01 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 16.273 ns | 0.4046 ns | 0.5116 ns | 1.43 | 0.05 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 29.662 ns | 0.1529 ns | 0.1355 ns | 2.59 | 0.01 | - | - | - | - | + + ## V3 - baseline + + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.572 (2004/?/20H1) + Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores + .NET Core SDK=3.1.403 + [Host] : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT + DefaultJob : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT + + ## v3 - baseline + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |------------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | ImHashMap_TryFind | 1 | 4.614 ns | 0.1073 ns | 0.0951 ns | 4.589 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1 | 7.398 ns | 0.0926 ns | 0.0821 ns | 7.413 ns | 1.60 | 0.04 | - | - | - | - | + | ImHashMap234_TryFind | 1 | 4.912 ns | 0.0595 ns | 0.0528 ns | 4.903 ns | 1.07 | 0.03 | - | - | - | - | + | | | | | | | | | | | | | + | ImHashMap_TryFind | 10 | 8.205 ns | 0.0765 ns | 0.0639 ns | 8.186 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 10 | 10.689 ns | 0.2738 ns | 0.2561 ns | 10.684 ns | 1.31 | 0.03 | - | - | - | - | + | ImHashMap234_TryFind | 10 | 6.418 ns | 0.0731 ns | 0.0611 ns | 6.412 ns | 0.78 | 0.01 | - | - | - | - | + | | | | | | | | | | | | | + | ImHashMap_TryFind | 100 | 10.398 ns | 0.0434 ns | 0.0385 ns | 10.391 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 100 | 13.982 ns | 0.2432 ns | 0.2275 ns | 13.946 ns | 1.35 | 0.02 | - | - | - | - | + | ImHashMap234_TryFind | 100 | 10.759 ns | 0.1757 ns | 0.1467 ns | 10.761 ns | 1.03 | 0.01 | - | - | - | - | + | | | | | | | | | | | | | + | ImHashMap_TryFind | 1000 | 14.397 ns | 0.2039 ns | 0.1907 ns | 14.369 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1000 | 30.810 ns | 1.0842 ns | 2.9681 ns | 29.878 ns | 2.25 | 0.38 | - | - | - | - | + | ImHashMap234_TryFind | 1000 | 19.953 ns | 0.4996 ns | 0.4429 ns | 19.772 ns | 1.39 | 0.03 | - | - | - | - | + + ### Leaf3Plus1 + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | ImHashMap_TryFind | 1 | 5.611 ns | 0.1067 ns | 0.0998 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1 | 6.348 ns | 0.2010 ns | 0.2150 ns | 1.14 | 0.05 | - | - | - | - | + | ImHashMap234_TryFind | 1 | 4.572 ns | 0.0966 ns | 0.0904 ns | 0.82 | 0.02 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 10 | 7.582 ns | 0.0703 ns | 0.0587 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 10 | 10.133 ns | 0.1315 ns | 0.1098 ns | 1.34 | 0.02 | - | - | - | - | + | ImHashMap234_TryFind | 10 | 5.763 ns | 0.0670 ns | 0.0594 ns | 0.76 | 0.01 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 100 | 10.642 ns | 0.2276 ns | 0.1901 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 100 | 13.526 ns | 0.1563 ns | 0.1385 ns | 1.27 | 0.03 | - | - | - | - | + | ImHashMap234_TryFind | 100 | 9.271 ns | 0.1695 ns | 0.1503 ns | 0.87 | 0.02 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 1000 | 14.909 ns | 0.2118 ns | 0.1981 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1000 | 37.315 ns | 1.4352 ns | 4.2316 ns | 2.76 | 0.21 | - | - | - | - | + | ImHashMap234_TryFind | 1000 | 24.536 ns | 0.6190 ns | 0.7602 ns | 1.66 | 0.06 | - | - | - | - | + + ### Leaf5Plus1 + Leaf3Plus1 + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | ImHashMap_TryFind | 1 | 6.659 ns | 0.1975 ns | 0.1751 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1 | 8.220 ns | 0.1876 ns | 0.1842 ns | 1.23 | 0.05 | - | - | - | - | + | ImHashMap234_TryFind | 1 | 5.644 ns | 0.1702 ns | 0.1592 ns | 0.85 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 5 | 7.223 ns | 0.2332 ns | 0.2068 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 5 | 9.832 ns | 0.2499 ns | 0.2337 ns | 1.36 | 0.05 | - | - | - | - | + | ImHashMap234_TryFind | 5 | 5.353 ns | 0.1591 ns | 0.1410 ns | 0.74 | 0.04 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 10 | 8.325 ns | 0.1680 ns | 0.1403 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 10 | 12.221 ns | 0.3406 ns | 0.5098 ns | 1.50 | 0.08 | - | - | - | - | + | ImHashMap234_TryFind | 10 | 7.012 ns | 0.2045 ns | 0.2273 ns | 0.85 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 100 | 12.118 ns | 0.3031 ns | 0.4629 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 100 | 18.145 ns | 0.4551 ns | 0.8321 ns | 1.50 | 0.08 | - | - | - | - | + | ImHashMap234_TryFind | 100 | 11.908 ns | 0.2829 ns | 0.3679 ns | 0.97 | 0.04 | - | - | - | - | + | | | | | | | | | | | | + | ImHashMap_TryFind | 1000 | 17.113 ns | 0.3848 ns | 0.3411 ns | 1.00 | 0.00 | - | - | - | - | + | Experimental_ImHashMap_TryFind | 1000 | 26.034 ns | 0.6183 ns | 0.8255 ns | 1.52 | 0.07 | - | - | - | - | + | ImHashMap234_TryFind | 1000 | 16.927 ns | 0.3743 ns | 0.5828 ns | 0.99 | 0.05 | - | - | - | - | + + ### V3 candidate + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |----------------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | V2_ImHashMap_AVL_TryFind | 1 | 6.084 ns | 0.1295 ns | 0.1082 ns | 1.00 | 0.00 | - | - | - | - | + | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 1 | 7.319 ns | 0.2009 ns | 0.1879 ns | 1.20 | 0.03 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 1 | 5.372 ns | 0.1142 ns | 0.0954 ns | 0.88 | 0.02 | - | - | - | - | + | V3_PartitionedHashMap_23Tree_TryFind | 1 | 6.578 ns | 0.1517 ns | 0.1345 ns | 1.08 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 5 | 6.867 ns | 0.1035 ns | 0.0918 ns | 1.00 | 0.00 | - | - | - | - | + | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 5 | 9.599 ns | 0.1509 ns | 0.1411 ns | 1.40 | 0.03 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 5 | 5.319 ns | 0.1028 ns | 0.1142 ns | 0.77 | 0.02 | - | - | - | - | + | V3_PartitionedHashMap_23Tree_TryFind | 5 | 6.550 ns | 0.0856 ns | 0.0801 ns | 0.95 | 0.01 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 10 | 7.646 ns | 0.0905 ns | 0.0802 ns | 1.00 | 0.00 | - | - | - | - | + | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 10 | 11.002 ns | 0.2011 ns | 0.1881 ns | 1.44 | 0.03 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 10 | 6.986 ns | 0.2160 ns | 0.2122 ns | 0.91 | 0.03 | - | - | - | - | + | V3_PartitionedHashMap_23Tree_TryFind | 10 | 6.714 ns | 0.2153 ns | 0.1798 ns | 0.88 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 100 | 12.413 ns | 0.3100 ns | 0.2748 ns | 1.00 | 0.00 | - | - | - | - | + | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 100 | 15.134 ns | 0.2375 ns | 0.2917 ns | 1.23 | 0.04 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 100 | 11.147 ns | 0.2990 ns | 0.3443 ns | 0.90 | 0.03 | - | - | - | - | + | V3_PartitionedHashMap_23Tree_TryFind | 100 | 8.245 ns | 0.1473 ns | 0.1150 ns | 0.67 | 0.01 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 1000 | 16.528 ns | 0.1799 ns | 0.1502 ns | 1.00 | 0.00 | - | - | - | - | + | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 1000 | 22.341 ns | 0.2584 ns | 0.2291 ns | 1.35 | 0.02 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 1000 | 13.178 ns | 0.2153 ns | 0.2014 ns | 0.80 | 0.01 | - | - | - | - | + | V3_PartitionedHashMap_23Tree_TryFind | 1000 | 11.198 ns | 0.2784 ns | 0.3094 ns | 0.68 | 0.02 | - | - | - | - | + + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | V3_ImHashMap_23Tree_TryFind | 1 | 4.467 ns | 0.0977 ns | 0.0816 ns | 1.00 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1 | 19.103 ns | 0.2302 ns | 0.2040 ns | 4.28 | 0.08 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 5 | 4.588 ns | 0.1057 ns | 0.0937 ns | 1.00 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 5 | 19.000 ns | 0.1738 ns | 0.1625 ns | 4.15 | 0.09 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 10 | 5.948 ns | 0.1194 ns | 0.1116 ns | 1.00 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 19.598 ns | 0.4600 ns | 0.4303 ns | 3.30 | 0.11 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 100 | 9.838 ns | 0.1607 ns | 0.1342 ns | 1.00 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 20.336 ns | 0.3591 ns | 0.2998 ns | 2.07 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 1000 | 11.980 ns | 0.2353 ns | 0.2201 ns | 1.00 | 0.00 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 19.939 ns | 0.4273 ns | 0.8824 ns | 1.73 | 0.10 | - | - | - | - | + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |----------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | V3_ImHashMap_23Tree_TryFind | 1 | 4.647 ns | 0.0516 ns | 0.0431 ns | 1.00 | 0.00 | - | - | - | - | + | ImmutableDict_TryGet | 1 | 23.396 ns | 0.3703 ns | 0.3092 ns | 5.04 | 0.10 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 5 | 5.242 ns | 0.1822 ns | 0.3143 ns | 1.00 | 0.00 | - | - | - | - | + | ImmutableDict_TryGet | 5 | 23.739 ns | 0.1941 ns | 0.1816 ns | 4.42 | 0.32 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 10 | 5.749 ns | 0.1206 ns | 0.1128 ns | 1.00 | 0.00 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 24.769 ns | 0.3152 ns | 0.2949 ns | 4.31 | 0.11 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 100 | 8.252 ns | 0.1754 ns | 0.1465 ns | 1.00 | 0.00 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 27.288 ns | 0.6118 ns | 0.6009 ns | 3.31 | 0.10 | - | - | - | - | + | | | | | | | | | | | | + | V3_ImHashMap_23Tree_TryFind | 1000 | 12.053 ns | 0.1955 ns | 0.1829 ns | 1.00 | 0.00 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 30.762 ns | 0.3980 ns | 0.3723 ns | 2.55 | 0.04 | - | - | - | - | + + ### Branch3 as two Branch2 -- looks ok + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |----------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | V2_ImHashMap_AVL_TryFind | 1 | 6.236 ns | 0.2150 ns | 0.3084 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 1 | 5.467 ns | 0.1410 ns | 0.1250 ns | 0.85 | 0.05 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 5 | 7.390 ns | 0.2037 ns | 0.1701 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 5 | 5.552 ns | 0.1412 ns | 0.1252 ns | 0.75 | 0.02 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 10 | 9.402 ns | 0.1381 ns | 0.1153 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 10 | 6.820 ns | 0.1766 ns | 0.1652 ns | 0.73 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 100 | 11.085 ns | 0.2337 ns | 0.2186 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 100 | 10.257 ns | 0.2037 ns | 0.1905 ns | 0.93 | 0.02 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 1000 | 16.883 ns | 0.2438 ns | 0.2280 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 1000 | 15.516 ns | 0.3248 ns | 0.4554 ns | 0.93 | 0.04 | - | - | - | - | + + ### V3 RTM + + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 + Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=5.0.201 + [Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT + DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT + + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | V2_ImHashMap_AVL_TryFind | 1 | 5.100 ns | 0.1212 ns | 0.1134 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 1 | 5.539 ns | 0.1676 ns | 0.1568 ns | 1.09 | 0.03 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 1 | 5.813 ns | 0.1844 ns | 0.1973 ns | 1.13 | 0.04 | - | - | - | - | + | DictionarySlim_TryGetValue | 1 | 6.614 ns | 0.0839 ns | 0.0744 ns | 1.29 | 0.03 | - | - | - | - | + | Dictionary_TryGetValue | 1 | 16.495 ns | 0.1270 ns | 0.1126 ns | 3.23 | 0.06 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1 | 12.715 ns | 0.1584 ns | 0.1323 ns | 2.49 | 0.05 | - | - | - | - | + | ImmutableDict_TryGet | 1 | 22.615 ns | 0.3097 ns | 0.2418 ns | 4.42 | 0.08 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 10 | 6.270 ns | 0.1421 ns | 0.1187 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 10 | 6.597 ns | 0.1095 ns | 0.1024 ns | 1.06 | 0.02 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 10 | 5.416 ns | 0.0908 ns | 0.0805 ns | 0.87 | 0.02 | - | - | - | - | + | DictionarySlim_TryGetValue | 10 | 7.543 ns | 0.1274 ns | 0.1064 ns | 1.20 | 0.02 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 16.907 ns | 0.1892 ns | 0.1769 ns | 2.70 | 0.05 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 12.694 ns | 0.1740 ns | 0.1453 ns | 2.03 | 0.04 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 24.049 ns | 0.3034 ns | 0.2838 ns | 3.84 | 0.07 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 100 | 9.459 ns | 0.2667 ns | 0.2739 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 100 | 9.526 ns | 0.1814 ns | 0.1697 ns | 1.01 | 0.03 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 100 | 5.827 ns | 0.0867 ns | 0.0768 ns | 0.62 | 0.02 | - | - | - | - | + | DictionarySlim_TryGetValue | 100 | 6.897 ns | 0.1033 ns | 0.0967 ns | 0.73 | 0.02 | - | - | - | - | + | Dictionary_TryGetValue | 100 | 18.189 ns | 0.2100 ns | 0.1640 ns | 1.92 | 0.06 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 13.412 ns | 0.2041 ns | 0.1909 ns | 1.42 | 0.04 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 26.023 ns | 0.3854 ns | 0.3417 ns | 2.76 | 0.08 | - | - | - | - | + | | | | | | | | | | | | + | V2_ImHashMap_AVL_TryFind | 1000 | 15.172 ns | 0.2617 ns | 0.2320 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_23Tree_TryFind | 1000 | 14.473 ns | 0.2030 ns | 0.1799 ns | 0.95 | 0.01 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 1000 | 8.182 ns | 0.0809 ns | 0.0756 ns | 0.54 | 0.01 | - | - | - | - | + | DictionarySlim_TryGetValue | 1000 | 6.893 ns | 0.1590 ns | 0.1410 ns | 0.45 | 0.01 | - | - | - | - | + | Dictionary_TryGetValue | 1000 | 18.230 ns | 0.2012 ns | 0.1882 ns | 1.20 | 0.02 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 14.920 ns | 0.2296 ns | 0.2035 ns | 0.98 | 0.01 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 29.555 ns | 0.5926 ns | 0.5543 ns | 1.95 | 0.04 | - | - | - | - | + + + ## Against static TypeDictionary by @rogeralsing (need to subtract the cost of enumerating the array when adding items to ImHashMap and PartitionedHashMap) + + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 + Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=5.0.201 + [Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT + DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |---------------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | V3_ImHashMap_GetValueOrDefault | 5 | 4.497 ns | 0.1607 ns | 0.1578 ns | 1.00 | 0.00 | - | - | - | - | + | V3_PartitionedHashMap_GetValueOrDefault | 5 | 4.763 ns | 0.1675 ns | 0.1484 ns | 1.06 | 0.05 | - | - | - | - | + | TypeDict_TryFind | 5 | 3.198 ns | 0.1221 ns | 0.1406 ns | 0.71 | 0.03 | - | - | - | - | + | Dictionary_TryGetValue | 5 | 16.248 ns | 0.3500 ns | 0.3102 ns | 3.61 | 0.12 | - | - | - | - | + + | V3_ImHashMap_GetValueOrDefault | 10 | 5.569 ns | 0.1801 ns | 0.2751 ns | 1.00 | 0.00 | - | - | - | - | + | V3_PartitionedHashMap_GetValueOrDefault | 10 | 5.016 ns | 0.1509 ns | 0.1178 ns | 0.89 | 0.05 | - | - | - | - | + | TypeDict_TryFind | 10 | 2.513 ns | 0.0824 ns | 0.0688 ns | 0.45 | 0.03 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 15.954 ns | 0.3234 ns | 0.3025 ns | 2.85 | 0.17 | - | - | - | - | + + | V3_ImHashMap_GetValueOrDefault | 20 | 6.747 ns | 0.1964 ns | 0.2017 ns | 1.00 | 0.00 | - | - | - | - | + | V3_PartitionedHashMap_GetValueOrDefault | 20 | 3.985 ns | 0.1034 ns | 0.0863 ns | 0.59 | 0.02 | - | - | - | - | + | TypeDict_TryFind | 20 | 2.476 ns | 0.1174 ns | 0.1257 ns | 0.37 | 0.02 | - | - | - | - | + | Dictionary_TryGetValue | 20 | 17.766 ns | 0.4131 ns | 0.3865 ns | 2.64 | 0.11 | - | - | - | - | + + + ## V4 + + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 + Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=6.0.202 + [Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT + DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| + | V4_ImHashMap_TryFind | 1 | 8.915 ns | 0.1853 ns | 0.1733 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_TryFind | 1 | 7.834 ns | 0.1769 ns | 0.1568 ns | 0.88 | 0.02 | - | - | - | - | + | V4_PartitionedHashMap_TryFind | 1 | 8.292 ns | 0.1082 ns | 0.0959 ns | 0.93 | 0.02 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 1 | 7.681 ns | 0.1245 ns | 0.1039 ns | 0.86 | 0.02 | - | - | - | - | + | DictionarySlim_TryGetValue | 1 | 8.861 ns | 0.1423 ns | 0.1188 ns | 0.99 | 0.02 | - | - | - | - | + | Dictionary_TryGetValue | 1 | 17.914 ns | 0.3447 ns | 0.3055 ns | 2.01 | 0.05 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1 | 13.381 ns | 0.2709 ns | 0.2401 ns | 1.50 | 0.04 | - | - | - | - | + | ImmutableDict_TryGet | 1 | 19.040 ns | 0.3068 ns | 0.2870 ns | 2.14 | 0.06 | - | - | - | - | + | | | | | | | | | | | | + | V4_ImHashMap_TryFind | 10 | 9.462 ns | 0.2690 ns | 0.2246 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_TryFind | 10 | 9.038 ns | 0.1650 ns | 0.1463 ns | 0.96 | 0.02 | - | - | - | - | + | V4_PartitionedHashMap_TryFind | 10 | 8.575 ns | 0.2031 ns | 0.1900 ns | 0.91 | 0.03 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 10 | 5.869 ns | 0.1026 ns | 0.1743 ns | 0.63 | 0.02 | - | - | - | - | + | DictionarySlim_TryGetValue | 10 | 7.384 ns | 0.1882 ns | 0.1668 ns | 0.78 | 0.03 | - | - | - | - | + | Dictionary_TryGetValue | 10 | 14.082 ns | 0.2168 ns | 0.1921 ns | 1.49 | 0.04 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 10 | 11.436 ns | 0.1398 ns | 0.1239 ns | 1.21 | 0.03 | - | - | - | - | + | ImmutableDict_TryGet | 10 | 17.927 ns | 0.4467 ns | 0.4780 ns | 1.90 | 0.09 | - | - | - | - | + | | | | | | | | | | | | + | V4_ImHashMap_TryFind | 100 | 12.147 ns | 0.1475 ns | 0.1308 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_TryFind | 100 | 11.357 ns | 0.1695 ns | 0.1323 ns | 0.94 | 0.01 | - | - | - | - | + | V4_PartitionedHashMap_TryFind | 100 | 8.520 ns | 0.2142 ns | 0.1899 ns | 0.70 | 0.02 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 100 | 7.785 ns | 0.1374 ns | 0.1147 ns | 0.64 | 0.01 | - | - | - | - | + | DictionarySlim_TryGetValue | 100 | 6.918 ns | 0.1544 ns | 0.1289 ns | 0.57 | 0.01 | - | - | - | - | + | Dictionary_TryGetValue | 100 | 13.804 ns | 0.3474 ns | 0.3717 ns | 1.14 | 0.04 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 100 | 11.432 ns | 0.2194 ns | 0.2052 ns | 0.94 | 0.02 | - | - | - | - | + | ImmutableDict_TryGet | 100 | 21.593 ns | 0.4048 ns | 0.3786 ns | 1.78 | 0.03 | - | - | - | - | + | | | | | | | | | | | | + | V4_ImHashMap_TryFind | 1000 | 15.492 ns | 0.3859 ns | 0.4881 ns | 1.00 | 0.00 | - | - | - | - | + | V3_ImHashMap_TryFind | 1000 | 14.339 ns | 0.2612 ns | 0.2444 ns | 0.92 | 0.03 | - | - | - | - | + | V4_PartitionedHashMap_TryFind | 1000 | 11.508 ns | 0.1839 ns | 0.1630 ns | 0.74 | 0.03 | - | - | - | - | + | V3_PartitionedHashMap_TryFind | 1000 | 9.508 ns | 0.2340 ns | 0.2074 ns | 0.61 | 0.02 | - | - | - | - | + | DictionarySlim_TryGetValue | 1000 | 7.099 ns | 0.1984 ns | 0.2037 ns | 0.46 | 0.01 | - | - | - | - | + | Dictionary_TryGetValue | 1000 | 13.696 ns | 0.1341 ns | 0.1120 ns | 0.88 | 0.03 | - | - | - | - | + | ConcurrentDictionary_TryGetValue | 1000 | 11.358 ns | 0.1464 ns | 0.1222 ns | 0.73 | 0.03 | - | - | - | - | + | ImmutableDict_TryGet | 1000 | 25.875 ns | 0.3752 ns | 0.3510 ns | 1.67 | 0.06 | - | - | - | - | + + ## Baseline FHashMap6 vs DictionarySlim vs Dictionary + + BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19042.928/20H2/October2020Update) + Intel Core i5-8350U CPU 1.70GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores + .NET SDK=7.0.100 + [Host] : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 + DefaultJob : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------:|------------:| + | Dictionary_TryGetValue | 100 | 15.234 ns | 0.3434 ns | 0.6779 ns | 14.971 ns | 1.00 | 0.00 | - | NA | + | DictionarySlim_TryGetValue | 100 | 9.790 ns | 0.5518 ns | 1.6271 ns | 8.944 ns | 0.70 | 0.11 | - | NA | + | FHashMap_TryGetValue | 100 | 20.591 ns | 0.4773 ns | 0.4232 ns | 20.521 ns | 1.32 | 0.09 | - | NA | + + ## Never happened? + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|---------:|---------:|---------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 100 | 11.14 ns | 0.613 ns | 1.749 ns | 10.75 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 100 | 11.29 ns | 0.641 ns | 1.850 ns | 10.77 ns | 1.03 | 0.20 | - | NA | + + ## Almost funny + + | Method | Count | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|----------:|------------:| + | DictionarySlim_TryGetValue | 100 | 8.703 ns | 0.0774 ns | 0.0686 ns | 1.00 | - | NA | + | FHashMap_TryGetValue | 100 | 7.181 ns | 0.0442 ns | 0.0345 ns | 0.83 | - | NA | + + ## Initial SIMD FHashMap7 vs DictionarySlim + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 100 | 8.968 ns | 0.2581 ns | 0.4168 ns | 8.796 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 100 | 8.866 ns | 0.4456 ns | 1.3069 ns | 9.244 ns | 0.86 | 0.09 | - | NA | + + ## Round 2 and 3, SIMD FHashMap7 vs DictionarySlim + + | Method | Count | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|----------:|------------:| + | DictionarySlim_TryGetValue | 100 | 8.925 ns | 0.0383 ns | 0.0299 ns | 1.00 | - | NA | + | FHashMap_TryGetValue | 100 | 6.376 ns | 0.1207 ns | 0.1129 ns | 0.72 | - | NA | + + ## Funny 4, SIMD FHashMap7 vs DictionarySlim + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 100 | 9.131 ns | 0.1991 ns | 0.1663 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 100 | 6.020 ns | 0.1766 ns | 0.2644 ns | 0.65 | 0.04 | - | NA | + + ## FHashMap7 vs DictionarySlim (multiple params) + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 10.945 ns | 0.3136 ns | 0.5574 ns | 10.770 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1 | 6.852 ns | 0.4387 ns | 1.2157 ns | 7.022 ns | 0.57 | 0.11 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 12.432 ns | 0.7856 ns | 2.3042 ns | 12.383 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 10 | 5.161 ns | 0.1755 ns | 0.2932 ns | 5.050 ns | 0.38 | 0.05 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 16.811 ns | 0.7006 ns | 2.0657 ns | 15.940 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 100 | 9.176 ns | 0.6841 ns | 1.9517 ns | 8.838 ns | 0.56 | 0.14 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 11.542 ns | 0.3251 ns | 0.7535 ns | 11.400 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1000 | 9.579 ns | 0.4608 ns | 1.2921 ns | 9.556 ns | 0.82 | 0.12 | - | NA | + + ## No!!! SIMD FHashMap7 vs DictionarySlim (multiple params) + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 9.134 ns | 0.2546 ns | 0.3569 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1 | 5.174 ns | 0.0317 ns | 0.0281 ns | 0.56 | 0.02 | - | NA | + | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 8.908 ns | 0.0430 ns | 0.0359 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 10 | 5.615 ns | 0.0722 ns | 0.0675 ns | 0.63 | 0.01 | - | NA | + | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 8.964 ns | 0.1536 ns | 0.1282 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 100 | 7.468 ns | 0.2187 ns | 0.2148 ns | 0.83 | 0.03 | - | NA | + | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 8.826 ns | 0.0929 ns | 0.0776 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1000 | 5.271 ns | 0.1362 ns | 0.1514 ns | 0.60 | 0.02 | - | NA | + + ## Small opti No!!! SIMD FHashMap7 vs DictionarySlim (multiple params) + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 8.965 ns | 0.3526 ns | 1.0285 ns | 8.416 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1 | 3.940 ns | 0.1476 ns | 0.2117 ns | 3.894 ns | 0.43 | 0.05 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 9.154 ns | 0.2472 ns | 0.3036 ns | 9.041 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 10 | 5.734 ns | 0.0922 ns | 0.0770 ns | 5.733 ns | 0.62 | 0.02 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 8.638 ns | 0.1980 ns | 0.1654 ns | 8.599 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 100 | 7.259 ns | 0.1361 ns | 0.1206 ns | 7.212 ns | 0.84 | 0.02 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 9.280 ns | 0.2481 ns | 0.4537 ns | 9.115 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1000 | 3.765 ns | 0.0650 ns | 0.0576 ns | 3.760 ns | 0.40 | 0.02 | - | NA | + + ## Adding small SIMD to FHashMap7 vs DictionarySlim (multiple params) - winning the medium + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 9.314 ns | 0.2492 ns | 0.4494 ns | 9.149 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1 | 4.819 ns | 0.1521 ns | 0.1423 ns | 4.775 ns | 0.51 | 0.03 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 9.012 ns | 0.2230 ns | 0.4297 ns | 8.841 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 10 | 6.407 ns | 0.1963 ns | 0.2182 ns | 6.375 ns | 0.70 | 0.05 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 9.261 ns | 0.2620 ns | 0.6019 ns | 8.994 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 100 | 7.136 ns | 0.1941 ns | 0.1515 ns | 7.104 ns | 0.74 | 0.06 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 9.066 ns | 0.1819 ns | 0.1612 ns | 9.013 ns | 1.00 | 0.00 | - | NA | + | FHashMap_TryGetValue | 1000 | 4.935 ns | 0.1353 ns | 0.1661 ns | 4.891 ns | 0.55 | 0.02 | - | NA | + + ## Lookup FHashMap8 vs DictionarySlim + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 9.446 ns | 0.2718 ns | 0.5038 ns | 9.329 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 1 | 5.485 ns | 0.1832 ns | 0.3009 ns | 5.402 ns | 0.58 | 0.04 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 9.659 ns | 0.2123 ns | 0.1882 ns | 9.654 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 10 | 7.044 ns | 0.1443 ns | 0.1205 ns | 7.003 ns | 0.73 | 0.01 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 9.368 ns | 0.2697 ns | 0.3600 ns | 9.259 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 100 | 9.770 ns | 0.2736 ns | 0.3360 ns | 9.748 ns | 1.04 | 0.06 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 9.743 ns | 0.2676 ns | 0.4686 ns | 9.573 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 1000 | 6.043 ns | 0.1946 ns | 0.4146 ns | 5.883 ns | 0.62 | 0.05 | - | NA | + + ## Lookup FHashMap8 vs DictionarySlim after simplifying + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 8.649 ns | 0.0707 ns | 0.0662 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 1 | 5.447 ns | 0.0784 ns | 0.0654 ns | 0.63 | 0.01 | - | NA | + | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 8.948 ns | 0.0405 ns | 0.0316 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 10 | 5.471 ns | 0.1656 ns | 0.2375 ns | 0.62 | 0.03 | - | NA | + | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 9.081 ns | 0.2568 ns | 0.5583 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 100 | 6.294 ns | 0.1585 ns | 0.1886 ns | 0.69 | 0.05 | - | NA | + | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 8.990 ns | 0.2087 ns | 0.1850 ns | 1.00 | 0.00 | - | NA | + | FHashMap8_TryGetValue | 1000 | 4.950 ns | 0.1508 ns | 0.2719 ns | 0.55 | 0.03 | - | NA | + + ## Lookup FHashMap7 vs FHashMap8 vs DictionarySlim vs Dictionary + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | + |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 9.470 ns | 0.2655 ns | 0.7269 ns | 9.193 ns | 1.00 | 0.00 | - | NA | + | Dictionary_TryGetValue | 1 | 12.842 ns | 0.3375 ns | 0.7192 ns | 12.628 ns | 1.36 | 0.12 | - | NA | + | FHashMap8_TryGetValue | 1 | 5.212 ns | 0.1734 ns | 0.1448 ns | 5.202 ns | 0.52 | 0.05 | - | NA | + | FHashMap7_TryGetValue | 1 | 4.365 ns | 0.1630 ns | 0.2981 ns | 4.269 ns | 0.45 | 0.05 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 9.585 ns | 0.2755 ns | 0.7992 ns | 9.292 ns | 1.00 | 0.00 | - | NA | + | Dictionary_TryGetValue | 10 | 10.675 ns | 0.2063 ns | 0.1723 ns | 10.674 ns | 1.13 | 0.10 | - | NA | + | FHashMap8_TryGetValue | 10 | 5.584 ns | 0.1895 ns | 0.2028 ns | 5.546 ns | 0.57 | 0.07 | - | NA | + | FHashMap7_TryGetValue | 10 | 5.676 ns | 0.0982 ns | 0.0964 ns | 5.680 ns | 0.58 | 0.06 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 11.095 ns | 0.4470 ns | 1.2236 ns | 11.427 ns | 1.00 | 0.00 | - | NA | + | Dictionary_TryGetValue | 100 | 10.694 ns | 0.2975 ns | 0.7013 ns | 10.484 ns | 0.94 | 0.11 | - | NA | + | FHashMap8_TryGetValue | 100 | 7.313 ns | 0.2226 ns | 0.4071 ns | 7.107 ns | 0.65 | 0.07 | - | NA | + | FHashMap7_TryGetValue | 100 | 7.119 ns | 0.2135 ns | 0.1997 ns | 7.095 ns | 0.67 | 0.11 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 9.049 ns | 0.2662 ns | 0.5953 ns | 8.826 ns | 1.00 | 0.00 | - | NA | + | Dictionary_TryGetValue | 1000 | 13.465 ns | 0.3476 ns | 0.3251 ns | 13.483 ns | 1.50 | 0.12 | - | NA | + | FHashMap8_TryGetValue | 1000 | 4.926 ns | 0.1781 ns | 0.3120 ns | 4.809 ns | 0.54 | 0.05 | - | NA | + | FHashMap7_TryGetValue | 1000 | 4.314 ns | 0.1559 ns | 0.1601 ns | 4.290 ns | 0.48 | 0.04 | - | NA | + + ## FHM9.1 vs Dict and DictSlim + + BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1702/22H2/2022Update/SunValley2) + 11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores + .NET SDK=7.0.304 + [Host] : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 + DefaultJob : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | + |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------------------:|---------------:|------------------------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 6.236 ns | 0.1823 ns | 0.1872 ns | 6.221 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | Dictionary_TryGetValue | 1 | 10.070 ns | 0.2035 ns | 0.2090 ns | 10.038 ns | 1.62 | 0.05 | 31 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 1 | 5.084 ns | 0.2665 ns | 0.7773 ns | 5.267 ns | 0.67 | 0.08 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 8.515 ns | 0.2330 ns | 0.3763 ns | 8.407 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | Dictionary_TryGetValue | 10 | 10.296 ns | 0.4517 ns | 1.3247 ns | 10.439 ns | 1.30 | 0.10 | 25 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 10 | 3.857 ns | 0.1311 ns | 0.2904 ns | 3.777 ns | 0.46 | 0.04 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 7.105 ns | 0.1931 ns | 0.5221 ns | 6.860 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | Dictionary_TryGetValue | 100 | 8.443 ns | 0.2081 ns | 0.2397 ns | 8.402 ns | 1.14 | 0.11 | 25 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 100 | 4.037 ns | 0.1339 ns | 0.2201 ns | 3.994 ns | 0.56 | 0.06 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 6.325 ns | 0.1933 ns | 0.4632 ns | 6.124 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | Dictionary_TryGetValue | 1000 | 9.146 ns | 0.2185 ns | 0.2044 ns | 9.176 ns | 1.42 | 0.10 | 25 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 1000 | 5.917 ns | 0.1505 ns | 0.1334 ns | 5.909 ns | 0.92 | 0.07 | 13 | 0 | 0 | - | NA | + + ## FHM9.1 sparse entries + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | CacheMisses/Op | BranchInstructions/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|---------------:|----------------------:|------------------------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 7.813 ns | 0.2096 ns | 0.2243 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | + | FHashMap91_TryGetValue | 1 | 4.608 ns | 0.1493 ns | 0.1888 ns | 0.59 | 0.03 | 0 | 13 | -0 | - | NA | + | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 7.677 ns | 0.1647 ns | 0.1375 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | + | FHashMap91_TryGetValue | 10 | 4.488 ns | 0.1183 ns | 0.1049 ns | 0.59 | 0.02 | 0 | 13 | 0 | - | NA | + | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 7.824 ns | 0.2102 ns | 0.2947 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | + | FHashMap91_TryGetValue | 100 | 3.459 ns | 0.1538 ns | 0.3277 ns | 0.45 | 0.05 | -0 | 13 | -0 | - | NA | + | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 7.608 ns | 0.1997 ns | 0.1770 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | + | FHashMap91_TryGetValue | 1000 | 6.384 ns | 0.1427 ns | 0.1114 ns | 0.84 | 0.03 | 0 | 14 | 0 | - | NA | + + ## No more sparce anymore + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | + |------------------------------ |------ |---------:|----------:|----------:|---------:|------:|--------:|----------------------:|---------------:|------------------------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 6.759 ns | 0.2002 ns | 0.2603 ns | 6.704 ns | 1.00 | 0.00 | 20 | 0 | -0 | - | NA | + | FHashMap91_TryGetValue | 1 | 4.512 ns | 0.1337 ns | 0.2271 ns | 4.432 ns | 0.67 | 0.05 | 11 | 0 | -0 | - | NA | + | FHashMap91_TryGetValue_Golden | 1 | 4.187 ns | 0.1022 ns | 0.0906 ns | 4.167 ns | 0.62 | 0.03 | 11 | -0 | -0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 8.225 ns | 0.4388 ns | 1.2938 ns | 7.602 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 10 | 4.434 ns | 0.1415 ns | 0.1453 ns | 4.434 ns | 0.58 | 0.06 | 11 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue_Golden | 10 | 4.498 ns | 0.1236 ns | 0.1033 ns | 4.472 ns | 0.59 | 0.05 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 7.067 ns | 0.1470 ns | 0.1303 ns | 7.080 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 100 | 4.481 ns | 0.1484 ns | 0.2311 ns | 4.398 ns | 0.64 | 0.04 | 11 | 0 | -0 | - | NA | + | FHashMap91_TryGetValue_Golden | 100 | 8.215 ns | 0.2181 ns | 0.2240 ns | 8.178 ns | 1.17 | 0.04 | 17 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 8.744 ns | 0.6189 ns | 1.8151 ns | 7.838 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 1000 | 5.755 ns | 0.1660 ns | 0.1912 ns | 5.715 ns | 0.73 | 0.09 | 14 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue_Golden | 1000 | 7.732 ns | 0.1953 ns | 0.2325 ns | 7.705 ns | 0.97 | 0.13 | 17 | 0 | 0 | - | NA | + + ## Final load factor 87.5% and no golden results + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | BranchInstructions/Op | BranchMispredictions/Op | CacheMisses/Op | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------------------:|------------------------:|---------------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 6.830 ns | 0.1073 ns | 0.1004 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 1 | 4.230 ns | 0.1149 ns | 0.1075 ns | 0.62 | 0.02 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 6.980 ns | 0.0918 ns | 0.0717 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 10 | 4.259 ns | 0.0673 ns | 0.0562 ns | 0.61 | 0.01 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 6.975 ns | 0.1016 ns | 0.0849 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 100 | 4.414 ns | 0.1426 ns | 0.2342 ns | 0.64 | 0.03 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 6.889 ns | 0.0773 ns | 0.0685 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 1000 | 5.633 ns | 0.1079 ns | 0.1243 ns | 0.82 | 0.02 | 14 | 0 | 0 | - | NA | + + ## ArrayEntries + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | CacheMisses/Op | BranchInstructions/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|---------------:|----------------------:|------------------------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1000 | 7.223 ns | 0.2535 ns | 0.7193 ns | 6.887 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | + | FHashMap91_TryGetValue | 1000 | 5.108 ns | 0.1634 ns | 0.2819 ns | 4.996 ns | 0.69 | 0.09 | -0 | 14 | 0 | - | NA | + + ## ArrayArrayEntries + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | BranchMispredictions/Op | CacheMisses/Op | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------------------:|------------------------:|---------------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 7.053 ns | 0.1174 ns | 0.1041 ns | 7.021 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 1 | 4.948 ns | 0.1446 ns | 0.2119 ns | 4.845 ns | 0.70 | 0.04 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 7.475 ns | 0.1732 ns | 0.2747 ns | 7.384 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 10 | 4.900 ns | 0.1541 ns | 0.1366 ns | 4.862 ns | 0.65 | 0.03 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 8.120 ns | 0.4747 ns | 1.3995 ns | 7.386 ns | 1.00 | 0.00 | 20 | -0 | 0 | - | NA | + | FHashMap91_TryGetValue | 100 | 5.129 ns | 0.1593 ns | 0.4251 ns | 4.985 ns | 0.64 | 0.12 | 11 | 0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 1000 | 7.289 ns | 0.1912 ns | 0.3088 ns | 7.137 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | + | FHashMap91_TryGetValue | 1000 | 6.262 ns | 0.1232 ns | 0.0962 ns | 6.277 ns | 0.86 | 0.04 | 14 | 0 | 0 | - | NA | + + ## SmallMap + net8.0 + + BenchmarkDotNet v0.13.10, Windows 11 (10.0.22621.2428/22H2/2022Update/SunValley2) + 11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores + .NET SDK 8.0.100-rc.2.23502.2 + [Host] : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2 + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | BranchInstructions/Op | BranchMispredictions/Op | CacheMisses/Op | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------------------:|------------------------:|---------------:|----------:|------------:| + | Dictionary_TryGetValue | 100 | 6.938 ns | 0.1719 ns | 0.2574 ns | 1.00 | 0.00 | 21 | 0 | 0 | - | NA | + | DictionarySlim_TryGetValue | 100 | 5.371 ns | 0.1277 ns | 0.1132 ns | 0.77 | 0.03 | 15 | 0 | 0 | - | NA | + | SmallMap_TryGetValue | 100 | 3.718 ns | 0.1360 ns | 0.2453 ns | 0.54 | 0.04 | 9 | 0 | 0 | - | NA | + + + ## SmallMap + FEC_FHashMap + net6.0 + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------------------:|---------------:|------------------------:|----------:|------------:| + | DictionarySlim_TryGetValue | 1 | 5.592 ns | 0.4244 ns | 1.2448 ns | 4.835 ns | 1.00 | 0.00 | 15 | 0 | 0 | - | NA | + | SmallMap_TryGetValue | 1 | 2.958 ns | 0.1059 ns | 0.0990 ns | 2.970 ns | 0.44 | 0.03 | 9 | -0 | -0 | - | NA | + | FHashMap_TryGetValue | 1 | 1.297 ns | 0.0757 ns | 0.0777 ns | 1.288 ns | 0.19 | 0.01 | 6 | -0 | 0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 5.208 ns | 0.1485 ns | 0.1240 ns | 5.181 ns | 1.00 | 0.00 | 15 | 0 | 0 | - | NA | + | SmallMap_TryGetValue | 10 | 3.709 ns | 0.1739 ns | 0.4551 ns | 3.548 ns | 0.69 | 0.07 | 9 | 0 | 0 | - | NA | + | FHashMap_TryGetValue | 10 | 5.539 ns | 0.4004 ns | 1.1487 ns | 5.612 ns | 0.98 | 0.22 | 10 | -0 | -0 | - | NA | + | | | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 5.296 ns | 0.1654 ns | 0.3898 ns | 5.176 ns | 1.00 | 0.00 | 15 | 0 | 0 | - | NA | + | SmallMap_TryGetValue | 100 | 3.549 ns | 0.1293 ns | 0.2490 ns | 3.486 ns | 0.66 | 0.07 | 9 | 0 | 0 | - | NA | + | FHashMap_TryGetValue | 100 | 4.265 ns | 0.1406 ns | 0.1098 ns | 4.268 ns | 0.80 | 0.08 | 10 | 0 | -0 | - | NA | + + ## SmallMap SIMD vs... + + BenchmarkDotNet v0.15.0, Windows 11 (10.0.26100.4202/24H2/2024Update/HudsonValley) + Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET SDK 9.0.203 + [Host] : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2 + DefaultJob : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2 + + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|-----:|----------:|------------:| + | FHashMap_TryGetValue | 1 | 1.580 ns | 0.0212 ns | 0.0199 ns | 1.587 ns | 0.35 | 0.03 | 1 | - | NA | + | DictionarySlim_TryGetValue | 1 | 4.592 ns | 0.1535 ns | 0.4202 ns | 4.452 ns | 1.01 | 0.13 | 2 | - | NA | + | SmallMap_TryGetValue | 1 | 5.367 ns | 0.0884 ns | 0.0738 ns | 5.378 ns | 1.18 | 0.10 | 3 | - | NA | + | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 10 | 4.165 ns | 0.0467 ns | 0.0414 ns | 4.156 ns | 1.00 | 0.01 | 1 | - | NA | + | FHashMap_TryGetValue | 10 | 4.455 ns | 0.0285 ns | 0.0253 ns | 4.457 ns | 1.07 | 0.01 | 2 | - | NA | + | SmallMap_TryGetValue | 10 | 5.302 ns | 0.1556 ns | 0.1852 ns | 5.369 ns | 1.27 | 0.05 | 3 | - | NA | + | | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 4.777 ns | 0.1559 ns | 0.4472 ns | 4.576 ns | 1.01 | 0.13 | 1 | - | NA | + | FHashMap_TryGetValue | 100 | 6.020 ns | 0.1795 ns | 0.2335 ns | 6.017 ns | 1.27 | 0.13 | 2 | - | NA | + | SmallMap_TryGetValue | 100 | 6.021 ns | 0.1808 ns | 0.4827 ns | 6.293 ns | 1.27 | 0.15 | 2 | - | NA | + + ## SmallMap vs FHashMap11 + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|-----:|----------:|------------:| + | DictionarySlim_TryGetValue | 1000 | 2.225 us | 0.0445 us | 0.1313 us | 2.162 us | 1.00 | 0.08 | 1 | - | NA | + | SmallMap_TryGetValue | 1000 | 2.364 us | 0.0387 us | 0.0302 us | 2.377 us | 1.07 | 0.06 | 1 | - | NA | + | FHashMap11_TryGetValue | 1000 | 2.387 us | 0.0477 us | 0.1170 us | 2.434 us | 1.08 | 0.08 | 1 | - | NA | + + ## APL FHashMap11 + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | + |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|-----:|----------:|------------:| + | DictionarySlim_TryGetValue | 1000 | 2.195 us | 0.0437 us | 0.1217 us | 1.00 | 0.08 | 1 | - | NA | + | FHashMap11_TryGetValue | 1000 | 2.333 us | 0.0465 us | 0.0929 us | 1.07 | 0.07 | 2 | - | NA | + | SmallMap_TryGetValue | 1000 | 2.514 us | 0.0489 us | 0.0789 us | 1.15 | 0.07 | 3 | - | NA | + + ## Reshuffle the lookup comparison + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | + |--------------------------- |------ |------------:|----------:|-----------:|------:|--------:|-----:|----------:|------------:| + | SmallMap_TryGetValue | 10 | 44.90 ns | 0.536 ns | 0.419 ns | 0.99 | 0.04 | 1 | - | NA | + | DictionarySlim_TryGetValue | 10 | 45.43 ns | 0.941 ns | 1.857 ns | 1.00 | 0.06 | 1 | - | NA | + | | | | | | | | | | | + | DictionarySlim_TryGetValue | 100 | 385.36 ns | 4.621 ns | 4.097 ns | 1.00 | 0.01 | 1 | - | NA | + | SmallMap_TryGetValue | 100 | 513.52 ns | 5.702 ns | 5.054 ns | 1.33 | 0.02 | 2 | - | NA | + | | | | | | | | | | | + | SmallMap_TryGetValue | 1000 | 4,255.63 ns | 82.286 ns | 130.514 ns | 0.89 | 0.04 | 1 | - | NA | + | DictionarySlim_TryGetValue | 1000 | 4,794.12 ns | 95.912 ns | 189.322 ns | 1.00 | 0.06 | 2 | - | NA | + + ## Stack hybrid from the FEC is tested + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|--------:|---------:|------:|--------:|-----:|-------:|----------:|------------:| + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 203.5 ns | 3.88 ns | 3.63 ns | 0.70 | 0.03 | 1 | 0.1147 | 720 B | 0.67 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 277.6 ns | 3.61 ns | 5.52 ns | 0.95 | 0.04 | 2 | - | - | 0.00 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 292.1 ns | 5.84 ns | 12.19 ns | 1.00 | 0.06 | 2 | 0.1707 | 1072 B | 1.00 | + + ## After inlining in the FecHashMap + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |-----------:|---------:|---------:|-----------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 110.8 ns | 2.26 ns | 4.90 ns | 110.1 ns | 0.38 | 0.02 | 1 | - | - | - | 0.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 194.1 ns | 1.80 ns | 1.50 ns | 194.2 ns | 0.67 | 0.03 | 2 | 0.1147 | - | 720 B | 0.67 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 288.7 ns | 5.82 ns | 14.04 ns | 280.1 ns | 1.00 | 0.07 | 3 | 0.1707 | - | 1072 B | 1.00 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,003.8 ns | 30.26 ns | 31.08 ns | 1,995.1 ns | 1.00 | 0.02 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,306.8 ns | 18.67 ns | 15.59 ns | 2,307.0 ns | 1.15 | 0.02 | 2 | 0.8507 | - | 5344 B | 0.71 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 3,473.7 ns | 15.97 ns | 13.34 ns | 3,470.3 ns | 1.73 | 0.03 | 3 | 0.7782 | - | 4904 B | 0.65 | + + ## Probes are separate from the packed hashes and indexes into their own array + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|----------:|----------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.118 us | 0.0421 us | 0.0679 us | 1.00 | 0.04 | 1 | 1.1902 | 0.0229 | 7.31 KB | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.346 us | 0.0404 us | 0.0378 us | 1.11 | 0.04 | 2 | 0.8507 | - | 5.22 KB | 0.71 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 3.294 us | 0.0648 us | 0.0970 us | 1.56 | 0.07 | 3 | 0.8278 | - | 5.08 KB | 0.69 | + + ## GoldenRatio Hash adjustment + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.150 us | 0.0428 us | 0.0783 us | 2.115 us | 1.00 | 0.05 | 1 | 1.1902 | 0.0229 | 7.31 KB | 1.00 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 3.092 us | 0.0608 us | 0.0871 us | 3.068 us | 1.44 | 0.06 | 2 | 0.8278 | - | 5.08 KB | 0.69 | + + ## After probes go to their own array + padding + wrapping + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |------------:|----------:|------------:|------------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 111.6 ns | 1.16 ns | 0.97 ns | 111.8 ns | 0.38 | 0.01 | 1 | - | - | - | 0.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 205.1 ns | 3.46 ns | 3.24 ns | 204.4 ns | 0.70 | 0.02 | 2 | 0.1147 | - | 720 B | 0.67 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 292.2 ns | 5.53 ns | 5.68 ns | 291.4 ns | 1.00 | 0.03 | 3 | 0.1707 | - | 1072 B | 1.00 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,121.8 ns | 42.34 ns | 91.14 ns | 2,070.2 ns | 1.00 | 0.06 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,181.5 ns | 43.38 ns | 91.50 ns | 2,127.6 ns | 1.03 | 0.06 | 1 | 0.8659 | 0.0038 | 5440 B | 0.73 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,347.8 ns | 14.31 ns | 12.69 ns | 2,343.7 ns | 1.11 | 0.05 | 2 | 0.8507 | - | 5344 B | 0.71 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 23,640.4 ns | 280.02 ns | 248.23 ns | 23,620.0 ns | 1.00 | 0.01 | 1 | 9.1553 | 1.2817 | 57808 B | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 45,226.9 ns | 293.55 ns | 245.13 ns | 45,221.9 ns | 1.91 | 0.02 | 2 | 7.8735 | 0.3052 | 49544 B | 0.86 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 49,441.1 ns | 987.34 ns | 1,780.38 ns | 49,657.1 ns | 2.09 | 0.08 | 3 | 8.9111 | 0.0610 | 55976 B | 0.97 | + + ## Initial SIMD for Lookup - Regression in @perf + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |------------:|----------:|------------:|------------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 111.3 ns | 1.88 ns | 1.67 ns | 110.6 ns | 0.38 | 0.01 | 1 | - | - | - | 0.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 211.8 ns | 4.21 ns | 7.91 ns | 210.4 ns | 0.72 | 0.03 | 2 | 0.1147 | - | 720 B | 0.67 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 293.1 ns | 5.18 ns | 4.33 ns | 293.5 ns | 1.00 | 0.02 | 3 | 0.1707 | - | 1072 B | 1.00 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,112.8 ns | 19.17 ns | 16.00 ns | 2,108.5 ns | 1.00 | 0.01 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,433.3 ns | 47.99 ns | 95.84 ns | 2,383.6 ns | 1.15 | 0.05 | 2 | 0.8507 | - | 5344 B | 0.71 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,548.7 ns | 50.70 ns | 106.95 ns | 2,491.5 ns | 1.21 | 0.05 | 2 | 0.8659 | 0.0038 | 5440 B | 0.73 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 23,127.6 ns | 233.36 ns | 182.20 ns | 23,143.0 ns | 1.00 | 0.01 | 1 | 9.1553 | 1.2817 | 57808 B | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 45,630.3 ns | 912.10 ns | 1,735.36 ns | 44,696.8 ns | 1.97 | 0.08 | 2 | 7.8735 | 0.3052 | 49544 B | 0.86 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 49,295.9 ns | 327.44 ns | 273.43 ns | 49,364.4 ns | 2.13 | 0.02 | 3 | 8.9111 | 0.0610 | 55976 B | 0.97 | + + ## Using StableArrayEntries + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |------------:|------------:|------------:|------------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 113.3 ns | 2.32 ns | 5.42 ns | 110.1 ns | 0.37 | 0.02 | 1 | - | - | - | 0.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 204.5 ns | 2.73 ns | 2.42 ns | 204.0 ns | 0.67 | 0.03 | 2 | 0.1147 | - | 720 B | 0.67 | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 305.6 ns | 6.11 ns | 14.40 ns | 299.0 ns | 1.00 | 0.06 | 3 | 0.1707 | - | 1072 B | 1.00 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,172.7 ns | 43.50 ns | 102.53 ns | 2,138.9 ns | 1.00 | 0.07 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,317.2 ns | 46.33 ns | 100.71 ns | 2,255.9 ns | 1.07 | 0.07 | 1 | 0.4921 | - | 3088 B | 0.41 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,452.2 ns | 49.04 ns | 109.69 ns | 2,512.7 ns | 1.13 | 0.07 | 2 | 0.8507 | - | 5344 B | 0.71 | + | | | | | | | | | | | | | | + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 29,665.4 ns | 588.68 ns | 1,581.45 ns | 29,786.1 ns | 1.00 | 0.08 | 1 | 9.1553 | 1.2817 | 57808 B | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 55,723.9 ns | 1,098.60 ns | 1,078.97 ns | 55,599.6 ns | 1.88 | 0.12 | 2 | 7.8735 | 0.3052 | 49544 B | 0.86 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 63,373.1 ns | 1,260.87 ns | 2,106.62 ns | 63,624.5 ns | 2.14 | 0.14 | 3 | 6.2256 | 0.2441 | 39288 B | 0.68 | + + ## Packing indexes, hashes and probes together + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|---------:|---------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 24.17 us | 0.483 us | 1.176 us | 23.48 us | 1.00 | 0.07 | 1 | 9.1553 | 1.2817 | 56.45 KB | 1.00 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 52.85 us | 1.053 us | 1.871 us | 52.14 us | 2.19 | 0.13 | 2 | 7.8125 | 0.2441 | 48.05 KB | 0.85 | + + ## Packing indexes, hashes and probes together with Resize vs no Resize + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |-------------------------------------------------------------- |------ |---------:|---------:|---------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | FecHashMap_Init1000_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 36.08 us | 0.329 us | 0.275 us | 36.08 us | 0.66 | 0.03 | 1 | 7.9346 | 0.4883 | 48.73 KB | 1.01 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 55.17 us | 0.985 us | 2.453 us | 54.41 us | 1.00 | 0.06 | 2 | 7.8125 | 0.2441 | 48.05 KB | 1.00 | + + ## SmallMap is meta longs SingleArrayEntries with index|hash-higher-bits|probe + + BenchmarkDotNet v0.15.2, Windows 11 (10.0.26200.9457) + Unknown processor + .NET SDK 11.0.100-rc.1.26425.128 + [Host] : .NET 10.0.11 (10.0.1126.37416), X64 RyuJIT AVX2 + DefaultJob : .NET 10.0.11 (10.0.1126.37416), X64 RyuJIT AVX2 + + + | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | + |--------------------------------------------------------- |------ |---------:|---------:|---------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| + | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 23.41 us | 0.468 us | 1.066 us | 23.33 us | 1.00 | 0.06 | 1 | 9.1553 | 1.2817 | 56.45 KB | 1.00 | + | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 36.28 us | 0.718 us | 1.331 us | 37.02 us | 1.55 | 0.09 | 2 | 7.8735 | 0.3052 | 48.38 KB | 0.86 | + | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 43.45 us | 0.868 us | 1.652 us | 44.48 us | 1.86 | 0.11 | 3 | 7.8735 | - | 48.23 KB | 0.85 | + + */ + // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] + // [Params(10, 100, 1000)] + [Params(1000)] + public int Count; + + private Type[] _presentKeys; + private Type[] _randomPresentKeys; + + [GlobalSetup] + public void Populate() + { + _presentKeys = _keys.Take(Count).ToArray(); + _randomPresentKeys = _presentKeys.OrderBy(_ => _seed.Next()).ToArray(); + + // _mapV4 = V4_ImHashMap_AddOrUpdate(); + // _mapV3 = V3_ImHashMap_AddOrUpdate(); + // _mapV2 = V2_AddOrUpdate(); + // _partMapV4 = V4_PartitionedHashMap_AddOrUpdate(); + // _partMapV3 = V3_PartitionedHashMap_AddOrUpdate(); + // _typeDict = TypeDictionary_Add(); + // _dict = Dict(); + // _dictSlim = DictSlim(); + // _fHashMap7 = FillFHashMap7(); + // _fHashMap9 = FillFHashMap9(); + // _fHashMap91 = FillFHashMap91(); + // _smallMap = FillSmallMap(); + // _fHashMap11 = FillFHashMap11(); + + // _fecSmallMap = new FecSmallMapTypeString(); + // FillFecHashMap(ref _fecSmallMap); + + // _concurrentDict = ConcurrentDict(); + // _immutableDict = ImmutableDict(); + } - public void Add(TValue value) - { - lock (_lockObject) - { - var id = TypeKey.Id; - if (id >= _values.Length) - Array.Resize(ref _values, id * 2); - _values[id] = value; - } - } + #region Population - public TValue Get() - { - var id = TypeKey.Id; - return id >= _values.Length ? default : _values[id]; - } + private ImTools.V2.ImHashMap _mapV2; + public ImTools.V2.ImHashMap V2_AddOrUpdate() + { + var map = ImTools.V2.ImHashMap.Empty; - // ReSharper disable once UnusedTypeParameter - private static class TypeKey - { - // ReSharper disable once StaticMemberInGenericType - internal static readonly int Id = Interlocked.Increment(ref typeIndex); - } + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key, "a"); + + map = map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; } - [MemoryDiagnoser, RankColumn, Orderer(SummaryOrderPolicy.FastestToSlowest)] - // [HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.BranchMispredictions, HardwareCounter.BranchInstructions)] - public class Lookup - { - /* - ## 21.01.2019: All versions. - - Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |---------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault_v1 | 13.74 ns | 0.0686 ns | 0.0642 ns | 0.79 | - | - | - | - | - GetValueOrDefault | 17.43 ns | 0.0924 ns | 0.0864 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v2 | 19.15 ns | 0.0786 ns | 0.0656 ns | 1.10 | - | - | - | - | - GetValueOrDefault_v3 | 25.73 ns | 0.0711 ns | 0.0665 ns | 1.48 | - | - | - | - | - - ## For some reason dropping lookup speed with only changes to AddOrUpdate - - Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |---------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault_v1 | 13.89 ns | 0.0938 ns | 0.0877 ns | 0.80 | - | - | - | - | - GetValueOrDefault | 17.40 ns | 0.0888 ns | 0.0831 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v2 | 19.04 ns | 0.0712 ns | 0.0666 ns | 1.09 | - | - | - | - | - GetValueOrDefault_v3 | 25.93 ns | 0.0474 ns | 0.0420 ns | 1.49 | - | - | - | - | - - ## Got back some perf by moving GetValueOrDefault to static method and specializing for Type - - Method | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |---------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault_v1 | 13.87 ns | 0.0400 ns | 0.0355 ns | 0.85 | - | - | - | - | - GetValueOrDefault | 16.34 ns | 0.0932 ns | 0.0826 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v2 | 19.18 ns | 0.0460 ns | 0.0430 ns | 1.17 | - | - | - | - | - GetValueOrDefault_v3 | 25.96 ns | 0.0756 ns | 0.0707 ns | 1.59 | - | - | - | - | - - ## Benchmark against variety of inputs on par with Populate benchmark - - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault_v1 | 5 | 6.155 ns | 0.0321 ns | 0.0301 ns | 0.98 | 0.01 | - | - | - | - | - GetValueOrDefault | 5 | 6.267 ns | 0.0510 ns | 0.0452 ns | 1.00 | 0.00 | - | - | - | - | - GetValueOrDefault_v2 | 5 | 7.439 ns | 0.0763 ns | 0.0676 ns | 1.19 | 0.02 | - | - | - | - | - GetValueOrDefault_v3 | 5 | 9.558 ns | 0.0409 ns | 0.0383 ns | 1.52 | 0.01 | - | - | - | - | - | | | | | | | | | | | - GetValueOrDefault_v1 | 40 | 10.897 ns | 0.0673 ns | 0.0629 ns | 0.95 | 0.01 | - | - | - | - | - GetValueOrDefault | 40 | 11.467 ns | 0.0325 ns | 0.0304 ns | 1.00 | 0.00 | - | - | - | - | - GetValueOrDefault_v2 | 40 | 14.012 ns | 0.1092 ns | 0.1022 ns | 1.22 | 0.01 | - | - | - | - | - GetValueOrDefault_v3 | 40 | 19.945 ns | 0.1032 ns | 0.0965 ns | 1.74 | 0.01 | - | - | - | - | - | | | | | | | | | | | - GetValueOrDefault_v1 | 200 | 13.664 ns | 0.0291 ns | 0.0258 ns | 0.97 | 0.00 | - | - | - | - | - GetValueOrDefault | 200 | 14.051 ns | 0.0524 ns | 0.0491 ns | 1.00 | 0.00 | - | - | - | - | - GetValueOrDefault_v2 | 200 | 16.722 ns | 0.0568 ns | 0.0531 ns | 1.19 | 0.01 | - | - | - | - | - GetValueOrDefault_v3 | 200 | 24.473 ns | 0.0792 ns | 0.0702 ns | 1.74 | 0.01 | - | - | - | - | - | | | | | | | | | | | - GetValueOrDefault_v1 | 1000 | 14.213 ns | 0.1528 ns | 0.1354 ns | 0.96 | 0.01 | - | - | - | - | - GetValueOrDefault | 1000 | 14.805 ns | 0.0518 ns | 0.0485 ns | 1.00 | 0.00 | - | - | - | - | - GetValueOrDefault_v2 | 1000 | 16.645 ns | 0.0447 ns | 0.0419 ns | 1.12 | 0.01 | - | - | - | - | - GetValueOrDefault_v3 | 1000 | 27.489 ns | 0.0890 ns | 0.0832 ns | 1.86 | 0.01 | - | - | - | - | - - ## Adding aggressive inlining to the Data { Hash, Key, Value } properties - - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault | 5 | 5.853 ns | 0.0685 ns | 0.0607 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v1 | 5 | 5.913 ns | 0.0373 ns | 0.0349 ns | 1.01 | - | - | - | - | - | | | | | | | | | | - GetValueOrDefault | 40 | 9.649 ns | 0.0235 ns | 0.0220 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v1 | 40 | 10.266 ns | 0.0236 ns | 0.0221 ns | 1.06 | - | - | - | - | - | | | | | | | | | | - GetValueOrDefault | 200 | 11.613 ns | 0.0554 ns | 0.0491 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v1 | 200 | 12.052 ns | 0.0555 ns | 0.0520 ns | 1.04 | - | - | - | - | - - ## Using `!= Empty` instead of `.Height != 0` drops some perf - - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault_v1 | 5 | 5.933 ns | 0.0310 ns | 0.0290 ns | 0.93 | 0.03 | - | - | - | - | - GetValueOrDefault | 5 | 6.386 ns | 0.1807 ns | 0.1602 ns | 1.00 | 0.00 | - | - | - | - | - | | | | | | | | | | | - GetValueOrDefault | 40 | 9.820 ns | 0.0521 ns | 0.0488 ns | 1.00 | 0.00 | - | - | - | - | - GetValueOrDefault_v1 | 40 | 10.257 ns | 0.0300 ns | 0.0266 ns | 1.05 | 0.01 | - | - | - | - | - | | | | | | | | | | | - GetValueOrDefault | 200 | 11.717 ns | 0.0689 ns | 0.0644 ns | 1.00 | 0.00 | - | - | - | - | - GetValueOrDefault_v1 | 200 | 12.104 ns | 0.0548 ns | 0.0486 ns | 1.03 | 0.01 | - | - | - | - | - - ## Removing `.Height != 0` check completely did not change much, but let it stay cause less code is better - - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault | 5 | 5.903 ns | 0.0612 ns | 0.0573 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v1 | 5 | 5.931 ns | 0.0503 ns | 0.0470 ns | 1.00 | - | - | - | - | - | | | | | | | | | | - GetValueOrDefault | 40 | 9.636 ns | 0.0419 ns | 0.0392 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v1 | 40 | 10.231 ns | 0.0333 ns | 0.0312 ns | 1.06 | - | - | - | - | - | | | | | | | | | | - GetValueOrDefault | 200 | 11.637 ns | 0.0721 ns | 0.0602 ns | 1.00 | - | - | - | - | - GetValueOrDefault_v1 | 200 | 12.042 ns | 0.0607 ns | 0.0568 ns | 1.03 | - | - | - | - | - - ## TryFind - - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - ----------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - TryFind_v1 | 5 | 5.229 ns | 0.0307 ns | 0.0257 ns | 1.00 | - | - | - | - | - TryFind | 5 | 6.766 ns | 0.0695 ns | 0.0650 ns | 1.30 | - | - | - | - | - | | | | | | | | | | - TryFind_v1 | 40 | 9.268 ns | 0.0116 ns | 0.0108 ns | 1.00 | - | - | - | - | - TryFind | 40 | 9.755 ns | 0.0219 ns | 0.0205 ns | 1.05 | - | - | - | - | - | | | | | | | | | | - TryFind_v1 | 200 | 11.773 ns | 0.0558 ns | 0.0494 ns | 1.00 | - | - | - | - | - TryFind | 200 | 12.212 ns | 0.0456 ns | 0.0380 ns | 1.04 | - | - | - | - | - - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - ----------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - TryFind | 5 | 5.906 ns | 0.0191 ns | 0.0178 ns | 0.97 | - | - | - | - | - TryFind_v1 | 5 | 6.079 ns | 0.0947 ns | 0.0839 ns | 1.00 | - | - | - | - | - | | | | | | | | | | - TryFind | 40 | 9.211 ns | 0.0214 ns | 0.0200 ns | 0.87 | - | - | - | - | - TryFind_v1 | 40 | 10.566 ns | 0.0149 ns | 0.0132 ns | 1.00 | - | - | - | - | - | | | | | | | | | | - TryFind | 200 | 11.400 ns | 0.1152 ns | 0.1078 ns | 0.88 | - | - | - | - | - TryFind_v1 | 200 | 12.929 ns | 0.0712 ns | 0.0666 ns | 1.00 | - | - | - | - | - - ## GetOrDefault a bit optimized - - Method | Count | Mean | Error | StdDev | Ratio | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------- |------ |----------:|----------:|----------:|------:|------------:|------------:|------------:|--------------------:| - GetValueOrDefault | 5 | 6.782 ns | 0.0449 ns | 0.0398 ns | 0.96 | - | - | - | - | - GetValueOrDefault_v1 | 5 | 7.042 ns | 0.0659 ns | 0.0616 ns | 1.00 | - | - | - | - | - | | | | | | | | | | - GetValueOrDefault | 40 | 10.962 ns | 0.0866 ns | 0.0768 ns | 0.99 | - | - | - | - | - GetValueOrDefault_v1 | 40 | 11.094 ns | 0.0973 ns | 0.0813 ns | 1.00 | - | - | - | - | - | | | | | | | | | | - GetValueOrDefault | 200 | 13.329 ns | 0.0338 ns | 0.0299 ns | 0.97 | - | - | - | - | - GetValueOrDefault_v1 | 200 | 13.722 ns | 0.0537 ns | 0.0448 ns | 1.00 | - | - | - | - | - - ## The whole result for the docs - - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - ---------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - TryFind_v1 | 10 | 7.274 ns | 0.0410 ns | 0.0384 ns | 0.98 | 0.01 | - | - | - | - | - TryFind | 10 | 7.422 ns | 0.0237 ns | 0.0222 ns | 1.00 | 0.00 | - | - | - | - | - ConcurrentDict_TryGet | 10 | 21.664 ns | 0.0213 ns | 0.0189 ns | 2.92 | 0.01 | - | - | - | - | - ImmutableDict_TryGet | 10 | 71.199 ns | 0.1312 ns | 0.1228 ns | 9.59 | 0.03 | - | - | - | - | - | | | | | | | | | | | - TryFind_v1 | 100 | 8.426 ns | 0.0236 ns | 0.0221 ns | 0.91 | 0.00 | - | - | - | - | - TryFind | 100 | 9.304 ns | 0.0305 ns | 0.0270 ns | 1.00 | 0.00 | - | - | - | - | - ConcurrentDict_TryGet | 100 | 21.791 ns | 0.1072 ns | 0.0951 ns | 2.34 | 0.01 | - | - | - | - | - ImmutableDict_TryGet | 100 | 74.985 ns | 0.1053 ns | 0.0879 ns | 8.06 | 0.03 | - | - | - | - | - | | | | | | | | | | | - TryFind | 1000 | 13.837 ns | 0.0291 ns | 0.0272 ns | 1.00 | 0.00 | - | - | - | - | - TryFind_v1 | 1000 | 16.108 ns | 0.0415 ns | 0.0367 ns | 1.16 | 0.00 | - | - | - | - | - ConcurrentDict_TryGet | 1000 | 21.876 ns | 0.0325 ns | 0.0288 ns | 1.58 | 0.00 | - | - | - | - | - ImmutableDict_TryGet | 1000 | 83.563 ns | 0.1046 ns | 0.0873 ns | 6.04 | 0.01 | - | - | - | - | - - - ## 2019-03-28: Comparing vs `Dictionary`: - - Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - TryFind | 10 | 7.722 ns | 0.0451 ns | 0.0422 ns | 7.718 ns | 1.00 | 0.00 | - | - | - | - | - Dictionary_TryGetValue | 10 | 18.475 ns | 0.0502 ns | 0.0470 ns | 18.470 ns | 2.39 | 0.01 | - | - | - | - | - ConcurrentDictionary_TryGetValue | 10 | 22.661 ns | 0.0463 ns | 0.0433 ns | 22.653 ns | 2.93 | 0.02 | - | - | - | - | - ImmutableDict_TryGet | 10 | 72.911 ns | 1.5234 ns | 2.1355 ns | 74.134 ns | 9.25 | 0.23 | - | - | - | - | - | | | | | | | | | | | | - TryFind | 100 | 9.987 ns | 0.0543 ns | 0.0508 ns | 9.978 ns | 1.00 | 0.00 | - | - | - | - | - Dictionary_TryGetValue | 100 | 18.110 ns | 0.0644 ns | 0.0602 ns | 18.088 ns | 1.81 | 0.01 | - | - | - | - | - ConcurrentDictionary_TryGetValue | 100 | 24.402 ns | 0.0978 ns | 0.0915 ns | 24.435 ns | 2.44 | 0.02 | - | - | - | - | - ImmutableDict_TryGet | 100 | 76.689 ns | 0.3632 ns | 0.3397 ns | 76.704 ns | 7.68 | 0.04 | - | - | - | - | - | | | | | | | | | | | | - TryFind | 1000 | 12.600 ns | 0.1506 ns | 0.1335 ns | 12.551 ns | 1.00 | 0.00 | - | - | - | - | - Dictionary_TryGetValue | 1000 | 19.023 ns | 0.0575 ns | 0.0538 ns | 19.036 ns | 1.51 | 0.02 | - | - | - | - | - ConcurrentDictionary_TryGetValue | 1000 | 22.651 ns | 0.1238 ns | 0.1097 ns | 22.613 ns | 1.80 | 0.02 | - | - | - | - | - ImmutableDict_TryGet | 1000 | 83.608 ns | 0.3105 ns | 0.2904 ns | 83.612 ns | 6.64 | 0.07 | - | - | - | - | - - ## 2019-03-29: Comparing vs `DictionarySlim`: - - Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - --------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - DictionarySlim_TryGetValue | 10 | 8.228 ns | 0.0682 ns | 0.0604 ns | 1.00 | 0.01 | - | - | - | - | - TryFind | 10 | 8.257 ns | 0.0796 ns | 0.0706 ns | 1.00 | 0.00 | - | - | - | - | - Dictionary_TryGetValue | 10 | 19.615 ns | 0.0251 ns | 0.0209 ns | 2.38 | 0.02 | - | - | - | - | - ConcurrentDictionary_TryGetValue | 10 | 22.339 ns | 0.0922 ns | 0.0863 ns | 2.71 | 0.03 | - | - | - | - | - ImmutableDict_TryGet | 10 | 69.872 ns | 0.2699 ns | 0.2524 ns | 8.46 | 0.07 | - | - | - | - | - | | | | | | | | | | | - DictionarySlim_TryGetValue | 100 | 8.351 ns | 0.1613 ns | 0.1508 ns | 0.69 | 0.01 | - | - | - | - | - TryFind | 100 | 12.144 ns | 0.0570 ns | 0.0533 ns | 1.00 | 0.00 | - | - | - | - | - Dictionary_TryGetValue | 100 | 17.985 ns | 0.0880 ns | 0.0823 ns | 1.48 | 0.01 | - | - | - | - | - ConcurrentDictionary_TryGetValue | 100 | 22.312 ns | 0.0564 ns | 0.0471 ns | 1.84 | 0.01 | - | - | - | - | - ImmutableDict_TryGet | 100 | 75.374 ns | 0.3042 ns | 0.2846 ns | 6.21 | 0.04 | - | - | - | - | - | | | | | | | | | | | - DictionarySlim_TryGetValue | 1000 | 8.202 ns | 0.0713 ns | 0.0667 ns | 0.55 | 0.01 | - | - | - | - | - TryFind | 1000 | 14.919 ns | 0.1101 ns | 0.0919 ns | 1.00 | 0.00 | - | - | - | - | - Dictionary_TryGetValue | 1000 | 18.073 ns | 0.2415 ns | 0.2141 ns | 1.21 | 0.02 | - | - | - | - | - ConcurrentDictionary_TryGetValue | 1000 | 22.406 ns | 0.1039 ns | 0.0921 ns | 1.50 | 0.01 | - | - | - | - | - ImmutableDict_TryGet | 1000 | 84.215 ns | 0.2835 ns | 0.2513 ns | 5.65 | 0.04 | - | - | - | - | - - ## 2019-04-08: Full test - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - | ImHashMap_TryFind | 10 | 9.072 ns | 0.0301 ns | 0.0282 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 10 | 8.405 ns | 0.0124 ns | 0.0116 ns | 0.93 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 10 | 8.199 ns | 0.0118 ns | 0.0105 ns | 0.90 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 18.151 ns | 0.0724 ns | 0.0677 ns | 2.00 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 22.281 ns | 0.1872 ns | 0.1462 ns | 2.45 | 0.02 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 70.143 ns | 0.2833 ns | 0.2650 ns | 7.73 | 0.04 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 100 | 12.698 ns | 0.0545 ns | 0.0510 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 100 | 12.440 ns | 0.0145 ns | 0.0129 ns | 0.98 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 100 | 8.197 ns | 0.0157 ns | 0.0139 ns | 0.65 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 100 | 18.108 ns | 0.0263 ns | 0.0205 ns | 1.43 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 22.834 ns | 0.0627 ns | 0.0524 ns | 1.80 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 76.253 ns | 0.2767 ns | 0.2311 ns | 6.00 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 1000 | 14.960 ns | 0.0457 ns | 0.0427 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 1000 | 14.614 ns | 0.0508 ns | 0.0451 ns | 0.98 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 1000 | 8.209 ns | 0.0534 ns | 0.0499 ns | 0.55 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 1000 | 18.256 ns | 0.0383 ns | 0.0320 ns | 1.22 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 22.261 ns | 0.1509 ns | 0.1411 ns | 1.49 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 83.095 ns | 0.3395 ns | 0.3176 ns | 5.55 | 0.03 | - | - | - | - | - - ## 2019-04-08: Different variants tested - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - | ImHashMap_TryFind2 | 10 | 7.979 ns | 0.0446 ns | 0.0417 ns | 0.93 | 0.01 | - | - | - | - | - | ImHashMap_TryFind3 | 10 | 7.908 ns | 0.0295 ns | 0.0262 ns | 0.92 | 0.00 | - | - | - | - | - | ImHashMap_TryFind | 10 | 8.608 ns | 0.0186 ns | 0.0174 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 10 | 8.248 ns | 0.0262 ns | 0.0232 ns | 0.96 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 10 | 7.212 ns | 0.0194 ns | 0.0181 ns | 0.84 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 17.707 ns | 0.0631 ns | 0.0590 ns | 2.06 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 22.210 ns | 0.1074 ns | 0.1004 ns | 2.58 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 72.093 ns | 0.4162 ns | 0.3893 ns | 8.37 | 0.05 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind2 | 100 | 11.245 ns | 0.0174 ns | 0.0163 ns | 0.92 | 0.00 | - | - | - | - | - | ImHashMap_TryFind3 | 100 | 11.488 ns | 0.0801 ns | 0.0710 ns | 0.94 | 0.01 | - | - | - | - | - | ImHashMap_TryFind | 100 | 12.165 ns | 0.0141 ns | 0.0118 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 100 | 12.272 ns | 0.0367 ns | 0.0343 ns | 1.01 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 100 | 7.019 ns | 0.0516 ns | 0.0458 ns | 0.58 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 100 | 17.825 ns | 0.1278 ns | 0.1196 ns | 1.47 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 22.189 ns | 0.1034 ns | 0.0968 ns | 1.82 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 75.564 ns | 0.3778 ns | 0.3534 ns | 6.21 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind2 | 1000 | 15.909 ns | 0.0924 ns | 0.0864 ns | 1.07 | 0.01 | - | - | - | - | - | ImHashMap_TryFind3 | 1000 | 13.643 ns | 0.0715 ns | 0.0669 ns | 0.92 | 0.01 | - | - | - | - | - | ImHashMap_TryFind | 1000 | 14.819 ns | 0.0465 ns | 0.0412 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 1000 | 14.541 ns | 0.0555 ns | 0.0520 ns | 0.98 | 0.01 | - | - | - | - | - | DictionarySlim_TryGetValue | 1000 | 7.678 ns | 0.0341 ns | 0.0302 ns | 0.52 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 1000 | 17.664 ns | 0.0403 ns | 0.0377 ns | 1.19 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 22.010 ns | 0.0497 ns | 0.0465 ns | 1.48 | 0.00 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 83.661 ns | 0.4033 ns | 0.3772 ns | 5.65 | 0.03 | - | - | - | - | - - ## Selecting the 3rd variant: - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | - |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------------:|------------:|------------:|--------------------:| - | ImHashMap_TryFind | 10 | 8.078 ns | 0.0405 ns | 0.0379 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 10 | 8.224 ns | 0.0114 ns | 0.0101 ns | 1.02 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 10 | 7.387 ns | 0.0406 ns | 0.0380 ns | 0.91 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 17.917 ns | 0.0429 ns | 0.0401 ns | 2.22 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 22.256 ns | 0.0726 ns | 0.0643 ns | 2.75 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 70.638 ns | 0.6266 ns | 0.5861 ns | 8.74 | 0.09 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 100 | 11.577 ns | 0.0168 ns | 0.0141 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 100 | 12.329 ns | 0.0295 ns | 0.0276 ns | 1.07 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 100 | 7.410 ns | 0.0398 ns | 0.0353 ns | 0.64 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 100 | 17.890 ns | 0.0425 ns | 0.0377 ns | 1.55 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 22.240 ns | 0.0654 ns | 0.0580 ns | 1.92 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 78.697 ns | 1.1242 ns | 1.0516 ns | 6.78 | 0.08 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 1000 | 13.731 ns | 0.0292 ns | 0.0258 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 1000 | 14.553 ns | 0.0370 ns | 0.0346 ns | 1.06 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 1000 | 7.345 ns | 0.0208 ns | 0.0194 ns | 0.53 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 1000 | 18.672 ns | 0.0483 ns | 0.0451 ns | 1.36 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 22.150 ns | 0.1141 ns | 0.1068 ns | 1.61 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 82.402 ns | 0.9798 ns | 0.9165 ns | 6.01 | 0.07 | - | - | - | - | - - ## V2: - - BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 - Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=3.1.100 - [Host] : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT - DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT - - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | ImHashMap_TryFind | 1 | 4.755 ns | 0.1690 ns | 0.1878 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 1 | 3.657 ns | 0.0458 ns | 0.0406 ns | 0.78 | 0.04 | - | - | - | - | - | ImHashMapSlots_TryFind | 1 | 2.518 ns | 0.0149 ns | 0.0132 ns | 0.53 | 0.02 | - | - | - | - | - | DictionarySlim_TryGetValue | 1 | 6.804 ns | 0.0272 ns | 0.0254 ns | 1.44 | 0.07 | - | - | - | - | - | Dictionary_TryGetValue | 1 | 16.495 ns | 0.3948 ns | 0.4993 ns | 3.49 | 0.16 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1 | 15.812 ns | 0.1016 ns | 0.0951 ns | 3.35 | 0.15 | - | - | - | - | - | ImmutableDict_TryGet | 1 | 24.346 ns | 0.1253 ns | 0.1172 ns | 5.16 | 0.23 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 10 | 6.080 ns | 0.0235 ns | 0.0208 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 10 | 6.091 ns | 0.0707 ns | 0.0590 ns | 1.00 | 0.01 | - | - | - | - | - | ImHashMapSlots_TryFind | 10 | 2.517 ns | 0.0206 ns | 0.0193 ns | 0.41 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 10 | 6.670 ns | 0.0278 ns | 0.0260 ns | 1.10 | 0.01 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 16.202 ns | 0.0634 ns | 0.0562 ns | 2.66 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 15.764 ns | 0.0659 ns | 0.0617 ns | 2.59 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 26.282 ns | 0.2232 ns | 0.2088 ns | 4.32 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 100 | 9.350 ns | 0.0315 ns | 0.0295 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 100 | 10.752 ns | 0.0199 ns | 0.0166 ns | 1.15 | 0.00 | - | - | - | - | - | ImHashMapSlots_TryFind | 100 | 5.664 ns | 0.0391 ns | 0.0366 ns | 0.61 | 0.01 | - | - | - | - | - | DictionarySlim_TryGetValue | 100 | 6.665 ns | 0.0287 ns | 0.0254 ns | 0.71 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 100 | 17.007 ns | 0.0615 ns | 0.0576 ns | 1.82 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 16.024 ns | 0.3340 ns | 0.3124 ns | 1.71 | 0.03 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 30.667 ns | 0.1026 ns | 0.0960 ns | 3.28 | 0.02 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 1000 | 12.729 ns | 0.0393 ns | 0.0368 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 1000 | 13.352 ns | 0.0638 ns | 0.0597 ns | 1.05 | 0.00 | - | - | - | - | - | ImHashMapSlots_TryFind | 1000 | 7.131 ns | 0.0246 ns | 0.0230 ns | 0.56 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 1000 | 6.686 ns | 0.0317 ns | 0.0297 ns | 0.53 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 1000 | 16.848 ns | 0.0593 ns | 0.0526 ns | 1.32 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 15.684 ns | 0.0695 ns | 0.0650 ns | 1.23 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 33.579 ns | 0.1077 ns | 0.0955 ns | 2.64 | 0.01 | - | - | - | - | - - - BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 - Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=3.1.100 - [Host] : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT - DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT - - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |-------------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | ImHashMap_TryFind | 1 | 4.365 ns | 0.0198 ns | 0.0185 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMapSlots32_TryFind | 1 | 2.400 ns | 0.0229 ns | 0.0214 ns | 0.55 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 1 | 3.385 ns | 0.0071 ns | 0.0063 ns | 0.78 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1 | 4.892 ns | 0.0134 ns | 0.0118 ns | 1.12 | 0.01 | - | - | - | - | - | Experimental_ImHashMapSlots32_TryFind | 1 | 6.268 ns | 0.0455 ns | 0.0425 ns | 1.44 | 0.01 | - | - | - | - | - | Experimental_ImHashMapSlots64_TryFind | 1 | 6.175 ns | 0.0335 ns | 0.0313 ns | 1.41 | 0.01 | - | - | - | - | - | DictionarySlim_TryGetValue | 1 | 6.190 ns | 0.0524 ns | 0.0490 ns | 1.42 | 0.02 | - | - | - | - | - | Dictionary_TryGetValue | 1 | 15.425 ns | 0.0636 ns | 0.0595 ns | 3.53 | 0.02 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1 | 14.754 ns | 0.0807 ns | 0.0716 ns | 3.38 | 0.02 | - | - | - | - | - | ImmutableDict_TryGet | 1 | 23.449 ns | 0.1027 ns | 0.0960 ns | 5.37 | 0.04 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 10 | 5.449 ns | 0.0216 ns | 0.0202 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMapSlots32_TryFind | 10 | 2.911 ns | 0.0089 ns | 0.0083 ns | 0.53 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 10 | 6.019 ns | 0.0129 ns | 0.0115 ns | 1.10 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 10 | 8.060 ns | 0.0239 ns | 0.0224 ns | 1.48 | 0.01 | - | - | - | - | - | Experimental_ImHashMapSlots32_TryFind | 10 | 5.605 ns | 0.0261 ns | 0.0244 ns | 1.03 | 0.00 | - | - | - | - | - | Experimental_ImHashMapSlots64_TryFind | 10 | 5.504 ns | 0.0456 ns | 0.0404 ns | 1.01 | 0.01 | - | - | - | - | - | DictionarySlim_TryGetValue | 10 | 5.216 ns | 0.0252 ns | 0.0236 ns | 0.96 | 0.01 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 15.346 ns | 0.0732 ns | 0.0685 ns | 2.82 | 0.02 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 14.870 ns | 0.1094 ns | 0.1023 ns | 2.73 | 0.02 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 24.888 ns | 0.0798 ns | 0.0746 ns | 4.57 | 0.02 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 100 | 7.665 ns | 0.0208 ns | 0.0184 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMapSlots32_TryFind | 100 | 5.240 ns | 0.0238 ns | 0.0223 ns | 0.68 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 100 | 9.222 ns | 0.0223 ns | 0.0208 ns | 1.20 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 100 | 12.248 ns | 0.0584 ns | 0.0546 ns | 1.60 | 0.01 | - | - | - | - | - | Experimental_ImHashMapSlots32_TryFind | 100 | 7.546 ns | 0.0966 ns | 0.0903 ns | 0.98 | 0.01 | - | - | - | - | - | Experimental_ImHashMapSlots64_TryFind | 100 | 5.780 ns | 0.0250 ns | 0.0234 ns | 0.75 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 100 | 6.452 ns | 0.0538 ns | 0.0503 ns | 0.84 | 0.01 | - | - | - | - | - | Dictionary_TryGetValue | 100 | 15.705 ns | 0.0586 ns | 0.0549 ns | 2.05 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 15.321 ns | 0.0413 ns | 0.0386 ns | 2.00 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 27.937 ns | 0.1050 ns | 0.0982 ns | 3.64 | 0.02 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 1000 | 11.459 ns | 0.0306 ns | 0.0286 ns | 1.00 | 0.00 | - | - | - | - | - | ImHashMapSlots32_TryFind | 1000 | 8.633 ns | 0.0154 ns | 0.0144 ns | 0.75 | 0.00 | - | - | - | - | - | ImHashMap_TryFind_V1 | 1000 | 12.486 ns | 0.0527 ns | 0.0493 ns | 1.09 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1000 | 16.002 ns | 0.0378 ns | 0.0353 ns | 1.40 | 0.00 | - | - | - | - | - | Experimental_ImHashMapSlots32_TryFind | 1000 | 11.673 ns | 0.0638 ns | 0.0597 ns | 1.02 | 0.00 | - | - | - | - | - | Experimental_ImHashMapSlots64_TryFind | 1000 | 9.817 ns | 0.0123 ns | 0.0115 ns | 0.86 | 0.00 | - | - | - | - | - | DictionarySlim_TryGetValue | 1000 | 6.469 ns | 0.0428 ns | 0.0401 ns | 0.56 | 0.00 | - | - | - | - | - | Dictionary_TryGetValue | 1000 | 19.170 ns | 0.0708 ns | 0.0628 ns | 1.67 | 0.01 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 16.273 ns | 0.4046 ns | 0.5116 ns | 1.43 | 0.05 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 29.662 ns | 0.1529 ns | 0.1355 ns | 2.59 | 0.01 | - | - | - | - | - - ## V3 - baseline - - BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.572 (2004/?/20H1) - Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores - .NET Core SDK=3.1.403 - [Host] : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT - DefaultJob : .NET Core 3.1.9 (CoreCLR 4.700.20.47201, CoreFX 4.700.20.47203), X64 RyuJIT - - ## v3 - baseline - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |------------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | ImHashMap_TryFind | 1 | 4.614 ns | 0.1073 ns | 0.0951 ns | 4.589 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1 | 7.398 ns | 0.0926 ns | 0.0821 ns | 7.413 ns | 1.60 | 0.04 | - | - | - | - | - | ImHashMap234_TryFind | 1 | 4.912 ns | 0.0595 ns | 0.0528 ns | 4.903 ns | 1.07 | 0.03 | - | - | - | - | - | | | | | | | | | | | | | - | ImHashMap_TryFind | 10 | 8.205 ns | 0.0765 ns | 0.0639 ns | 8.186 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 10 | 10.689 ns | 0.2738 ns | 0.2561 ns | 10.684 ns | 1.31 | 0.03 | - | - | - | - | - | ImHashMap234_TryFind | 10 | 6.418 ns | 0.0731 ns | 0.0611 ns | 6.412 ns | 0.78 | 0.01 | - | - | - | - | - | | | | | | | | | | | | | - | ImHashMap_TryFind | 100 | 10.398 ns | 0.0434 ns | 0.0385 ns | 10.391 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 100 | 13.982 ns | 0.2432 ns | 0.2275 ns | 13.946 ns | 1.35 | 0.02 | - | - | - | - | - | ImHashMap234_TryFind | 100 | 10.759 ns | 0.1757 ns | 0.1467 ns | 10.761 ns | 1.03 | 0.01 | - | - | - | - | - | | | | | | | | | | | | | - | ImHashMap_TryFind | 1000 | 14.397 ns | 0.2039 ns | 0.1907 ns | 14.369 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1000 | 30.810 ns | 1.0842 ns | 2.9681 ns | 29.878 ns | 2.25 | 0.38 | - | - | - | - | - | ImHashMap234_TryFind | 1000 | 19.953 ns | 0.4996 ns | 0.4429 ns | 19.772 ns | 1.39 | 0.03 | - | - | - | - | - - ### Leaf3Plus1 - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | ImHashMap_TryFind | 1 | 5.611 ns | 0.1067 ns | 0.0998 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1 | 6.348 ns | 0.2010 ns | 0.2150 ns | 1.14 | 0.05 | - | - | - | - | - | ImHashMap234_TryFind | 1 | 4.572 ns | 0.0966 ns | 0.0904 ns | 0.82 | 0.02 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 10 | 7.582 ns | 0.0703 ns | 0.0587 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 10 | 10.133 ns | 0.1315 ns | 0.1098 ns | 1.34 | 0.02 | - | - | - | - | - | ImHashMap234_TryFind | 10 | 5.763 ns | 0.0670 ns | 0.0594 ns | 0.76 | 0.01 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 100 | 10.642 ns | 0.2276 ns | 0.1901 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 100 | 13.526 ns | 0.1563 ns | 0.1385 ns | 1.27 | 0.03 | - | - | - | - | - | ImHashMap234_TryFind | 100 | 9.271 ns | 0.1695 ns | 0.1503 ns | 0.87 | 0.02 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 1000 | 14.909 ns | 0.2118 ns | 0.1981 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1000 | 37.315 ns | 1.4352 ns | 4.2316 ns | 2.76 | 0.21 | - | - | - | - | - | ImHashMap234_TryFind | 1000 | 24.536 ns | 0.6190 ns | 0.7602 ns | 1.66 | 0.06 | - | - | - | - | - - ### Leaf5Plus1 + Leaf3Plus1 - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | ImHashMap_TryFind | 1 | 6.659 ns | 0.1975 ns | 0.1751 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1 | 8.220 ns | 0.1876 ns | 0.1842 ns | 1.23 | 0.05 | - | - | - | - | - | ImHashMap234_TryFind | 1 | 5.644 ns | 0.1702 ns | 0.1592 ns | 0.85 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 5 | 7.223 ns | 0.2332 ns | 0.2068 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 5 | 9.832 ns | 0.2499 ns | 0.2337 ns | 1.36 | 0.05 | - | - | - | - | - | ImHashMap234_TryFind | 5 | 5.353 ns | 0.1591 ns | 0.1410 ns | 0.74 | 0.04 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 10 | 8.325 ns | 0.1680 ns | 0.1403 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 10 | 12.221 ns | 0.3406 ns | 0.5098 ns | 1.50 | 0.08 | - | - | - | - | - | ImHashMap234_TryFind | 10 | 7.012 ns | 0.2045 ns | 0.2273 ns | 0.85 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 100 | 12.118 ns | 0.3031 ns | 0.4629 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 100 | 18.145 ns | 0.4551 ns | 0.8321 ns | 1.50 | 0.08 | - | - | - | - | - | ImHashMap234_TryFind | 100 | 11.908 ns | 0.2829 ns | 0.3679 ns | 0.97 | 0.04 | - | - | - | - | - | | | | | | | | | | | | - | ImHashMap_TryFind | 1000 | 17.113 ns | 0.3848 ns | 0.3411 ns | 1.00 | 0.00 | - | - | - | - | - | Experimental_ImHashMap_TryFind | 1000 | 26.034 ns | 0.6183 ns | 0.8255 ns | 1.52 | 0.07 | - | - | - | - | - | ImHashMap234_TryFind | 1000 | 16.927 ns | 0.3743 ns | 0.5828 ns | 0.99 | 0.05 | - | - | - | - | - - ### V3 candidate - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |----------------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | V2_ImHashMap_AVL_TryFind | 1 | 6.084 ns | 0.1295 ns | 0.1082 ns | 1.00 | 0.00 | - | - | - | - | - | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 1 | 7.319 ns | 0.2009 ns | 0.1879 ns | 1.20 | 0.03 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 1 | 5.372 ns | 0.1142 ns | 0.0954 ns | 0.88 | 0.02 | - | - | - | - | - | V3_PartitionedHashMap_23Tree_TryFind | 1 | 6.578 ns | 0.1517 ns | 0.1345 ns | 1.08 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 5 | 6.867 ns | 0.1035 ns | 0.0918 ns | 1.00 | 0.00 | - | - | - | - | - | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 5 | 9.599 ns | 0.1509 ns | 0.1411 ns | 1.40 | 0.03 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 5 | 5.319 ns | 0.1028 ns | 0.1142 ns | 0.77 | 0.02 | - | - | - | - | - | V3_PartitionedHashMap_23Tree_TryFind | 5 | 6.550 ns | 0.0856 ns | 0.0801 ns | 0.95 | 0.01 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 10 | 7.646 ns | 0.0905 ns | 0.0802 ns | 1.00 | 0.00 | - | - | - | - | - | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 10 | 11.002 ns | 0.2011 ns | 0.1881 ns | 1.44 | 0.03 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 10 | 6.986 ns | 0.2160 ns | 0.2122 ns | 0.91 | 0.03 | - | - | - | - | - | V3_PartitionedHashMap_23Tree_TryFind | 10 | 6.714 ns | 0.2153 ns | 0.1798 ns | 0.88 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 100 | 12.413 ns | 0.3100 ns | 0.2748 ns | 1.00 | 0.00 | - | - | - | - | - | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 100 | 15.134 ns | 0.2375 ns | 0.2917 ns | 1.23 | 0.04 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 100 | 11.147 ns | 0.2990 ns | 0.3443 ns | 0.90 | 0.03 | - | - | - | - | - | V3_PartitionedHashMap_23Tree_TryFind | 100 | 8.245 ns | 0.1473 ns | 0.1150 ns | 0.67 | 0.01 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 1000 | 16.528 ns | 0.1799 ns | 0.1502 ns | 1.00 | 0.00 | - | - | - | - | - | V2_ImHashMap_AVLOptimizedForAdd_TryFind | 1000 | 22.341 ns | 0.2584 ns | 0.2291 ns | 1.35 | 0.02 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 1000 | 13.178 ns | 0.2153 ns | 0.2014 ns | 0.80 | 0.01 | - | - | - | - | - | V3_PartitionedHashMap_23Tree_TryFind | 1000 | 11.198 ns | 0.2784 ns | 0.3094 ns | 0.68 | 0.02 | - | - | - | - | - - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | V3_ImHashMap_23Tree_TryFind | 1 | 4.467 ns | 0.0977 ns | 0.0816 ns | 1.00 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1 | 19.103 ns | 0.2302 ns | 0.2040 ns | 4.28 | 0.08 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 5 | 4.588 ns | 0.1057 ns | 0.0937 ns | 1.00 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 5 | 19.000 ns | 0.1738 ns | 0.1625 ns | 4.15 | 0.09 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 10 | 5.948 ns | 0.1194 ns | 0.1116 ns | 1.00 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 19.598 ns | 0.4600 ns | 0.4303 ns | 3.30 | 0.11 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 100 | 9.838 ns | 0.1607 ns | 0.1342 ns | 1.00 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 20.336 ns | 0.3591 ns | 0.2998 ns | 2.07 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 1000 | 11.980 ns | 0.2353 ns | 0.2201 ns | 1.00 | 0.00 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 19.939 ns | 0.4273 ns | 0.8824 ns | 1.73 | 0.10 | - | - | - | - | - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |----------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | V3_ImHashMap_23Tree_TryFind | 1 | 4.647 ns | 0.0516 ns | 0.0431 ns | 1.00 | 0.00 | - | - | - | - | - | ImmutableDict_TryGet | 1 | 23.396 ns | 0.3703 ns | 0.3092 ns | 5.04 | 0.10 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 5 | 5.242 ns | 0.1822 ns | 0.3143 ns | 1.00 | 0.00 | - | - | - | - | - | ImmutableDict_TryGet | 5 | 23.739 ns | 0.1941 ns | 0.1816 ns | 4.42 | 0.32 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 10 | 5.749 ns | 0.1206 ns | 0.1128 ns | 1.00 | 0.00 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 24.769 ns | 0.3152 ns | 0.2949 ns | 4.31 | 0.11 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 100 | 8.252 ns | 0.1754 ns | 0.1465 ns | 1.00 | 0.00 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 27.288 ns | 0.6118 ns | 0.6009 ns | 3.31 | 0.10 | - | - | - | - | - | | | | | | | | | | | | - | V3_ImHashMap_23Tree_TryFind | 1000 | 12.053 ns | 0.1955 ns | 0.1829 ns | 1.00 | 0.00 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 30.762 ns | 0.3980 ns | 0.3723 ns | 2.55 | 0.04 | - | - | - | - | - - ### Branch3 as two Branch2 -- looks ok - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |----------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | V2_ImHashMap_AVL_TryFind | 1 | 6.236 ns | 0.2150 ns | 0.3084 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 1 | 5.467 ns | 0.1410 ns | 0.1250 ns | 0.85 | 0.05 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 5 | 7.390 ns | 0.2037 ns | 0.1701 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 5 | 5.552 ns | 0.1412 ns | 0.1252 ns | 0.75 | 0.02 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 10 | 9.402 ns | 0.1381 ns | 0.1153 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 10 | 6.820 ns | 0.1766 ns | 0.1652 ns | 0.73 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 100 | 11.085 ns | 0.2337 ns | 0.2186 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 100 | 10.257 ns | 0.2037 ns | 0.1905 ns | 0.93 | 0.02 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 1000 | 16.883 ns | 0.2438 ns | 0.2280 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 1000 | 15.516 ns | 0.3248 ns | 0.4554 ns | 0.93 | 0.04 | - | - | - | - | - - ### V3 RTM - - BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 - Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=5.0.201 - [Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT - DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT - - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | V2_ImHashMap_AVL_TryFind | 1 | 5.100 ns | 0.1212 ns | 0.1134 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 1 | 5.539 ns | 0.1676 ns | 0.1568 ns | 1.09 | 0.03 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 1 | 5.813 ns | 0.1844 ns | 0.1973 ns | 1.13 | 0.04 | - | - | - | - | - | DictionarySlim_TryGetValue | 1 | 6.614 ns | 0.0839 ns | 0.0744 ns | 1.29 | 0.03 | - | - | - | - | - | Dictionary_TryGetValue | 1 | 16.495 ns | 0.1270 ns | 0.1126 ns | 3.23 | 0.06 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1 | 12.715 ns | 0.1584 ns | 0.1323 ns | 2.49 | 0.05 | - | - | - | - | - | ImmutableDict_TryGet | 1 | 22.615 ns | 0.3097 ns | 0.2418 ns | 4.42 | 0.08 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 10 | 6.270 ns | 0.1421 ns | 0.1187 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 10 | 6.597 ns | 0.1095 ns | 0.1024 ns | 1.06 | 0.02 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 10 | 5.416 ns | 0.0908 ns | 0.0805 ns | 0.87 | 0.02 | - | - | - | - | - | DictionarySlim_TryGetValue | 10 | 7.543 ns | 0.1274 ns | 0.1064 ns | 1.20 | 0.02 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 16.907 ns | 0.1892 ns | 0.1769 ns | 2.70 | 0.05 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 12.694 ns | 0.1740 ns | 0.1453 ns | 2.03 | 0.04 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 24.049 ns | 0.3034 ns | 0.2838 ns | 3.84 | 0.07 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 100 | 9.459 ns | 0.2667 ns | 0.2739 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 100 | 9.526 ns | 0.1814 ns | 0.1697 ns | 1.01 | 0.03 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 100 | 5.827 ns | 0.0867 ns | 0.0768 ns | 0.62 | 0.02 | - | - | - | - | - | DictionarySlim_TryGetValue | 100 | 6.897 ns | 0.1033 ns | 0.0967 ns | 0.73 | 0.02 | - | - | - | - | - | Dictionary_TryGetValue | 100 | 18.189 ns | 0.2100 ns | 0.1640 ns | 1.92 | 0.06 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 13.412 ns | 0.2041 ns | 0.1909 ns | 1.42 | 0.04 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 26.023 ns | 0.3854 ns | 0.3417 ns | 2.76 | 0.08 | - | - | - | - | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_TryFind | 1000 | 15.172 ns | 0.2617 ns | 0.2320 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_23Tree_TryFind | 1000 | 14.473 ns | 0.2030 ns | 0.1799 ns | 0.95 | 0.01 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 1000 | 8.182 ns | 0.0809 ns | 0.0756 ns | 0.54 | 0.01 | - | - | - | - | - | DictionarySlim_TryGetValue | 1000 | 6.893 ns | 0.1590 ns | 0.1410 ns | 0.45 | 0.01 | - | - | - | - | - | Dictionary_TryGetValue | 1000 | 18.230 ns | 0.2012 ns | 0.1882 ns | 1.20 | 0.02 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 14.920 ns | 0.2296 ns | 0.2035 ns | 0.98 | 0.01 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 29.555 ns | 0.5926 ns | 0.5543 ns | 1.95 | 0.04 | - | - | - | - | - - - ## Against static TypeDictionary by @rogeralsing (need to subtract the cost of enumerating the array when adding items to ImHashMap and PartitionedHashMap) - - BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 - Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=5.0.201 - [Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT - DefaultJob : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |---------------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | V3_ImHashMap_GetValueOrDefault | 5 | 4.497 ns | 0.1607 ns | 0.1578 ns | 1.00 | 0.00 | - | - | - | - | - | V3_PartitionedHashMap_GetValueOrDefault | 5 | 4.763 ns | 0.1675 ns | 0.1484 ns | 1.06 | 0.05 | - | - | - | - | - | TypeDict_TryFind | 5 | 3.198 ns | 0.1221 ns | 0.1406 ns | 0.71 | 0.03 | - | - | - | - | - | Dictionary_TryGetValue | 5 | 16.248 ns | 0.3500 ns | 0.3102 ns | 3.61 | 0.12 | - | - | - | - | - - | V3_ImHashMap_GetValueOrDefault | 10 | 5.569 ns | 0.1801 ns | 0.2751 ns | 1.00 | 0.00 | - | - | - | - | - | V3_PartitionedHashMap_GetValueOrDefault | 10 | 5.016 ns | 0.1509 ns | 0.1178 ns | 0.89 | 0.05 | - | - | - | - | - | TypeDict_TryFind | 10 | 2.513 ns | 0.0824 ns | 0.0688 ns | 0.45 | 0.03 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 15.954 ns | 0.3234 ns | 0.3025 ns | 2.85 | 0.17 | - | - | - | - | - - | V3_ImHashMap_GetValueOrDefault | 20 | 6.747 ns | 0.1964 ns | 0.2017 ns | 1.00 | 0.00 | - | - | - | - | - | V3_PartitionedHashMap_GetValueOrDefault | 20 | 3.985 ns | 0.1034 ns | 0.0863 ns | 0.59 | 0.02 | - | - | - | - | - | TypeDict_TryFind | 20 | 2.476 ns | 0.1174 ns | 0.1257 ns | 0.37 | 0.02 | - | - | - | - | - | Dictionary_TryGetValue | 20 | 17.766 ns | 0.4131 ns | 0.3865 ns | 2.64 | 0.11 | - | - | - | - | - - - ## V4 - - BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 - Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=6.0.202 - [Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT - DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |--------------------------------- |------ |----------:|----------:|----------:|------:|--------:|------:|------:|------:|----------:| - | V4_ImHashMap_TryFind | 1 | 8.915 ns | 0.1853 ns | 0.1733 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_TryFind | 1 | 7.834 ns | 0.1769 ns | 0.1568 ns | 0.88 | 0.02 | - | - | - | - | - | V4_PartitionedHashMap_TryFind | 1 | 8.292 ns | 0.1082 ns | 0.0959 ns | 0.93 | 0.02 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 1 | 7.681 ns | 0.1245 ns | 0.1039 ns | 0.86 | 0.02 | - | - | - | - | - | DictionarySlim_TryGetValue | 1 | 8.861 ns | 0.1423 ns | 0.1188 ns | 0.99 | 0.02 | - | - | - | - | - | Dictionary_TryGetValue | 1 | 17.914 ns | 0.3447 ns | 0.3055 ns | 2.01 | 0.05 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1 | 13.381 ns | 0.2709 ns | 0.2401 ns | 1.50 | 0.04 | - | - | - | - | - | ImmutableDict_TryGet | 1 | 19.040 ns | 0.3068 ns | 0.2870 ns | 2.14 | 0.06 | - | - | - | - | - | | | | | | | | | | | | - | V4_ImHashMap_TryFind | 10 | 9.462 ns | 0.2690 ns | 0.2246 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_TryFind | 10 | 9.038 ns | 0.1650 ns | 0.1463 ns | 0.96 | 0.02 | - | - | - | - | - | V4_PartitionedHashMap_TryFind | 10 | 8.575 ns | 0.2031 ns | 0.1900 ns | 0.91 | 0.03 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 10 | 5.869 ns | 0.1026 ns | 0.1743 ns | 0.63 | 0.02 | - | - | - | - | - | DictionarySlim_TryGetValue | 10 | 7.384 ns | 0.1882 ns | 0.1668 ns | 0.78 | 0.03 | - | - | - | - | - | Dictionary_TryGetValue | 10 | 14.082 ns | 0.2168 ns | 0.1921 ns | 1.49 | 0.04 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 10 | 11.436 ns | 0.1398 ns | 0.1239 ns | 1.21 | 0.03 | - | - | - | - | - | ImmutableDict_TryGet | 10 | 17.927 ns | 0.4467 ns | 0.4780 ns | 1.90 | 0.09 | - | - | - | - | - | | | | | | | | | | | | - | V4_ImHashMap_TryFind | 100 | 12.147 ns | 0.1475 ns | 0.1308 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_TryFind | 100 | 11.357 ns | 0.1695 ns | 0.1323 ns | 0.94 | 0.01 | - | - | - | - | - | V4_PartitionedHashMap_TryFind | 100 | 8.520 ns | 0.2142 ns | 0.1899 ns | 0.70 | 0.02 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 100 | 7.785 ns | 0.1374 ns | 0.1147 ns | 0.64 | 0.01 | - | - | - | - | - | DictionarySlim_TryGetValue | 100 | 6.918 ns | 0.1544 ns | 0.1289 ns | 0.57 | 0.01 | - | - | - | - | - | Dictionary_TryGetValue | 100 | 13.804 ns | 0.3474 ns | 0.3717 ns | 1.14 | 0.04 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 100 | 11.432 ns | 0.2194 ns | 0.2052 ns | 0.94 | 0.02 | - | - | - | - | - | ImmutableDict_TryGet | 100 | 21.593 ns | 0.4048 ns | 0.3786 ns | 1.78 | 0.03 | - | - | - | - | - | | | | | | | | | | | | - | V4_ImHashMap_TryFind | 1000 | 15.492 ns | 0.3859 ns | 0.4881 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_TryFind | 1000 | 14.339 ns | 0.2612 ns | 0.2444 ns | 0.92 | 0.03 | - | - | - | - | - | V4_PartitionedHashMap_TryFind | 1000 | 11.508 ns | 0.1839 ns | 0.1630 ns | 0.74 | 0.03 | - | - | - | - | - | V3_PartitionedHashMap_TryFind | 1000 | 9.508 ns | 0.2340 ns | 0.2074 ns | 0.61 | 0.02 | - | - | - | - | - | DictionarySlim_TryGetValue | 1000 | 7.099 ns | 0.1984 ns | 0.2037 ns | 0.46 | 0.01 | - | - | - | - | - | Dictionary_TryGetValue | 1000 | 13.696 ns | 0.1341 ns | 0.1120 ns | 0.88 | 0.03 | - | - | - | - | - | ConcurrentDictionary_TryGetValue | 1000 | 11.358 ns | 0.1464 ns | 0.1222 ns | 0.73 | 0.03 | - | - | - | - | - | ImmutableDict_TryGet | 1000 | 25.875 ns | 0.3752 ns | 0.3510 ns | 1.67 | 0.06 | - | - | - | - | - - ## Baseline FHashMap6 vs DictionarySlim vs Dictionary - - BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19042.928/20H2/October2020Update) - Intel Core i5-8350U CPU 1.70GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores - .NET SDK=7.0.100 - [Host] : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.0 (7.0.22.51805), X64 RyuJIT AVX2 - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------:|------------:| - | Dictionary_TryGetValue | 100 | 15.234 ns | 0.3434 ns | 0.6779 ns | 14.971 ns | 1.00 | 0.00 | - | NA | - | DictionarySlim_TryGetValue | 100 | 9.790 ns | 0.5518 ns | 1.6271 ns | 8.944 ns | 0.70 | 0.11 | - | NA | - | FHashMap_TryGetValue | 100 | 20.591 ns | 0.4773 ns | 0.4232 ns | 20.521 ns | 1.32 | 0.09 | - | NA | - - ## Never happened? - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|---------:|---------:|---------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 100 | 11.14 ns | 0.613 ns | 1.749 ns | 10.75 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 100 | 11.29 ns | 0.641 ns | 1.850 ns | 10.77 ns | 1.03 | 0.20 | - | NA | - - ## Almost funny - - | Method | Count | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|----------:|------------:| - | DictionarySlim_TryGetValue | 100 | 8.703 ns | 0.0774 ns | 0.0686 ns | 1.00 | - | NA | - | FHashMap_TryGetValue | 100 | 7.181 ns | 0.0442 ns | 0.0345 ns | 0.83 | - | NA | - - ## Initial SIMD FHashMap7 vs DictionarySlim - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 100 | 8.968 ns | 0.2581 ns | 0.4168 ns | 8.796 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 100 | 8.866 ns | 0.4456 ns | 1.3069 ns | 9.244 ns | 0.86 | 0.09 | - | NA | - - ## Round 2 and 3, SIMD FHashMap7 vs DictionarySlim - - | Method | Count | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|----------:|------------:| - | DictionarySlim_TryGetValue | 100 | 8.925 ns | 0.0383 ns | 0.0299 ns | 1.00 | - | NA | - | FHashMap_TryGetValue | 100 | 6.376 ns | 0.1207 ns | 0.1129 ns | 0.72 | - | NA | - - ## Funny 4, SIMD FHashMap7 vs DictionarySlim - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 100 | 9.131 ns | 0.1991 ns | 0.1663 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 100 | 6.020 ns | 0.1766 ns | 0.2644 ns | 0.65 | 0.04 | - | NA | - - ## FHashMap7 vs DictionarySlim (multiple params) - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 10.945 ns | 0.3136 ns | 0.5574 ns | 10.770 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1 | 6.852 ns | 0.4387 ns | 1.2157 ns | 7.022 ns | 0.57 | 0.11 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 12.432 ns | 0.7856 ns | 2.3042 ns | 12.383 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 10 | 5.161 ns | 0.1755 ns | 0.2932 ns | 5.050 ns | 0.38 | 0.05 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 16.811 ns | 0.7006 ns | 2.0657 ns | 15.940 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 100 | 9.176 ns | 0.6841 ns | 1.9517 ns | 8.838 ns | 0.56 | 0.14 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 11.542 ns | 0.3251 ns | 0.7535 ns | 11.400 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1000 | 9.579 ns | 0.4608 ns | 1.2921 ns | 9.556 ns | 0.82 | 0.12 | - | NA | - - ## No!!! SIMD FHashMap7 vs DictionarySlim (multiple params) - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 9.134 ns | 0.2546 ns | 0.3569 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1 | 5.174 ns | 0.0317 ns | 0.0281 ns | 0.56 | 0.02 | - | NA | - | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 8.908 ns | 0.0430 ns | 0.0359 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 10 | 5.615 ns | 0.0722 ns | 0.0675 ns | 0.63 | 0.01 | - | NA | - | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 8.964 ns | 0.1536 ns | 0.1282 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 100 | 7.468 ns | 0.2187 ns | 0.2148 ns | 0.83 | 0.03 | - | NA | - | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 8.826 ns | 0.0929 ns | 0.0776 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1000 | 5.271 ns | 0.1362 ns | 0.1514 ns | 0.60 | 0.02 | - | NA | - - ## Small opti No!!! SIMD FHashMap7 vs DictionarySlim (multiple params) - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 8.965 ns | 0.3526 ns | 1.0285 ns | 8.416 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1 | 3.940 ns | 0.1476 ns | 0.2117 ns | 3.894 ns | 0.43 | 0.05 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 9.154 ns | 0.2472 ns | 0.3036 ns | 9.041 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 10 | 5.734 ns | 0.0922 ns | 0.0770 ns | 5.733 ns | 0.62 | 0.02 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 8.638 ns | 0.1980 ns | 0.1654 ns | 8.599 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 100 | 7.259 ns | 0.1361 ns | 0.1206 ns | 7.212 ns | 0.84 | 0.02 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 9.280 ns | 0.2481 ns | 0.4537 ns | 9.115 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1000 | 3.765 ns | 0.0650 ns | 0.0576 ns | 3.760 ns | 0.40 | 0.02 | - | NA | - - ## Adding small SIMD to FHashMap7 vs DictionarySlim (multiple params) - winning the medium - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 9.314 ns | 0.2492 ns | 0.4494 ns | 9.149 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1 | 4.819 ns | 0.1521 ns | 0.1423 ns | 4.775 ns | 0.51 | 0.03 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 9.012 ns | 0.2230 ns | 0.4297 ns | 8.841 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 10 | 6.407 ns | 0.1963 ns | 0.2182 ns | 6.375 ns | 0.70 | 0.05 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 9.261 ns | 0.2620 ns | 0.6019 ns | 8.994 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 100 | 7.136 ns | 0.1941 ns | 0.1515 ns | 7.104 ns | 0.74 | 0.06 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 9.066 ns | 0.1819 ns | 0.1612 ns | 9.013 ns | 1.00 | 0.00 | - | NA | - | FHashMap_TryGetValue | 1000 | 4.935 ns | 0.1353 ns | 0.1661 ns | 4.891 ns | 0.55 | 0.02 | - | NA | - - ## Lookup FHashMap8 vs DictionarySlim - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 9.446 ns | 0.2718 ns | 0.5038 ns | 9.329 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 1 | 5.485 ns | 0.1832 ns | 0.3009 ns | 5.402 ns | 0.58 | 0.04 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 9.659 ns | 0.2123 ns | 0.1882 ns | 9.654 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 10 | 7.044 ns | 0.1443 ns | 0.1205 ns | 7.003 ns | 0.73 | 0.01 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 9.368 ns | 0.2697 ns | 0.3600 ns | 9.259 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 100 | 9.770 ns | 0.2736 ns | 0.3360 ns | 9.748 ns | 1.04 | 0.06 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 9.743 ns | 0.2676 ns | 0.4686 ns | 9.573 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 1000 | 6.043 ns | 0.1946 ns | 0.4146 ns | 5.883 ns | 0.62 | 0.05 | - | NA | - - ## Lookup FHashMap8 vs DictionarySlim after simplifying - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 8.649 ns | 0.0707 ns | 0.0662 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 1 | 5.447 ns | 0.0784 ns | 0.0654 ns | 0.63 | 0.01 | - | NA | - | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 8.948 ns | 0.0405 ns | 0.0316 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 10 | 5.471 ns | 0.1656 ns | 0.2375 ns | 0.62 | 0.03 | - | NA | - | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 9.081 ns | 0.2568 ns | 0.5583 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 100 | 6.294 ns | 0.1585 ns | 0.1886 ns | 0.69 | 0.05 | - | NA | - | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 8.990 ns | 0.2087 ns | 0.1850 ns | 1.00 | 0.00 | - | NA | - | FHashMap8_TryGetValue | 1000 | 4.950 ns | 0.1508 ns | 0.2719 ns | 0.55 | 0.03 | - | NA | - - ## Lookup FHashMap7 vs FHashMap8 vs DictionarySlim vs Dictionary - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Allocated | Alloc Ratio | - |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 9.470 ns | 0.2655 ns | 0.7269 ns | 9.193 ns | 1.00 | 0.00 | - | NA | - | Dictionary_TryGetValue | 1 | 12.842 ns | 0.3375 ns | 0.7192 ns | 12.628 ns | 1.36 | 0.12 | - | NA | - | FHashMap8_TryGetValue | 1 | 5.212 ns | 0.1734 ns | 0.1448 ns | 5.202 ns | 0.52 | 0.05 | - | NA | - | FHashMap7_TryGetValue | 1 | 4.365 ns | 0.1630 ns | 0.2981 ns | 4.269 ns | 0.45 | 0.05 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 9.585 ns | 0.2755 ns | 0.7992 ns | 9.292 ns | 1.00 | 0.00 | - | NA | - | Dictionary_TryGetValue | 10 | 10.675 ns | 0.2063 ns | 0.1723 ns | 10.674 ns | 1.13 | 0.10 | - | NA | - | FHashMap8_TryGetValue | 10 | 5.584 ns | 0.1895 ns | 0.2028 ns | 5.546 ns | 0.57 | 0.07 | - | NA | - | FHashMap7_TryGetValue | 10 | 5.676 ns | 0.0982 ns | 0.0964 ns | 5.680 ns | 0.58 | 0.06 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 11.095 ns | 0.4470 ns | 1.2236 ns | 11.427 ns | 1.00 | 0.00 | - | NA | - | Dictionary_TryGetValue | 100 | 10.694 ns | 0.2975 ns | 0.7013 ns | 10.484 ns | 0.94 | 0.11 | - | NA | - | FHashMap8_TryGetValue | 100 | 7.313 ns | 0.2226 ns | 0.4071 ns | 7.107 ns | 0.65 | 0.07 | - | NA | - | FHashMap7_TryGetValue | 100 | 7.119 ns | 0.2135 ns | 0.1997 ns | 7.095 ns | 0.67 | 0.11 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 9.049 ns | 0.2662 ns | 0.5953 ns | 8.826 ns | 1.00 | 0.00 | - | NA | - | Dictionary_TryGetValue | 1000 | 13.465 ns | 0.3476 ns | 0.3251 ns | 13.483 ns | 1.50 | 0.12 | - | NA | - | FHashMap8_TryGetValue | 1000 | 4.926 ns | 0.1781 ns | 0.3120 ns | 4.809 ns | 0.54 | 0.05 | - | NA | - | FHashMap7_TryGetValue | 1000 | 4.314 ns | 0.1559 ns | 0.1601 ns | 4.290 ns | 0.48 | 0.04 | - | NA | - - ## FHM9.1 vs Dict and DictSlim - - BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1702/22H2/2022Update/SunValley2) - 11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores - .NET SDK=7.0.304 - [Host] : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.7 (7.0.723.27404), X64 RyuJIT AVX2 - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | - |--------------------------- |------ |----------:|----------:|----------:|----------:|------:|--------:|----------------------:|---------------:|------------------------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 6.236 ns | 0.1823 ns | 0.1872 ns | 6.221 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | Dictionary_TryGetValue | 1 | 10.070 ns | 0.2035 ns | 0.2090 ns | 10.038 ns | 1.62 | 0.05 | 31 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 1 | 5.084 ns | 0.2665 ns | 0.7773 ns | 5.267 ns | 0.67 | 0.08 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 8.515 ns | 0.2330 ns | 0.3763 ns | 8.407 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | Dictionary_TryGetValue | 10 | 10.296 ns | 0.4517 ns | 1.3247 ns | 10.439 ns | 1.30 | 0.10 | 25 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 10 | 3.857 ns | 0.1311 ns | 0.2904 ns | 3.777 ns | 0.46 | 0.04 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 7.105 ns | 0.1931 ns | 0.5221 ns | 6.860 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | Dictionary_TryGetValue | 100 | 8.443 ns | 0.2081 ns | 0.2397 ns | 8.402 ns | 1.14 | 0.11 | 25 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 100 | 4.037 ns | 0.1339 ns | 0.2201 ns | 3.994 ns | 0.56 | 0.06 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 6.325 ns | 0.1933 ns | 0.4632 ns | 6.124 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | Dictionary_TryGetValue | 1000 | 9.146 ns | 0.2185 ns | 0.2044 ns | 9.176 ns | 1.42 | 0.10 | 25 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 1000 | 5.917 ns | 0.1505 ns | 0.1334 ns | 5.909 ns | 0.92 | 0.07 | 13 | 0 | 0 | - | NA | - - ## FHM9.1 sparse entries - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | CacheMisses/Op | BranchInstructions/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|---------------:|----------------------:|------------------------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 7.813 ns | 0.2096 ns | 0.2243 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | - | FHashMap91_TryGetValue | 1 | 4.608 ns | 0.1493 ns | 0.1888 ns | 0.59 | 0.03 | 0 | 13 | -0 | - | NA | - | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 7.677 ns | 0.1647 ns | 0.1375 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | - | FHashMap91_TryGetValue | 10 | 4.488 ns | 0.1183 ns | 0.1049 ns | 0.59 | 0.02 | 0 | 13 | 0 | - | NA | - | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 7.824 ns | 0.2102 ns | 0.2947 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | - | FHashMap91_TryGetValue | 100 | 3.459 ns | 0.1538 ns | 0.3277 ns | 0.45 | 0.05 | -0 | 13 | -0 | - | NA | - | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 7.608 ns | 0.1997 ns | 0.1770 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | - | FHashMap91_TryGetValue | 1000 | 6.384 ns | 0.1427 ns | 0.1114 ns | 0.84 | 0.03 | 0 | 14 | 0 | - | NA | - - ## No more sparce anymore - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | - |------------------------------ |------ |---------:|----------:|----------:|---------:|------:|--------:|----------------------:|---------------:|------------------------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 6.759 ns | 0.2002 ns | 0.2603 ns | 6.704 ns | 1.00 | 0.00 | 20 | 0 | -0 | - | NA | - | FHashMap91_TryGetValue | 1 | 4.512 ns | 0.1337 ns | 0.2271 ns | 4.432 ns | 0.67 | 0.05 | 11 | 0 | -0 | - | NA | - | FHashMap91_TryGetValue_Golden | 1 | 4.187 ns | 0.1022 ns | 0.0906 ns | 4.167 ns | 0.62 | 0.03 | 11 | -0 | -0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 8.225 ns | 0.4388 ns | 1.2938 ns | 7.602 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 10 | 4.434 ns | 0.1415 ns | 0.1453 ns | 4.434 ns | 0.58 | 0.06 | 11 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue_Golden | 10 | 4.498 ns | 0.1236 ns | 0.1033 ns | 4.472 ns | 0.59 | 0.05 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 7.067 ns | 0.1470 ns | 0.1303 ns | 7.080 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 100 | 4.481 ns | 0.1484 ns | 0.2311 ns | 4.398 ns | 0.64 | 0.04 | 11 | 0 | -0 | - | NA | - | FHashMap91_TryGetValue_Golden | 100 | 8.215 ns | 0.2181 ns | 0.2240 ns | 8.178 ns | 1.17 | 0.04 | 17 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 8.744 ns | 0.6189 ns | 1.8151 ns | 7.838 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 1000 | 5.755 ns | 0.1660 ns | 0.1912 ns | 5.715 ns | 0.73 | 0.09 | 14 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue_Golden | 1000 | 7.732 ns | 0.1953 ns | 0.2325 ns | 7.705 ns | 0.97 | 0.13 | 17 | 0 | 0 | - | NA | - - ## Final load factor 87.5% and no golden results - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | BranchInstructions/Op | BranchMispredictions/Op | CacheMisses/Op | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------------------:|------------------------:|---------------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 6.830 ns | 0.1073 ns | 0.1004 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 1 | 4.230 ns | 0.1149 ns | 0.1075 ns | 0.62 | 0.02 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 6.980 ns | 0.0918 ns | 0.0717 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 10 | 4.259 ns | 0.0673 ns | 0.0562 ns | 0.61 | 0.01 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 6.975 ns | 0.1016 ns | 0.0849 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 100 | 4.414 ns | 0.1426 ns | 0.2342 ns | 0.64 | 0.03 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 6.889 ns | 0.0773 ns | 0.0685 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 1000 | 5.633 ns | 0.1079 ns | 0.1243 ns | 0.82 | 0.02 | 14 | 0 | 0 | - | NA | - - ## ArrayEntries - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | CacheMisses/Op | BranchInstructions/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|---------------:|----------------------:|------------------------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1000 | 7.223 ns | 0.2535 ns | 0.7193 ns | 6.887 ns | 1.00 | 0.00 | 0 | 20 | 0 | - | NA | - | FHashMap91_TryGetValue | 1000 | 5.108 ns | 0.1634 ns | 0.2819 ns | 4.996 ns | 0.69 | 0.09 | -0 | 14 | 0 | - | NA | - - ## ArrayArrayEntries - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | BranchMispredictions/Op | CacheMisses/Op | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------------------:|------------------------:|---------------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 7.053 ns | 0.1174 ns | 0.1041 ns | 7.021 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 1 | 4.948 ns | 0.1446 ns | 0.2119 ns | 4.845 ns | 0.70 | 0.04 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 7.475 ns | 0.1732 ns | 0.2747 ns | 7.384 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 10 | 4.900 ns | 0.1541 ns | 0.1366 ns | 4.862 ns | 0.65 | 0.03 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 8.120 ns | 0.4747 ns | 1.3995 ns | 7.386 ns | 1.00 | 0.00 | 20 | -0 | 0 | - | NA | - | FHashMap91_TryGetValue | 100 | 5.129 ns | 0.1593 ns | 0.4251 ns | 4.985 ns | 0.64 | 0.12 | 11 | 0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 1000 | 7.289 ns | 0.1912 ns | 0.3088 ns | 7.137 ns | 1.00 | 0.00 | 20 | 0 | 0 | - | NA | - | FHashMap91_TryGetValue | 1000 | 6.262 ns | 0.1232 ns | 0.0962 ns | 6.277 ns | 0.86 | 0.04 | 14 | 0 | 0 | - | NA | - - ## SmallMap + net8.0 - - BenchmarkDotNet v0.13.10, Windows 11 (10.0.22621.2428/22H2/2022Update/SunValley2) - 11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores - .NET SDK 8.0.100-rc.2.23502.2 - [Host] : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2 - DefaultJob : .NET 8.0.0 (8.0.23.47906), X64 RyuJIT AVX2 - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | BranchInstructions/Op | BranchMispredictions/Op | CacheMisses/Op | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|----------------------:|------------------------:|---------------:|----------:|------------:| - | Dictionary_TryGetValue | 100 | 6.938 ns | 0.1719 ns | 0.2574 ns | 1.00 | 0.00 | 21 | 0 | 0 | - | NA | - | DictionarySlim_TryGetValue | 100 | 5.371 ns | 0.1277 ns | 0.1132 ns | 0.77 | 0.03 | 15 | 0 | 0 | - | NA | - | SmallMap_TryGetValue | 100 | 3.718 ns | 0.1360 ns | 0.2453 ns | 0.54 | 0.04 | 9 | 0 | 0 | - | NA | - - - ## SmallMap + FEC_FHashMap + net6.0 - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | BranchInstructions/Op | CacheMisses/Op | BranchMispredictions/Op | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|----------------------:|---------------:|------------------------:|----------:|------------:| - | DictionarySlim_TryGetValue | 1 | 5.592 ns | 0.4244 ns | 1.2448 ns | 4.835 ns | 1.00 | 0.00 | 15 | 0 | 0 | - | NA | - | SmallMap_TryGetValue | 1 | 2.958 ns | 0.1059 ns | 0.0990 ns | 2.970 ns | 0.44 | 0.03 | 9 | -0 | -0 | - | NA | - | FHashMap_TryGetValue | 1 | 1.297 ns | 0.0757 ns | 0.0777 ns | 1.288 ns | 0.19 | 0.01 | 6 | -0 | 0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 5.208 ns | 0.1485 ns | 0.1240 ns | 5.181 ns | 1.00 | 0.00 | 15 | 0 | 0 | - | NA | - | SmallMap_TryGetValue | 10 | 3.709 ns | 0.1739 ns | 0.4551 ns | 3.548 ns | 0.69 | 0.07 | 9 | 0 | 0 | - | NA | - | FHashMap_TryGetValue | 10 | 5.539 ns | 0.4004 ns | 1.1487 ns | 5.612 ns | 0.98 | 0.22 | 10 | -0 | -0 | - | NA | - | | | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 5.296 ns | 0.1654 ns | 0.3898 ns | 5.176 ns | 1.00 | 0.00 | 15 | 0 | 0 | - | NA | - | SmallMap_TryGetValue | 100 | 3.549 ns | 0.1293 ns | 0.2490 ns | 3.486 ns | 0.66 | 0.07 | 9 | 0 | 0 | - | NA | - | FHashMap_TryGetValue | 100 | 4.265 ns | 0.1406 ns | 0.1098 ns | 4.268 ns | 0.80 | 0.08 | 10 | 0 | -0 | - | NA | - - ## SmallMap SIMD vs... - - BenchmarkDotNet v0.15.0, Windows 11 (10.0.26100.4202/24H2/2024Update/HudsonValley) - Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET SDK 9.0.203 - [Host] : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2 - DefaultJob : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX2 - - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|-----:|----------:|------------:| - | FHashMap_TryGetValue | 1 | 1.580 ns | 0.0212 ns | 0.0199 ns | 1.587 ns | 0.35 | 0.03 | 1 | - | NA | - | DictionarySlim_TryGetValue | 1 | 4.592 ns | 0.1535 ns | 0.4202 ns | 4.452 ns | 1.01 | 0.13 | 2 | - | NA | - | SmallMap_TryGetValue | 1 | 5.367 ns | 0.0884 ns | 0.0738 ns | 5.378 ns | 1.18 | 0.10 | 3 | - | NA | - | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 10 | 4.165 ns | 0.0467 ns | 0.0414 ns | 4.156 ns | 1.00 | 0.01 | 1 | - | NA | - | FHashMap_TryGetValue | 10 | 4.455 ns | 0.0285 ns | 0.0253 ns | 4.457 ns | 1.07 | 0.01 | 2 | - | NA | - | SmallMap_TryGetValue | 10 | 5.302 ns | 0.1556 ns | 0.1852 ns | 5.369 ns | 1.27 | 0.05 | 3 | - | NA | - | | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 4.777 ns | 0.1559 ns | 0.4472 ns | 4.576 ns | 1.01 | 0.13 | 1 | - | NA | - | FHashMap_TryGetValue | 100 | 6.020 ns | 0.1795 ns | 0.2335 ns | 6.017 ns | 1.27 | 0.13 | 2 | - | NA | - | SmallMap_TryGetValue | 100 | 6.021 ns | 0.1808 ns | 0.4827 ns | 6.293 ns | 1.27 | 0.15 | 2 | - | NA | - - ## SmallMap vs FHashMap11 - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|-----:|----------:|------------:| - | DictionarySlim_TryGetValue | 1000 | 2.225 us | 0.0445 us | 0.1313 us | 2.162 us | 1.00 | 0.08 | 1 | - | NA | - | SmallMap_TryGetValue | 1000 | 2.364 us | 0.0387 us | 0.0302 us | 2.377 us | 1.07 | 0.06 | 1 | - | NA | - | FHashMap11_TryGetValue | 1000 | 2.387 us | 0.0477 us | 0.1170 us | 2.434 us | 1.08 | 0.08 | 1 | - | NA | - - ## APL FHashMap11 - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | - |--------------------------- |------ |---------:|----------:|----------:|------:|--------:|-----:|----------:|------------:| - | DictionarySlim_TryGetValue | 1000 | 2.195 us | 0.0437 us | 0.1217 us | 1.00 | 0.08 | 1 | - | NA | - | FHashMap11_TryGetValue | 1000 | 2.333 us | 0.0465 us | 0.0929 us | 1.07 | 0.07 | 2 | - | NA | - | SmallMap_TryGetValue | 1000 | 2.514 us | 0.0489 us | 0.0789 us | 1.15 | 0.07 | 3 | - | NA | - - ## Reshuffle the lookup comparison - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Allocated | Alloc Ratio | - |--------------------------- |------ |------------:|----------:|-----------:|------:|--------:|-----:|----------:|------------:| - | SmallMap_TryGetValue | 10 | 44.90 ns | 0.536 ns | 0.419 ns | 0.99 | 0.04 | 1 | - | NA | - | DictionarySlim_TryGetValue | 10 | 45.43 ns | 0.941 ns | 1.857 ns | 1.00 | 0.06 | 1 | - | NA | - | | | | | | | | | | | - | DictionarySlim_TryGetValue | 100 | 385.36 ns | 4.621 ns | 4.097 ns | 1.00 | 0.01 | 1 | - | NA | - | SmallMap_TryGetValue | 100 | 513.52 ns | 5.702 ns | 5.054 ns | 1.33 | 0.02 | 2 | - | NA | - | | | | | | | | | | | - | SmallMap_TryGetValue | 1000 | 4,255.63 ns | 82.286 ns | 130.514 ns | 0.89 | 0.04 | 1 | - | NA | - | DictionarySlim_TryGetValue | 1000 | 4,794.12 ns | 95.912 ns | 189.322 ns | 1.00 | 0.06 | 2 | - | NA | - - ## Stack hybrid from the FEC is tested - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Allocated | Alloc Ratio | - |--------------------------------------------------------- |------ |---------:|--------:|---------:|------:|--------:|-----:|-------:|----------:|------------:| - | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 203.5 ns | 3.88 ns | 3.63 ns | 0.70 | 0.03 | 1 | 0.1147 | 720 B | 0.67 | - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 277.6 ns | 3.61 ns | 5.52 ns | 0.95 | 0.04 | 2 | - | - | 0.00 | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 292.1 ns | 5.84 ns | 12.19 ns | 1.00 | 0.06 | 2 | 0.1707 | 1072 B | 1.00 | - - ## After inlining in the FecHashMap - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | - |--------------------------------------------------------- |------ |-----------:|---------:|---------:|-----------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 110.8 ns | 2.26 ns | 4.90 ns | 110.1 ns | 0.38 | 0.02 | 1 | - | - | - | 0.00 | - | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 194.1 ns | 1.80 ns | 1.50 ns | 194.2 ns | 0.67 | 0.03 | 2 | 0.1147 | - | 720 B | 0.67 | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 288.7 ns | 5.82 ns | 14.04 ns | 280.1 ns | 1.00 | 0.07 | 3 | 0.1707 | - | 1072 B | 1.00 | - | | | | | | | | | | | | | | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,003.8 ns | 30.26 ns | 31.08 ns | 1,995.1 ns | 1.00 | 0.02 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | - | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,306.8 ns | 18.67 ns | 15.59 ns | 2,307.0 ns | 1.15 | 0.02 | 2 | 0.8507 | - | 5344 B | 0.71 | - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 3,473.7 ns | 15.97 ns | 13.34 ns | 3,470.3 ns | 1.73 | 0.03 | 3 | 0.7782 | - | 4904 B | 0.65 | - - ## Probes are separate from the packed hashes and indexes into their own array - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | - |--------------------------------------------------------- |------ |---------:|----------:|----------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.118 us | 0.0421 us | 0.0679 us | 1.00 | 0.04 | 1 | 1.1902 | 0.0229 | 7.31 KB | 1.00 | - | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.346 us | 0.0404 us | 0.0378 us | 1.11 | 0.04 | 2 | 0.8507 | - | 5.22 KB | 0.71 | - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 3.294 us | 0.0648 us | 0.0970 us | 1.56 | 0.07 | 3 | 0.8278 | - | 5.08 KB | 0.69 | - - ## GoldenRatio Hash adjustment - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | - |--------------------------------------------------------- |------ |---------:|----------:|----------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2.150 us | 0.0428 us | 0.0783 us | 2.115 us | 1.00 | 0.05 | 1 | 1.1902 | 0.0229 | 7.31 KB | 1.00 | - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 3.092 us | 0.0608 us | 0.0871 us | 3.068 us | 1.44 | 0.06 | 2 | 0.8278 | - | 5.08 KB | 0.69 | - - ## After probes go to their own array + padding + wrapping - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | - |--------------------------------------------------------- |------ |------------:|----------:|------------:|------------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 111.6 ns | 1.16 ns | 0.97 ns | 111.8 ns | 0.38 | 0.01 | 1 | - | - | - | 0.00 | - | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 205.1 ns | 3.46 ns | 3.24 ns | 204.4 ns | 0.70 | 0.02 | 2 | 0.1147 | - | 720 B | 0.67 | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 292.2 ns | 5.53 ns | 5.68 ns | 291.4 ns | 1.00 | 0.03 | 3 | 0.1707 | - | 1072 B | 1.00 | - | | | | | | | | | | | | | | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,121.8 ns | 42.34 ns | 91.14 ns | 2,070.2 ns | 1.00 | 0.06 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,181.5 ns | 43.38 ns | 91.50 ns | 2,127.6 ns | 1.03 | 0.06 | 1 | 0.8659 | 0.0038 | 5440 B | 0.73 | - | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,347.8 ns | 14.31 ns | 12.69 ns | 2,343.7 ns | 1.11 | 0.05 | 2 | 0.8507 | - | 5344 B | 0.71 | - | | | | | | | | | | | | | | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 23,640.4 ns | 280.02 ns | 248.23 ns | 23,620.0 ns | 1.00 | 0.01 | 1 | 9.1553 | 1.2817 | 57808 B | 1.00 | - | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 45,226.9 ns | 293.55 ns | 245.13 ns | 45,221.9 ns | 1.91 | 0.02 | 2 | 7.8735 | 0.3052 | 49544 B | 0.86 | - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 49,441.1 ns | 987.34 ns | 1,780.38 ns | 49,657.1 ns | 2.09 | 0.08 | 3 | 8.9111 | 0.0610 | 55976 B | 0.97 | - - ## Initial SIMD for Lookup - Regression in @perf - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | - |--------------------------------------------------------- |------ |------------:|----------:|------------:|------------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 111.3 ns | 1.88 ns | 1.67 ns | 110.6 ns | 0.38 | 0.01 | 1 | - | - | - | 0.00 | - | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 211.8 ns | 4.21 ns | 7.91 ns | 210.4 ns | 0.72 | 0.03 | 2 | 0.1147 | - | 720 B | 0.67 | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 293.1 ns | 5.18 ns | 4.33 ns | 293.5 ns | 1.00 | 0.02 | 3 | 0.1707 | - | 1072 B | 1.00 | - | | | | | | | | | | | | | | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,112.8 ns | 19.17 ns | 16.00 ns | 2,108.5 ns | 1.00 | 0.01 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | - | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,433.3 ns | 47.99 ns | 95.84 ns | 2,383.6 ns | 1.15 | 0.05 | 2 | 0.8507 | - | 5344 B | 0.71 | - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,548.7 ns | 50.70 ns | 106.95 ns | 2,491.5 ns | 1.21 | 0.05 | 2 | 0.8659 | 0.0038 | 5440 B | 0.73 | - | | | | | | | | | | | | | | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 23,127.6 ns | 233.36 ns | 182.20 ns | 23,143.0 ns | 1.00 | 0.01 | 1 | 9.1553 | 1.2817 | 57808 B | 1.00 | - | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 45,630.3 ns | 912.10 ns | 1,735.36 ns | 44,696.8 ns | 1.97 | 0.08 | 2 | 7.8735 | 0.3052 | 49544 B | 0.86 | - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 49,295.9 ns | 327.44 ns | 273.43 ns | 49,364.4 ns | 2.13 | 0.02 | 3 | 8.9111 | 0.0610 | 55976 B | 0.97 | - - ## Using StableArrayEntries - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | - |--------------------------------------------------------- |------ |------------:|------------:|------------:|------------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 113.3 ns | 2.32 ns | 5.42 ns | 110.1 ns | 0.37 | 0.02 | 1 | - | - | - | 0.00 | - | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 204.5 ns | 2.73 ns | 2.42 ns | 204.0 ns | 0.67 | 0.03 | 2 | 0.1147 | - | 720 B | 0.67 | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 10 | 305.6 ns | 6.11 ns | 14.40 ns | 299.0 ns | 1.00 | 0.06 | 3 | 0.1707 | - | 1072 B | 1.00 | - | | | | | | | | | | | | | | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,172.7 ns | 43.50 ns | 102.53 ns | 2,138.9 ns | 1.00 | 0.07 | 1 | 1.1902 | 0.0229 | 7488 B | 1.00 | - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,317.2 ns | 46.33 ns | 100.71 ns | 2,255.9 ns | 1.07 | 0.07 | 1 | 0.4921 | - | 3088 B | 0.41 | - | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 100 | 2,452.2 ns | 49.04 ns | 109.69 ns | 2,512.7 ns | 1.13 | 0.07 | 2 | 0.8507 | - | 5344 B | 0.71 | - | | | | | | | | | | | | | | - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 29,665.4 ns | 588.68 ns | 1,581.45 ns | 29,786.1 ns | 1.00 | 0.08 | 1 | 9.1553 | 1.2817 | 57808 B | 1.00 | - | SmallMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 55,723.9 ns | 1,098.60 ns | 1,078.97 ns | 55,599.6 ns | 1.88 | 0.12 | 2 | 7.8735 | 0.3052 | 49544 B | 0.86 | - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 63,373.1 ns | 1,260.87 ns | 2,106.62 ns | 63,624.5 ns | 2.14 | 0.14 | 3 | 6.2256 | 0.2441 | 39288 B | 0.68 | - - ## Packing indexes, hashes and probes together - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | - |--------------------------------------------------------- |------ |---------:|---------:|---------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| - | DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 24.17 us | 0.483 us | 1.176 us | 23.48 us | 1.00 | 0.07 | 1 | 9.1553 | 1.2817 | 56.45 KB | 1.00 | - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 52.85 us | 1.053 us | 1.871 us | 52.14 us | 2.19 | 0.13 | 2 | 7.8125 | 0.2441 | 48.05 KB | 0.85 | - - ## Packing indexes, hashes and probes together with Resize vs no Resize - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Rank | Gen0 | Gen1 | Allocated | Alloc Ratio | - |-------------------------------------------------------------- |------ |---------:|---------:|---------:|---------:|------:|--------:|-----:|-------:|-------:|----------:|------------:| - | FecHashMap_Init1000_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 36.08 us | 0.329 us | 0.275 us | 36.08 us | 0.66 | 0.03 | 1 | 7.9346 | 0.4883 | 48.73 KB | 1.01 | - | FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent | 1000 | 55.17 us | 0.985 us | 2.453 us | 54.41 us | 1.00 | 0.06 | 2 | 7.8125 | 0.2441 | 48.05 KB | 1.00 | - - */ - // [Params(1, 10, 100, 1000)]// the 1000 does not add anything as the LookupKey stored higher in the tree, 1000)] - // [Params(10, 100, 1000)] - [Params(1000)] - public int Count; - - private Type[] _presentKeys; - private Type[] _randomPresentKeys; - - [GlobalSetup] - public void Populate() - { - _presentKeys = _keys.Take(Count).ToArray(); - _randomPresentKeys = _presentKeys.OrderBy(_ => _seed.Next()).ToArray(); - - // _mapV4 = V4_ImHashMap_AddOrUpdate(); - // _mapV3 = V3_ImHashMap_AddOrUpdate(); - // _mapV2 = V2_AddOrUpdate(); - // _partMapV4 = V4_PartitionedHashMap_AddOrUpdate(); - // _partMapV3 = V3_PartitionedHashMap_AddOrUpdate(); - // _typeDict = TypeDictionary_Add(); - // _dict = Dict(); - // _dictSlim = DictSlim(); - // _fHashMap7 = FillFHashMap7(); - // _fHashMap9 = FillFHashMap9(); - // _fHashMap91 = FillFHashMap91(); - // _smallMap = FillSmallMap(); - // _fHashMap11 = FillFHashMap11(); - - // _fecSmallMap = new FecSmallMapTypeString(); - // FillFecHashMap(ref _fecSmallMap); - - // _concurrentDict = ConcurrentDict(); - // _immutableDict = ImmutableDict(); - } + private ImTools.V2.ImHashMap[] _mapSlots; + public ImTools.V2.ImHashMap[] ImHashMapSlots_AddOrUpdate() + { + var map = ImHashMapSlots.CreateWithEmpty(); - #region Population + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key, "a"); - private ImTools.V2.ImHashMap _mapV2; - public ImTools.V2.ImHashMap V2_AddOrUpdate() - { - var map = ImTools.V2.ImHashMap.Empty; + map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; + } - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key, "a"); + private ImTools.V2.Experimental.ImMap> _mapExp; + public ImTools.V2.Experimental.ImMap> Experimental_ImHashMap_AddOrUpdate() + { + var map = ImTools.V2.Experimental.ImMap>.Empty; - map = map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - private ImTools.V2.ImHashMap[] _mapSlots; - public ImTools.V2.ImHashMap[] ImHashMapSlots_AddOrUpdate() - { - var map = ImHashMapSlots.CreateWithEmpty(); + return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + } - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key, "a"); + private ImTools.Experiments.RefEqHashMap _refEqHashMap; + public ImTools.Experiments.RefEqHashMap RefEqHashMap_AddOrUpdate() + { + var map = new ImTools.Experiments.RefEqHashMap(5); - map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key, "a"); - private ImTools.V2.Experimental.ImMap> _mapExp; - public ImTools.V2.Experimental.ImMap> Experimental_ImHashMap_AddOrUpdate() - { - var map = ImTools.V2.Experimental.ImMap>.Empty; + map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); + return map; + } - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + private ImTools.ImHashMap _mapV4; + public ImTools.ImHashMap V4_ImHashMap_AddOrUpdate() + { + var map = ImTools.ImHashMap.Empty; - return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - } + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - private ImTools.Experiments.RefEqHashMap _refEqHashMap; - public ImTools.Experiments.RefEqHashMap RefEqHashMap_AddOrUpdate() - { - var map = new ImTools.Experiments.RefEqHashMap(5); + return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + } - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key, "a"); + private ImToolsV3.ImHashMap _mapV3; + public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() + { + var map = ImToolsV3.ImHashMap.Empty; - map.AddOrUpdate(typeof(ImHashMapBenchmarks), "!"); - return map; - } + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - private ImTools.ImHashMap _mapV4; - public ImTools.ImHashMap V4_ImHashMap_AddOrUpdate() - { - var map = ImTools.ImHashMap.Empty; + return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + } - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + private ImTools.ImHashMap[] _partMapV4; + public ImTools.ImHashMap[] V4_PartitionedHashMap_AddOrUpdate() + { + var map = ImTools.PartitionedHashMap.CreateEmpty(); - return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - } + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key.GetHashCode(), key, "a"); - private ImToolsV3.ImHashMap _mapV3; - public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() - { - var map = ImToolsV3.ImHashMap.Empty; + map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + return map; + } - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + private ImToolsV3.ImHashMap[] _partMapV3; + public ImToolsV3.ImHashMap[] V3_PartitionedHashMap_AddOrUpdate() + { + var map = ImToolsV3.PartitionedHashMap.CreateEmpty(); - return map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - } + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key.GetHashCode(), key, "a"); - private ImTools.ImHashMap[] _partMapV4; - public ImTools.ImHashMap[] V4_PartitionedHashMap_AddOrUpdate() - { - var map = ImTools.PartitionedHashMap.CreateEmpty(); + map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); + return map; + } - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key.GetHashCode(), key, "a"); + private ImToolsV3.ImHashMap[] _partMap23_32; + public ImToolsV3.ImHashMap[] V3_PartitionedHashMap32_AddOrUpdate() + { + var maps = ImToolsV3.PartitionedHashMap.CreateEmpty(32); - map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - return map; - } + foreach (var key in _keys.Take(Count)) + maps.AddOrUpdate(key.GetHashCode(), key, "a", 31); - private ImToolsV3.ImHashMap[] _partMapV3; - public ImToolsV3.ImHashMap[] V3_PartitionedHashMap_AddOrUpdate() - { - var map = ImToolsV3.PartitionedHashMap.CreateEmpty(); + maps.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!", 31); + return maps; + } - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key.GetHashCode(), key, "a"); + public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots32_AddOrUpdate() + { + var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(32); - map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!"); - return map; - } + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a"), 31); - private ImToolsV3.ImHashMap[] _partMap23_32; - public ImToolsV3.ImHashMap[] V3_PartitionedHashMap32_AddOrUpdate() - { - var maps = ImToolsV3.PartitionedHashMap.CreateEmpty(32); + map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!"), 31); + return map; + } - foreach (var key in _keys.Take(Count)) - maps.AddOrUpdate(key.GetHashCode(), key, "a", 31); + private ImTools.V2.Experimental.ImMap>[] _mapSlotsExp32; - maps.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), typeof(ImHashMapBenchmarks), "!", 31); - return maps; - } + public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots64_AddOrUpdate() + { + var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(64); - public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots32_AddOrUpdate() - { - var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(32); + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a"), 63); - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a"), 31); + map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!"), 63); + return map; + } - map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!"), 31); - return map; - } + private ImTools.V2.Experimental.ImMap>[] _mapSlotsExp64; - private ImTools.V2.Experimental.ImMap>[] _mapSlotsExp32; + public Dictionary Dict() + { + var map = new Dictionary(); - public ImTools.V2.Experimental.ImMap>[] Experimental_ImHashMapSlots64_AddOrUpdate() - { - var map = ImTools.V2.Experimental.ImMapSlots.CreateWithEmpty>(64); + foreach (var key in _keys.Take(Count)) + map.TryAdd(key, "a"); - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key.GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(key, "a"), 63); + map.TryAdd(typeof(ImHashMapBenchmarks), "!"); - map.AddOrUpdate(typeof(ImHashMapBenchmarks).GetHashCode(), new ImTools.V2.Experimental.ImMap.KValue(typeof(ImHashMapBenchmarks), "!"), 63); - return map; - } + return map; + } - private ImTools.V2.Experimental.ImMap>[] _mapSlotsExp64; + private Dictionary _dict; - public Dictionary Dict() - { - var map = new Dictionary(); + public DictionarySlim DictSlim() + { + var dict = new DictionarySlim(); - foreach (var key in _keys.Take(Count)) - map.TryAdd(key, "a"); + foreach (var key in _keys.Take(Count)) + dict.GetOrAddValueRef(key) = "a"; - map.TryAdd(typeof(ImHashMapBenchmarks), "!"); + dict.GetOrAddValueRef(LookupKey) = "!"; + return dict; + } - return map; - } + private DictionarySlim _dictSlim; - private Dictionary _dict; + public ImTools.Experiments.FHashMap7> FillFHashMap7() + { + var map = new ImTools.Experiments.FHashMap7>(); - public DictionarySlim DictSlim() - { - var dict = new DictionarySlim(); + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key, "a"); - foreach (var key in _keys.Take(Count)) - dict.GetOrAddValueRef(key) = "a"; + map.AddOrUpdate(LookupKey, "!"); + return map; + } - dict.GetOrAddValueRef(LookupKey) = "!"; - return dict; - } + private ImTools.Experiments.FHashMap7> _fHashMap7; - private DictionarySlim _dictSlim; + public ImTools.Experiments.FHashMap9> FillFHashMap9() + { + var map = new ImTools.Experiments.FHashMap9>(); - public ImTools.Experiments.FHashMap7> FillFHashMap7() - { - var map = new ImTools.Experiments.FHashMap7>(); + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key, "a"); - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key, "a"); + map.AddOrUpdate(LookupKey, "!"); + return map; + } - map.AddOrUpdate(LookupKey, "!"); - return map; - } + private ImTools.Experiments.FHashMap9> _fHashMap9; - private ImTools.Experiments.FHashMap7> _fHashMap7; + public FHashMap91TypeString FillFHashMap91() + { + var map = new FHashMap91TypeString(); - public ImTools.Experiments.FHashMap9> FillFHashMap9() - { - var map = new ImTools.Experiments.FHashMap9>(); + foreach (var key in _keys.Take(Count)) + map.GetOrAddValueRef(key) = "a"; - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key, "a"); + map.GetOrAddValueRef(LookupKey) = "!"; + return map; + } - map.AddOrUpdate(LookupKey, "!"); - return map; - } + private FHashMap91TypeString _fHashMap91; - private ImTools.Experiments.FHashMap9> _fHashMap9; + public SmallMapTypeString FillSmallMap() + { + var map = new SmallMapTypeString(); - public FHashMap91TypeString FillFHashMap91() - { - var map = new FHashMap91TypeString(); + foreach (var key in _keys.Take(Count)) + map.GetOrAddValueRef(key) = "a"; - foreach (var key in _keys.Take(Count)) - map.GetOrAddValueRef(key) = "a"; + map.GetOrAddValueRef(LookupKey) = "!"; + return map; + } - map.GetOrAddValueRef(LookupKey) = "!"; - return map; - } + private SmallMapTypeString _smallMap; - private FHashMap91TypeString _fHashMap91; + public FHashMap11> FillFHashMap11() + { + var map = new FHashMap11>(); - public SmallMapTypeString FillSmallMap() - { - var map = new SmallMapTypeString(); + foreach (var key in _keys.Take(Count)) + map.GetOrAddValueRef(key) = "a"; - foreach (var key in _keys.Take(Count)) - map.GetOrAddValueRef(key) = "a"; + map.GetOrAddValueRef(LookupKey) = "!"; + return map; + } - map.GetOrAddValueRef(LookupKey) = "!"; - return map; - } + private FHashMap11> _fHashMap11; - private SmallMapTypeString _smallMap; + public void FillFecHashMap(ref FecSmallMapTypeString map) + { + foreach (var key in _keys.Take(Count)) + map.Map.AddOrUpdate(key, "a"); - public FHashMap11> FillFHashMap11() - { - var map = new FHashMap11>(); + map.Map.AddOrUpdate(LookupKey, "!"); + } - foreach (var key in _keys.Take(Count)) - map.GetOrAddValueRef(key) = "a"; + private FecSmallMapTypeString _fecSmallMap; - map.GetOrAddValueRef(LookupKey) = "!"; - return map; - } + public ConcurrentDictionary ConcurrentDict() + { + var map = new ConcurrentDictionary(); - private FHashMap11> _fHashMap11; + foreach (var key in _keys.Take(Count)) + map.TryAdd(key, "a"); - public void FillFecHashMap(ref FecSmallMapTypeString map) - { - foreach (var key in _keys.Take(Count)) - map.Map.AddOrUpdate(key, "a"); + map.TryAdd(typeof(ImHashMapBenchmarks), "!"); + return map; + } - map.Map.AddOrUpdate(LookupKey, "!"); - } + private ConcurrentDictionary _concurrentDict; - private FecSmallMapTypeString _fecSmallMap; + public ImmutableDictionary ImmutableDict() + { + var builder = ImmutableDictionary.CreateBuilder(); - public ConcurrentDictionary ConcurrentDict() - { - var map = new ConcurrentDictionary(); + foreach (var key in _keys.Take(Count)) + builder.Add(key, "a"); + builder.Add(typeof(ImHashMapBenchmarks), "!"); + return builder.ToImmutable(); + } - foreach (var key in _keys.Take(Count)) - map.TryAdd(key, "a"); + private ImmutableDictionary _immutableDict; - map.TryAdd(typeof(ImHashMapBenchmarks), "!"); - return map; - } + public TypeDictionary TypeDictionary_Add() + { + var map = new TypeDictionary(); + + map.Add("a"); + map.Add("a"); + map.Add("a"); + map.Add("a"); + map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + // map.Add("a"); + + map.Add("!"); + + return map; + } - private ConcurrentDictionary _concurrentDict; + private TypeDictionary _typeDict; - public ImmutableDictionary ImmutableDict() - { - var builder = ImmutableDictionary.CreateBuilder(); + #endregion - foreach (var key in _keys.Take(Count)) - builder.Add(key, "a"); - builder.Add(typeof(ImHashMapBenchmarks), "!"); - return builder.ToImmutable(); - } + public static Type LookupKey = typeof(Console); - private ImmutableDictionary _immutableDict; + // [Benchmark(Baseline = true)] + public string V2_ImHashMap_AVL_TryFind() + { + _mapV2.TryFind(LookupKey, out var result); + return result; + } - public TypeDictionary TypeDictionary_Add() - { - var map = new TypeDictionary(); - - map.Add("a"); - map.Add("a"); - map.Add("a"); - map.Add("a"); - map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - // map.Add("a"); - - map.Add("!"); - - return map; - } + // [Benchmark] + public string ImHashMapSlots32_TryFind() + { + var hash = LookupKey.GetHashCode(); + _mapSlots[hash & ImHashMapSlots.HASH_MASK_TO_FIND_SLOT].TryFind(hash, LookupKey, out var result); + return result; + } - private TypeDictionary _typeDict; + // [Benchmark] + public string V2_ImHashMap_AVLOptimizedForAdd_TryFind() + { + _mapExp.TryFind(LookupKey.GetHashCode(), LookupKey, out var result); + return (string)result; + } - #endregion + // [Benchmark(Baseline = true)] + public string V4_ImHashMap_TryFind() + { + _mapV4.TryFind(LookupKey.GetHashCode(), LookupKey, out var result); + return result; + } - public static Type LookupKey = typeof(Console); + // [Benchmark] + public string V3_ImHashMap_TryFind() + { + _mapV3.TryFind(LookupKey.GetHashCode(), LookupKey, out var result); + return result; + } - // [Benchmark(Baseline = true)] - public string V2_ImHashMap_AVL_TryFind() - { - _mapV2.TryFind(LookupKey, out var result); - return result; - } + // [Benchmark] + public string V4_ImMap_GetValueOrDefault() => + _mapV4.GetValueOrDefaultByReferenceEquals(LookupKey.GetHashCode(), LookupKey); + + // [Benchmark] + public string V3_ImHashMap_GetValueOrDefault() => + _mapV3.GetValueOrDefaultByReferenceEquals(LookupKey.GetHashCode(), LookupKey); + + // [Benchmark] + public string V3_PartitionedHashMap_GetValueOrDefault() => + _partMapV3.GetValueOrDefaultByReferenceEquals(LookupKey); + + // [Benchmark] + public string V3_PartitionedHashMap32_GetValueOrDefault() => + _partMap23_32.GetValueOrDefaultByReferenceEquals(LookupKey, 31); + + // [Benchmark] + public string TypeDict_TryFind() => + _typeDict.Get(); + + // [Benchmark] + // public string V3_ImHashMap_23Tree_TryFind_SIMPLIFIED() + // { + // var entry = _map234.GetEntryOrDefault(LookupKey.GetHashCode()); + // if (entry == null) + // return null; + // return ((ImTools.Experimental.ImHashMap234.KeyValueEntry)entry).Value; + // } + + // [Benchmark] + public string V4_PartitionedHashMap_TryFind() + { + var hash = LookupKey.GetHashCode(); + _partMapV4[hash & ImToolsV3.PartitionedHashMap.PARTITION_HASH_MASK].TryFind(hash, LookupKey, out var result); + return result; + } - // [Benchmark] - public string ImHashMapSlots32_TryFind() - { - var hash = LookupKey.GetHashCode(); - _mapSlots[hash & ImHashMapSlots.HASH_MASK_TO_FIND_SLOT].TryFind(hash, LookupKey, out var result); - return result; - } + // [Benchmark] + public string V3_PartitionedHashMap_TryFind() + { + var hash = LookupKey.GetHashCode(); + _partMapV3[hash & ImToolsV3.PartitionedHashMap.PARTITION_HASH_MASK].TryFind(hash, LookupKey, out var result); + return result; + } - // [Benchmark] - public string V2_ImHashMap_AVLOptimizedForAdd_TryFind() - { - _mapExp.TryFind(LookupKey.GetHashCode(), LookupKey, out var result); - return (string)result; - } + // [Benchmark] + public string Experimental_ImHashMapSlots32_TryFind() + { + var hash = LookupKey.GetHashCode(); + _mapSlotsExp32[hash & ImHashMapSlots.HASH_MASK_TO_FIND_SLOT].TryFind(hash, LookupKey, out var result); + return (string)result; + } - // [Benchmark(Baseline = true)] - public string V4_ImHashMap_TryFind() - { - _mapV4.TryFind(LookupKey.GetHashCode(), LookupKey, out var result); - return result; - } + // [Benchmark] + public string Experimental_ImHashMapSlots64_TryFind() + { + var hash = LookupKey.GetHashCode(); + _mapSlotsExp64[hash & 63].TryFind(hash, LookupKey, out var result); + return (string)result; + } - // [Benchmark] - public string V3_ImHashMap_TryFind() - { - _mapV3.TryFind(LookupKey.GetHashCode(), LookupKey, out var result); - return result; - } + // [Benchmark] + // [Benchmark(Baseline = true)] + public string Dictionary_TryGetValue() + { + _dict.TryGetValue(LookupKey, out var result); + return result; + } - // [Benchmark] - public string V4_ImMap_GetValueOrDefault() => - _mapV4.GetValueOrDefaultByReferenceEquals(LookupKey.GetHashCode(), LookupKey); - - // [Benchmark] - public string V3_ImHashMap_GetValueOrDefault() => - _mapV3.GetValueOrDefaultByReferenceEquals(LookupKey.GetHashCode(), LookupKey); - - // [Benchmark] - public string V3_PartitionedHashMap_GetValueOrDefault() => - _partMapV3.GetValueOrDefaultByReferenceEquals(LookupKey); - - // [Benchmark] - public string V3_PartitionedHashMap32_GetValueOrDefault() => - _partMap23_32.GetValueOrDefaultByReferenceEquals(LookupKey, 31); - - // [Benchmark] - public string TypeDict_TryFind() => - _typeDict.Get(); - - // [Benchmark] - // public string V3_ImHashMap_23Tree_TryFind_SIMPLIFIED() - // { - // var entry = _map234.GetEntryOrDefault(LookupKey.GetHashCode()); - // if (entry == null) - // return null; - // return ((ImTools.Experimental.ImHashMap234.KeyValueEntry)entry).Value; - // } - - // [Benchmark] - public string V4_PartitionedHashMap_TryFind() - { - var hash = LookupKey.GetHashCode(); - _partMapV4[hash & ImToolsV3.PartitionedHashMap.PARTITION_HASH_MASK].TryFind(hash, LookupKey, out var result); - return result; - } + // [Benchmark] + [Benchmark(Baseline = true)] + public int DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent() + { + var dict = new DictionarySlim(); - // [Benchmark] - public string V3_PartitionedHashMap_TryFind() - { - var hash = LookupKey.GetHashCode(); - _partMapV3[hash & ImToolsV3.PartitionedHashMap.PARTITION_HASH_MASK].TryFind(hash, LookupKey, out var result); - return result; - } + foreach (var key in _presentKeys) + dict.GetOrAddValueRef(key) = "a"; - // [Benchmark] - public string Experimental_ImHashMapSlots32_TryFind() + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) { - var hash = LookupKey.GetHashCode(); - _mapSlotsExp32[hash & ImHashMapSlots.HASH_MASK_TO_FIND_SLOT].TryFind(hash, LookupKey, out var result); - return (string)result; + if (dict.TryGetValue(_randomPresentKeys[i], out var result)) + count += result.Length; + if (!dict.TryGetValue(_missingKeys[i], out var _)) + --count; } + return count; + } - // [Benchmark] - public string Experimental_ImHashMapSlots64_TryFind() - { - var hash = LookupKey.GetHashCode(); - _mapSlotsExp64[hash & 63].TryFind(hash, LookupKey, out var result); - return (string)result; - } + [Benchmark] + public int SmallMap_PopulateThenLookup_HalfMissed_HalfPresent() + { + var map = new SmallMapTypeString(); - // [Benchmark] - // [Benchmark(Baseline = true)] - public string Dictionary_TryGetValue() - { - _dict.TryGetValue(LookupKey, out var result); - return result; - } + foreach (var key in _presentKeys) + map.GetOrAddValueRef(key) = "a"; - // [Benchmark] - [Benchmark(Baseline = true)] - public int DictionarySlim_PopulateThenLookup_HalfMissed_HalfPresent() + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) { - var dict = new DictionarySlim(); - - foreach (var key in _presentKeys) - dict.GetOrAddValueRef(key) = "a"; - - var count = 0; - var iters = Count / 2; - for (var i = 0; i < iters; ++i) - { - if (dict.TryGetValue(_randomPresentKeys[i], out var result)) - count += result.Length; - if (!dict.TryGetValue(_missingKeys[i], out var _)) - --count; - } - return count; + if (map.TryGetValue(_randomPresentKeys[i], out var result)) + count += result.Length; + if (!map.TryGetValue(_missingKeys[i], out var _)) + --count; } + return count; + } - // [Benchmark] - public int SmallMap_PopulateThenLookup_HalfMissed_HalfPresent() - { - var map = new SmallMapTypeString(); - - foreach (var key in _presentKeys) - map.GetOrAddValueRef(key) = "a"; - - var count = 0; - var iters = Count / 2; - for (var i = 0; i < iters; ++i) - { - if (map.TryGetValue(_randomPresentKeys[i], out var result)) - count += result.Length; - if (!map.TryGetValue(_missingKeys[i], out var _)) - --count; - } - return count; - } + // [Benchmark(Baseline = true)] + [Benchmark] + public int FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent() + { + var m = new FecSmallMapTypeString(); + ref var map = ref m.Map; - // [Benchmark(Baseline = true)] - [Benchmark] - public int FecHashMap_PopulateThenLookup_HalfMissed_HalfPresent() - { - var m = new FecSmallMapTypeString(); - ref var map = ref m.Map; - - foreach (var key in _presentKeys) - map.AddOrUpdate(key, "a"); - - var count = 0; - var iters = Count / 2; - for (var i = 0; i < iters; ++i) - { - ref var result = ref map.TryGetEntryRef(_randomPresentKeys[i], out var found); - if (found) - count += result.Value.Length; - _ = ref map.TryGetEntryRef(_missingKeys[i], out found); - if (!found) - --count; - } - return count; - } + foreach (var key in _presentKeys) + map.AddOrUpdate(key, "a"); - // [Benchmark] - public int FecHashMap_Init1000_PopulateThenLookup_HalfMissed_HalfPresent() + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) { - var m = new FecSmallMapTypeString(1024); - ref var map = ref m.Map; - - foreach (var key in _presentKeys) - map.AddOrUpdate(key, "a"); - - var count = 0; - var iters = Count / 2; - for (var i = 0; i < iters; ++i) - { - ref var result = ref map.TryGetEntryRef(_randomPresentKeys[i], out var found); - if (found) - count += result.Value.Length; - _ = ref map.TryGetEntryRef(_missingKeys[i], out found); - if (!found) - --count; - } - return count; + ref var result = ref map.TryGetEntryRef(_randomPresentKeys[i], out var found); + if (found) + count += result.Value.Length; + _ = ref map.TryGetEntryRef(_missingKeys[i], out found); + if (!found) + --count; } + return count; + } + + // [Benchmark] + public int FecHashMap_Init1000_PopulateThenLookup_HalfMissed_HalfPresent() + { + var m = new FecSmallMapTypeString(1024); + ref var map = ref m.Map; + + foreach (var key in _presentKeys) + map.AddOrUpdate(key, "a"); - // [Benchmark] - public int FecHashMap_SingleArrEntries_PopulateThenLookup_HalfMissed_HalfPresent() + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) { - var m = new FecSmallMapTypeString_SingleArrEntries(); - ref var map = ref m.Map; - - foreach (var key in _presentKeys) - map.AddOrUpdate(key, "a"); - - var count = 0; - var iters = Count / 2; - for (var i = 0; i < iters; ++i) - { - ref var result = ref map.TryGetEntryRef(_randomPresentKeys[i], out var found); - if (found) - count += result.Value.Length; - _ = ref map.TryGetEntryRef(_missingKeys[i], out found); - if (!found) - --count; - } - return count; + ref var result = ref map.TryGetEntryRef(_randomPresentKeys[i], out var found); + if (found) + count += result.Value.Length; + _ = ref map.TryGetEntryRef(_missingKeys[i], out found); + if (!found) + --count; } + return count; + } - // [Benchmark] - public int FHashMap11_TryGetValue() - { - var count = 0; - foreach (var k in _randomPresentKeys) - { - _fHashMap11.TryGetValue(k, out var result); - count += result.Length; - } + // [Benchmark] + public int FecHashMap_SingleArrEntries_PopulateThenLookup_HalfMissed_HalfPresent() + { + var m = new FecSmallMapTypeString_SingleArrEntries(); + ref var map = ref m.Map; - return count; - } + foreach (var key in _presentKeys) + map.AddOrUpdate(key, "a"); - // [Benchmark(Baseline = true)] - // [Benchmark] - public string FHashMap7_TryGetValue() + var count = 0; + var iters = Count / 2; + for (var i = 0; i < iters; ++i) { - _fHashMap7.TryGetValue(LookupKey, out var result); - return result; + ref var result = ref map.TryGetEntryRef(_randomPresentKeys[i], out var found); + if (found) + count += result.Value.Length; + _ = ref map.TryGetEntryRef(_missingKeys[i], out found); + if (!found) + --count; } + return count; + } - // [Benchmark] - public string FHashMap9_TryGetValue() + // [Benchmark] + public int FHashMap11_TryGetValue() + { + var count = 0; + foreach (var k in _randomPresentKeys) { - _fHashMap9.TryGetValue(LookupKey, out var result); - return result; + _fHashMap11.TryGetValue(k, out var result); + count += result.Length; } - // [Benchmark] - public string FHashMap91_TryGetValue() - { - _fHashMap91.TryGetValue(LookupKey, out var result); - return result; - } + return count; + } - // [Benchmark] - public string ConcurrentDictionary_TryGetValue() - { - _concurrentDict.TryGetValue(LookupKey, out var result); - return result; - } + // [Benchmark(Baseline = true)] + // [Benchmark] + public string FHashMap7_TryGetValue() + { + _fHashMap7.TryGetValue(LookupKey, out var result); + return result; + } - // [Benchmark] - public string ImmutableDict_TryGet() - { - _immutableDict.TryGetValue(LookupKey, out var result); - return result; - } + // [Benchmark] + public string FHashMap9_TryGetValue() + { + _fHashMap9.TryGetValue(LookupKey, out var result); + return result; } - [MemoryDiagnoser] - public class Enumerate - { - /* - ## V2: - - BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 - Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=3.0.100 - [Host] : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT - DefaultJob : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |------------------------------ |------ |--------------:|-----------:|-----------:|------:|--------:|--------:|-------:|------:|----------:| - | ImHashMap_EnumerateToArray | 1 | 147.31 ns | 0.567 ns | 0.531 ns | 1.00 | 0.00 | 0.0441 | - | - | 208 B | - | ImHashMap_V1_EnumerateToArray | 1 | 160.45 ns | 0.856 ns | 0.801 ns | 1.09 | 0.01 | 0.0560 | - | - | 264 B | - | ImHashMap_FoldToArray | 1 | 55.05 ns | 0.436 ns | 0.387 ns | 0.37 | 0.00 | 0.0356 | - | - | 168 B | - | ImHashMapSlots_FoldToArray | 1 | 89.21 ns | 1.473 ns | 1.378 ns | 0.61 | 0.01 | 0.0271 | - | - | 128 B | - | DictionarySlim_ToArray | 1 | 150.88 ns | 1.424 ns | 1.189 ns | 1.02 | 0.01 | 0.0408 | - | - | 192 B | - | Dictionary_ToArray | 1 | 40.47 ns | 0.864 ns | 1.093 ns | 0.28 | 0.01 | 0.0119 | - | - | 56 B | - | ConcurrentDictionary_ToArray | 1 | 232.31 ns | 1.981 ns | 1.654 ns | 1.58 | 0.01 | 0.0114 | - | - | 56 B | - | ImmutableDict_ToArray | 1 | 618.68 ns | 11.308 ns | 10.578 ns | 4.20 | 0.07 | 0.0114 | - | - | 56 B | - | | | | | | | | | | | | - | ImHashMap_EnumerateToArray | 10 | 423.85 ns | 8.425 ns | 9.364 ns | 1.00 | 0.00 | 0.1001 | - | - | 472 B | - | ImHashMap_V1_EnumerateToArray | 10 | 492.81 ns | 4.461 ns | 4.173 ns | 1.16 | 0.03 | 0.1726 | - | - | 816 B | - | ImHashMap_FoldToArray | 10 | 213.14 ns | 4.054 ns | 3.981 ns | 0.50 | 0.02 | 0.1054 | - | - | 496 B | - | ImHashMapSlots_FoldToArray | 10 | 255.16 ns | 1.863 ns | 1.743 ns | 0.60 | 0.01 | 0.1016 | - | - | 480 B | - | DictionarySlim_ToArray | 10 | 450.09 ns | 8.918 ns | 10.616 ns | 1.06 | 0.04 | 0.1354 | - | - | 640 B | - | Dictionary_ToArray | 10 | 87.40 ns | 1.696 ns | 1.586 ns | 0.21 | 0.01 | 0.0424 | - | - | 200 B | - | ConcurrentDictionary_ToArray | 10 | 499.22 ns | 4.804 ns | 4.494 ns | 1.17 | 0.03 | 0.0420 | - | - | 200 B | - | ImmutableDict_ToArray | 10 | 1,954.98 ns | 10.732 ns | 10.038 ns | 4.60 | 0.11 | 0.0381 | - | - | 200 B | - | | | | | | | | | | | | - | ImHashMap_EnumerateToArray | 100 | 2,735.61 ns | 34.407 ns | 32.185 ns | 1.00 | 0.00 | 0.4768 | - | - | 2248 B | - | ImHashMap_V1_EnumerateToArray | 100 | 3,368.76 ns | 6.822 ns | 6.048 ns | 1.23 | 0.02 | 1.1597 | 0.0267 | - | 5472 B | - | ImHashMap_FoldToArray | 100 | 1,433.97 ns | 14.981 ns | 14.013 ns | 0.52 | 0.01 | 0.6599 | 0.0038 | - | 3112 B | - | ImHashMapSlots_FoldToArray | 100 | 1,541.62 ns | 8.594 ns | 8.039 ns | 0.56 | 0.01 | 0.6714 | 0.0038 | - | 3168 B | - | DictionarySlim_ToArray | 100 | 2,505.97 ns | 44.927 ns | 37.516 ns | 0.92 | 0.02 | 0.8469 | 0.0076 | - | 4000 B | - | Dictionary_ToArray | 100 | 549.34 ns | 6.559 ns | 6.136 ns | 0.20 | 0.00 | 0.3481 | 0.0019 | - | 1640 B | - | ConcurrentDictionary_ToArray | 100 | 2,236.03 ns | 10.044 ns | 9.395 ns | 0.82 | 0.01 | 0.3471 | - | - | 1640 B | - | ImmutableDict_ToArray | 100 | 15,683.87 ns | 68.105 ns | 60.373 ns | 5.74 | 0.08 | 0.3357 | - | - | 1640 B | - | | | | | | | | | | | | - | ImHashMap_EnumerateToArray | 1000 | 25,723.37 ns | 504.158 ns | 560.370 ns | 1.00 | 0.00 | 3.5706 | 0.1526 | - | 16808 B | - | ImHashMap_V1_EnumerateToArray | 1000 | 34,316.66 ns | 583.573 ns | 545.874 ns | 1.34 | 0.04 | 10.3149 | 1.8921 | - | 48833 B | - | ImHashMap_FoldToArray | 1000 | 16,277.05 ns | 38.330 ns | 33.979 ns | 0.64 | 0.02 | 5.2490 | 0.3052 | - | 24752 B | - | ImHashMapSlots_FoldToArray | 1000 | 15,167.14 ns | 261.927 ns | 218.721 ns | 0.59 | 0.02 | 5.2490 | 0.2899 | - | 24784 B | - | DictionarySlim_ToArray | 1000 | 22,273.30 ns | 384.716 ns | 359.864 ns | 0.87 | 0.01 | 6.9885 | 0.6714 | - | 32896 B | - | Dictionary_ToArray | 1000 | 4,997.89 ns | 20.869 ns | 18.500 ns | 0.20 | 0.00 | 3.3951 | 0.1831 | - | 16040 B | - | ConcurrentDictionary_ToArray | 1000 | 36,859.40 ns | 193.536 ns | 181.034 ns | 1.44 | 0.04 | 3.3569 | 0.1831 | - | 16040 B | - | ImmutableDict_ToArray | 1000 | 155,798.41 ns | 484.101 ns | 452.828 ns | 6.08 | 0.14 | 3.1738 | - | - | 16040 B | - - | Method | Count | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated | - |----------------------------------- |------ |-------------:|-----------:|-----------:|------:|-------:|-------:|------:|----------:| - | ImHashMap_EnumerateToArray | 1 | 153.03 ns | 0.774 ns | 0.686 ns | 1.00 | 0.0441 | - | - | 208 B | - | Experimental_ImHashMap_FoldToArray | 1 | 62.26 ns | 0.180 ns | 0.159 ns | 0.41 | 0.0271 | - | - | 128 B | - | | | | | | | | | | | - | ImHashMap_EnumerateToArray | 10 | 417.19 ns | 4.075 ns | 3.403 ns | 1.00 | 0.1001 | - | - | 472 B | - | Experimental_ImHashMap_FoldToArray | 10 | 237.62 ns | 1.079 ns | 1.009 ns | 0.57 | 0.1016 | - | - | 480 B | - | | | | | | | | | | | - | ImHashMap_EnumerateToArray | 100 | 2,693.86 ns | 7.426 ns | 6.946 ns | 1.00 | 0.4768 | - | - | 2248 B | - | Experimental_ImHashMap_FoldToArray | 100 | 1,517.72 ns | 6.061 ns | 5.669 ns | 0.56 | 0.6561 | 0.0038 | - | 3096 B | - | | | | | | | | | | | - | ImHashMap_EnumerateToArray | 1000 | 26,018.52 ns | 175.262 ns | 146.352 ns | 1.00 | 3.5706 | 0.1831 | - | 16808 B | - | Experimental_ImHashMap_FoldToArray | 1000 | 15,321.95 ns | 69.421 ns | 64.937 ns | 0.59 | 5.2490 | 0.2747 | - | 24736 B | - - ## V3 baseline - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |----------------------------------------- |------ |-------------:|----------:|----------:|------:|--------:|-------:|------:|------:|----------:| - | V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 198.4 ns | 3.64 ns | 3.04 ns | 1.00 | 0.00 | 0.0496 | - | - | 208 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 158.0 ns | 3.23 ns | 3.17 ns | 0.80 | 0.02 | 0.0362 | - | - | 152 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 535.6 ns | 7.97 ns | 8.86 ns | 1.00 | 0.00 | 0.1125 | - | - | 472 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 558.9 ns | 5.35 ns | 5.25 ns | 1.04 | 0.02 | 0.1354 | - | - | 568 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 3,601.3 ns | 54.44 ns | 50.92 ns | 1.00 | 0.00 | 0.5341 | - | - | 2248 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 9,800.0 ns | 110.20 ns | 97.69 ns | 2.72 | 0.04 | 1.0529 | - | - | 4424 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 34,261.7 ns | 371.47 ns | 310.20 ns | 1.00 | 0.00 | 3.9673 | - | - | 16808 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 128,626.8 ns | 988.46 ns | 924.61 ns | 3.76 | 0.04 | 9.2773 | - | - | 38912 B | - - ### Static Enumerate - incomplete - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |----------------------------------------- |------ |------------:|----------:|----------:|------:|--------:|-------:|------:|------:|----------:| - | V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 164.7 ns | 2.23 ns | 1.98 ns | 1.00 | 0.00 | 0.0496 | - | - | 208 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 245.0 ns | 4.92 ns | 5.66 ns | 1.49 | 0.04 | 0.0567 | - | - | 240 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 295.6 ns | 5.87 ns | 5.77 ns | 1.00 | 0.00 | 0.0801 | - | - | 336 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 497.5 ns | 5.77 ns | 5.12 ns | 1.68 | 0.04 | 0.0849 | - | - | 360 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 459.0 ns | 4.86 ns | 4.55 ns | 1.00 | 0.00 | 0.1116 | - | - | 472 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 683.6 ns | 9.36 ns | 8.30 ns | 1.49 | 0.02 | 0.1335 | - | - | 560 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 3,219.3 ns | 36.20 ns | 30.23 ns | 1.00 | 0.00 | 0.5341 | - | - | 2248 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 5,009.4 ns | 57.55 ns | 51.02 ns | 1.56 | 0.02 | 0.5264 | - | - | 2208 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 29,919.7 ns | 363.29 ns | 322.05 ns | 1.00 | 0.00 | 3.9673 | - | - | 16808 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 21,530.8 ns | 292.37 ns | 259.18 ns | 0.72 | 0.01 | 1.8921 | - | - | 8016 B | - - ### Struct enumerator for leafs - - BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.630 (2004/?/20H1) - Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores - .NET Core SDK=5.0.100 - [Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT - DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT - - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |----------------------------------------- |------ |-------------:|------------:|------------:|------:|--------:|-------:|------:|------:|----------:| - | V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 131.8 ns | 2.43 ns | 2.27 ns | 1.00 | 0.00 | 0.0458 | - | - | 192 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 107.7 ns | 0.56 ns | 0.44 ns | 0.82 | 0.01 | 0.0305 | - | - | 128 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 251.5 ns | 3.12 ns | 2.77 ns | 1.00 | 0.00 | 0.0782 | - | - | 328 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 214.5 ns | 4.37 ns | 4.86 ns | 0.86 | 0.02 | 0.0591 | - | - | 248 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 402.3 ns | 2.40 ns | 2.00 ns | 1.00 | 0.00 | 0.1106 | - | - | 464 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 469.7 ns | 8.19 ns | 14.13 ns | 1.19 | 0.04 | 0.1144 | - | - | 480 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 2,987.6 ns | 34.44 ns | 28.76 ns | 1.00 | 0.00 | 0.5341 | - | - | 2240 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 8,085.2 ns | 54.39 ns | 50.88 ns | 2.71 | 0.03 | 0.9308 | - | - | 3904 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 29,107.9 ns | 372.54 ns | 330.25 ns | 1.00 | 0.00 | 3.9978 | - | - | 16800 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 117,098.5 ns | 1,388.31 ns | 1,298.62 ns | 4.02 | 0.07 | 8.0566 | - | - | 33992 B | - - - ### Static iterative enumerator with List as a stack - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |----------------------------------------- |------ |------------:|----------:|----------:|------:|--------:|-------:|------:|------:|----------:| - | V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 130.0 ns | 1.58 ns | 1.76 ns | 1.00 | 0.00 | 0.0458 | - | - | 192 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 129.1 ns | 1.55 ns | 1.45 ns | 0.99 | 0.02 | 0.0610 | - | - | 256 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 251.6 ns | 3.58 ns | 3.35 ns | 1.00 | 0.00 | 0.0782 | - | - | 328 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 241.4 ns | 2.97 ns | 2.78 ns | 0.96 | 0.01 | 0.0896 | - | - | 376 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 414.8 ns | 8.12 ns | 10.84 ns | 1.00 | 0.00 | 0.1106 | - | - | 464 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 403.7 ns | 6.25 ns | 5.84 ns | 0.98 | 0.03 | 0.1373 | - | - | 576 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 2,947.2 ns | 23.48 ns | 20.81 ns | 1.00 | 0.00 | 0.5341 | - | - | 2240 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 2,822.2 ns | 16.96 ns | 15.87 ns | 0.96 | 0.01 | 0.5646 | - | - | 2376 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 28,594.2 ns | 307.61 ns | 287.73 ns | 1.00 | 0.00 | 3.9673 | - | - | 16800 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 27,555.7 ns | 318.08 ns | 281.97 ns | 0.96 | 0.01 | 4.0588 | - | - | 16992 B | - - ### Branch3 is a Branch2 - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |----------------------------------------- |------ |-----------:|---------:|---------:|------:|--------:|-------:|------:|------:|----------:| - | V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 329.7 ns | 6.40 ns | 5.99 ns | 1.00 | 0.00 | 0.0782 | - | - | 328 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 313.1 ns | 5.69 ns | 5.59 ns | 0.95 | 0.02 | 0.0877 | - | - | 368 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 513.4 ns | 4.55 ns | 4.26 ns | 1.00 | 0.00 | 0.1106 | - | - | 464 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 525.6 ns | 7.40 ns | 6.92 ns | 1.02 | 0.02 | 0.1354 | - | - | 568 B | - | | | | | | | | | | | | - | V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 3,658.7 ns | 53.67 ns | 47.58 ns | 1.00 | 0.00 | 0.5341 | - | - | 2240 B | - | V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 3,388.2 ns | 67.71 ns | 63.34 ns | 0.93 | 0.02 | 0.5646 | - | - | 2368 B | - - ### V3 RTM - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |------------------------------ |------ |-------------:|-------------:|-------------:|-------------:|------:|--------:|-------:|------:|------:|----------:| - | V2_ImHashMap_foreach | 1 | 53.16 ns | 1.107 ns | 1.317 ns | 53.11 ns | 1.00 | 0.00 | 0.0166 | - | - | 104 B | - | V3_ImHashMap_foreach | 1 | 62.12 ns | 1.327 ns | 1.986 ns | 61.59 ns | 1.16 | 0.05 | 0.0267 | - | - | 168 B | - | V3_PartitionedHashMap_foreach | 1 | 238.62 ns | 4.811 ns | 6.084 ns | 235.92 ns | 4.50 | 0.13 | 0.0534 | - | - | 336 B | - | DictionarySlim_foreach | 1 | 12.90 ns | 0.167 ns | 0.156 ns | 12.90 ns | 0.24 | 0.01 | - | - | - | - | - | Dictionary_foreach | 1 | 14.19 ns | 0.217 ns | 0.181 ns | 14.14 ns | 0.27 | 0.01 | - | - | - | - | - | ConcurrentDictionary_foreach | 1 | 153.40 ns | 2.768 ns | 4.142 ns | 151.56 ns | 2.89 | 0.11 | 0.0100 | - | - | 64 B | - | ImmutableDict_foreach | 1 | 268.98 ns | 5.361 ns | 9.528 ns | 268.86 ns | 5.01 | 0.26 | - | - | - | - | - | | | | | | | | | | | | | - | V2_ImHashMap_foreach | 10 | 233.42 ns | 4.541 ns | 4.859 ns | 232.71 ns | 1.00 | 0.00 | 0.0200 | - | - | 128 B | - | V3_ImHashMap_foreach | 10 | 249.12 ns | 4.915 ns | 5.852 ns | 246.87 ns | 1.07 | 0.03 | 0.0391 | - | - | 248 B | - | V3_PartitionedHashMap_foreach | 10 | 746.26 ns | 14.990 ns | 18.409 ns | 748.53 ns | 3.20 | 0.13 | 0.1602 | - | - | 1008 B | - | DictionarySlim_foreach | 10 | 72.54 ns | 0.970 ns | 0.907 ns | 72.42 ns | 0.31 | 0.01 | - | - | - | - | - | Dictionary_foreach | 10 | 58.52 ns | 0.938 ns | 0.733 ns | 58.58 ns | 0.25 | 0.01 | - | - | - | - | - | ConcurrentDictionary_foreach | 10 | 468.65 ns | 9.252 ns | 12.351 ns | 464.12 ns | 2.01 | 0.06 | 0.0095 | - | - | 64 B | - | ImmutableDict_foreach | 10 | 1,127.30 ns | 15.601 ns | 14.593 ns | 1,123.20 ns | 4.82 | 0.10 | - | - | - | - | - | | | | | | | | | | | | | - | V2_ImHashMap_foreach | 100 | 2,355.54 ns | 46.224 ns | 63.271 ns | 2,337.40 ns | 1.00 | 0.00 | 0.0229 | - | - | 160 B | - | V3_ImHashMap_foreach | 100 | 2,423.13 ns | 31.652 ns | 46.395 ns | 2,412.92 ns | 1.03 | 0.04 | 0.0496 | - | - | 320 B | - | V3_PartitionedHashMap_foreach | 100 | 4,268.51 ns | 25.429 ns | 22.542 ns | 4,266.90 ns | 1.81 | 0.05 | 0.4501 | - | - | 2856 B | - | DictionarySlim_foreach | 100 | 570.39 ns | 5.827 ns | 4.866 ns | 570.93 ns | 0.24 | 0.01 | - | - | - | - | - | Dictionary_foreach | 100 | 548.46 ns | 8.579 ns | 7.605 ns | 547.90 ns | 0.23 | 0.01 | - | - | - | - | - | ConcurrentDictionary_foreach | 100 | 2,967.70 ns | 45.435 ns | 44.623 ns | 2,958.11 ns | 1.26 | 0.04 | 0.0076 | - | - | 64 B | - | ImmutableDict_foreach | 100 | 9,988.48 ns | 198.973 ns | 297.813 ns | 9,821.03 ns | 4.24 | 0.14 | - | - | - | - | - | | | | | | | | | | | | | - | V2_ImHashMap_foreach | 1000 | 23,828.30 ns | 433.708 ns | 362.166 ns | 23,743.77 ns | 1.00 | 0.00 | 0.0305 | - | - | 192 B | - | V3_ImHashMap_foreach | 1000 | 26,014.69 ns | 294.125 ns | 245.608 ns | 25,965.82 ns | 1.09 | 0.02 | 0.0610 | - | - | 552 B | - | V3_PartitionedHashMap_foreach | 1000 | 36,582.53 ns | 709.641 ns | 897.469 ns | 36,594.84 ns | 1.54 | 0.04 | 0.4883 | - | - | 3240 B | - | DictionarySlim_foreach | 1000 | 5,591.13 ns | 43.627 ns | 40.809 ns | 5,602.25 ns | 0.23 | 0.00 | - | - | - | - | - | Dictionary_foreach | 1000 | 5,319.86 ns | 51.684 ns | 45.817 ns | 5,308.36 ns | 0.22 | 0.00 | - | - | - | - | - | ConcurrentDictionary_foreach | 1000 | 38,718.40 ns | 466.979 ns | 389.949 ns | 38,728.64 ns | 1.63 | 0.03 | - | - | - | 64 B | - | ImmutableDict_foreach | 1000 | 99,156.35 ns | 1,962.968 ns | 2,181.834 ns | 98,166.03 ns | 4.17 | 0.11 | - | - | - | - | - - ## V3.2 - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |--------------------- |------ |-------------:|-----------:|-----------:|------:|--------:|-------:|------:|------:|----------:| - | V3_ImHashMap_foreach | 1 | 49.59 ns | 0.758 ns | 0.633 ns | 1.00 | 0.00 | 0.0255 | - | - | 160 B | - | V2_ImHashMap_foreach | 1 | 45.96 ns | 0.273 ns | 0.242 ns | 0.93 | 0.01 | 0.0166 | - | - | 104 B | - | | | | | | | | | | | | - | V3_ImHashMap_foreach | 10 | 214.40 ns | 3.915 ns | 3.662 ns | 1.00 | 0.00 | 0.0381 | - | - | 240 B | - | V2_ImHashMap_foreach | 10 | 204.50 ns | 0.963 ns | 0.854 ns | 0.95 | 0.02 | 0.0203 | - | - | 128 B | - | | | | | | | | | | | | - | V3_ImHashMap_foreach | 100 | 2,072.90 ns | 40.826 ns | 41.926 ns | 1.00 | 0.00 | 0.0496 | - | - | 328 B | - | V2_ImHashMap_foreach | 100 | 2,206.31 ns | 29.262 ns | 25.940 ns | 1.07 | 0.03 | 0.0229 | - | - | 160 B | - | | | | | | | | | | | | - | V3_ImHashMap_foreach | 1000 | 21,043.39 ns | 298.240 ns | 232.846 ns | 1.00 | 0.00 | 0.0305 | - | - | 328 B | - | V2_ImHashMap_foreach | 1000 | 21,484.35 ns | 168.389 ns | 149.272 ns | 1.02 | 0.01 | 0.0305 | - | - | 192 B | - - ## V4.0 - baseline - - BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 - Intel Core i5-8350U CPU 1.70GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores - .NET Core SDK=6.0.102 - [Host] : .NET Core 6.0.2 (CoreCLR 6.0.222.6406, CoreFX 6.0.222.6406), X64 RyuJIT - DefaultJob : .NET Core 6.0.2 (CoreCLR 6.0.222.6406, CoreFX 6.0.222.6406), X64 RyuJIT - - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |----------------------- |------ |-------------:|-------------:|-------------:|-------------:|------:|--------:|-------:|------:|------:|----------:| - | V3_ImHashMap_Enumerate | 1 | 78.29 ns | 3.211 ns | 9.417 ns | 74.97 ns | 1.00 | 0.00 | 0.0509 | - | - | 160 B | - | Dictionary_Enumerate | 1 | 23.15 ns | 0.712 ns | 2.008 ns | 22.54 ns | 0.30 | 0.04 | - | - | - | - | - | | | | | | | | | | | | | - | V3_ImHashMap_Enumerate | 10 | 351.19 ns | 7.127 ns | 20.791 ns | 344.93 ns | 1.00 | 0.00 | 0.0763 | - | - | 240 B | - | Dictionary_Enumerate | 10 | 92.85 ns | 1.957 ns | 4.128 ns | 91.81 ns | 0.27 | 0.02 | - | - | - | - | - | | | | | | | | | | | | | - | V3_ImHashMap_Enumerate | 100 | 3,049.03 ns | 64.798 ns | 186.958 ns | 2,987.63 ns | 1.00 | 0.00 | 0.0763 | - | - | 240 B | - | Dictionary_Enumerate | 100 | 844.11 ns | 16.992 ns | 38.698 ns | 837.12 ns | 0.28 | 0.02 | - | - | - | - | - | | | | | | | | | | | | | - | V3_ImHashMap_Enumerate | 1000 | 34,113.03 ns | 1,105.023 ns | 3,116.738 ns | 33,509.27 ns | 1.00 | 0.00 | 0.1221 | - | - | 480 B | - | Dictionary_Enumerate | 1000 | 14,166.55 ns | 237.173 ns | 221.852 ns | 14,124.19 ns | 0.43 | 0.03 | - | - | - | - | - - ## V4 baseline - - BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 - Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=6.0.202 - [Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT - DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT - - | Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |-------------------------------- |------ |-------------:|-----------:|-----------:|-------------:|------:|--------:|-------:|------:|------:|----------:| - | V4_ImHashMap_Enumerate | 1 | 39.51 ns | 0.865 ns | 0.996 ns | 39.43 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_Enumerate | 1 | 44.11 ns | 0.879 ns | 0.822 ns | 44.28 ns | 1.12 | 0.04 | 0.0255 | - | - | 160 B | - | V4_PartitionedHashMap_Enumerate | 1 | 118.94 ns | 1.644 ns | 1.538 ns | 118.91 ns | 3.01 | 0.09 | - | - | - | - | - | V3_PartitionedHashMap_Enumerate | 1 | 180.82 ns | 3.462 ns | 3.238 ns | 181.09 ns | 4.57 | 0.14 | 0.0522 | - | - | 328 B | - | DictionarySlim_Enumerate | 1 | 12.77 ns | 0.254 ns | 0.238 ns | 12.76 ns | 0.32 | 0.01 | - | - | - | - | - | Dictionary_Enumerate | 1 | 15.24 ns | 0.652 ns | 1.817 ns | 14.36 ns | 0.46 | 0.05 | - | - | - | - | - | ConcurrentDictionary_foreach | 1 | 177.04 ns | 2.073 ns | 1.939 ns | 176.68 ns | 4.47 | 0.12 | 0.0100 | - | - | 64 B | - | ImmutableDict_Enumerate | 1 | 161.96 ns | 0.760 ns | 0.635 ns | 161.98 ns | 4.08 | 0.12 | - | - | - | - | - | | | | | | | | | | | | | - | V4_ImHashMap_Enumerate | 10 | 191.91 ns | 3.010 ns | 2.668 ns | 192.57 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_Enumerate | 10 | 223.10 ns | 1.885 ns | 1.574 ns | 223.12 ns | 1.16 | 0.02 | 0.0381 | - | - | 240 B | - | V4_PartitionedHashMap_Enumerate | 10 | 379.75 ns | 4.543 ns | 4.027 ns | 379.74 ns | 1.98 | 0.03 | - | - | - | - | - | V3_PartitionedHashMap_Enumerate | 10 | 604.47 ns | 4.257 ns | 3.774 ns | 603.87 ns | 3.15 | 0.05 | 0.1793 | - | - | 1128 B | - | DictionarySlim_Enumerate | 10 | 73.15 ns | 1.438 ns | 1.345 ns | 72.82 ns | 0.38 | 0.01 | - | - | - | - | - | Dictionary_Enumerate | 10 | 57.95 ns | 0.749 ns | 0.701 ns | 57.86 ns | 0.30 | 0.01 | - | - | - | - | - | ConcurrentDictionary_foreach | 10 | 505.36 ns | 7.959 ns | 7.445 ns | 504.00 ns | 2.64 | 0.05 | 0.0095 | - | - | 64 B | - | ImmutableDict_Enumerate | 10 | 556.35 ns | 10.730 ns | 9.512 ns | 558.53 ns | 2.90 | 0.06 | - | - | - | - | - | | | | | | | | | | | | | - | V4_ImHashMap_Enumerate | 100 | 2,023.65 ns | 33.506 ns | 29.702 ns | 2,031.78 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_Enumerate | 100 | 1,992.46 ns | 23.400 ns | 20.744 ns | 1,992.05 ns | 0.98 | 0.02 | 0.0381 | - | - | 240 B | - | V4_PartitionedHashMap_Enumerate | 100 | 2,626.85 ns | 42.089 ns | 37.311 ns | 2,616.34 ns | 1.30 | 0.02 | - | - | - | - | - | V3_PartitionedHashMap_Enumerate | 100 | 3,469.16 ns | 30.415 ns | 26.962 ns | 3,469.32 ns | 1.71 | 0.03 | 0.4349 | - | - | 2728 B | - | DictionarySlim_Enumerate | 100 | 615.29 ns | 10.217 ns | 9.057 ns | 617.89 ns | 0.30 | 0.01 | - | - | - | - | - | Dictionary_Enumerate | 100 | 578.30 ns | 3.310 ns | 2.764 ns | 578.14 ns | 0.29 | 0.00 | - | - | - | - | - | ConcurrentDictionary_foreach | 100 | 3,444.60 ns | 55.779 ns | 52.176 ns | 3,425.75 ns | 1.70 | 0.03 | 0.0076 | - | - | 64 B | - | ImmutableDict_Enumerate | 100 | 4,557.81 ns | 71.187 ns | 63.105 ns | 4,552.69 ns | 2.25 | 0.03 | - | - | - | - | - | | | | | | | | | | | | | - | V4_ImHashMap_Enumerate | 1000 | 22,259.46 ns | 204.173 ns | 180.994 ns | 22,241.67 ns | 1.00 | 0.00 | - | - | - | - | - | V3_ImHashMap_Enumerate | 1000 | 21,710.99 ns | 288.512 ns | 240.921 ns | 21,770.78 ns | 0.98 | 0.02 | 0.0610 | - | - | 480 B | - | V4_PartitionedHashMap_Enumerate | 1000 | 28,097.70 ns | 392.513 ns | 306.449 ns | 28,134.91 ns | 1.26 | 0.02 | - | - | - | - | - | V3_PartitionedHashMap_Enumerate | 1000 | 31,132.68 ns | 473.705 ns | 443.104 ns | 31,208.71 ns | 1.40 | 0.01 | 0.4272 | - | - | 2728 B | - | DictionarySlim_Enumerate | 1000 | 6,472.19 ns | 53.546 ns | 47.467 ns | 6,482.73 ns | 0.29 | 0.00 | - | - | - | - | - | Dictionary_Enumerate | 1000 | 5,700.68 ns | 65.696 ns | 61.452 ns | 5,698.15 ns | 0.26 | 0.00 | - | - | - | - | - | ConcurrentDictionary_foreach | 1000 | 43,550.74 ns | 848.746 ns | 752.391 ns | 43,765.79 ns | 1.96 | 0.04 | - | - | - | 64 B | - | ImmutableDict_Enumerate | 1000 | 46,089.57 ns | 524.157 ns | 464.651 ns | 46,183.41 ns | 2.07 | 0.03 | - | - | - | - | - - ## Interesting first result - - BenchmarkDotNet v0.13.6, Windows 11 (10.0.22621.1992/22H2/2022Update/SunValley2) - 11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores - .NET SDK 7.0.306 - [Host] : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | - |------------------------- |------ |---------:|---------:|---------:|------:|--------:|----------:|------------:| - | DictionarySlim_Enumerate | 100 | 513.2 ns | 10.32 ns | 17.23 ns | 1.00 | 0.00 | - | NA | - | Dictionary_Enumerate | 100 | 331.4 ns | 6.69 ns | 13.21 ns | 0.65 | 0.04 | - | NA | - | FHashMap91_Enumerate | 100 | 979.7 ns | 18.99 ns | 22.61 ns | 1.91 | 0.09 | - | NA | - - ## ... now inlining :) - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | - |------------------------- |------ |---------:|--------:|--------:|------:|--------:|----------:|------------:| - | DictionarySlim_Enumerate | 100 | 454.0 ns | 7.96 ns | 7.45 ns | 1.00 | 0.00 | - | NA | - | Dictionary_Enumerate | 100 | 308.2 ns | 6.17 ns | 6.60 ns | 0.68 | 0.02 | - | NA | - | FHashMap91_Enumerate | 100 | 110.0 ns | 2.09 ns | 1.96 ns | 0.24 | 0.01 | - | NA | - - */ - - // [Params(1, 10, 100, 1_000)] - [Params(10, 100, 1000)] - public int Count; - - [GlobalSetup] - public void Populate() - { - _mapV2 = V2_AddOrUpdate(); - // _mapExp = Experimental_ImHashMap_AddOrUpdate(); - _mapV4 = V4_ImMap_AddOrUpdate(); - _mapV3 = V3_ImHashMap_AddOrUpdate(); - _mapPartV4 = V4_PartionedHashMap_AddOrUpdate(); - _mapPartV3 = V3_PartionedHashMap_AddOrUpdate(); - _dict = Dict(); - _dictSlim = DictSlim_GetOrAddValueRef(); - _fHashMap = FHashMap_AddOrUpdate(); - _concurrentDict = ConcurrentDict(); - _immutableDict = ImmutableDict(); - } + // [Benchmark] + public string FHashMap91_TryGetValue() + { + _fHashMap91.TryGetValue(LookupKey, out var result); + return result; + } - #region Population + // [Benchmark] + public string ConcurrentDictionary_TryGetValue() + { + _concurrentDict.TryGetValue(LookupKey, out var result); + return result; + } - public ImTools.V2.ImHashMap V2_AddOrUpdate() - { - var map = ImTools.V2.ImHashMap.Empty; + // [Benchmark] + public string ImmutableDict_TryGet() + { + _immutableDict.TryGetValue(LookupKey, out var result); + return result; + } + } - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key, "a"); + [MemoryDiagnoser] + public class Enumerate + { + /* + ## V2: + + BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362 + Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=3.0.100 + [Host] : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT + DefaultJob : .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), X64 RyuJIT + +| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | +|------------------------------ |------ |--------------:|-----------:|-----------:|------:|--------:|--------:|-------:|------:|----------:| +| ImHashMap_EnumerateToArray | 1 | 147.31 ns | 0.567 ns | 0.531 ns | 1.00 | 0.00 | 0.0441 | - | - | 208 B | +| ImHashMap_V1_EnumerateToArray | 1 | 160.45 ns | 0.856 ns | 0.801 ns | 1.09 | 0.01 | 0.0560 | - | - | 264 B | +| ImHashMap_FoldToArray | 1 | 55.05 ns | 0.436 ns | 0.387 ns | 0.37 | 0.00 | 0.0356 | - | - | 168 B | +| ImHashMapSlots_FoldToArray | 1 | 89.21 ns | 1.473 ns | 1.378 ns | 0.61 | 0.01 | 0.0271 | - | - | 128 B | +| DictionarySlim_ToArray | 1 | 150.88 ns | 1.424 ns | 1.189 ns | 1.02 | 0.01 | 0.0408 | - | - | 192 B | +| Dictionary_ToArray | 1 | 40.47 ns | 0.864 ns | 1.093 ns | 0.28 | 0.01 | 0.0119 | - | - | 56 B | +| ConcurrentDictionary_ToArray | 1 | 232.31 ns | 1.981 ns | 1.654 ns | 1.58 | 0.01 | 0.0114 | - | - | 56 B | +| ImmutableDict_ToArray | 1 | 618.68 ns | 11.308 ns | 10.578 ns | 4.20 | 0.07 | 0.0114 | - | - | 56 B | +| | | | | | | | | | | | +| ImHashMap_EnumerateToArray | 10 | 423.85 ns | 8.425 ns | 9.364 ns | 1.00 | 0.00 | 0.1001 | - | - | 472 B | +| ImHashMap_V1_EnumerateToArray | 10 | 492.81 ns | 4.461 ns | 4.173 ns | 1.16 | 0.03 | 0.1726 | - | - | 816 B | +| ImHashMap_FoldToArray | 10 | 213.14 ns | 4.054 ns | 3.981 ns | 0.50 | 0.02 | 0.1054 | - | - | 496 B | +| ImHashMapSlots_FoldToArray | 10 | 255.16 ns | 1.863 ns | 1.743 ns | 0.60 | 0.01 | 0.1016 | - | - | 480 B | +| DictionarySlim_ToArray | 10 | 450.09 ns | 8.918 ns | 10.616 ns | 1.06 | 0.04 | 0.1354 | - | - | 640 B | +| Dictionary_ToArray | 10 | 87.40 ns | 1.696 ns | 1.586 ns | 0.21 | 0.01 | 0.0424 | - | - | 200 B | +| ConcurrentDictionary_ToArray | 10 | 499.22 ns | 4.804 ns | 4.494 ns | 1.17 | 0.03 | 0.0420 | - | - | 200 B | +| ImmutableDict_ToArray | 10 | 1,954.98 ns | 10.732 ns | 10.038 ns | 4.60 | 0.11 | 0.0381 | - | - | 200 B | +| | | | | | | | | | | | +| ImHashMap_EnumerateToArray | 100 | 2,735.61 ns | 34.407 ns | 32.185 ns | 1.00 | 0.00 | 0.4768 | - | - | 2248 B | +| ImHashMap_V1_EnumerateToArray | 100 | 3,368.76 ns | 6.822 ns | 6.048 ns | 1.23 | 0.02 | 1.1597 | 0.0267 | - | 5472 B | +| ImHashMap_FoldToArray | 100 | 1,433.97 ns | 14.981 ns | 14.013 ns | 0.52 | 0.01 | 0.6599 | 0.0038 | - | 3112 B | +| ImHashMapSlots_FoldToArray | 100 | 1,541.62 ns | 8.594 ns | 8.039 ns | 0.56 | 0.01 | 0.6714 | 0.0038 | - | 3168 B | +| DictionarySlim_ToArray | 100 | 2,505.97 ns | 44.927 ns | 37.516 ns | 0.92 | 0.02 | 0.8469 | 0.0076 | - | 4000 B | +| Dictionary_ToArray | 100 | 549.34 ns | 6.559 ns | 6.136 ns | 0.20 | 0.00 | 0.3481 | 0.0019 | - | 1640 B | +| ConcurrentDictionary_ToArray | 100 | 2,236.03 ns | 10.044 ns | 9.395 ns | 0.82 | 0.01 | 0.3471 | - | - | 1640 B | +| ImmutableDict_ToArray | 100 | 15,683.87 ns | 68.105 ns | 60.373 ns | 5.74 | 0.08 | 0.3357 | - | - | 1640 B | +| | | | | | | | | | | | +| ImHashMap_EnumerateToArray | 1000 | 25,723.37 ns | 504.158 ns | 560.370 ns | 1.00 | 0.00 | 3.5706 | 0.1526 | - | 16808 B | +| ImHashMap_V1_EnumerateToArray | 1000 | 34,316.66 ns | 583.573 ns | 545.874 ns | 1.34 | 0.04 | 10.3149 | 1.8921 | - | 48833 B | +| ImHashMap_FoldToArray | 1000 | 16,277.05 ns | 38.330 ns | 33.979 ns | 0.64 | 0.02 | 5.2490 | 0.3052 | - | 24752 B | +| ImHashMapSlots_FoldToArray | 1000 | 15,167.14 ns | 261.927 ns | 218.721 ns | 0.59 | 0.02 | 5.2490 | 0.2899 | - | 24784 B | +| DictionarySlim_ToArray | 1000 | 22,273.30 ns | 384.716 ns | 359.864 ns | 0.87 | 0.01 | 6.9885 | 0.6714 | - | 32896 B | +| Dictionary_ToArray | 1000 | 4,997.89 ns | 20.869 ns | 18.500 ns | 0.20 | 0.00 | 3.3951 | 0.1831 | - | 16040 B | +| ConcurrentDictionary_ToArray | 1000 | 36,859.40 ns | 193.536 ns | 181.034 ns | 1.44 | 0.04 | 3.3569 | 0.1831 | - | 16040 B | +| ImmutableDict_ToArray | 1000 | 155,798.41 ns | 484.101 ns | 452.828 ns | 6.08 | 0.14 | 3.1738 | - | - | 16040 B | + +| Method | Count | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated | +|----------------------------------- |------ |-------------:|-----------:|-----------:|------:|-------:|-------:|------:|----------:| +| ImHashMap_EnumerateToArray | 1 | 153.03 ns | 0.774 ns | 0.686 ns | 1.00 | 0.0441 | - | - | 208 B | +| Experimental_ImHashMap_FoldToArray | 1 | 62.26 ns | 0.180 ns | 0.159 ns | 0.41 | 0.0271 | - | - | 128 B | +| | | | | | | | | | | +| ImHashMap_EnumerateToArray | 10 | 417.19 ns | 4.075 ns | 3.403 ns | 1.00 | 0.1001 | - | - | 472 B | +| Experimental_ImHashMap_FoldToArray | 10 | 237.62 ns | 1.079 ns | 1.009 ns | 0.57 | 0.1016 | - | - | 480 B | +| | | | | | | | | | | +| ImHashMap_EnumerateToArray | 100 | 2,693.86 ns | 7.426 ns | 6.946 ns | 1.00 | 0.4768 | - | - | 2248 B | +| Experimental_ImHashMap_FoldToArray | 100 | 1,517.72 ns | 6.061 ns | 5.669 ns | 0.56 | 0.6561 | 0.0038 | - | 3096 B | +| | | | | | | | | | | +| ImHashMap_EnumerateToArray | 1000 | 26,018.52 ns | 175.262 ns | 146.352 ns | 1.00 | 3.5706 | 0.1831 | - | 16808 B | +| Experimental_ImHashMap_FoldToArray | 1000 | 15,321.95 ns | 69.421 ns | 64.937 ns | 0.59 | 5.2490 | 0.2747 | - | 24736 B | + +## V3 baseline + +| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | +|----------------------------------------- |------ |-------------:|----------:|----------:|------:|--------:|-------:|------:|------:|----------:| +| V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 198.4 ns | 3.64 ns | 3.04 ns | 1.00 | 0.00 | 0.0496 | - | - | 208 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 158.0 ns | 3.23 ns | 3.17 ns | 0.80 | 0.02 | 0.0362 | - | - | 152 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 535.6 ns | 7.97 ns | 8.86 ns | 1.00 | 0.00 | 0.1125 | - | - | 472 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 558.9 ns | 5.35 ns | 5.25 ns | 1.04 | 0.02 | 0.1354 | - | - | 568 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 3,601.3 ns | 54.44 ns | 50.92 ns | 1.00 | 0.00 | 0.5341 | - | - | 2248 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 9,800.0 ns | 110.20 ns | 97.69 ns | 2.72 | 0.04 | 1.0529 | - | - | 4424 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 34,261.7 ns | 371.47 ns | 310.20 ns | 1.00 | 0.00 | 3.9673 | - | - | 16808 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 128,626.8 ns | 988.46 ns | 924.61 ns | 3.76 | 0.04 | 9.2773 | - | - | 38912 B | + +### Static Enumerate - incomplete + +| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | +|----------------------------------------- |------ |------------:|----------:|----------:|------:|--------:|-------:|------:|------:|----------:| +| V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 164.7 ns | 2.23 ns | 1.98 ns | 1.00 | 0.00 | 0.0496 | - | - | 208 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 245.0 ns | 4.92 ns | 5.66 ns | 1.49 | 0.04 | 0.0567 | - | - | 240 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 295.6 ns | 5.87 ns | 5.77 ns | 1.00 | 0.00 | 0.0801 | - | - | 336 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 497.5 ns | 5.77 ns | 5.12 ns | 1.68 | 0.04 | 0.0849 | - | - | 360 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 459.0 ns | 4.86 ns | 4.55 ns | 1.00 | 0.00 | 0.1116 | - | - | 472 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 683.6 ns | 9.36 ns | 8.30 ns | 1.49 | 0.02 | 0.1335 | - | - | 560 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 3,219.3 ns | 36.20 ns | 30.23 ns | 1.00 | 0.00 | 0.5341 | - | - | 2248 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 5,009.4 ns | 57.55 ns | 51.02 ns | 1.56 | 0.02 | 0.5264 | - | - | 2208 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 29,919.7 ns | 363.29 ns | 322.05 ns | 1.00 | 0.00 | 3.9673 | - | - | 16808 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 21,530.8 ns | 292.37 ns | 259.18 ns | 0.72 | 0.01 | 1.8921 | - | - | 8016 B | + +### Struct enumerator for leafs + +BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.630 (2004/?/20H1) +Intel Core i7-8565U CPU 1.80GHz (Whiskey Lake), 1 CPU, 8 logical and 4 physical cores +.NET Core SDK=5.0.100 +[Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT +DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT + + +| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | +|----------------------------------------- |------ |-------------:|------------:|------------:|------:|--------:|-------:|------:|------:|----------:| +| V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 131.8 ns | 2.43 ns | 2.27 ns | 1.00 | 0.00 | 0.0458 | - | - | 192 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 107.7 ns | 0.56 ns | 0.44 ns | 0.82 | 0.01 | 0.0305 | - | - | 128 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 251.5 ns | 3.12 ns | 2.77 ns | 1.00 | 0.00 | 0.0782 | - | - | 328 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 214.5 ns | 4.37 ns | 4.86 ns | 0.86 | 0.02 | 0.0591 | - | - | 248 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 402.3 ns | 2.40 ns | 2.00 ns | 1.00 | 0.00 | 0.1106 | - | - | 464 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 469.7 ns | 8.19 ns | 14.13 ns | 1.19 | 0.04 | 0.1144 | - | - | 480 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 2,987.6 ns | 34.44 ns | 28.76 ns | 1.00 | 0.00 | 0.5341 | - | - | 2240 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 8,085.2 ns | 54.39 ns | 50.88 ns | 2.71 | 0.03 | 0.9308 | - | - | 3904 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 29,107.9 ns | 372.54 ns | 330.25 ns | 1.00 | 0.00 | 3.9978 | - | - | 16800 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 117,098.5 ns | 1,388.31 ns | 1,298.62 ns | 4.02 | 0.07 | 8.0566 | - | - | 33992 B | + + +### Static iterative enumerator with List as a stack + +| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | +|----------------------------------------- |------ |------------:|----------:|----------:|------:|--------:|-------:|------:|------:|----------:| +| V2_ImHashMap_AVL_EnumerateAndToArray | 1 | 130.0 ns | 1.58 ns | 1.76 ns | 1.00 | 0.00 | 0.0458 | - | - | 192 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 1 | 129.1 ns | 1.55 ns | 1.45 ns | 0.99 | 0.02 | 0.0610 | - | - | 256 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 251.6 ns | 3.58 ns | 3.35 ns | 1.00 | 0.00 | 0.0782 | - | - | 328 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 241.4 ns | 2.97 ns | 2.78 ns | 0.96 | 0.01 | 0.0896 | - | - | 376 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 414.8 ns | 8.12 ns | 10.84 ns | 1.00 | 0.00 | 0.1106 | - | - | 464 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 403.7 ns | 6.25 ns | 5.84 ns | 0.98 | 0.03 | 0.1373 | - | - | 576 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 2,947.2 ns | 23.48 ns | 20.81 ns | 1.00 | 0.00 | 0.5341 | - | - | 2240 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 2,822.2 ns | 16.96 ns | 15.87 ns | 0.96 | 0.01 | 0.5646 | - | - | 2376 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 1000 | 28,594.2 ns | 307.61 ns | 287.73 ns | 1.00 | 0.00 | 3.9673 | - | - | 16800 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 1000 | 27,555.7 ns | 318.08 ns | 281.97 ns | 0.96 | 0.01 | 4.0588 | - | - | 16992 B | + +### Branch3 is a Branch2 + +| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | +|----------------------------------------- |------ |-----------:|---------:|---------:|------:|--------:|-------:|------:|------:|----------:| +| V2_ImHashMap_AVL_EnumerateAndToArray | 5 | 329.7 ns | 6.40 ns | 5.99 ns | 1.00 | 0.00 | 0.0782 | - | - | 328 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 5 | 313.1 ns | 5.69 ns | 5.59 ns | 0.95 | 0.02 | 0.0877 | - | - | 368 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 10 | 513.4 ns | 4.55 ns | 4.26 ns | 1.00 | 0.00 | 0.1106 | - | - | 464 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 10 | 525.6 ns | 7.40 ns | 6.92 ns | 1.02 | 0.02 | 0.1354 | - | - | 568 B | +| | | | | | | | | | | | +| V2_ImHashMap_AVL_EnumerateAndToArray | 100 | 3,658.7 ns | 53.67 ns | 47.58 ns | 1.00 | 0.00 | 0.5341 | - | - | 2240 B | +| V3_ImHashMap_23Tree_EnumerateAndToArray | 100 | 3,388.2 ns | 67.71 ns | 63.34 ns | 0.93 | 0.02 | 0.5646 | - | - | 2368 B | + +### V3 RTM + +| Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | +|------------------------------ |------ |-------------:|-------------:|-------------:|-------------:|------:|--------:|-------:|------:|------:|----------:| +| V2_ImHashMap_foreach | 1 | 53.16 ns | 1.107 ns | 1.317 ns | 53.11 ns | 1.00 | 0.00 | 0.0166 | - | - | 104 B | +| V3_ImHashMap_foreach | 1 | 62.12 ns | 1.327 ns | 1.986 ns | 61.59 ns | 1.16 | 0.05 | 0.0267 | - | - | 168 B | +| V3_PartitionedHashMap_foreach | 1 | 238.62 ns | 4.811 ns | 6.084 ns | 235.92 ns | 4.50 | 0.13 | 0.0534 | - | - | 336 B | +| DictionarySlim_foreach | 1 | 12.90 ns | 0.167 ns | 0.156 ns | 12.90 ns | 0.24 | 0.01 | - | - | - | - | +| Dictionary_foreach | 1 | 14.19 ns | 0.217 ns | 0.181 ns | 14.14 ns | 0.27 | 0.01 | - | - | - | - | +| ConcurrentDictionary_foreach | 1 | 153.40 ns | 2.768 ns | 4.142 ns | 151.56 ns | 2.89 | 0.11 | 0.0100 | - | - | 64 B | +| ImmutableDict_foreach | 1 | 268.98 ns | 5.361 ns | 9.528 ns | 268.86 ns | 5.01 | 0.26 | - | - | - | - | +| | | | | | | | | | | | | +| V2_ImHashMap_foreach | 10 | 233.42 ns | 4.541 ns | 4.859 ns | 232.71 ns | 1.00 | 0.00 | 0.0200 | - | - | 128 B | +| V3_ImHashMap_foreach | 10 | 249.12 ns | 4.915 ns | 5.852 ns | 246.87 ns | 1.07 | 0.03 | 0.0391 | - | - | 248 B | +| V3_PartitionedHashMap_foreach | 10 | 746.26 ns | 14.990 ns | 18.409 ns | 748.53 ns | 3.20 | 0.13 | 0.1602 | - | - | 1008 B | +| DictionarySlim_foreach | 10 | 72.54 ns | 0.970 ns | 0.907 ns | 72.42 ns | 0.31 | 0.01 | - | - | - | - | +| Dictionary_foreach | 10 | 58.52 ns | 0.938 ns | 0.733 ns | 58.58 ns | 0.25 | 0.01 | - | - | - | - | +| ConcurrentDictionary_foreach | 10 | 468.65 ns | 9.252 ns | 12.351 ns | 464.12 ns | 2.01 | 0.06 | 0.0095 | - | - | 64 B | +| ImmutableDict_foreach | 10 | 1,127.30 ns | 15.601 ns | 14.593 ns | 1,123.20 ns | 4.82 | 0.10 | - | - | - | - | +| | | | | | | | | | | | | +| V2_ImHashMap_foreach | 100 | 2,355.54 ns | 46.224 ns | 63.271 ns | 2,337.40 ns | 1.00 | 0.00 | 0.0229 | - | - | 160 B | +| V3_ImHashMap_foreach | 100 | 2,423.13 ns | 31.652 ns | 46.395 ns | 2,412.92 ns | 1.03 | 0.04 | 0.0496 | - | - | 320 B | +| V3_PartitionedHashMap_foreach | 100 | 4,268.51 ns | 25.429 ns | 22.542 ns | 4,266.90 ns | 1.81 | 0.05 | 0.4501 | - | - | 2856 B | +| DictionarySlim_foreach | 100 | 570.39 ns | 5.827 ns | 4.866 ns | 570.93 ns | 0.24 | 0.01 | - | - | - | - | +| Dictionary_foreach | 100 | 548.46 ns | 8.579 ns | 7.605 ns | 547.90 ns | 0.23 | 0.01 | - | - | - | - | +| ConcurrentDictionary_foreach | 100 | 2,967.70 ns | 45.435 ns | 44.623 ns | 2,958.11 ns | 1.26 | 0.04 | 0.0076 | - | - | 64 B | +| ImmutableDict_foreach | 100 | 9,988.48 ns | 198.973 ns | 297.813 ns | 9,821.03 ns | 4.24 | 0.14 | - | - | - | - | +| | | | | | | | | | | | | +| V2_ImHashMap_foreach | 1000 | 23,828.30 ns | 433.708 ns | 362.166 ns | 23,743.77 ns | 1.00 | 0.00 | 0.0305 | - | - | 192 B | +| V3_ImHashMap_foreach | 1000 | 26,014.69 ns | 294.125 ns | 245.608 ns | 25,965.82 ns | 1.09 | 0.02 | 0.0610 | - | - | 552 B | +| V3_PartitionedHashMap_foreach | 1000 | 36,582.53 ns | 709.641 ns | 897.469 ns | 36,594.84 ns | 1.54 | 0.04 | 0.4883 | - | - | 3240 B | +| DictionarySlim_foreach | 1000 | 5,591.13 ns | 43.627 ns | 40.809 ns | 5,602.25 ns | 0.23 | 0.00 | - | - | - | - | +| Dictionary_foreach | 1000 | 5,319.86 ns | 51.684 ns | 45.817 ns | 5,308.36 ns | 0.22 | 0.00 | - | - | - | - | +| ConcurrentDictionary_foreach | 1000 | 38,718.40 ns | 466.979 ns | 389.949 ns | 38,728.64 ns | 1.63 | 0.03 | - | - | - | 64 B | +| ImmutableDict_foreach | 1000 | 99,156.35 ns | 1,962.968 ns | 2,181.834 ns | 98,166.03 ns | 4.17 | 0.11 | - | - | - | - | + +## V3.2 + +| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | +|--------------------- |------ |-------------:|-----------:|-----------:|------:|--------:|-------:|------:|------:|----------:| +| V3_ImHashMap_foreach | 1 | 49.59 ns | 0.758 ns | 0.633 ns | 1.00 | 0.00 | 0.0255 | - | - | 160 B | +| V2_ImHashMap_foreach | 1 | 45.96 ns | 0.273 ns | 0.242 ns | 0.93 | 0.01 | 0.0166 | - | - | 104 B | +| | | | | | | | | | | | +| V3_ImHashMap_foreach | 10 | 214.40 ns | 3.915 ns | 3.662 ns | 1.00 | 0.00 | 0.0381 | - | - | 240 B | +| V2_ImHashMap_foreach | 10 | 204.50 ns | 0.963 ns | 0.854 ns | 0.95 | 0.02 | 0.0203 | - | - | 128 B | +| | | | | | | | | | | | +| V3_ImHashMap_foreach | 100 | 2,072.90 ns | 40.826 ns | 41.926 ns | 1.00 | 0.00 | 0.0496 | - | - | 328 B | +| V2_ImHashMap_foreach | 100 | 2,206.31 ns | 29.262 ns | 25.940 ns | 1.07 | 0.03 | 0.0229 | - | - | 160 B | +| | | | | | | | | | | | +| V3_ImHashMap_foreach | 1000 | 21,043.39 ns | 298.240 ns | 232.846 ns | 1.00 | 0.00 | 0.0305 | - | - | 328 B | +| V2_ImHashMap_foreach | 1000 | 21,484.35 ns | 168.389 ns | 149.272 ns | 1.02 | 0.01 | 0.0305 | - | - | 192 B | + +## V4.0 - baseline - return map; - } +BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 +Intel Core i5-8350U CPU 1.70GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores +.NET Core SDK=6.0.102 +[Host] : .NET Core 6.0.2 (CoreCLR 6.0.222.6406, CoreFX 6.0.222.6406), X64 RyuJIT +DefaultJob : .NET Core 6.0.2 (CoreCLR 6.0.222.6406, CoreFX 6.0.222.6406), X64 RyuJIT + + +| Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | +|----------------------- |------ |-------------:|-------------:|-------------:|-------------:|------:|--------:|-------:|------:|------:|----------:| +| V3_ImHashMap_Enumerate | 1 | 78.29 ns | 3.211 ns | 9.417 ns | 74.97 ns | 1.00 | 0.00 | 0.0509 | - | - | 160 B | +| Dictionary_Enumerate | 1 | 23.15 ns | 0.712 ns | 2.008 ns | 22.54 ns | 0.30 | 0.04 | - | - | - | - | +| | | | | | | | | | | | | +| V3_ImHashMap_Enumerate | 10 | 351.19 ns | 7.127 ns | 20.791 ns | 344.93 ns | 1.00 | 0.00 | 0.0763 | - | - | 240 B | +| Dictionary_Enumerate | 10 | 92.85 ns | 1.957 ns | 4.128 ns | 91.81 ns | 0.27 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | +| V3_ImHashMap_Enumerate | 100 | 3,049.03 ns | 64.798 ns | 186.958 ns | 2,987.63 ns | 1.00 | 0.00 | 0.0763 | - | - | 240 B | +| Dictionary_Enumerate | 100 | 844.11 ns | 16.992 ns | 38.698 ns | 837.12 ns | 0.28 | 0.02 | - | - | - | - | +| | | | | | | | | | | | | +| V3_ImHashMap_Enumerate | 1000 | 34,113.03 ns | 1,105.023 ns | 3,116.738 ns | 33,509.27 ns | 1.00 | 0.00 | 0.1221 | - | - | 480 B | +| Dictionary_Enumerate | 1000 | 14,166.55 ns | 237.173 ns | 221.852 ns | 14,124.19 ns | 0.43 | 0.03 | - | - | - | - | + +## V4 baseline - private ImTools.V2.ImHashMap _mapV2; +BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 +Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores +.NET Core SDK=6.0.202 +[Host] : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT +DefaultJob : .NET Core 6.0.4 (CoreCLR 6.0.422.16404, CoreFX 6.0.422.16404), X64 RyuJIT + +| Method | Count | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | +|-------------------------------- |------ |-------------:|-----------:|-----------:|-------------:|------:|--------:|-------:|------:|------:|----------:| +| V4_ImHashMap_Enumerate | 1 | 39.51 ns | 0.865 ns | 0.996 ns | 39.43 ns | 1.00 | 0.00 | - | - | - | - | +| V3_ImHashMap_Enumerate | 1 | 44.11 ns | 0.879 ns | 0.822 ns | 44.28 ns | 1.12 | 0.04 | 0.0255 | - | - | 160 B | +| V4_PartitionedHashMap_Enumerate | 1 | 118.94 ns | 1.644 ns | 1.538 ns | 118.91 ns | 3.01 | 0.09 | - | - | - | - | +| V3_PartitionedHashMap_Enumerate | 1 | 180.82 ns | 3.462 ns | 3.238 ns | 181.09 ns | 4.57 | 0.14 | 0.0522 | - | - | 328 B | +| DictionarySlim_Enumerate | 1 | 12.77 ns | 0.254 ns | 0.238 ns | 12.76 ns | 0.32 | 0.01 | - | - | - | - | +| Dictionary_Enumerate | 1 | 15.24 ns | 0.652 ns | 1.817 ns | 14.36 ns | 0.46 | 0.05 | - | - | - | - | +| ConcurrentDictionary_foreach | 1 | 177.04 ns | 2.073 ns | 1.939 ns | 176.68 ns | 4.47 | 0.12 | 0.0100 | - | - | 64 B | +| ImmutableDict_Enumerate | 1 | 161.96 ns | 0.760 ns | 0.635 ns | 161.98 ns | 4.08 | 0.12 | - | - | - | - | +| | | | | | | | | | | | | +| V4_ImHashMap_Enumerate | 10 | 191.91 ns | 3.010 ns | 2.668 ns | 192.57 ns | 1.00 | 0.00 | - | - | - | - | +| V3_ImHashMap_Enumerate | 10 | 223.10 ns | 1.885 ns | 1.574 ns | 223.12 ns | 1.16 | 0.02 | 0.0381 | - | - | 240 B | +| V4_PartitionedHashMap_Enumerate | 10 | 379.75 ns | 4.543 ns | 4.027 ns | 379.74 ns | 1.98 | 0.03 | - | - | - | - | +| V3_PartitionedHashMap_Enumerate | 10 | 604.47 ns | 4.257 ns | 3.774 ns | 603.87 ns | 3.15 | 0.05 | 0.1793 | - | - | 1128 B | +| DictionarySlim_Enumerate | 10 | 73.15 ns | 1.438 ns | 1.345 ns | 72.82 ns | 0.38 | 0.01 | - | - | - | - | +| Dictionary_Enumerate | 10 | 57.95 ns | 0.749 ns | 0.701 ns | 57.86 ns | 0.30 | 0.01 | - | - | - | - | +| ConcurrentDictionary_foreach | 10 | 505.36 ns | 7.959 ns | 7.445 ns | 504.00 ns | 2.64 | 0.05 | 0.0095 | - | - | 64 B | +| ImmutableDict_Enumerate | 10 | 556.35 ns | 10.730 ns | 9.512 ns | 558.53 ns | 2.90 | 0.06 | - | - | - | - | +| | | | | | | | | | | | | +| V4_ImHashMap_Enumerate | 100 | 2,023.65 ns | 33.506 ns | 29.702 ns | 2,031.78 ns | 1.00 | 0.00 | - | - | - | - | +| V3_ImHashMap_Enumerate | 100 | 1,992.46 ns | 23.400 ns | 20.744 ns | 1,992.05 ns | 0.98 | 0.02 | 0.0381 | - | - | 240 B | +| V4_PartitionedHashMap_Enumerate | 100 | 2,626.85 ns | 42.089 ns | 37.311 ns | 2,616.34 ns | 1.30 | 0.02 | - | - | - | - | +| V3_PartitionedHashMap_Enumerate | 100 | 3,469.16 ns | 30.415 ns | 26.962 ns | 3,469.32 ns | 1.71 | 0.03 | 0.4349 | - | - | 2728 B | +| DictionarySlim_Enumerate | 100 | 615.29 ns | 10.217 ns | 9.057 ns | 617.89 ns | 0.30 | 0.01 | - | - | - | - | +| Dictionary_Enumerate | 100 | 578.30 ns | 3.310 ns | 2.764 ns | 578.14 ns | 0.29 | 0.00 | - | - | - | - | +| ConcurrentDictionary_foreach | 100 | 3,444.60 ns | 55.779 ns | 52.176 ns | 3,425.75 ns | 1.70 | 0.03 | 0.0076 | - | - | 64 B | +| ImmutableDict_Enumerate | 100 | 4,557.81 ns | 71.187 ns | 63.105 ns | 4,552.69 ns | 2.25 | 0.03 | - | - | - | - | +| | | | | | | | | | | | | +| V4_ImHashMap_Enumerate | 1000 | 22,259.46 ns | 204.173 ns | 180.994 ns | 22,241.67 ns | 1.00 | 0.00 | - | - | - | - | +| V3_ImHashMap_Enumerate | 1000 | 21,710.99 ns | 288.512 ns | 240.921 ns | 21,770.78 ns | 0.98 | 0.02 | 0.0610 | - | - | 480 B | +| V4_PartitionedHashMap_Enumerate | 1000 | 28,097.70 ns | 392.513 ns | 306.449 ns | 28,134.91 ns | 1.26 | 0.02 | - | - | - | - | +| V3_PartitionedHashMap_Enumerate | 1000 | 31,132.68 ns | 473.705 ns | 443.104 ns | 31,208.71 ns | 1.40 | 0.01 | 0.4272 | - | - | 2728 B | +| DictionarySlim_Enumerate | 1000 | 6,472.19 ns | 53.546 ns | 47.467 ns | 6,482.73 ns | 0.29 | 0.00 | - | - | - | - | +| Dictionary_Enumerate | 1000 | 5,700.68 ns | 65.696 ns | 61.452 ns | 5,698.15 ns | 0.26 | 0.00 | - | - | - | - | +| ConcurrentDictionary_foreach | 1000 | 43,550.74 ns | 848.746 ns | 752.391 ns | 43,765.79 ns | 1.96 | 0.04 | - | - | - | 64 B | +| ImmutableDict_Enumerate | 1000 | 46,089.57 ns | 524.157 ns | 464.651 ns | 46,183.41 ns | 2.07 | 0.03 | - | - | - | - | + +## Interesting first result + +BenchmarkDotNet v0.13.6, Windows 11 (10.0.22621.1992/22H2/2022Update/SunValley2) +11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores +.NET SDK 7.0.306 +[Host] : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 +DefaultJob : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2 - private ImTools.V2.Experimental.ImMap> _mapExp; +| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | +|------------------------- |------ |---------:|---------:|---------:|------:|--------:|----------:|------------:| +| DictionarySlim_Enumerate | 100 | 513.2 ns | 10.32 ns | 17.23 ns | 1.00 | 0.00 | - | NA | +| Dictionary_Enumerate | 100 | 331.4 ns | 6.69 ns | 13.21 ns | 0.65 | 0.04 | - | NA | +| FHashMap91_Enumerate | 100 | 979.7 ns | 18.99 ns | 22.61 ns | 1.91 | 0.09 | - | NA | - public ImTools.V2.Experimental.ImMap> Experimental_ImHashMap_AddOrUpdate() - { - var map = ImTools.V2.Experimental.ImMap>.Empty; +## ... now inlining :) - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); +| Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio | +|------------------------- |------ |---------:|--------:|--------:|------:|--------:|----------:|------------:| +| DictionarySlim_Enumerate | 100 | 454.0 ns | 7.96 ns | 7.45 ns | 1.00 | 0.00 | - | NA | +| Dictionary_Enumerate | 100 | 308.2 ns | 6.17 ns | 6.60 ns | 0.68 | 0.02 | - | NA | +| FHashMap91_Enumerate | 100 | 110.0 ns | 2.09 ns | 1.96 ns | 0.24 | 0.01 | - | NA | - return map; - } +*/ - private ImToolsV3.ImHashMap _mapV3; - public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() - { - var map = ImToolsV3.ImHashMap.Empty; + // [Params(1, 10, 100, 1_000)] + [Params(10, 100, 1000)] + public int Count; - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + [GlobalSetup] + public void Populate() + { + _mapV2 = V2_AddOrUpdate(); + // _mapExp = Experimental_ImHashMap_AddOrUpdate(); + _mapV4 = V4_ImMap_AddOrUpdate(); + _mapV3 = V3_ImHashMap_AddOrUpdate(); + _mapPartV4 = V4_PartionedHashMap_AddOrUpdate(); + _mapPartV3 = V3_PartionedHashMap_AddOrUpdate(); + _dict = Dict(); + _dictSlim = DictSlim_GetOrAddValueRef(); + _fHashMap = FHashMap_AddOrUpdate(); + _concurrentDict = ConcurrentDict(); + _immutableDict = ImmutableDict(); + } - return map; - } + #region Population - private ImTools.ImHashMap _mapV4; + public ImTools.V2.ImHashMap V2_AddOrUpdate() + { + var map = ImTools.V2.ImHashMap.Empty; - public ImTools.ImHashMap V4_ImMap_AddOrUpdate() - { - var map = ImTools.ImHashMap.Empty; + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key, "a"); - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + return map; + } - return map; - } + private ImTools.V2.ImHashMap _mapV2; - private ImTools.ImHashMap[] _mapPartV4; - public ImTools.ImHashMap[] V4_PartionedHashMap_AddOrUpdate() - { - var map = ImTools.PartitionedHashMap.CreateEmpty(); + private ImTools.V2.Experimental.ImMap> _mapExp; - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key, "a"); + public ImTools.V2.Experimental.ImMap> Experimental_ImHashMap_AddOrUpdate() + { + var map = ImTools.V2.Experimental.ImMap>.Empty; - return map; - } + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - private ImToolsV3.ImHashMap[] _mapPartV3; - public ImToolsV3.ImHashMap[] V3_PartionedHashMap_AddOrUpdate() - { - var map = ImToolsV3.PartitionedHashMap.CreateEmpty(); + return map; + } + + private ImToolsV3.ImHashMap _mapV3; + public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() + { + var map = ImToolsV3.ImHashMap.Empty; - foreach (var key in _keys.Take(Count)) - map.AddOrUpdate(key, "a"); + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - return map; - } + return map; + } + private ImTools.ImHashMap _mapV4; - public Dictionary Dict() - { - var map = new Dictionary(); + public ImTools.ImHashMap V4_ImMap_AddOrUpdate() + { + var map = ImTools.ImHashMap.Empty; - foreach (var key in _keys.Take(Count)) - map.TryAdd(key, "a"); + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); - return map; - } + return map; + } - private Dictionary _dict; + private ImTools.ImHashMap[] _mapPartV4; + public ImTools.ImHashMap[] V4_PartionedHashMap_AddOrUpdate() + { + var map = ImTools.PartitionedHashMap.CreateEmpty(); - public DictionarySlim DictSlim_GetOrAddValueRef() - { - var dict = new DictionarySlim(); + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key, "a"); - foreach (var key in _keys.Take(Count)) - dict.GetOrAddValueRef(key) = "a"; + return map; + } - return dict; - } + private ImToolsV3.ImHashMap[] _mapPartV3; + public ImToolsV3.ImHashMap[] V3_PartionedHashMap_AddOrUpdate() + { + var map = ImToolsV3.PartitionedHashMap.CreateEmpty(); - private FHashMap91TypeString _fHashMap; + foreach (var key in _keys.Take(Count)) + map.AddOrUpdate(key, "a"); - public FHashMap91TypeString FHashMap_AddOrUpdate() - { - var map = new FHashMap91TypeString(); + return map; + } - foreach (var key in _keys.Take(Count)) - map.GetOrAddValueRef(key) = "a"; - return map; - } + public Dictionary Dict() + { + var map = new Dictionary(); - private DictionarySlim _dictSlim; + foreach (var key in _keys.Take(Count)) + map.TryAdd(key, "a"); - public ConcurrentDictionary ConcurrentDict() - { - var map = new ConcurrentDictionary(); + return map; + } - foreach (var key in _keys.Take(Count)) - map.TryAdd(key, "a"); + private Dictionary _dict; - return map; - } + public DictionarySlim DictSlim_GetOrAddValueRef() + { + var dict = new DictionarySlim(); - private ConcurrentDictionary _concurrentDict; + foreach (var key in _keys.Take(Count)) + dict.GetOrAddValueRef(key) = "a"; - public ImmutableDictionary ImmutableDict() - { - var builder = ImmutableDictionary.CreateBuilder(); + return dict; + } - foreach (var key in _keys.Take(Count)) - builder.Add(key, "a"); + private FHashMap91TypeString _fHashMap; - return builder.ToImmutable(); - } + public FHashMap91TypeString FHashMap_AddOrUpdate() + { + var map = new FHashMap91TypeString(); - private ImmutableDictionary _immutableDict; + foreach (var key in _keys.Take(Count)) + map.GetOrAddValueRef(key) = "a"; - #endregion + return map; + } - // [Benchmark(Baseline = true)] - public object V4_ImHashMap_Enumerate() - { - var s = ""; - foreach (var x in _mapV4.Enumerate()) - s = x.Value; - return s; - } + private DictionarySlim _dictSlim; - // [Benchmark] - public object V3_ImHashMap_Enumerate() - { - var s = ""; - foreach (var x in _mapV3.Enumerate()) - s = x.Value; - return s; - } + public ConcurrentDictionary ConcurrentDict() + { + var map = new ConcurrentDictionary(); - // [Benchmark] - public object V4_PartitionedHashMap_Enumerate() - { - var s = ""; - foreach (var x in _mapPartV4.Enumerate()) - s = x.Value; - return s; - } + foreach (var key in _keys.Take(Count)) + map.TryAdd(key, "a"); - // [Benchmark] - public object V3_PartitionedHashMap_Enumerate() - { - var s = ""; - foreach (var x in _mapPartV3.Enumerate()) - s = x.Value; - return s; - } + return map; + } - // [Benchmark] - public object V2_ImHashMap_Enumerate() - { - var s = ""; - foreach (var x in _mapV2.Enumerate()) - s = x.Value; - return s; - } + private ConcurrentDictionary _concurrentDict; - [Benchmark(Baseline = true)] - public object DictionarySlim_Enumerate() - { - var s = ""; - foreach (var x in _dictSlim) - s = x.Value; - return s; - } + public ImmutableDictionary ImmutableDict() + { + var builder = ImmutableDictionary.CreateBuilder(); - [Benchmark] - public object Dictionary_Enumerate() - { - var s = ""; - foreach (var x in _dict) - s = x.Value; - return s; - } + foreach (var key in _keys.Take(Count)) + builder.Add(key, "a"); - [Benchmark] - public object FHashMap91_Enumerate() - { - var s = ""; - foreach (var x in _fHashMap) - s = x.Value; - return s; - } + return builder.ToImmutable(); + } - // [Benchmark] - public object ConcurrentDictionary_foreach() - { - var s = ""; - foreach (var x in _concurrentDict) - s = x.Value; - return s; - } + private ImmutableDictionary _immutableDict; - // [Benchmark] - public object ImmutableDict_Enumerate() - { - var s = ""; - foreach (var x in _immutableDict) - s = x.Value; - return s; - } + #endregion + + // [Benchmark(Baseline = true)] + public object V4_ImHashMap_Enumerate() + { + var s = ""; + foreach (var x in _mapV4.Enumerate()) + s = x.Value; + return s; } - [MemoryDiagnoser] - public class ToArray - { - /* - BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 - Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=5.0.202 - [Host] : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT - DefaultJob : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT - - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |---------------------- |------ |----------:|---------:|----------:|------:|--------:|-------:|------:|------:|----------:| - | UsingLambda | 1 | 29.07 ns | 0.672 ns | 1.722 ns | 1.00 | 0.00 | 0.0051 | - | - | 32 B | - | UsingGenericStruct | 1 | 27.87 ns | 0.657 ns | 1.660 ns | 0.96 | 0.08 | 0.0051 | - | - | 32 B | - | UsingNonGenericStruct | 1 | 15.11 ns | 0.391 ns | 0.930 ns | 0.52 | 0.05 | 0.0051 | - | - | 32 B | - | | | | | | | | | | | | - | UsingLambda | 10 | 159.52 ns | 3.168 ns | 5.379 ns | 1.00 | 0.00 | 0.0293 | - | - | 184 B | - | UsingGenericStruct | 10 | 195.96 ns | 4.683 ns | 13.435 ns | 1.28 | 0.09 | 0.0293 | - | - | 184 B | - | UsingNonGenericStruct | 10 | 134.08 ns | 2.785 ns | 7.578 ns | 0.85 | 0.06 | 0.0293 | - | - | 184 B | - */ - [Params(1, 10)]//, 100, 1_000)] - public int Count; - - [GlobalSetup] - public void Populate() - { - _mapV3 = V3_ImHashMap_AddOrUpdate(); - } + // [Benchmark] + public object V3_ImHashMap_Enumerate() + { + var s = ""; + foreach (var x in _mapV3.Enumerate()) + s = x.Value; + return s; + } - [Benchmark(Baseline = true)] - public object UsingLambda() => _mapV3.ToArray(); + // [Benchmark] + public object V4_PartitionedHashMap_Enumerate() + { + var s = ""; + foreach (var x in _mapPartV4.Enumerate()) + s = x.Value; + return s; + } - private ImToolsV3.ImHashMap _mapV3; - public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() - { - var map = ImToolsV3.ImHashMap.Empty; + // [Benchmark] + public object V3_PartitionedHashMap_Enumerate() + { + var s = ""; + foreach (var x in _mapPartV3.Enumerate()) + s = x.Value; + return s; + } - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + // [Benchmark] + public object V2_ImHashMap_Enumerate() + { + var s = ""; + foreach (var x in _mapV2.Enumerate()) + s = x.Value; + return s; + } - return map; - } + [Benchmark(Baseline = true)] + public object DictionarySlim_Enumerate() + { + var s = ""; + foreach (var x in _dictSlim) + s = x.Value; + return s; } - [MemoryDiagnoser] - public class GetAndUpdate_vs_AddOrGetAndReplace - { - /* - BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 - Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores - .NET Core SDK=5.0.202 - [Host] : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT - DefaultJob : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT - - ## Initial results - - | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - |-------------------------------------------------- |------ |---------:|---------:|---------:|------:|--------:|-------:|------:|------:|----------:| - | Get_then_AddOrUpdate | 1 | 29.34 ns | 0.397 ns | 0.371 ns | 1.00 | 0.00 | 0.0114 | - | - | 72 B | - | AddOrGet_then_Replace | 1 | 25.17 ns | 0.568 ns | 0.504 ns | 0.86 | 0.02 | 0.0114 | - | - | 72 B | - | AddOrGet_then_Replace_inlined_without_lambda_cost | 1 | 24.05 ns | 0.316 ns | 0.296 ns | 0.82 | 0.01 | 0.0115 | - | - | 72 B | - | | | | | | | | | | | | - | Get_then_AddOrUpdate | 5 | 32.54 ns | 0.725 ns | 0.917 ns | 1.00 | 0.00 | 0.0114 | - | - | 72 B | - | AddOrGet_then_Replace | 5 | 26.16 ns | 0.584 ns | 0.546 ns | 0.80 | 0.03 | 0.0114 | - | - | 72 B | - | AddOrGet_then_Replace_inlined_without_lambda_cost | 5 | 25.00 ns | 0.266 ns | 0.236 ns | 0.76 | 0.02 | 0.0114 | - | - | 72 B | - | | | | | | | | | | | | - | Get_then_AddOrUpdate | 10 | 51.65 ns | 1.085 ns | 0.962 ns | 1.00 | 0.00 | 0.0178 | - | - | 112 B | - | AddOrGet_then_Replace | 10 | 48.35 ns | 0.521 ns | 0.487 ns | 0.94 | 0.02 | 0.0178 | - | - | 112 B | - | AddOrGet_then_Replace_inlined_without_lambda_cost | 10 | 38.43 ns | 0.861 ns | 2.144 ns | 0.79 | 0.05 | 0.0178 | - | - | 112 B | - | | | | | | | | | | | | - | Get_then_AddOrUpdate | 50 | 91.46 ns | 1.870 ns | 2.079 ns | 1.00 | 0.00 | 0.0370 | - | - | 232 B | - | AddOrGet_then_Replace | 50 | 85.13 ns | 1.755 ns | 2.573 ns | 0.94 | 0.03 | 0.0370 | - | - | 232 B | - | AddOrGet_then_Replace_inlined_without_lambda_cost | 50 | 85.02 ns | 1.775 ns | 1.823 ns | 0.93 | 0.03 | 0.0370 | - | - | 232 B | - - */ - [Params(1, 5, 10)]//, 50, 100, 1_000)] - public int Count; - - [GlobalSetup] - public void Populate() - { - _map = V3_ImHashMap_AddOrUpdate(); - } + [Benchmark] + public object Dictionary_Enumerate() + { + var s = ""; + foreach (var x in _dict) + s = x.Value; + return s; + } - [Benchmark(Baseline = true)] - public object Get_then_AddOrUpdate() - { - var key = typeof(GlobalSetupAttribute); - var hash = key.GetHashCode(); - var val = "!"; - var m = _map; - var s = m.GetValueOrDefault(hash, key); - if (s != null) - s = Handle(s, val); - else - s = val; - - return m.AddOrUpdate(hash, key, s); - } + [Benchmark] + public object FHashMap91_Enumerate() + { + var s = ""; + foreach (var x in _fHashMap) + s = x.Value; + return s; + } - [Benchmark] - public object AddOrGet_then_Replace() - { - var key = typeof(GlobalSetupAttribute); - var hash = key.GetHashCode(); - var val = "!"; + // [Benchmark] + public object ConcurrentDictionary_foreach() + { + var s = ""; + foreach (var x in _concurrentDict) + s = x.Value; + return s; + } - return _map.AddOrUpdate(hash, key, val, (_, o, n) => Handle(o, n)); - } + // [Benchmark] + public object ImmutableDict_Enumerate() + { + var s = ""; + foreach (var x in _immutableDict) + s = x.Value; + return s; + } + } - [MethodImpl(MethodImplOptions.NoInlining)] - static string Handle(string a, string b) => a + b; + [MemoryDiagnoser] + public class ToArray + { + /* + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 + Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=5.0.202 + [Host] : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT + DefaultJob : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT + + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |---------------------- |------ |----------:|---------:|----------:|------:|--------:|-------:|------:|------:|----------:| + | UsingLambda | 1 | 29.07 ns | 0.672 ns | 1.722 ns | 1.00 | 0.00 | 0.0051 | - | - | 32 B | + | UsingGenericStruct | 1 | 27.87 ns | 0.657 ns | 1.660 ns | 0.96 | 0.08 | 0.0051 | - | - | 32 B | + | UsingNonGenericStruct | 1 | 15.11 ns | 0.391 ns | 0.930 ns | 0.52 | 0.05 | 0.0051 | - | - | 32 B | + | | | | | | | | | | | | + | UsingLambda | 10 | 159.52 ns | 3.168 ns | 5.379 ns | 1.00 | 0.00 | 0.0293 | - | - | 184 B | + | UsingGenericStruct | 10 | 195.96 ns | 4.683 ns | 13.435 ns | 1.28 | 0.09 | 0.0293 | - | - | 184 B | + | UsingNonGenericStruct | 10 | 134.08 ns | 2.785 ns | 7.578 ns | 0.85 | 0.06 | 0.0293 | - | - | 184 B | + */ + [Params(1, 10)]//, 100, 1_000)] + public int Count; + + [GlobalSetup] + public void Populate() + { + _mapV3 = V3_ImHashMap_AddOrUpdate(); + } - private ImToolsV3.ImHashMap _map; - public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() - { - var map = ImToolsV3.ImHashMap.Empty; + [Benchmark(Baseline = true)] + public object UsingLambda() => _mapV3.ToArray(); - foreach (var key in _keys.Take(Count)) - map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + private ImToolsV3.ImHashMap _mapV3; + public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() + { + var map = ImToolsV3.ImHashMap.Empty; - return map; - } + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + + return map; + } + } + + [MemoryDiagnoser] + public class GetAndUpdate_vs_AddOrGetAndReplace + { + /* + BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 + Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores + .NET Core SDK=5.0.202 + [Host] : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT + DefaultJob : .NET Core 5.0.5 (CoreCLR 5.0.521.16609, CoreFX 5.0.521.16609), X64 RyuJIT + + ## Initial results + + | Method | Count | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | + |-------------------------------------------------- |------ |---------:|---------:|---------:|------:|--------:|-------:|------:|------:|----------:| + | Get_then_AddOrUpdate | 1 | 29.34 ns | 0.397 ns | 0.371 ns | 1.00 | 0.00 | 0.0114 | - | - | 72 B | + | AddOrGet_then_Replace | 1 | 25.17 ns | 0.568 ns | 0.504 ns | 0.86 | 0.02 | 0.0114 | - | - | 72 B | + | AddOrGet_then_Replace_inlined_without_lambda_cost | 1 | 24.05 ns | 0.316 ns | 0.296 ns | 0.82 | 0.01 | 0.0115 | - | - | 72 B | + | | | | | | | | | | | | + | Get_then_AddOrUpdate | 5 | 32.54 ns | 0.725 ns | 0.917 ns | 1.00 | 0.00 | 0.0114 | - | - | 72 B | + | AddOrGet_then_Replace | 5 | 26.16 ns | 0.584 ns | 0.546 ns | 0.80 | 0.03 | 0.0114 | - | - | 72 B | + | AddOrGet_then_Replace_inlined_without_lambda_cost | 5 | 25.00 ns | 0.266 ns | 0.236 ns | 0.76 | 0.02 | 0.0114 | - | - | 72 B | + | | | | | | | | | | | | + | Get_then_AddOrUpdate | 10 | 51.65 ns | 1.085 ns | 0.962 ns | 1.00 | 0.00 | 0.0178 | - | - | 112 B | + | AddOrGet_then_Replace | 10 | 48.35 ns | 0.521 ns | 0.487 ns | 0.94 | 0.02 | 0.0178 | - | - | 112 B | + | AddOrGet_then_Replace_inlined_without_lambda_cost | 10 | 38.43 ns | 0.861 ns | 2.144 ns | 0.79 | 0.05 | 0.0178 | - | - | 112 B | + | | | | | | | | | | | | + | Get_then_AddOrUpdate | 50 | 91.46 ns | 1.870 ns | 2.079 ns | 1.00 | 0.00 | 0.0370 | - | - | 232 B | + | AddOrGet_then_Replace | 50 | 85.13 ns | 1.755 ns | 2.573 ns | 0.94 | 0.03 | 0.0370 | - | - | 232 B | + | AddOrGet_then_Replace_inlined_without_lambda_cost | 50 | 85.02 ns | 1.775 ns | 1.823 ns | 0.93 | 0.03 | 0.0370 | - | - | 232 B | + + */ + [Params(1, 5, 10)]//, 50, 100, 1_000)] + public int Count; + + [GlobalSetup] + public void Populate() + { + _map = V3_ImHashMap_AddOrUpdate(); + } + + [Benchmark(Baseline = true)] + public object Get_then_AddOrUpdate() + { + var key = typeof(GlobalSetupAttribute); + var hash = key.GetHashCode(); + var val = "!"; + var m = _map; + var s = m.GetValueOrDefault(hash, key); + if (s != null) + s = Handle(s, val); + else + s = val; + + return m.AddOrUpdate(hash, key, s); + } + + [Benchmark] + public object AddOrGet_then_Replace() + { + var key = typeof(GlobalSetupAttribute); + var hash = key.GetHashCode(); + var val = "!"; + + return _map.AddOrUpdate(hash, key, val, (_, o, n) => Handle(o, n)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static string Handle(string a, string b) => a + b; + + private ImToolsV3.ImHashMap _map; + public ImToolsV3.ImHashMap V3_ImHashMap_AddOrUpdate() + { + var map = ImToolsV3.ImHashMap.Empty; + + foreach (var key in _keys.Take(Count)) + map = map.AddOrUpdate(key.GetHashCode(), key, "a"); + + return map; } } } diff --git a/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj b/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj index 2aab43ac..aa4214c6 100644 --- a/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj +++ b/playground/ImTools.Benchmarks/ImTools.Benchmarks.csproj @@ -2,7 +2,7 @@ Exe - net11.0;net10.0;net8.0 + net11.0;net10.0 x64 @@ -18,11 +18,11 @@ - - + + - - + + diff --git a/playground/ImTools.Benchmarks/Program.cs b/playground/ImTools.Benchmarks/Program.cs index 68f93de0..b76b82e5 100644 --- a/playground/ImTools.Benchmarks/Program.cs +++ b/playground/ImTools.Benchmarks/Program.cs @@ -9,11 +9,11 @@ static void Main(string[] args) // With args: full BenchmarkSwitcher (filters, jobs, etc.) // dotnet run -c Release -f net10.0 -- --filter *SmallGrrVsSmallList* // Without args: default baseline benchmark for the hybrid list work. - if (args is { Length: > 0 }) - { - BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); - return; - } + // if (args is { Length: > 0 }) + // { + // BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + // return; + // } // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); @@ -31,11 +31,12 @@ static void Main(string[] args) // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + + // BenchmarkRunner.Run(); + BenchmarkRunner.Run(); - BenchmarkRunner.Run(); - // BenchmarkRunner.Run(); // var bm = new Playground.ImHashMapBenchmarks.Lookup(); // bm.Count = 1000; // bm.Populate(); From 088d625e395db5ca32f70e42403a50fbf90f369f Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 18 Oct 2026 16:58:17 +0200 Subject: [PATCH 51/54] simplify InitDoNotCheckCapacity --- .vscode/settings.json | 3 +++ playground/Experiments/FEC_ImTools.cs | 33 ++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 60a55d7b..a91c68fc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,9 @@ { "cSpell.words": [ + "Aval", + "Bval", "cref", + "Cval", "Hasher", "langword", "mentries", diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 145b92b8..68412306 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -243,7 +243,7 @@ public interface IStack : IStack { } -// todo: @wip possibly separate intoduce IStackCap interface for the StackCap, because nothing in this interface requires the stack. +// todo: @wip possibly separate introduce IStackCap interface for the StackCap, because nothing in this interface requires the stack. /// Abstracts over collection of the items on stack of the fixed Capacity, /// to be used as a part of the hybrid data structures which grow from stack to heap public interface IStack @@ -761,6 +761,9 @@ public interface IGrowingArray : ICapIndex /// Initializes the entries storage to the specified capacity void Init(uint capacity); + /// Initializes the entries storage to the specified capacity without checking the capacity. + void InitDoNotCheckCapacity(uint capacityPowerOfTwo); + /// Doubles the current capacity of the array preserving the existing elements and count. void DoubleCapacity(); @@ -785,6 +788,14 @@ public void Init(uint capacity) _entries = new T[_capacityPowerOfTwo]; } + /// + [MethodImpl((MethodImplOptions)256)] + public void InitDoNotCheckCapacity(uint capacityPowerOfTwo) + { + _capacityPowerOfTwo = (int)capacityPowerOfTwo; + _entries = new T[capacityPowerOfTwo]; + } + /// [MethodImpl((MethodImplOptions)256)] public void DoubleCapacity() @@ -867,6 +878,20 @@ public void Init(uint capacity) _segments.GetSurePresentRef(0) = new T[firstSegmentCap]; } + /// + [MethodImpl((MethodImplOptions)256)] + public void InitDoNotCheckCapacity(uint capacityPowerOfTwo) + { + _totalCapacityPowerOfTwo = (int)capacityPowerOfTwo; + _firstSegmentCapacity = (int)capacityPowerOfTwo; + _firstSegmentCapacityBitIndex = (byte)(31 - BitOps.LeadingZeroCount(capacityPowerOfTwo)); + + // Reset so re-Init works + _segments = default; + _segments.Count = 1; + _segments.GetSurePresentRef(0) = new T[capacityPowerOfTwo]; + } + /// [MethodImpl((MethodImplOptions)256)] public void DoubleCapacity() @@ -1743,8 +1768,10 @@ public SmallMap(uint capacityPowerOfTwoPlease) : BitOps.NextPowerOfTwo(capacityPowerOfTwoPlease); _capacityPowerOfTwo = (int)capacityPowerOfTwo; - _packedEntryIndexesHashesProbes.Init(capacityPowerOfTwo); - _heapEntries.Init(capacityPowerOfTwo); + + // capacity was checked above + _packedEntryIndexesHashesProbes.InitDoNotCheckCapacity(capacityPowerOfTwo); + _heapEntries.InitDoNotCheckCapacity(capacityPowerOfTwo); } /// From 11e28a4afe520f9c453dbd032926d5162d88d9bf Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 18 Oct 2026 23:03:35 +0200 Subject: [PATCH 52/54] simplier calculation of segment index --- playground/Experiments/FEC_ImTools.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 68412306..91a9dec3 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -839,8 +839,8 @@ public struct Grr : IGrowingArray, IEnumerable internal int _totalCapacityPowerOfTwo; /// Cached first segment length (power of two). Avoids recomputing 1 << bitIndex on the hot path. internal int _firstSegmentCapacity; - /// log2(_firstSegmentCapacity). Used for segment index math after the first segment. - internal byte _firstSegmentCapacityBitIndex; + /// helps to calculate the segment index by subtracting the leading zero count of the global index from this value. + internal byte _firstSegmentCapacityLeadingZerosPlusOne; internal SmallList _segments; /// The number of segments. @@ -857,8 +857,12 @@ private int GetSegmentIndexPastFirst(uint index) { Debug.Assert(index >= (uint)_firstSegmentCapacity, $"Index {index} should be >= firstSegmentCapacity:{_firstSegmentCapacity}"); - // index >= F = 2^firstBit => LZC(index) <= 31 - firstBit => segmentIndex >= 1 - return 32 - _firstSegmentCapacityBitIndex - BitOps.LeadingZeroCount(index); + + // Example A: input index = 17, first segment capacity = 16 + // 28(_firstSegmentCapacityLeadingZerosPlusOne) - 27(LeadingZeroCount(17)) == 1 + // Example B: input index = 33, first segment capacity = 16 + // 28(_firstSegmentCapacityLeadingZerosPlusOne) - 26(LeadingZeroCount(31)) == 2 + return _firstSegmentCapacityLeadingZerosPlusOne - BitOps.LeadingZeroCount(index); } /// Creates the first segment with capacity rounded up to the next power-of-two (min 16). @@ -871,7 +875,7 @@ public void Init(uint capacity) : (int)BitOps.NextPowerOfTwo(capacity); _totalCapacityPowerOfTwo = firstSegmentCap; _firstSegmentCapacity = firstSegmentCap; - _firstSegmentCapacityBitIndex = (byte)(31 - BitOps.LeadingZeroCount((uint)firstSegmentCap)); + _firstSegmentCapacityLeadingZerosPlusOne = (byte)(1 + BitOps.LeadingZeroCount((uint)firstSegmentCap)); // Reset so re-Init works (SmallList.InitCount asserts Count == 0) _segments = default; _segments.InitCount(1); @@ -882,9 +886,9 @@ public void Init(uint capacity) [MethodImpl((MethodImplOptions)256)] public void InitDoNotCheckCapacity(uint capacityPowerOfTwo) { - _totalCapacityPowerOfTwo = (int)capacityPowerOfTwo; + _totalCapacityPowerOfTwo = (int)capacityPowerOfTwo; // default is 16 == 0x0...010000 _firstSegmentCapacity = (int)capacityPowerOfTwo; - _firstSegmentCapacityBitIndex = (byte)(31 - BitOps.LeadingZeroCount(capacityPowerOfTwo)); + _firstSegmentCapacityLeadingZerosPlusOne = (byte)(1 + BitOps.LeadingZeroCount(capacityPowerOfTwo)); // 1 + (32 - 5) == 28 for capacityPowerOfTwo == 16 // Reset so re-Init works _segments = default; From 438be8be3aa4bb68a26542925229910081dd965c Mon Sep 17 00:00:00 2001 From: dadhi Date: Sun, 18 Oct 2026 23:23:00 +0200 Subject: [PATCH 53/54] inlining GetSegmentIndex --- playground/Experiments/FEC_ImTools.cs | 29 ++++++++------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index 91a9dec3..f1e500fc 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -849,22 +849,6 @@ public struct Grr : IGrowingArray, IEnumerable /// public int Capacity => _totalCapacityPowerOfTwo; - /// Maps a global index that is already known to be past the first segment into a segment index. - /// Layout: [0 .. F) seg0, [F .. 2F) seg1, [2F .. 4F) seg2, ... so segment = log2(index) - log2(F) + 1 for index >= F. - /// Equivalently: segmentIndex = (32 - firstBit) - LZC(index). - [MethodImpl((MethodImplOptions)256)] - private int GetSegmentIndexPastFirst(uint index) - { - Debug.Assert(index >= (uint)_firstSegmentCapacity, - $"Index {index} should be >= firstSegmentCapacity:{_firstSegmentCapacity}"); - - // Example A: input index = 17, first segment capacity = 16 - // 28(_firstSegmentCapacityLeadingZerosPlusOne) - 27(LeadingZeroCount(17)) == 1 - // Example B: input index = 33, first segment capacity = 16 - // 28(_firstSegmentCapacityLeadingZerosPlusOne) - 26(LeadingZeroCount(31)) == 2 - return _firstSegmentCapacityLeadingZerosPlusOne - BitOps.LeadingZeroCount(index); - } - /// Creates the first segment with capacity rounded up to the next power-of-two (min 16). /// Safe to call again to re-initialize (discards previous segments). [MethodImpl((MethodImplOptions)256)] @@ -930,17 +914,20 @@ public ref T GetSurePresentRef(int index) Debug.Assert(index >= 0 && index < _totalCapacityPowerOfTwo, $"Index {index} should be in the range of 0 to capacity:{_totalCapacityPowerOfTwo}"); - var firstCap = _firstSegmentCapacity; - // Hot path: first segment (often the only one for small maps) - if (index < firstCap) + if (index < _firstSegmentCapacity) return ref _segments.GetSurePresentRef(0).GetSurePresentRef(index); - var segmentIndex = GetSegmentIndexPastFirst((uint)index); + // Example A: input index = 17, first segment capacity = 16 + // 28(_firstSegmentCapacityLeadingZerosPlusOne) - 27(LeadingZeroCount(17)) == 1 + // Example B: input index = 33, first segment capacity = 16 + // 28(_firstSegmentCapacityLeadingZerosPlusOne) - 26(LeadingZeroCount(31)) == 2 + var segmentIndex = _firstSegmentCapacityLeadingZerosPlusOne - BitOps.LeadingZeroCount((uint)index); + Debug.Assert(segmentIndex >= 1 && segmentIndex < _segments.Count, $"segmentIndex {segmentIndex} out of range for index:{index}, segmentCount:{_segments.Count}"); // Prefix capacity before segment S (S >= 1) is firstCap << (S - 1) == total capacity of segments 0..S-1 - var insideIndex = index - (firstCap << (segmentIndex - 1)); + var insideIndex = index - (_firstSegmentCapacity << (segmentIndex - 1)); return ref _segments.GetSurePresentRef(segmentIndex).GetSurePresentRef(insideIndex); } From 44730ea01911b232adb15948f29068e2fb49e3a7 Mon Sep 17 00:00:00 2001 From: dadhi Date: Fri, 23 Oct 2026 23:24:52 +0200 Subject: [PATCH 54/54] fixes abd ideas like GrrPowerOf2 and not --- playground/Experiments/FEC_ImTools.cs | 69 ++++++++++--------- .../SmallGrrVsSmallListBenchmarks.cs | 2 +- src/ImTools/ImTools.cs | 4 +- test/ImTools.UnitTests/GrrTests.cs | 6 +- 4 files changed, 42 insertions(+), 39 deletions(-) diff --git a/playground/Experiments/FEC_ImTools.cs b/playground/Experiments/FEC_ImTools.cs index f1e500fc..d8b1d8b8 100644 --- a/playground/Experiments/FEC_ImTools.cs +++ b/playground/Experiments/FEC_ImTools.cs @@ -46,6 +46,7 @@ namespace FastExpressionCompiler.ImTools; using System.Collections.Generic; using System.Diagnostics; using System.Text; +using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Diagnostics.CodeAnalysis; @@ -56,7 +57,6 @@ namespace FastExpressionCompiler.ImTools; #endif using static SmallMap; -using System.Linq; /// Helpers and polyfills for the missing things in the old .NET versions public static class RefTools @@ -724,13 +724,13 @@ public static uint NextPowerOfTwo(uint value) #endif } - /// Counts the number of leading zero bits in the specified value. + /// Counts the number of leading zero bits in the specified value. /// Useful to get Log2(value) and the number of bits needed to represent the value. [MethodImpl((MethodImplOptions)256)] - public static int LeadingZeroCount(uint value) + public static byte LeadingZeroCount(uint value) { #if NET8_0_OR_GREATER - return BitOperations.LeadingZeroCount(value); + return (byte)BitOperations.LeadingZeroCount(value); #else if (value == 0) return 32; var count = 0; @@ -739,7 +739,7 @@ public static int LeadingZeroCount(uint value) if (value <= 0x0FFFFFFF) { count += 4; value <<= 4; } if (value <= 0x3FFFFFFF) { count += 2; value <<= 2; } if (value <= 0x7FFFFFFF) { count += 1; } - return count; + return (byte)count; #endif } } @@ -826,13 +826,14 @@ public ref T GetSurePresentRef(int index) } } +// todo: @perf do we actually need the power of 2 size? For example, it is not used for the SmallMap meta/hashes anyway because it is slow /// A growing array that grows by appending a new segment doubling the total capacity. /// Existing segments are never resized/copied, so item references stay valid (stable). /// Random access is still O(1) via power-of-two segment sizes. -/// Segment sizes starting from first segment F: F, F, 2F, 4F, 8F, ... (first two equal so total doubles each add). +/// Segment sizes starting from first segment F: 0:F, 1:F, 2:2F, 3:4F, 4:8F, ... (first two equal so total doubles each add). /// -public struct Grr : IGrowingArray, IEnumerable - where TStack : struct, IStack +public struct GrrPowerOf2 : IGrowingArray, IEnumerable + where TSegmentsStack : struct, IStack { public const int MinCapacityAndFirstSegmentSize = 16; @@ -841,7 +842,8 @@ public struct Grr : IGrowingArray, IEnumerable internal int _firstSegmentCapacity; /// helps to calculate the segment index by subtracting the leading zero count of the global index from this value. internal byte _firstSegmentCapacityLeadingZerosPlusOne; - internal SmallList _segments; + + internal SmallList _segments; /// The number of segments. public int SegmentCount => _segments.Count; @@ -933,13 +935,14 @@ public ref T GetSurePresentRef(int index) /// Returns an enumerator over all allocated slots (Capacity items, not a separate Count). [MethodImpl((MethodImplOptions)256)] - public GrrEnumerator GetEnumerator() => new GrrEnumerator(_segments); + public GrrPowerOf2Enumerator GetEnumerator() => new GrrPowerOf2Enumerator(_segments); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + /// Enumerator over Grr segment slots (full segment arrays, including unused capacity tails). -public struct GrrEnumerator : IEnumerator, IEnumerator +public struct GrrPowerOf2Enumerator : IEnumerator, IEnumerator where TStack : struct, IStack { private T _current; @@ -948,7 +951,7 @@ public struct GrrEnumerator : IEnumerator, IEnumerator private int _insideIndex; private SmallList _segments; - internal GrrEnumerator(SmallList segments) + internal GrrPowerOf2Enumerator(SmallList segments) { _current = default; _segmentIndex = 0; @@ -1001,10 +1004,10 @@ public void Dispose() { } /// /// Hybrid growable list: first items live on a fixed , -/// overflow lives in a stable segmented (no resize/copy of old heap items). +/// overflow lives in a stable segmented (no resize/copy of old heap items). /// API mirrors but heap growth keeps references into already-added heap items stable. /// -public struct SmallGrr : IEnumerable +public struct SmallGrrPowerOf2 : IEnumerable where TStack : struct, IStack where TSegStack : struct, IStack { @@ -1017,7 +1020,7 @@ public struct SmallGrr : IEnumerable #pragma warning restore CS0649 /// Heap overflow stored as stable segments. Valid indexes are 0 .. Count - Stack.Capacity - 1. - public Grr Rest; + public GrrPowerOf2 Rest; /// Indexer with bounds checks. [UnscopedRef] @@ -1153,16 +1156,16 @@ public SmallGrrEnumerator GetEnumerator() => IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } -/// Enumerator for over used items. +/// Enumerator for over used items. public struct SmallGrrEnumerator : IEnumerator, IEnumerator where TStack : struct, IStack where TSegStack : struct, IStack { - private SmallGrr _list; + private SmallGrrPowerOf2 _list; private int _index; private T _current; - internal SmallGrrEnumerator(SmallGrr list) + internal SmallGrrEnumerator(SmallGrrPowerOf2 list) { _list = list; _index = -1; @@ -1681,7 +1684,7 @@ public override string ToString() => /// - Metadata (packed entry-index | hash | probe) is stored in a single array (nothing references it, so no need for stable segments) and grows independently /// - Robin-Hood open addressing on the metadata table /// - Entry indexes preserve insertion order (stable enumeration by index 0..Count-1) -/// - Prefer for heap entries when you need stable refs across growth; +/// - Prefer for heap entries when you need stable refs across growth; /// use when raw random-access speed matters more than ref stability /// /// Not implemented yet: removal / tombstones ( is reserved for that). @@ -1711,8 +1714,8 @@ public struct SmallMap _packedEntryIndexesHashesProbes; @@ -2192,29 +2195,29 @@ internal void ResizeMetadata() } } -/// Map with 4 stack entries; heap overflow in stable . +/// Map with 4 stack entries; heap overflow in stable . public struct SmallMap4 where TEq : struct, IEq { public SmallMap, TEq, Size4, Stack4, Stack4>, - Grr, Stack8[]>>> Map; + GrrPowerOf2, Stack8[]>>> Map; public SmallMap4() => Map = new(MinCapacity); public SmallMap4(uint capacityPowerOfTwo) => Map = new(capacityPowerOfTwo); } -/// Map with 8 stack entries; heap overflow in stable . +/// Map with 8 stack entries; heap overflow in stable . public struct SmallMap8 where TEq : struct, IEq { public SmallMap, TEq, Size8, Stack8, Stack8>, - Grr, Stack8[]>>> Map; + GrrPowerOf2, Stack8[]>>> Map; public SmallMap8() => Map = new(MinCapacity); public SmallMap8(uint capacityPowerOfTwo) => Map = new(capacityPowerOfTwo); } -/// Map with 16 stack entries; heap overflow in stable (refs stay valid across growth). +/// Map with 16 stack entries; heap overflow in stable (refs stay valid across growth). public struct SmallMap16 where TEq : struct, IEq { public SmallMap, TEq, Size16, Stack16, Stack16>, - Grr, Stack8[]>>> Map; + GrrPowerOf2, Stack8[]>>> Map; public SmallMap16() => Map = new(MinCapacity); public SmallMap16(uint capacityPowerOfTwo) => Map = new(capacityPowerOfTwo); } @@ -2228,29 +2231,29 @@ public struct SmallMap16_SingleArrEntries where TEq : struct, IEq public SmallMap16_SingleArrEntries(uint capacityPowerOfTwo) => Map = new(capacityPowerOfTwo); } -/// Set with 4 stack keys; heap overflow in stable . +/// Set with 4 stack keys; heap overflow in stable . public struct SmallSet4 where TEq : struct, IEq { public SmallMap, TEq, Size4, Stack4, Stack4>, - Grr, Stack8[]>>> Set; + GrrPowerOf2, Stack8[]>>> Set; public SmallSet4() => Set = new(MinCapacity); public SmallSet4(uint capacityPowerOfTwo) => Set = new(capacityPowerOfTwo); } -/// Set with 8 stack keys; heap overflow in stable . +/// Set with 8 stack keys; heap overflow in stable . public struct SmallSet8 where TEq : struct, IEq { public SmallMap, TEq, Size8, Stack8, Stack8>, - Grr, Stack8[]>>> Set; + GrrPowerOf2, Stack8[]>>> Set; public SmallSet8() => Set = new(MinCapacity); public SmallSet8(uint capacityPowerOfTwo) => Set = new(capacityPowerOfTwo); } -/// Set with 16 stack keys; heap overflow in stable . +/// Set with 16 stack keys; heap overflow in stable . public struct SmallSet16 where TEq : struct, IEq { public SmallMap, TEq, Size16, Stack16, Stack16>, - Grr, Stack8[]>>> Set; + GrrPowerOf2, Stack8[]>>> Set; public SmallSet16() => Set = new(MinCapacity); public SmallSet16(uint capacityPowerOfTwo) => Set = new(capacityPowerOfTwo); } diff --git a/playground/ImTools.Benchmarks/SmallGrrVsSmallListBenchmarks.cs b/playground/ImTools.Benchmarks/SmallGrrVsSmallListBenchmarks.cs index 38c57826..e695bf73 100644 --- a/playground/ImTools.Benchmarks/SmallGrrVsSmallListBenchmarks.cs +++ b/playground/ImTools.Benchmarks/SmallGrrVsSmallListBenchmarks.cs @@ -20,7 +20,7 @@ public class SmallGrrVsSmallListBenchmarks private int[] _indices = null!; private SmallList> _smallList; - private SmallGrr, Stack8> _smallGrr; + private SmallGrrPowerOf2, Stack8> _smallGrr; [GlobalSetup] public void Setup() diff --git a/src/ImTools/ImTools.cs b/src/ImTools/ImTools.cs index f0a9dacb..343badcb 100644 --- a/src/ImTools/ImTools.cs +++ b/src/ImTools/ImTools.cs @@ -7653,9 +7653,9 @@ internal int ResizeHashes(int indexMask) _indexMask = (1 << HSmallMap.MinCapacityBits) - 1; _packedHashesAndIndexes = new int[1 << HSmallMap.MinCapacityBits]; #if DEBUG - Debug.WriteLine($"[ResizeHashes] new empty hashes {1} -> {_packedHashesAndIndexes.Length}"); + Debug.WriteLine($"[ResizeHashes] new empty hashes {_indexMask + 1} -> {_packedHashesAndIndexes.Length}"); #endif - return (1 << HSmallMap.MinCapacityBits) - 1; + return _indexMask; } var oldCapacity = indexMask + 1; diff --git a/test/ImTools.UnitTests/GrrTests.cs b/test/ImTools.UnitTests/GrrTests.cs index 9e49bd89..e634363d 100644 --- a/test/ImTools.UnitTests/GrrTests.cs +++ b/test/ImTools.UnitTests/GrrTests.cs @@ -13,9 +13,9 @@ namespace ImTools.Experiments.UnitTests; [TestFixture] public class GrrTests { - private static Grr> NewGrr(uint capacity = 0) + private static GrrPowerOf2> NewGrr(uint capacity = 0) { - var g = default(Grr>); + var g = default(GrrPowerOf2>); g.Init(capacity); return g; } @@ -145,7 +145,7 @@ public void Enumerator_Reset_restarts() [TestFixture] public class SmallGrrTests { - private static SmallGrr, Stack8> New() => default; + private static SmallGrrPowerOf2, Stack8> New() => default; [Test] public void Add_stays_on_stack_until_capacity()