From 67b41cccb87dc0d96cce96864bdbf488680c68cf Mon Sep 17 00:00:00 2001 From: Wiljan Ruizendaal Date: Sun, 14 Apr 2024 15:28:16 +0200 Subject: [PATCH] remove cold data from array --- .../Faster.Map.Benchmark/AddBenchmark.cs | 5 +- .../Faster.Map.Benchmark/GetBenchmark.cs | 2 +- benchmarks/Faster.Map.Benchmark/Program.cs | 4 +- .../Faster.Map.Benchmark/RemoveBenchmark.cs | 2 +- .../StringWrapperBenchmark.cs | 28 +- src/Faster.Map.RobinhoodMap/RobinHoodMap.cs | 250 +++++++++--------- .../Faster.Map.DenseMap.Tests.csproj | 2 - .../RobinhoodGetOrUpdateByRefTests.cs | 4 +- .../RobinhoodRemoveUnitTests.cs | 9 +- 9 files changed, 150 insertions(+), 156 deletions(-) diff --git a/benchmarks/Faster.Map.Benchmark/AddBenchmark.cs b/benchmarks/Faster.Map.Benchmark/AddBenchmark.cs index f101bc6..dcb049f 100644 --- a/benchmarks/Faster.Map.Benchmark/AddBenchmark.cs +++ b/benchmarks/Faster.Map.Benchmark/AddBenchmark.cs @@ -16,7 +16,7 @@ public class AddBenchmark #region Fields //fixed size, dont want to measure resize() - private DenseMap _dense = new DenseMap(1000000, 0.5); + private DenseMap _dense = new DenseMap(1000000); private Dictionary dic = new Dictionary(1000000); private RobinhoodMap _robinhoodMap = new RobinhoodMap(1000000); private QuadMap _quadmap = new QuadMap(1000000); @@ -27,7 +27,7 @@ public class AddBenchmark #region Properties - [ParamsAttribute(1, 10, 100, 1000, 10000, 100000, 1000000)] + [ParamsAttribute( 1000000)] public int Length { get; set; } #endregion @@ -54,6 +54,7 @@ public void ResetMaps() { _dense.Clear(); dic.Clear(); + _quadmap.Clear(); _robinhoodMap.Clear(); } diff --git a/benchmarks/Faster.Map.Benchmark/GetBenchmark.cs b/benchmarks/Faster.Map.Benchmark/GetBenchmark.cs index 5d57e1d..1f19be5 100644 --- a/benchmarks/Faster.Map.Benchmark/GetBenchmark.cs +++ b/benchmarks/Faster.Map.Benchmark/GetBenchmark.cs @@ -26,7 +26,7 @@ public class GetBenchmark #region Properties - [Params(1, 1000, 100000, 1000000)] + [Params(1, 10, 100, 1000, 10000, 100000, 1000000)] public int Length { get; set; } #endregion diff --git a/benchmarks/Faster.Map.Benchmark/Program.cs b/benchmarks/Faster.Map.Benchmark/Program.cs index 3622cd5..8ed181b 100644 --- a/benchmarks/Faster.Map.Benchmark/Program.cs +++ b/benchmarks/Faster.Map.Benchmark/Program.cs @@ -3,8 +3,6 @@ using BenchmarkDotNet.Exporters.Csv; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Running; -using ObjectLayoutInspector; -using Faster.Map.Core; namespace Faster.Map.Benchmark { @@ -12,7 +10,7 @@ class Program { static void Main(string[] args) { - BenchmarkRunner.Run(); + BenchmarkRunner.Run(); } } } diff --git a/benchmarks/Faster.Map.Benchmark/RemoveBenchmark.cs b/benchmarks/Faster.Map.Benchmark/RemoveBenchmark.cs index 90bcee6..5a53396 100644 --- a/benchmarks/Faster.Map.Benchmark/RemoveBenchmark.cs +++ b/benchmarks/Faster.Map.Benchmark/RemoveBenchmark.cs @@ -30,7 +30,7 @@ public class RemoveBenchmark #region Properties - [Params(1, 10, 100, 1000, 10000, 100000, 1000000)] + [Params( 1000000)] public int Length { get; set; } #endregion diff --git a/benchmarks/Faster.Map.Benchmark/StringWrapperBenchmark.cs b/benchmarks/Faster.Map.Benchmark/StringWrapperBenchmark.cs index c8a1de0..ae6558d 100644 --- a/benchmarks/Faster.Map.Benchmark/StringWrapperBenchmark.cs +++ b/benchmarks/Faster.Map.Benchmark/StringWrapperBenchmark.cs @@ -48,10 +48,10 @@ public void Setup() foreach (var key in keys) { - // dic.Add(key, key); + dic.Add(key, key); // _denseMap.Emplace(key, key); _robinhoodMap.Emplace(key, key); - _quadMap.Emplace(key, key); + //_quadMap.Emplace(key, key); } } @@ -74,23 +74,23 @@ public void RobinhoodMap() } } - [Benchmark] - public void QuadMap() - { - foreach (var key in keys) - { - _quadMap.Get(key, out var _); - } - } - - //[Benchmark(Baseline = true)] - //public void Dictionary() + //[Benchmark] + //public void QuadMap() //{ // foreach (var key in keys) // { - // dic.TryGetValue(key, out var _); + // _quadMap.Get(key, out var _); // } //} + [Benchmark(Baseline = true)] + public void Dictionary() + { + foreach (var key in keys) + { + dic.TryGetValue(key, out var _); + } + } + } } diff --git a/src/Faster.Map.RobinhoodMap/RobinHoodMap.cs b/src/Faster.Map.RobinhoodMap/RobinHoodMap.cs index 6499f40..d5f2b62 100644 --- a/src/Faster.Map.RobinhoodMap/RobinHoodMap.cs +++ b/src/Faster.Map.RobinhoodMap/RobinHoodMap.cs @@ -2,7 +2,6 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; #nullable enable @@ -49,12 +48,12 @@ public IEnumerable> Entries get { //iterate backwards so we can remove the current item - for (int i = _entries.Length - 1; i >= 0; --i) + for (int i = _meta.Length - 1; i >= 0; --i) { - var entry = _entries[i]; - if (entry.Psl is not 0) + var meta = _meta[i]; + if (meta is not 0) { - yield return new KeyValuePair(entry.Key, entry.Value); + yield return new KeyValuePair(_entries[i].Key, _entries[i].Value); } } } @@ -71,12 +70,12 @@ public IEnumerable Keys get { //iterate backwards so we can remove the current item - for (int i = _entries.Length - 1; i >= 0; --i) + for (int i = _meta.Length - 1; i >= 0; --i) { - var entry = _entries[i]; - if (entry.Psl != 0) + var meta = _meta[i]; + if (meta != 0) { - yield return entry.Key; + yield return _entries[i].Key; } } } @@ -92,12 +91,12 @@ public IEnumerable Values { get { - for (int i = _entries.Length - 1; i >= 0; --i) + for (int i = _meta.Length - 1; i >= 0; --i) { - var entry = _entries[i]; - if (entry.Psl is not 0) + var meta = _meta[i]; + if (meta is not 0) { - yield return entry.Value; + yield return _entries[i].Value; } } } @@ -107,6 +106,7 @@ public IEnumerable Values #region Fields + private byte[] _meta; private Entry[] _entries; private uint _length; private readonly double _loadFactor; @@ -114,7 +114,6 @@ public IEnumerable Values private byte _shift = 32; private byte _maxProbeSequenceLength; private readonly IEqualityComparer _keyComparer; - private int _maxLookupsBeforeResize; #endregion @@ -147,17 +146,22 @@ public RobinhoodMap(uint length, double loadFactor) : this(length, loadFactor, E /// Used to compare keys to resolve hashcollisions public RobinhoodMap(uint length, double loadFactor, IEqualityComparer keyComparer) { - //default length is 8 - _length = NextPow2(length); + _length = BitOperations.RoundUpToPowerOf2(length); _loadFactor = loadFactor; + if (_length < 8) + { + _length = 8; + } + _maxProbeSequenceLength = (byte)BitOperations.Log2(_length); - _maxLookupsBeforeResize = (int)(_length * loadFactor); + _maxLookupsBeforeResize = (int)((_length + _maxProbeSequenceLength) * loadFactor); _keyComparer = keyComparer ?? EqualityComparer.Default; _shift = (byte)(_shift - BitOperations.Log2(_length)); var size = (int)_length + _maxProbeSequenceLength; - _entries = GC.AllocateArray(size); + _entries = GC.AllocateUninitializedArray(size); + _meta = GC.AllocateUninitializedArray(size); } #endregion @@ -174,42 +178,48 @@ public RobinhoodMap(uint length, double loadFactor, IEqualityComparer keyC public bool Emplace(TKey key, TValue value) { //Resize if loadfactor is reached - if (Count >= _maxLookupsBeforeResize) + if (Count > _maxLookupsBeforeResize) { Resize(); } - var hashcode = (uint)key.GetHashCode(); - var index = (_goldenRatio * hashcode) >> _shift; - // create default entry - var insert = new Entry(key, value, 1); + var index = Hash(key); + + byte distance = 0; + var entry = new Entry(key, value); do { - ref var entry = ref Find(_entries, index); + ref var meta = ref Find(_meta, index); //Empty spot, add Entry - if (entry.Psl == 0) + if (meta == 0) { - entry = insert; + ++meta; + + Find(_entries, index) = entry; ++Count; return true; } - //equals check - if (_keyComparer.Equals(insert.Key, entry.Key)) + //Steal from the rich, give to the poor + if (distance > meta) { - return false; + Swap(ref distance, ref meta); + Swap(ref entry, ref Find(_entries, index)); + goto next; } - //Steal from the rich, give to the poor - if (insert.Psl > entry.Psl) + //equals check + if (_keyComparer.Equals(key, Find(_entries, index).Key)) { - Swap(ref insert, ref entry); + return false; } + next: + //increase probe sequence length - ++insert.Psl; + ++distance; ++index; } while (true); } @@ -223,8 +233,7 @@ public bool Emplace(TKey key, TValue value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Get(TKey key, out TValue value) { - var hashcode = (uint)key.GetHashCode(); - var index = (_goldenRatio * hashcode) >> _shift; + var index = Hash(key); var maxDistance = index + _maxProbeSequenceLength; do { @@ -268,38 +277,43 @@ public ref TValue GetOrUpdate(TKey key) Resize(); } - var hashcode = (uint)key.GetHashCode(); - var index = (_goldenRatio * hashcode) >> _shift; - // create default entry - var insert = new Entry(key, default, 1); + var index = Hash(key); + var entry = new Entry(key, default); + byte distance = 0; do { - ref var entry = ref Find(_entries, index); + ref var meta = ref Find(_meta, index); //Empty spot, add Entry - if (entry.Psl == 0) + if (meta == 0) { - entry = insert; + ++meta; + ref var x = ref Find(_entries, index); + x = entry; + ++Count; - return ref entry.Value; + return ref x.Value; } - //equals check - if (_keyComparer.Equals(insert.Key, entry.Key)) + //Steal from the rich, give to the poor + if (distance > meta) { - //update - return ref entry.Value; + Swap(ref distance, ref meta); + Swap(ref entry, ref Find(_entries, index)); + goto next; } - //Steal from the rich, give to the poor - if (insert.Psl > entry.Psl) + //equals check + if (_keyComparer.Equals(key, Find(_entries, index).Key)) { - Swap(ref insert, ref entry); + return ref Find(_entries, index).Value; } + next: + //increase probe sequence length - ++insert.Psl; + ++distance; ++index; } while (true); } @@ -310,8 +324,7 @@ public ref TValue GetOrUpdate(TKey key) [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Update(TKey key, TValue value) { - var hashcode = (uint)key.GetHashCode(); - var index = (_goldenRatio * hashcode) >> _shift; + var index = Hash(key); var maxDistance = index + _maxProbeSequenceLength; do @@ -326,7 +339,7 @@ public bool Update(TKey key, TValue value) } while (++index < maxDistance); - //EntryTwo not found + //Entry not found return false; } @@ -338,32 +351,35 @@ public bool Update(TKey key, TValue value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Remove(TKey key) { - var hashcode = (uint)key.GetHashCode(); - var index = (_goldenRatio * hashcode) >> _shift; + var index = Hash(key); var maxDistance = index + _maxProbeSequenceLength; do - { - ref var entry = ref _entries[index]; - + { //validate hash en compare keys - if (_keyComparer.Equals(key, entry.Key)) + if (_keyComparer.Equals(key, Find(_entries, index).Key)) { - //Get next EntryTwo - ref var next = ref Find(_entries, ++index); - - entry = default; + uint nextIndex = index + 1; + var nextMeta = Find(_meta, nextIndex); - while (next.Psl > 1) + while (nextMeta > 1) { //decrease next psl by 1 - next.Psl--; - //swap upper EntryTwo with lower - Swap(ref next, ref _entries[index - 1]); + nextMeta--; + + Find(_meta, index) = nextMeta; + Find(_entries, index) = Find(_entries, nextIndex); + + index++; + nextIndex++; + //increase index by one - next = ref Find(_entries, ++index); + nextMeta = Find(_meta, nextIndex); } + Find(_meta, index) = default; + Find(_entries, index) = default; + --Count; return true; } @@ -385,9 +401,8 @@ public bool Remove(TKey key) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Contains(TKey key) - { - var hashcode = (uint)key.GetHashCode(); - var index = (_goldenRatio * hashcode) >> _shift; + { + var index = Hash(key); var maxDistance = index + _maxProbeSequenceLength; do @@ -412,13 +427,13 @@ public void Copy(RobinhoodMap denseMap) { for (var i = 0; i < denseMap._entries.Length; ++i) { - var entry = denseMap._entries[i]; - if (entry.Psl is 0) + var meta = denseMap._meta[i]; + if (meta is 0) { continue; } - Emplace(entry.Key, entry.Value); + Emplace(denseMap._entries[i].Key, denseMap._entries[i].Value); } } @@ -428,6 +443,7 @@ public void Copy(RobinhoodMap denseMap) public void Clear() { Array.Clear(_entries); + Array.Clear(_meta); Count = 0; } @@ -469,38 +485,45 @@ public TValue this[TKey key] #region Private Methods [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ref Entry Find(Entry[] array, uint index) + private uint Hash(TKey key) + { + var hashcode = (uint)key.GetHashCode(); + return (_goldenRatio * hashcode) >> _shift; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ref T Find(T[] array, uint index) { ref var arr0 = ref MemoryMarshal.GetArrayDataReference(array); return ref Unsafe.Add(ref arr0, index); - } + } - private void EmplaceInternal(TKey key, TValue value) + private void EmplaceInternal(ref Entry entry) { - var hashcode = (uint)key.GetHashCode(); - var index = (_goldenRatio * hashcode) >> _shift; - // create default entry - var insert = new Entry(key, value, 1); + var index = Hash(entry.Key); + byte distance = 0; do { - ref var entry = ref Find(_entries, index); + ref var meta = ref Find(_meta, index); //Empty spot, add Entry - if (entry.Psl == 0) + if (meta == 0) { - entry = insert; + Find(_entries, index) = entry; + ++meta; return; } //Steal from the rich, give to the poor - if (insert.Psl > entry.Psl) + if (distance > meta) { - Swap(ref insert, ref entry); + Swap(ref distance, ref meta); + Swap(ref entry, ref Find(_entries, index)); } //increase probe sequence length - ++insert.Psl; + ++distance; ++index; } while (true); } @@ -511,74 +534,49 @@ private void EmplaceInternal(TKey key, TValue value) /// The x. /// The y. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Swap(ref Entry x, ref Entry y) - { - var tmp = x; - - x = y; - y = tmp; - } + private static void Swap(ref T x, ref T y) => (x, y) = (y, x); /// /// Resizes this instance. /// private void Resize() { - _length = _length << 1; + _length <<= 1; _shift--; _maxProbeSequenceLength = (byte)BitOperations.Log2(_length); - _maxLookupsBeforeResize = (int)(_length * _loadFactor); + _maxLookupsBeforeResize = (int)((_length + _maxProbeSequenceLength) * _loadFactor); var size = Unsafe.As(ref _length) + _maxProbeSequenceLength; var oldEntries = _entries; + var oldMeta = _meta; - _entries = GC.AllocateArray(size); + _entries = GC.AllocateUninitializedArray(size); + _meta = GC.AllocateArray(size); - for (uint i = 0; i < oldEntries.Length; ++i) + for (uint i = 0; i < oldMeta.Length; ++i) { - var entry = Find(oldEntries, i); - if (entry.Psl == 0) + if (oldMeta[i] == 0) { continue; } - EmplaceInternal(entry.Key, entry.Value); + EmplaceInternal(ref Find(oldEntries, i)); } } - /// - /// calculates next power of 2 - /// - /// The c. - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint NextPow2(uint c) - { - c--; - c |= c >> 1; - c |= c >> 2; - c |= c >> 4; - c |= c >> 8; - c |= c >> 16; - return ++c; - } - - [DebuggerDisplay("{Key} {Value} {Psl}")] - [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct Entry + [DebuggerDisplay("{Key} {Value}")] + [StructLayout(LayoutKind.Sequential)] + internal record struct Entry { - public byte Psl; public TKey Key; public TValue Value; - public Entry(TKey key, TValue value, byte psl) + public Entry(TKey key, TValue value) { Key = key; Value = value; - Psl = psl; } } diff --git a/unittests/Faster.Map.DenseMap.Tests/Faster.Map.DenseMap.Tests.csproj b/unittests/Faster.Map.DenseMap.Tests/Faster.Map.DenseMap.Tests.csproj index 95bf3b8..d28fc2f 100644 --- a/unittests/Faster.Map.DenseMap.Tests/Faster.Map.DenseMap.Tests.csproj +++ b/unittests/Faster.Map.DenseMap.Tests/Faster.Map.DenseMap.Tests.csproj @@ -23,8 +23,6 @@ - - diff --git a/unittests/Faster.Map.RobinhoodMap.Tests/RobinhoodGetOrUpdateByRefTests.cs b/unittests/Faster.Map.RobinhoodMap.Tests/RobinhoodGetOrUpdateByRefTests.cs index 6fac9e6..b1bed57 100644 --- a/unittests/Faster.Map.RobinhoodMap.Tests/RobinhoodGetOrUpdateByRefTests.cs +++ b/unittests/Faster.Map.RobinhoodMap.Tests/RobinhoodGetOrUpdateByRefTests.cs @@ -31,7 +31,7 @@ public void EmplaceOrUpdate_ReturnsDefaultValue_WhenKeyDoesNotExist() { // Arrange var dictionary = new RobinhoodMap(); // Initialize your dictionary - + uint nonExistingKey = 50030 /* Create a key that doesn't exist in the dictionary */; // Act @@ -91,7 +91,7 @@ public void GetOrUpdate_ByRefValue_Using_string_values() var dictionary = new RobinhoodMap(); // Initialize your dictionary var existingKey = 2u/* Create an existing key */; - + // Act ref var updatedValue = ref dictionary.GetOrUpdate(existingKey); diff --git a/unittests/Faster.Map.RobinhoodMap.Tests/RobinhoodRemoveUnitTests.cs b/unittests/Faster.Map.RobinhoodMap.Tests/RobinhoodRemoveUnitTests.cs index 651860e..f96379f 100644 --- a/unittests/Faster.Map.RobinhoodMap.Tests/RobinhoodRemoveUnitTests.cs +++ b/unittests/Faster.Map.RobinhoodMap.Tests/RobinhoodRemoveUnitTests.cs @@ -80,8 +80,7 @@ public void ShiftRemove_RemovesEntryAtGivenIndex_ShiftsEntriesDown() [InlineData(1000)] [InlineData(10000)] [InlineData(100000)] - [InlineData(1000000)] - [InlineData(10000000)] + [InlineData(1000000)] public void ShiftRemove_ShiftsOneMillionEntriesDown(int amount) { // Arrange @@ -95,7 +94,7 @@ public void ShiftRemove_ShiftsOneMillionEntriesDown(int amount) } // Assert - Assert.Equal(0, map.Count); + Assert.Equal(0, map.Count); } [Fact] @@ -104,12 +103,12 @@ public void ShiftRemove_RemoveOnlyOne() // Arrange var dictionary = new RobinhoodMap(); dictionary.Emplace(1, "One"); - + // Act dictionary.Remove(1); // Removing entry with key 2 // Assert - Assert.Equal(0, dictionary.Count); + Assert.Equal(0, dictionary.Count); } } }