Skip to content

Commit

Permalink
Add remove to CMAP
Browse files Browse the repository at this point in the history
  • Loading branch information
Wsm2110 committed Jun 8, 2024
1 parent 70071ca commit a1b53b9
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 84 deletions.
114 changes: 96 additions & 18 deletions src/Concurrent/CMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading;
using System;
using System.Diagnostics;
using System.Reflection.PortableExecutable;

namespace Faster.Map.Concurrent
{
Expand Down Expand Up @@ -106,7 +107,7 @@ public IEnumerable<TValue> Values
private const int _inProgressMarker = -124;
private double _loadFactor;

private readonly IEqualityComparer<TKey> _comparer;
private readonly IEqualityComparer<TKey> _keyComparer;

uint[] _powersOfTwo = {
0x1, // 2^0
Expand Down Expand Up @@ -175,7 +176,7 @@ public IEnumerable<TValue> Values
/// <param name="initialCapacity">The length of the hashmap. Will always take the closest power of two</param>
/// <param name="loadFactor">The loadfactor determines when the hashmap will resize(default is 0.5d) i.e size 32 loadfactor 0.5 hashmap will resize at 16</param>
/// <param name="keyComparer">Used to compare keys to resolve hashcollisions</param>
public CMap(uint initialCapacity, double loadFactor, IEqualityComparer<TKey>? keyComparer)
public CMap(uint initialCapacity, double loadFactor, IEqualityComparer<TKey> keyComparer)
{
if (initialCapacity <= 0 || loadFactor <= 0 || loadFactor > 1)
{
Expand All @@ -187,7 +188,7 @@ public CMap(uint initialCapacity, double loadFactor, IEqualityComparer<TKey>? ke
initialCapacity = 16;
}
_loadFactor = loadFactor;
_comparer = EqualityComparer<TKey>.Default;
_keyComparer = keyComparer;
_table = new Table(BitOperations.RoundUpToPowerOf2(initialCapacity), _loadFactor);
}

Expand Down Expand Up @@ -250,7 +251,7 @@ public bool Emplace(TKey key, TValue value)
// Resize the table if the count exceeds the threshold
if (table._count >= table.Threshold)
{
Resize(); // Resize the table to accommodate more entries
Resize(index); // Resize the table to accommodate more entries
}

return true; // Successfully inserted the entry
Expand Down Expand Up @@ -281,14 +282,14 @@ public bool Emplace(TKey key, TValue value)
// Resize the table if the count exceeds the threshold
if (table._count >= table.Threshold)
{
Resize(); // Resize the table to accommodate more entries
Resize(index); // Resize the table to accommodate more entries
}

return true; // Successfully inserted the entry
}

// Check if the bucket is occupied by an entry with the same key
if (h2 == entry.Meta && _comparer.Equals(key, entry.Key))
if (h2 == entry.Meta && _keyComparer.Equals(key, entry.Key))
{
return false; // Key already exists, insertion failed
}
Expand Down Expand Up @@ -332,7 +333,7 @@ public bool Get(TKey key, out TValue value)
{
// Retrieve the entry from the table at the calculated index
var entry = Find(table.Entries, index);
if (h2 == entry.Meta && _comparer.Equals(key, entry.Key))
if (h2 == entry.Meta && _keyComparer.Equals(key, entry.Key))
{
// If the entry's metadata and key match, return the value
value = entry.Value;
Expand Down Expand Up @@ -383,16 +384,8 @@ public bool Update(TKey key, TValue newValue)
// Retrieve the entry from the table at the calculated index
ref var entry = ref Find(table.Entries, index);

// If the entry indicates a resize operation, perform the resize
if (_resizeBucket == entry.Meta)
{
Resize(index); // Resize the table
jumpDistance = 0; // Reset the jump distance
goto start; // Restart the update process with the new table
}

// If the entry's metadata and key match, proceed with the update
if (h2 == entry.Meta && _comparer.Equals(key, entry.Key))
if (h2 == entry.Meta && _keyComparer.Equals(key, entry.Key))
{
// Guarantee that only one thread can access the critical section at a time
// the enter method uses Interlocked.CompareExchange and thus provides a full memory fence, ensuring thread safety
Expand Down Expand Up @@ -423,6 +416,91 @@ public bool Update(TKey key, TValue newValue)
return false;
}

// If the entry indicates a resize operation, perform the resize
if (_resizeBucket == entry.Meta)
{
Resize(index); // Resize the table
jumpDistance = 0; // Reset the jump distance
goto start; // Restart the update process with the new table
}

// Increment the jump distance and calculate the next index using triangular probing
jumpDistance += 1;
index += jumpDistance;
index &= table.LengthMinusOne; // Ensure the index wraps around the table size
} while (true); // Continue probing until a matching entry is found or the table is exhausted
}

/// <summary>
/// This method demonstrates a sophisticated approach to updating values in a concurrent hash table, leveraging quadratic probing, atomic operations, and handle the ABA problem effectively.
/// The use of aggressive inlining and careful memory management ensures that the method performs efficiently even under high concurrency.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Remove(TKey key)
{
// Calculate the hash code for the given key
var hashcode = key.GetHashCode();
byte jumpDistance = 0; // Initialize jump distance for quadratic probing

start:

// Get the current state of the table
var table = _table;
var index = table.GetBucket(hashcode); // Calculate the initial bucket index
var h2 = table.H2(hashcode); // Calculate the secondary hash

do
{
// Retrieve the entry from the table at the calculated index
ref var entry = ref Find(table.Entries, index);

// If the entry's metadata and key match, proceed with the update
if (h2 == entry.Meta && _keyComparer.Equals(key, entry.Key))
{
// Guarantee that only one thread can access the critical section at a time
// the enter method uses Interlocked.CompareExchange and thus provides a full memory fence, ensuring thread safety
// And ensures that the changes made by one thread are visible to others
entry.Enter();

if (h2 == entry.Meta)
{
// reset current entry
entry.Meta = _tombstone;
entry.Key = default;
entry.Value = default;

// Check if the table has been resized during the operation
if (_table != table)
{
// If resized, restart with the new table
jumpDistance = 0;
entry.Exit();
goto start;
}


entry.Exit();

Interlocked.Decrement(ref table._count);

return true;
}
}

// If the entry indicates an empty bucket, the key does not exist in the table
if (_emptyBucket == entry.Meta)
{
return false;
}

// If the entry indicates a resize operation, perform the resize
if (_resizeBucket == entry.Meta)
{
Resize(index); // Resize the table
jumpDistance = 0; // Reset the jump distance
goto start; // Restart the update process with the new table
}

// Increment the jump distance and calculate the next index using triangular probing
jumpDistance += 1;
index += jumpDistance;
Expand Down Expand Up @@ -465,7 +543,7 @@ public bool Update(TKey key, TValue newValue, TValue comparisonValue)
}

// If the entry's metadata and key match, proceed with the update
if (h2 == entry.Meta && _comparer.Equals(key, entry.Key))
if (h2 == entry.Meta && _keyComparer.Equals(key, entry.Key))
{
// Guarantee that only one thread can access the critical section at a time
// the enter method uses Interlocked.CompareExchange and thus provides a full memory fence, ensuring thread safety
Expand Down Expand Up @@ -532,7 +610,7 @@ public bool Remove(TKey key, out TValue value)
ref var entry = ref Find(table.Entries, index);

// If the entry's metadata and key match, proceed with the update
if (h2 == entry.Meta && _comparer.Equals(key, entry.Key))
if (h2 == entry.Meta && _keyComparer.Equals(key, entry.Key))
{
// Guarantee that only one thread can access the critical section at a time
// the enter method uses Interlocked.CompareExchange and thus provides a full memory fence, ensuring thread safety
Expand Down
93 changes: 27 additions & 66 deletions unittests/Faster.Map.CMap.Tests/RemoveTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,73 +147,34 @@ public static void TestRemove2(int removesPerThread)
}
}

//[Fact]
//public static void TestRemove3()
//{
// var dict = new CMap<int, int>();

// dict[99] = -99;

// ICollection<KeyValuePair<int, int>> col = dict;

// // Make sure we cannot "remove" a key/value pair which is not in the dictionary
// for (int i = 0; i < 200; i++)
// {
// if (i != 99)
// {
// Assert.False(col.Remove(new KeyValuePair<int, int>(i, -99)), "Should not remove not existing a key/value pair - new KeyValuePair<int, int>(i, -99)");
// Assert.False(col.Remove(new KeyValuePair<int, int>(99, -i)), "Should not remove not existing a key/value pair - new KeyValuePair<int, int>(99, -i)");
// }
// }

// // Can we remove a key/value pair successfully?
// Assert.True(col.Remove(new KeyValuePair<int, int>(99, -99)), "Failed to remove existing key/value pair");

// // Make sure the key/value pair is gone
// Assert.False(col.Remove(new KeyValuePair<int, int>(99, -99)), "Should not remove the key/value pair which has been removed");

// // And that the dictionary is empty. We will check the count in a few different ways:
// Assert.Equal(0, dict.Count);
// Assert.Equal(0, dict.ToArray().Length);
//}

//[Fact]
//public static void TryRemove_KeyValuePair_ArgumentValidation()
//{
// AssertExtensions.Throws<ArgumentNullException>("item", () => new ConcurrentDictionary<string, int>().TryRemove(new KeyValuePair<string, int>(null, 42)));
// new ConcurrentDictionary<int, int>().TryRemove(new KeyValuePair<int, int>(0, 0)); // no error when using default value type
// new ConcurrentDictionary<int?, int>().TryRemove(new KeyValuePair<int?, int>(0, 0)); // or nullable
//}

//[Fact]
//public static void TryRemove_KeyValuePair_RemovesSuccessfullyAsAppropriate()
//{
// var dict = new CMap<string, int>();

// for (int i = 0; i < 2; i++)
// {
// Assert.False(dict.TRemove(KeyValuePair.Create("key", 42)));
// Assert.Equal(0, dict.Count);
// Assert.True(dict.Emplace("key", 42));
// Assert.Equal(1, dict.Count);
// Assert.True(dict.Remove(KeyValuePair.Create("key", 42)));
// Assert.Equal(0, dict.Count);
// }

// Assert.True(dict.Emplace("key", 42));
// Assert.False(dict.Remove("key", 43))); // value doesn't match
//}

//[Fact]
//public static void TryRemove_KeyValuePair_MatchesKeyWithDefaultComparer()
//{
// var dict = new CMap<string, string>(5, 0.5, StringComparer.OrdinalIgnoreCase);
// dict.Emplace("key", "value");

// Assert.False(dict.TryRemove(KeyValuePair.Create("key", "VALUE")));
// Assert.True(dict.TryRemove(KeyValuePair.Create("KEY", "value")));
//}
[Fact]
public static void TestRemove3()
{
var _map = new CMap<int, int>();

_map[99] = -99;

var col = _map.Entries.ToList();

// Make sure we cannot "remove" a key/value pair which is not in the dictionary
for (int i = 0; i < 200; i++)
{
if (i != 99)
{
Assert.False(col.Remove(new KeyValuePair<int, int>(i, -99)), "Should not remove not existing a key/value pair - new KeyValuePair<int, int>(i, -99)");
Assert.False(col.Remove(new KeyValuePair<int, int>(99, -i)), "Should not remove not existing a key/value pair - new KeyValuePair<int, int>(99, -i)");
}
}

// Can we remove a key/value pair successfully?
Assert.True(col.Remove(new KeyValuePair<int, int>(99, -99)), "Failed to remove existing key/value pair");

// Make sure the key/value pair is gone
Assert.False(col.Remove(new KeyValuePair<int, int>(99, -99)), "Should not remove the key/value pair which has been removed");

// And that the dictionary is empty. We will check the count in a few different ways:
Assert.Empty(col);
}

}
}

0 comments on commit a1b53b9

Please sign in to comment.