Simplify block allocation logic
This commit is contained in:
1 parent
98c24430ba
commit
0f2d05f60d
2 files changed
+431
No files matched your search
@@ -0,0 +1,346 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Recyclable.Collections
|
||||||
|
{
|
||||||
|
internal sealed class RecyclableDictionary<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>>, IDisposable
|
||||||
|
where TKey : notnull
|
||||||
|
{
|
||||||
|
private static readonly bool _needsClearing = !typeof(TKey).IsValueType || !typeof(TValue).IsValueType;
|
||||||
|
|
||||||
|
private readonly int _blockSize;
|
||||||
|
private readonly int _blockSizeMinus1;
|
||||||
|
private readonly byte _blockShift;
|
||||||
|
|
||||||
|
private Entry[][] _entries;
|
||||||
|
private int[] _buckets;
|
||||||
|
private int _count;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
private struct Entry
|
||||||
|
{
|
||||||
|
public int HashCode;
|
||||||
|
public int Next;
|
||||||
|
public TKey Key;
|
||||||
|
public TValue Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecyclableDictionary(int blockSize = RecyclableDefaults.BlockSize)
|
||||||
|
{
|
||||||
|
if (!BitOperations.IsPow2((uint)blockSize))
|
||||||
|
{
|
||||||
|
blockSize = (int)BitOperations.RoundUpToPowerOf2((uint)blockSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
_blockSize = blockSize;
|
||||||
|
_blockSizeMinus1 = blockSize - 1;
|
||||||
|
_blockShift = (byte)(31 - BitOperations.LeadingZeroCount((uint)blockSize));
|
||||||
|
|
||||||
|
_entries = new Entry[4][];
|
||||||
|
_entries[0] = new Entry[blockSize];
|
||||||
|
_buckets = new int[4];
|
||||||
|
Array.Fill(_buckets, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => _count;
|
||||||
|
|
||||||
|
public TValue this[TKey key]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var index = FindIndex(key, out _);
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
return GetEntry(index).Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThrowHelper.ThrowArgumentOutOfRangeException(nameof(key), "Key not found");
|
||||||
|
return default!;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var index = FindIndex(key, out _);
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
ref var entry = ref GetEntry(index);
|
||||||
|
entry.Value = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Add(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Add(TKey key, TValue value)
|
||||||
|
{
|
||||||
|
var hash = key.GetHashCode() & int.MaxValue;
|
||||||
|
if (_count >= (_buckets.Length * 3) / 4)
|
||||||
|
{
|
||||||
|
ResizeBuckets(_buckets.Length * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureCapacity(_count + 1);
|
||||||
|
|
||||||
|
int bucket = hash & (_buckets.Length - 1);
|
||||||
|
int index = _buckets[bucket];
|
||||||
|
while (index >= 0)
|
||||||
|
{
|
||||||
|
ref var check = ref GetEntry(index);
|
||||||
|
if (check.HashCode == hash && EqualityComparer<TKey>.Default.Equals(check.Key, key))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("An element with the same key already exists.", nameof(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
index = check.Next;
|
||||||
|
}
|
||||||
|
|
||||||
|
int newIndex = _count++;
|
||||||
|
ref var entry = ref GetEntry(newIndex);
|
||||||
|
entry.HashCode = hash;
|
||||||
|
entry.Key = key;
|
||||||
|
entry.Value = value;
|
||||||
|
entry.Next = _buckets[bucket];
|
||||||
|
_buckets[bucket] = newIndex;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool ContainsKey(TKey key) => FindIndex(key, out _) >= 0;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool TryGetValue(TKey key, out TValue value)
|
||||||
|
{
|
||||||
|
var index = FindIndex(key, out _);
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
value = GetEntry(index).Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = default!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool Remove(TKey key)
|
||||||
|
{
|
||||||
|
if (_count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash = key.GetHashCode() & int.MaxValue;
|
||||||
|
int bucket = hash & (_buckets.Length - 1);
|
||||||
|
int prev = -1;
|
||||||
|
int index = _buckets[bucket];
|
||||||
|
while (index >= 0)
|
||||||
|
{
|
||||||
|
ref var entry = ref GetEntry(index);
|
||||||
|
if (entry.HashCode == hash && EqualityComparer<TKey>.Default.Equals(entry.Key, key))
|
||||||
|
{
|
||||||
|
if (prev < 0)
|
||||||
|
{
|
||||||
|
_buckets[bucket] = entry.Next;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GetEntry(prev).Next = entry.Next;
|
||||||
|
}
|
||||||
|
|
||||||
|
int lastIndex = _count - 1;
|
||||||
|
if (index != lastIndex)
|
||||||
|
{
|
||||||
|
MoveEntry(lastIndex, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearEntry(lastIndex);
|
||||||
|
_count--;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
prev = index;
|
||||||
|
index = entry.Next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
if (_needsClearing)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < _count; i++)
|
||||||
|
{
|
||||||
|
ClearEntry(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_count = 0;
|
||||||
|
Array.Fill(_buckets, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TKey GetKey(int index) => GetEntry(index).Key;
|
||||||
|
|
||||||
|
public TValue GetValue(int index) => GetEntry(index).Value;
|
||||||
|
|
||||||
|
public Enumerator GetEnumerator() => new(this);
|
||||||
|
|
||||||
|
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() => new Enumerator(this);
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Clear();
|
||||||
|
_entries = Array.Empty<Entry[]>();
|
||||||
|
_buckets = Array.Empty<int>();
|
||||||
|
_disposed = true;
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ref Entry GetEntry(int index)
|
||||||
|
{
|
||||||
|
return ref _entries[index >> _blockShift][index & _blockSizeMinus1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearEntry(int index)
|
||||||
|
{
|
||||||
|
ref var entry = ref GetEntry(index);
|
||||||
|
entry.HashCode = 0;
|
||||||
|
entry.Next = -1;
|
||||||
|
if (_needsClearing)
|
||||||
|
{
|
||||||
|
entry.Key = default!;
|
||||||
|
entry.Value = default!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MoveEntry(int sourceIndex, int targetIndex)
|
||||||
|
{
|
||||||
|
ref var source = ref GetEntry(sourceIndex);
|
||||||
|
ref var target = ref GetEntry(targetIndex);
|
||||||
|
target = source;
|
||||||
|
|
||||||
|
int bucket = source.HashCode & (_buckets.Length - 1);
|
||||||
|
int cur = _buckets[bucket];
|
||||||
|
int prev = -1;
|
||||||
|
while (cur != sourceIndex)
|
||||||
|
{
|
||||||
|
prev = cur;
|
||||||
|
cur = GetEntry(cur).Next;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev < 0)
|
||||||
|
{
|
||||||
|
_buckets[bucket] = targetIndex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GetEntry(prev).Next = targetIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureCapacity(int min)
|
||||||
|
{
|
||||||
|
int requiredBlocks = (min + _blockSizeMinus1) >> _blockShift;
|
||||||
|
if (_entries.Length < requiredBlocks)
|
||||||
|
{
|
||||||
|
Array.Resize(ref _entries, requiredBlocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < requiredBlocks; i++)
|
||||||
|
{
|
||||||
|
_entries[i] ??= new Entry[_blockSize];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResizeBuckets(int newSize)
|
||||||
|
{
|
||||||
|
var newBuckets = new int[newSize];
|
||||||
|
Array.Fill(newBuckets, -1);
|
||||||
|
for (int i = 0; i < _count; i++)
|
||||||
|
{
|
||||||
|
ref var entry = ref GetEntry(i);
|
||||||
|
int bucket = entry.HashCode & (newSize - 1);
|
||||||
|
entry.Next = newBuckets[bucket];
|
||||||
|
newBuckets[bucket] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
_buckets = newBuckets;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int FindIndex(TKey key, out int hash)
|
||||||
|
{
|
||||||
|
if (_count == 0)
|
||||||
|
{
|
||||||
|
hash = 0;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
hash = key.GetHashCode() & int.MaxValue;
|
||||||
|
int bucket = hash & (_buckets.Length - 1);
|
||||||
|
int index = _buckets[bucket];
|
||||||
|
while (index >= 0)
|
||||||
|
{
|
||||||
|
ref var entry = ref GetEntry(index);
|
||||||
|
if (entry.HashCode == hash && EqualityComparer<TKey>.Default.Equals(entry.Key, key))
|
||||||
|
{
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
index = entry.Next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>
|
||||||
|
{
|
||||||
|
private readonly RecyclableDictionary<TKey, TValue> _dictionary;
|
||||||
|
private int _index;
|
||||||
|
private KeyValuePair<TKey, TValue> _current;
|
||||||
|
|
||||||
|
internal Enumerator(RecyclableDictionary<TKey, TValue> dictionary)
|
||||||
|
{
|
||||||
|
_dictionary = dictionary;
|
||||||
|
_index = 0;
|
||||||
|
_current = default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyValuePair<TKey, TValue> Current => _current;
|
||||||
|
|
||||||
|
object IEnumerator.Current => _current;
|
||||||
|
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
if (_index >= _dictionary._count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry = _dictionary.GetEntry(_index++);
|
||||||
|
_current = new KeyValuePair<TKey, TValue>(entry.Key, entry.Value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_index = 0;
|
||||||
|
_current = default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Recyclable.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Recyclable.CollectionsTests
|
||||||
|
{
|
||||||
|
public class RecyclableDictionaryTests
|
||||||
|
{
|
||||||
|
private static readonly (int, string)[] _testData = new[] { (1, "a"), (2, "b"), (3, "c"), (4, "d"), (5, "e") };
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddShouldStoreItems()
|
||||||
|
{
|
||||||
|
using var dict = new RecyclableDictionary<int, string>();
|
||||||
|
foreach (var (key, value) in _testData)
|
||||||
|
{
|
||||||
|
dict.Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = dict.Should().HaveCount(_testData.Length);
|
||||||
|
for (var i = 0; i < _testData.Length; i++)
|
||||||
|
{
|
||||||
|
dict.GetKey(i).Should().Be(_testData[i].Item1);
|
||||||
|
dict.GetValue(i).Should().Be(_testData[i].Item2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IndexerShouldUpdateValue()
|
||||||
|
{
|
||||||
|
using var dict = new RecyclableDictionary<int, string>();
|
||||||
|
dict.Add(1, "a");
|
||||||
|
|
||||||
|
dict[1] = "b";
|
||||||
|
|
||||||
|
_ = dict[1].Should().Be("b");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveShouldSwapWithLast()
|
||||||
|
{
|
||||||
|
using var dict = new RecyclableDictionary<int, string>();
|
||||||
|
foreach (var (key, value) in _testData)
|
||||||
|
{
|
||||||
|
dict.Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
dict.Remove(3).Should().BeTrue();
|
||||||
|
_ = dict.ContainsKey(3).Should().BeFalse();
|
||||||
|
_ = dict.Should().HaveCount(_testData.Length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryGetValueShouldReturnValue()
|
||||||
|
{
|
||||||
|
using var dict = new RecyclableDictionary<int, string>();
|
||||||
|
dict.Add(1, "a");
|
||||||
|
|
||||||
|
var found = dict.TryGetValue(1, out var value);
|
||||||
|
|
||||||
|
_ = found.Should().BeTrue();
|
||||||
|
_ = value.Should().Be("a");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnumeratorShouldYieldItems()
|
||||||
|
{
|
||||||
|
using var dict = new RecyclableDictionary<int, string>();
|
||||||
|
foreach (var (key, value) in _testData)
|
||||||
|
{
|
||||||
|
dict.Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var actual = new List<KeyValuePair<int, string>>();
|
||||||
|
var enumerator = dict.GetEnumerator();
|
||||||
|
while (enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
actual.Add(enumerator.Current);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = actual.Should().ContainInConsecutiveOrder(_testData.Select(x => new KeyValuePair<int, string>(x.Item1, x.Item2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user