Skip to content

Commit 7eaa117

Browse files
committed
CHG: add remove testcases - minor changes
1 parent 160a986 commit 7eaa117

File tree

3 files changed

+143
-47
lines changed

3 files changed

+143
-47
lines changed

src/BlitzMap.cs

Lines changed: 56 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,16 @@ public BlitzMap(int length) : this(length, 0.8) { }
135135
/// <param name="loadFactor">The load factor to control resizing behavior.</param>
136136
public BlitzMap(int length, double loadFactor)
137137
{
138+
if (loadFactor <= 0.0 || loadFactor >= 1.0)
139+
{
140+
throw new ArgumentOutOfRangeException(nameof(loadFactor), "Load factor must be > 0.0 and < 1.0");
141+
}
142+
143+
if (loadFactor > 0.9)
144+
{
145+
loadFactor = 0.9;
146+
}
147+
138148
_length = (int)BitOperations.RoundUpToPowerOf2((uint)length);
139149
_mask = (uint)_length - 1;
140150
_loadFactor = loadFactor;
@@ -596,77 +606,70 @@ public bool InsertOrUpdate(TKey key, TValue value)
596606
[MethodImpl(MethodImplOptions.AggressiveInlining)]
597607
public bool Remove(TKey key)
598608
{
599-
// Generate a hash code for the provided key
600609
var hashcode = _hasher.ComputeHash(key);
601-
602-
// Calculate the primary index using the bitwise AND with the mask
603-
// This constrains the index within the array bounds (_mask is typically array length - 1)
604-
var index = hashcode & _mask;
605-
606-
// Calculate the secondary signature for collision detection
607-
// The signature is the remaining bits of the hash not used by the index
610+
uint index = hashcode & _mask;
608611
var signature = hashcode & ~_mask;
609612

610-
// Obtain references to the start of the bucket and entry arrays
611613
ref var bucketBase = ref MemoryMarshal.GetArrayDataReference(_buckets);
612614
ref var entryBase = ref MemoryMarshal.GetArrayDataReference(_entries);
613615

614-
// Start with the initial bucket determined by the hash index
616+
uint prevIndex = INACTIVE;
615617
ref Bucket bucket = ref Unsafe.Add(ref bucketBase, index);
616618

617-
// Traverse the linked list of buckets to find the matching key
618619
while (true)
619620
{
620-
// Check if the signature matches the desired key's signature
621-
if (signature == (bucket.Signature & ~_mask))
621+
if (bucket.Signature != INACTIVE &&
622+
signature == (bucket.Signature & ~_mask))
622623
{
623-
// Retrieve the entry associated with the current bucket
624-
var entryIndex = bucket.Signature & _mask;
624+
uint entryIndex = bucket.Signature & _mask;
625625
ref var entry = ref Unsafe.Add(ref entryBase, entryIndex);
626626

627-
// Verify that the key matches using the equality comparer
628627
if (_eq.Equals(key, entry.Key))
629628
{
630-
// Erase the bucket and get the index of the next bucket
631-
// Update the last slot by decrementing the total filled count
632-
var lastSlot = --_count;
633-
634-
ref var swap = ref Unsafe.Add(ref entryBase, lastSlot);
629+
// --- unlink bucket from chain ---
630+
if (prevIndex == INACTIVE)
631+
{
632+
// removing head
633+
if (bucket.Next != INACTIVE)
634+
{
635+
bucket = Unsafe.Add(ref bucketBase, bucket.Next);
636+
}
637+
else
638+
{
639+
bucket.Signature = INACTIVE;
640+
bucket.Next = INACTIVE;
641+
}
642+
}
643+
else
644+
{
645+
ref var prev = ref Unsafe.Add(ref bucketBase, prevIndex);
646+
prev.Next = bucket.Next;
647+
}
635648

636-
// If the current slot is not the last filled slot
649+
// --- compact entries ---
650+
uint lastSlot = --_count;
637651
if (entryIndex != lastSlot)
638652
{
639-
// Determine the last bucket to update
640-
var lastBucket = (_lastBucket == INACTIVE || index == _lastBucket)
641-
? FindLastBucket(ref bucketBase, ref entryBase, lastSlot) : _lastBucket;
642-
643-
// Move the last pair to the current slot
644-
entry = swap;
653+
ref var moved = ref Unsafe.Add(ref entryBase, lastSlot);
654+
entry = moved;
645655

646-
// Update the index of the last bucket to point to the new slot
647-
Unsafe.Add(ref bucketBase, lastBucket).Signature = entryIndex | (Unsafe.Add(ref bucketBase, lastBucket).Signature & ~_mask);
656+
// fix bucket pointing to moved entry
657+
uint movedBucket = FindLastBucket(ref bucketBase, ref entryBase, lastSlot);
658+
Unsafe.Add(ref bucketBase, movedBucket).Signature =
659+
entryIndex | (Unsafe.Add(ref bucketBase, movedBucket).Signature & ~_mask);
648660
}
649661

650-
// Clear the last entry, as it has been moved
651-
swap = default;
652-
653-
// Mark the end of the list as inactive
662+
Unsafe.Add(ref entryBase, lastSlot) = default;
654663
_lastBucket = INACTIVE;
655-
656-
// Set the erased bucket to inactive
657-
_buckets[index].Signature = INACTIVE;
658-
return true; // Key found, return success
664+
return true;
659665
}
660666
}
661667

662-
// If the next bucket index is inactive, the search is exhausted
663668
if (bucket.Next == INACTIVE)
664-
{
665-
return false; // Key not found, return failure
666-
}
669+
return false;
667670

671+
prevIndex = index;
668672
index = bucket.Next;
669-
// Move to the next bucket in the chain
670673
bucket = ref Unsafe.Add(ref bucketBase, index);
671674
}
672675
}
@@ -903,8 +906,17 @@ private uint FindEmptyBucket(ref Bucket bucketBase, uint index, uint cint)
903906
public void Clear()
904907
{
905908
Array.Clear(_entries);
906-
_buckets.AsSpan().Fill(new Bucket { Signature = INACTIVE, Next = INACTIVE });
909+
910+
_buckets.AsSpan().Fill(new Bucket
911+
{
912+
Signature = INACTIVE,
913+
Next = INACTIVE
914+
});
915+
907916
_count = 0;
917+
_last = 0;
918+
_lastBucket = INACTIVE;
919+
_numBuckets = (uint)_length >> 1;
908920
}
909921

910922
/// <summary>

src/Faster.Map.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
<PackageReadmeFile>README.md</PackageReadmeFile>
1010
<CopyRight>MIT</CopyRight>
1111
<PackageReleaseNotes>
12-
Adding blitzmap a new highly optimized hashmap
12+
minor changes and fixes
1313
</PackageReleaseNotes>
1414
<PackageProjectUrl>https://github.com/Wsm2110/Faster.Map</PackageProjectUrl>
15-
<AssemblyVersion>7.0.1</AssemblyVersion>
16-
<FileVersion>7.0.1</FileVersion>
15+
<AssemblyVersion>7.0.2</AssemblyVersion>
16+
<FileVersion>7.0.2</FileVersion>
1717
<Title>Fastest .net hashmap</Title>
1818
<Version>7.0.1</Version>
1919
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>

unittests/Faster.Map.BlitzMap.Tests/RemoveTests.cs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,91 @@ public void RemoveLargeDateset()
857857
var key = keys[i];
858858
map.Remove(key);
859859
}
860+
}
861+
862+
[Fact]
863+
public void Remove_MiddleOfChain_PreservesLookup()
864+
{
865+
var map = new BlitzMap<int, string>(4);
866+
867+
// Force collisions
868+
map.Insert(1, "A");
869+
map.Insert(5, "B"); // same bucket as 1 if mask=3
870+
map.Insert(9, "C");
871+
872+
Assert.True(map.Remove(5));
860873

874+
Assert.True(map.Contains(1));
875+
Assert.False(map.Contains(5));
876+
Assert.True(map.Contains(9));
877+
878+
Assert.Equal("A", map[1]);
879+
Assert.Equal("C", map[9]);
861880
}
862881

882+
[Fact]
883+
public void Remove_HeadOfChain_Works()
884+
{
885+
var map = new BlitzMap<int, int>(4);
886+
887+
map.Insert(1, 10);
888+
map.Insert(5, 20);
889+
890+
Assert.True(map.Remove(1));
891+
Assert.False(map.Contains(1));
892+
Assert.True(map.Contains(5));
893+
Assert.Equal(20, map[5]);
894+
}
895+
896+
897+
[Fact]
898+
public void Remove_TailOfChain_Works()
899+
{
900+
var map = new BlitzMap<int, int>(4);
901+
902+
map.Insert(1, 10);
903+
map.Insert(5, 20);
904+
map.Insert(9, 30);
905+
906+
Assert.True(map.Remove(9));
907+
908+
Assert.True(map.Contains(1));
909+
Assert.True(map.Contains(5));
910+
Assert.False(map.Contains(9));
911+
}
912+
913+
[Fact]
914+
public void Remove_ThenInsert_ReusesSlotCorrectly()
915+
{
916+
var map = new BlitzMap<int, int>(4);
917+
918+
map.Insert(1, 1);
919+
map.Insert(5, 2);
920+
921+
map.Remove(1);
922+
map.Insert(9, 3);
923+
924+
Assert.False(map.Contains(1));
925+
Assert.True(map.Contains(5));
926+
Assert.True(map.Contains(9));
927+
}
928+
929+
[Fact]
930+
public void Stress_CollisionsAndRemovals()
931+
{
932+
var map = new BlitzMap<int, int>(8);
933+
934+
for (int i = 0; i < 1000; i++)
935+
map.Insert(i * 4, i);
936+
937+
for (int i = 0; i < 500; i++)
938+
Assert.True(map.Remove(i * 4));
939+
940+
for (int i = 500; i < 1000; i++)
941+
Assert.Equal(i, map[i * 4]);
942+
}
943+
944+
945+
946+
863947
}

0 commit comments

Comments
 (0)