Last active
March 18, 2023 15:58
-
-
Save teoadal/30b3177ae2608b1418075f0959803e71 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using BenchmarkDotNet.Attributes; | |
using BenchmarkDotNet.Jobs; | |
namespace Benchmarks.GlossaryBenchmarks; | |
[SimpleJob(RuntimeMoniker.Net70)] | |
[SimpleJob(RuntimeMoniker.Net60)] | |
[SimpleJob(RuntimeMoniker.NetCoreApp31)] | |
[SimpleJob(RuntimeMoniker.Net48)] | |
[MeanColumn] | |
public class BenchmarkTryGetValue | |
{ | |
[Benchmark(Baseline = true)] | |
public int DictionaryTryGetValue() | |
{ | |
var dictionary = _dictionary; | |
var sum = 0; | |
foreach (var key in _keys) | |
{ | |
if (dictionary.TryGetValue(key, out var value)) | |
{ | |
sum += value; | |
} | |
} | |
return sum; | |
} | |
[Benchmark] | |
public int GlossaryTryGetValue() | |
{ | |
var glossary = _glossary; | |
var sum = 0; | |
foreach (var key in _keys) | |
{ | |
if (glossary.TryGetValue(key, out var value)) | |
{ | |
sum += value; | |
} | |
} | |
return sum; | |
} | |
[Benchmark] | |
public int GlossaryNonSealedTryGetValue() | |
{ | |
var glossary = _glossaryNonSealed; | |
var sum = 0; | |
foreach (var key in _keys) | |
{ | |
if (glossary.TryGetValue(key, out var value)) | |
{ | |
sum += value; | |
} | |
} | |
return sum; | |
} | |
[Benchmark] | |
public int GlossarySealedTryGetValue() | |
{ | |
var glossary = _glossarySealed; | |
var sum = 0; | |
foreach (var key in _keys) | |
{ | |
if (glossary.TryGetValue(key, out var value)) | |
{ | |
sum += value; | |
} | |
} | |
return sum; | |
} | |
#region Configuration | |
private Dictionary<int, int> _dictionary = null!; | |
private Glossary<int> _glossary; | |
private GlossaryNonSealedClass<int> _glossaryNonSealed = null!; | |
private GlossarySealedClass<int> _glossarySealed = null!; | |
private int[] _keys = null!; | |
[GlobalSetup] | |
public void Init() | |
{ | |
const int count = 1021; | |
_dictionary = new Dictionary<int, int>(count); | |
_glossary = new Glossary<int>(count); | |
_glossaryNonSealed = new GlossaryNonSealedClass<int>(count); | |
_glossarySealed = new GlossarySealedClass<int>(count); | |
_keys = Enumerable.Range(0, count).ToArray(); | |
for (var i = 0; i < count; i++) | |
{ | |
var number = _keys[i]; | |
_dictionary.Add(number, number); | |
_glossary.Add(number, number); | |
_glossaryNonSealed.Add(number, number); | |
_glossarySealed.Add(number, number); | |
} | |
Shuffle(_keys, new Random(1234)); | |
} | |
private static void Shuffle<T>(T[] array, Random random) | |
{ | |
var n = array.Length; | |
while (n > 1) | |
{ | |
var k = random.Next(n--); | |
(array[n], array[k]) = (array[k], array[n]); | |
} | |
} | |
#endregion | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.Diagnostics.CodeAnalysis; | |
using System.Runtime.CompilerServices; | |
namespace Benchmarks.GlossaryBenchmarks; | |
[DebuggerDisplay("Length = {Length}")] | |
[SuppressMessage("ReSharper", "InvertIf")] | |
internal struct Glossary<TValue> | |
where TValue : struct | |
{ | |
private static TValue _defaultValue; | |
private const int StartOfFreeList = -3; | |
private int[] _buckets; | |
private Entry[] _entries; | |
private int _length; | |
private int _freeCount; | |
private int _freeList; | |
public Glossary(int capacity) | |
{ | |
capacity = GlossaryHelper.GetPrime(capacity); | |
_buckets = new int[capacity]; | |
_entries = new Entry[capacity]; | |
_freeCount = 0; | |
_freeList = 0; | |
_length = 0; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public ref TValue Add(int key) | |
{ | |
if (_buckets.Length == 0) Resize(); | |
var keyHash = (uint) key; | |
ref var bucket = ref GetBucket(keyHash); | |
var entries = _entries; | |
var i = bucket - 1; | |
while ((uint) i < (uint) entries.Length) | |
{ | |
ref readonly var existsEntry = ref entries[i]; | |
if (existsEntry.Key == key) GlossaryHelper.KeyAlreadyExists(key); | |
i = existsEntry.Next; | |
} | |
int index; | |
if (_freeCount > 0) | |
{ | |
index = _freeList; | |
_freeList = StartOfFreeList - entries[index].Next; | |
_freeCount--; | |
} | |
else | |
{ | |
index = _length; | |
if (index == entries.Length) | |
{ | |
Resize(); | |
bucket = ref GetBucket(keyHash); | |
entries = _entries; | |
} | |
_length++; | |
} | |
ref var entry = ref entries[index]; | |
entry.Key = key; | |
entry.Next = bucket - 1; | |
bucket = index + 1; | |
return ref entry.Value; | |
} | |
public void Add(int key, in TValue value) | |
{ | |
ref var entryValue = ref Add(key); | |
entryValue = value; | |
} | |
public void Clear() | |
{ | |
if (_length == 0) return; | |
Array.Clear(_buckets, 0, _buckets.Length); | |
Array.Clear(_entries, 0, _entries.Length); | |
_freeCount = 0; | |
_freeList = 0; | |
_length = 0; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public readonly bool ContainsKey(int key) | |
{ | |
if (_length > 0) | |
{ | |
var i = _buckets[(uint) key % (uint) _buckets.Length] - 1; | |
var entries = _entries; | |
while ((uint) i < (uint) entries.Length) | |
{ | |
ref readonly var entry = ref entries[i]; | |
if (entry.Key == key) return true; | |
i = entry.Next; | |
} | |
} | |
return false; | |
} | |
public readonly bool ContainsValue(in TValue value, IEqualityComparer<TValue>? comparer = null) | |
{ | |
comparer ??= EqualityComparer<TValue>.Default; | |
foreach (var entry in _entries.AsSpan(0, _length)) | |
{ | |
if (entry.Next >= -1 && comparer.Equals(entry.Value, value)) | |
{ | |
return true; | |
} | |
} | |
return false; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public readonly Enumerator GetEnumerator() => new(_entries, _length); | |
public readonly ref TValue GetValue(int key) | |
{ | |
if (_length > 0) | |
{ | |
var i = _buckets[(uint) key % (uint) _buckets.Length] - 1; | |
var entries = _entries; | |
while ((uint) i < (uint) entries.Length) | |
{ | |
ref var entry = ref entries[i]; | |
if (entry.Key == key) return ref entry.Value; | |
i = entry.Next; | |
} | |
} | |
GlossaryHelper.KeyNotFound(key); | |
return ref _defaultValue; | |
} | |
public readonly int Length | |
{ | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
get => _length - _freeCount; | |
} | |
public bool Remove(int key, bool clear = false) | |
{ | |
if (_length == 0) return false; | |
ref var bucket = ref GetBucket((uint) key); | |
var entries = _entries; | |
var i = bucket - 1; | |
var last = -1; | |
while (i >= 0) | |
{ | |
ref var entry = ref entries[i]; | |
if (entry.Key == key) | |
{ | |
if (clear) entry.Value = default; | |
if (last < 0) bucket = entry.Next + 1; | |
else entries[last].Next = entry.Next; | |
entry.Next = StartOfFreeList - _freeList; | |
_freeCount++; | |
_freeList = i; | |
return true; | |
} | |
last = i; | |
i = entry.Next; | |
} | |
return false; | |
} | |
public bool Remove(int key, out TValue value) | |
{ | |
if (_length == 0) | |
{ | |
value = default; | |
return false; | |
} | |
ref var bucket = ref GetBucket((uint) key); | |
var entries = _entries; | |
var i = bucket - 1; | |
var last = -1; | |
while (i >= 0) | |
{ | |
ref var entry = ref entries[i]; | |
if (entry.Key == key) | |
{ | |
value = entry.Value; // copy | |
entry.Value = default; | |
if (last < 0) bucket = entry.Next + 1; | |
else entries[last].Next = entry.Next; | |
entry.Next = StartOfFreeList - _freeList; | |
_freeCount++; | |
_freeList = i; | |
return true; | |
} | |
last = i; | |
i = entry.Next; | |
} | |
value = default; | |
return false; | |
} | |
public void TrimExcess() | |
{ | |
if (_length == 0) | |
{ | |
_buckets = Array.Empty<int>(); | |
_entries = Array.Empty<Entry>(); | |
_freeCount = 0; | |
_freeList = -1; | |
return; | |
} | |
var oldEntries = _entries; | |
var newSize = _length; | |
var buckets = new int[newSize]; | |
var entries = new Entry[newSize]; | |
var count = 0; | |
for (var i = 0; i < newSize; i++) | |
{ | |
if (oldEntries[i].Next < -1) continue; | |
ref var entry = ref entries[count]; | |
entry = oldEntries[i]; | |
var bucket = (uint) oldEntries[i].Key % (uint) newSize; | |
entry.Next = buckets[bucket] - 1; | |
buckets[bucket] = count + 1; | |
count++; | |
} | |
_buckets = buckets; | |
_entries = entries; | |
_freeCount = 0; | |
_freeList = -1; | |
} | |
public readonly bool TryGetValue(int key, out TValue value) | |
{ | |
if (_length > 0) | |
{ | |
var i = _buckets[(uint) key % (uint) _buckets.Length] - 1; | |
var entries = _entries; | |
while ((uint) i < (uint) entries.Length) | |
{ | |
ref readonly var entry = ref entries[i]; | |
if (entry.Key == key) | |
{ | |
value = entry.Value; | |
return true; | |
} | |
i = entry.Next; | |
} | |
} | |
value = default; | |
return false; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
private readonly ref int GetBucket(uint keyHash) => ref _buckets[keyHash % (uint) _buckets.Length]; | |
private void Resize() | |
{ | |
var length = _length; | |
var newSize = GlossaryHelper.GetPrime(length == 0 ? 4 : length << 1); | |
var buckets = new int[newSize]; | |
var entries = new Entry[newSize]; | |
Array.Copy(_entries, entries, length); | |
for (var i = 0; i < length; i++) | |
{ | |
ref var entry = ref entries[i]; | |
if (entry.Next < -1) continue; | |
ref var bucket = ref buckets[(uint) entry.Key % (uint) buckets.Length]; | |
entry.Next = bucket - 1; | |
bucket = i + 1; | |
} | |
_buckets = buckets; | |
_entries = entries; | |
} | |
public struct Entry | |
{ | |
internal int Next; | |
public int Key; | |
public TValue Value; | |
} | |
public ref struct Enumerator | |
{ | |
private int _index; | |
private readonly int _length; | |
private readonly Entry[] _entries; | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public Enumerator(Entry[] entries, int length) | |
{ | |
_index = -1; | |
_length = length; | |
_entries = entries; | |
} | |
public bool MoveNext() | |
{ | |
var index = _index + 1; | |
var length = _length; | |
while ((uint) index < (uint) length) | |
{ | |
ref readonly var entry = ref _entries[index]; | |
if (entry.Next >= -1) | |
{ | |
_index = index; | |
return true; | |
} | |
index++; | |
} | |
_index = _length; | |
return false; | |
} | |
public readonly ref Entry Current | |
{ | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
get => ref _entries[_index]; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace Benchmarks.GlossaryBenchmarks; | |
internal static class GlossaryHelper | |
{ | |
public static int GetPrime(int min) | |
{ | |
foreach (var prime in Primes) | |
{ | |
if (prime >= min) return prime; | |
} | |
for (var candidate = min | 1; candidate < int.MaxValue; candidate += 2) | |
{ | |
if (IsPrime(candidate) && (candidate - 1) % 101 != 0) return candidate; | |
} | |
return min; | |
} | |
public static void KeyAlreadyExists(int key) | |
{ | |
throw new Exception($"Key {key} already exists"); | |
} | |
public static void KeyNotFound(int key) | |
{ | |
throw new Exception($"Key {key} isn't found"); | |
} | |
private static bool IsPrime(int candidate) | |
{ | |
if ((candidate & 1) == 0) return candidate == 2; | |
var num = (int) Math.Sqrt(candidate); | |
for (var index = 3; index <= num; index += 2) | |
{ | |
if (candidate % index == 0) | |
return false; | |
} | |
return true; | |
} | |
private static readonly int[] Primes = { | |
3, | |
7, | |
11, | |
17, | |
23, | |
29, | |
37, | |
47, | |
59, | |
71, | |
89, | |
107, | |
131, | |
163, | |
197, | |
239, | |
293, | |
353, | |
431, | |
521, | |
631, | |
761, | |
919, | |
1103, | |
1327, | |
1597, | |
1931, | |
2333, | |
2801, | |
3371, | |
4049, | |
4861, | |
5839, | |
7013, | |
8419, | |
10103, | |
12143, | |
14591, | |
17519, | |
21023, | |
25229, | |
30293, | |
36353, | |
43627, | |
52361, | |
62851, | |
75431, | |
90523, | |
108631, | |
130363, | |
156437, | |
187751, | |
225307, | |
270371, | |
324449, | |
389357, | |
467237, | |
560689, | |
672827, | |
807403, | |
968897, | |
1162687, | |
1395263, | |
1674319, | |
2009191, | |
2411033, | |
2893249, | |
3471899, | |
4166287, | |
4999559, | |
5999471, | |
7199369 | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Runtime.CompilerServices; | |
namespace Benchmarks.GlossaryBenchmarks; | |
internal class GlossaryNonSealedClass<TValue> | |
where TValue : struct | |
{ | |
private static TValue _defaultValue; | |
private const int StartOfFreeList = -3; | |
private int[] _buckets; | |
private Entry[] _entries; | |
private int _length; | |
private int _freeCount; | |
private int _freeList; | |
public GlossaryNonSealedClass(int capacity) | |
{ | |
capacity = GlossaryHelper.GetPrime(capacity); | |
_buckets = new int[capacity]; | |
_entries = new Entry[capacity]; | |
_freeCount = 0; | |
_freeList = 0; | |
_length = 0; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public ref TValue Add(int key) | |
{ | |
if (_buckets.Length == 0) Resize(); | |
var keyHash = (uint) key; | |
ref var bucket = ref GetBucket(keyHash); | |
var entries = _entries; | |
var i = bucket - 1; | |
while ((uint) i < (uint) entries.Length) | |
{ | |
ref readonly var existsEntry = ref entries[i]; | |
if (existsEntry.Key == key) GlossaryHelper.KeyAlreadyExists(key); | |
i = existsEntry.Next; | |
} | |
int index; | |
if (_freeCount > 0) | |
{ | |
index = _freeList; | |
_freeList = StartOfFreeList - entries[index].Next; | |
_freeCount--; | |
} | |
else | |
{ | |
index = _length; | |
if (index == entries.Length) | |
{ | |
Resize(); | |
bucket = ref GetBucket(keyHash); | |
entries = _entries; | |
} | |
_length++; | |
} | |
ref var entry = ref entries[index]; | |
entry.Key = key; | |
entry.Next = bucket - 1; | |
bucket = index + 1; | |
return ref entry.Value; | |
} | |
public void Add(int key, in TValue value) | |
{ | |
ref var entryValue = ref Add(key); | |
entryValue = value; | |
} | |
public void Clear() | |
{ | |
if (_length == 0) return; | |
Array.Clear(_buckets, 0, _buckets.Length); | |
Array.Clear(_entries, 0, _entries.Length); | |
_freeCount = 0; | |
_freeList = 0; | |
_length = 0; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public bool ContainsKey(int key) | |
{ | |
if (_length > 0) | |
{ | |
var i = _buckets[(uint) key % (uint) _buckets.Length] - 1; | |
var entries = _entries; | |
while ((uint) i < (uint) entries.Length) | |
{ | |
ref readonly var entry = ref entries[i]; | |
if (entry.Key == key) return true; | |
i = entry.Next; | |
} | |
} | |
return false; | |
} | |
public bool ContainsValue(in TValue value, IEqualityComparer<TValue>? comparer = null) | |
{ | |
comparer ??= EqualityComparer<TValue>.Default; | |
foreach (var entry in _entries.AsSpan(0, _length)) | |
{ | |
if (entry.Next >= -1 && comparer.Equals(entry.Value, value)) | |
{ | |
return true; | |
} | |
} | |
return false; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public Enumerator GetEnumerator() => new(_entries, _length); | |
public ref TValue GetValue(int key) | |
{ | |
if (_length > 0) | |
{ | |
var i = _buckets[(uint) key % (uint) _buckets.Length] - 1; | |
var entries = _entries; | |
while ((uint) i < (uint) entries.Length) | |
{ | |
ref var entry = ref entries[i]; | |
if (entry.Key == key) return ref entry.Value; | |
i = entry.Next; | |
} | |
} | |
GlossaryHelper.KeyNotFound(key); | |
return ref _defaultValue; | |
} | |
public int Length | |
{ | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
get => _length - _freeCount; | |
} | |
public bool Remove(int key, bool clear = false) | |
{ | |
if (_length == 0) return false; | |
ref var bucket = ref GetBucket((uint) key); | |
var entries = _entries; | |
var i = bucket - 1; | |
var last = -1; | |
while (i >= 0) | |
{ | |
ref var entry = ref entries[i]; | |
if (entry.Key == key) | |
{ | |
if (clear) entry.Value = default; | |
if (last < 0) bucket = entry.Next + 1; | |
else entries[last].Next = entry.Next; | |
entry.Next = StartOfFreeList - _freeList; | |
_freeCount++; | |
_freeList = i; | |
return true; | |
} | |
last = i; | |
i = entry.Next; | |
} | |
return false; | |
} | |
public bool Remove(int key, out TValue value) | |
{ | |
if (_length == 0) | |
{ | |
value = default; | |
return false; | |
} | |
ref var bucket = ref GetBucket((uint) key); | |
var entries = _entries; | |
var i = bucket - 1; | |
var last = -1; | |
while (i >= 0) | |
{ | |
ref var entry = ref entries[i]; | |
if (entry.Key == key) | |
{ | |
value = entry.Value; // copy | |
entry.Value = default; | |
if (last < 0) bucket = entry.Next + 1; | |
else entries[last].Next = entry.Next; | |
entry.Next = StartOfFreeList - _freeList; | |
_freeCount++; | |
_freeList = i; | |
return true; | |
} | |
last = i; | |
i = entry.Next; | |
} | |
value = default; | |
return false; | |
} | |
public void TrimExcess() | |
{ | |
if (_length == 0) | |
{ | |
_buckets = Array.Empty<int>(); | |
_entries = Array.Empty<Entry>(); | |
_freeCount = 0; | |
_freeList = -1; | |
return; | |
} | |
var oldEntries = _entries; | |
var newSize = _length; | |
var buckets = new int[newSize]; | |
var entries = new Entry[newSize]; | |
var count = 0; | |
for (var i = 0; i < newSize; i++) | |
{ | |
if (oldEntries[i].Next < -1) continue; | |
ref var entry = ref entries[count]; | |
entry = oldEntries[i]; | |
var bucket = (uint) oldEntries[i].Key % (uint) newSize; | |
entry.Next = buckets[bucket] - 1; | |
buckets[bucket] = count + 1; | |
count++; | |
} | |
_buckets = buckets; | |
_entries = entries; | |
_freeCount = 0; | |
_freeList = -1; | |
} | |
public bool TryGetValue(int key, out TValue value) | |
{ | |
if (_length > 0) | |
{ | |
var i = _buckets[(uint) key % (uint) _buckets.Length] - 1; | |
var entries = _entries; | |
while ((uint) i < (uint) entries.Length) | |
{ | |
ref readonly var entry = ref entries[i]; | |
if (entry.Key == key) | |
{ | |
value = entry.Value; | |
return true; | |
} | |
i = entry.Next; | |
} | |
} | |
value = default; | |
return false; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
private ref int GetBucket(uint keyHash) => ref _buckets[keyHash % (uint) _buckets.Length]; | |
private void Resize() | |
{ | |
var length = _length; | |
var newSize = GlossaryHelper.GetPrime(length == 0 ? 4 : length << 1); | |
var buckets = new int[newSize]; | |
var entries = new Entry[newSize]; | |
Array.Copy(_entries, entries, length); | |
for (var i = 0; i < length; i++) | |
{ | |
ref var entry = ref entries[i]; | |
if (entry.Next < -1) continue; | |
ref var bucket = ref buckets[(uint) entry.Key % (uint) buckets.Length]; | |
entry.Next = bucket - 1; | |
bucket = i + 1; | |
} | |
_buckets = buckets; | |
_entries = entries; | |
} | |
public struct Entry | |
{ | |
internal int Next; | |
public int Key; | |
public TValue Value; | |
} | |
public ref struct Enumerator | |
{ | |
private int _index; | |
private readonly int _length; | |
private readonly Entry[] _entries; | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public Enumerator(Entry[] entries, int length) | |
{ | |
_index = -1; | |
_length = length; | |
_entries = entries; | |
} | |
public bool MoveNext() | |
{ | |
var index = _index + 1; | |
var length = _length; | |
while ((uint) index < (uint) length) | |
{ | |
ref readonly var entry = ref _entries[index]; | |
if (entry.Next >= -1) | |
{ | |
_index = index; | |
return true; | |
} | |
index++; | |
} | |
_index = _length; | |
return false; | |
} | |
public readonly ref Entry Current | |
{ | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
get => ref _entries[_index]; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Runtime.CompilerServices; | |
namespace Benchmarks.GlossaryBenchmarks; | |
internal sealed class GlossarySealedClass<TValue> | |
where TValue : struct | |
{ | |
private static TValue _defaultValue; | |
private const int StartOfFreeList = -3; | |
private int[] _buckets; | |
private Entry[] _entries; | |
private int _length; | |
private int _freeCount; | |
private int _freeList; | |
public GlossarySealedClass(int capacity) | |
{ | |
capacity = GlossaryHelper.GetPrime(capacity); | |
_buckets = new int[capacity]; | |
_entries = new Entry[capacity]; | |
_freeCount = 0; | |
_freeList = 0; | |
_length = 0; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public ref TValue Add(int key) | |
{ | |
if (_buckets.Length == 0) Resize(); | |
var keyHash = (uint) key; | |
ref var bucket = ref GetBucket(keyHash); | |
var entries = _entries; | |
var i = bucket - 1; | |
while ((uint) i < (uint) entries.Length) | |
{ | |
ref readonly var existsEntry = ref entries[i]; | |
if (existsEntry.Key == key) GlossaryHelper.KeyAlreadyExists(key); | |
i = existsEntry.Next; | |
} | |
int index; | |
if (_freeCount > 0) | |
{ | |
index = _freeList; | |
_freeList = StartOfFreeList - entries[index].Next; | |
_freeCount--; | |
} | |
else | |
{ | |
index = _length; | |
if (index == entries.Length) | |
{ | |
Resize(); | |
bucket = ref GetBucket(keyHash); | |
entries = _entries; | |
} | |
_length++; | |
} | |
ref var entry = ref entries[index]; | |
entry.Key = key; | |
entry.Next = bucket - 1; | |
bucket = index + 1; | |
return ref entry.Value; | |
} | |
public void Add(int key, in TValue value) | |
{ | |
ref var entryValue = ref Add(key); | |
entryValue = value; | |
} | |
public void Clear() | |
{ | |
if (_length == 0) return; | |
Array.Clear(_buckets, 0, _buckets.Length); | |
Array.Clear(_entries, 0, _entries.Length); | |
_freeCount = 0; | |
_freeList = 0; | |
_length = 0; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public bool ContainsKey(int key) | |
{ | |
if (_length > 0) | |
{ | |
var i = _buckets[(uint) key % (uint) _buckets.Length] - 1; | |
var entries = _entries; | |
while ((uint) i < (uint) entries.Length) | |
{ | |
ref readonly var entry = ref entries[i]; | |
if (entry.Key == key) return true; | |
i = entry.Next; | |
} | |
} | |
return false; | |
} | |
public bool ContainsValue(in TValue value, IEqualityComparer<TValue>? comparer = null) | |
{ | |
comparer ??= EqualityComparer<TValue>.Default; | |
foreach (var entry in _entries.AsSpan(0, _length)) | |
{ | |
if (entry.Next >= -1 && comparer.Equals(entry.Value, value)) | |
{ | |
return true; | |
} | |
} | |
return false; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public Enumerator GetEnumerator() => new(_entries, _length); | |
public ref TValue GetValue(int key) | |
{ | |
if (_length > 0) | |
{ | |
var i = _buckets[(uint) key % (uint) _buckets.Length] - 1; | |
var entries = _entries; | |
while ((uint) i < (uint) entries.Length) | |
{ | |
ref var entry = ref entries[i]; | |
if (entry.Key == key) return ref entry.Value; | |
i = entry.Next; | |
} | |
} | |
GlossaryHelper.KeyNotFound(key); | |
return ref _defaultValue; | |
} | |
public int Length | |
{ | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
get => _length - _freeCount; | |
} | |
public bool Remove(int key, bool clear = false) | |
{ | |
if (_length == 0) return false; | |
ref var bucket = ref GetBucket((uint) key); | |
var entries = _entries; | |
var i = bucket - 1; | |
var last = -1; | |
while (i >= 0) | |
{ | |
ref var entry = ref entries[i]; | |
if (entry.Key == key) | |
{ | |
if (clear) entry.Value = default; | |
if (last < 0) bucket = entry.Next + 1; | |
else entries[last].Next = entry.Next; | |
entry.Next = StartOfFreeList - _freeList; | |
_freeCount++; | |
_freeList = i; | |
return true; | |
} | |
last = i; | |
i = entry.Next; | |
} | |
return false; | |
} | |
public bool Remove(int key, out TValue value) | |
{ | |
if (_length == 0) | |
{ | |
value = default; | |
return false; | |
} | |
ref var bucket = ref GetBucket((uint) key); | |
var entries = _entries; | |
var i = bucket - 1; | |
var last = -1; | |
while (i >= 0) | |
{ | |
ref var entry = ref entries[i]; | |
if (entry.Key == key) | |
{ | |
value = entry.Value; // copy | |
entry.Value = default; | |
if (last < 0) bucket = entry.Next + 1; | |
else entries[last].Next = entry.Next; | |
entry.Next = StartOfFreeList - _freeList; | |
_freeCount++; | |
_freeList = i; | |
return true; | |
} | |
last = i; | |
i = entry.Next; | |
} | |
value = default; | |
return false; | |
} | |
public void TrimExcess() | |
{ | |
if (_length == 0) | |
{ | |
_buckets = Array.Empty<int>(); | |
_entries = Array.Empty<Entry>(); | |
_freeCount = 0; | |
_freeList = -1; | |
return; | |
} | |
var oldEntries = _entries; | |
var newSize = _length; | |
var buckets = new int[newSize]; | |
var entries = new Entry[newSize]; | |
var count = 0; | |
for (var i = 0; i < newSize; i++) | |
{ | |
if (oldEntries[i].Next < -1) continue; | |
ref var entry = ref entries[count]; | |
entry = oldEntries[i]; | |
var bucket = (uint) oldEntries[i].Key % (uint) newSize; | |
entry.Next = buckets[bucket] - 1; | |
buckets[bucket] = count + 1; | |
count++; | |
} | |
_buckets = buckets; | |
_entries = entries; | |
_freeCount = 0; | |
_freeList = -1; | |
} | |
public bool TryGetValue(int key, out TValue value) | |
{ | |
if (_length > 0) | |
{ | |
var i = _buckets[(uint) key % (uint) _buckets.Length] - 1; | |
var entries = _entries; | |
while ((uint) i < (uint) entries.Length) | |
{ | |
ref readonly var entry = ref entries[i]; | |
if (entry.Key == key) | |
{ | |
value = entry.Value; | |
return true; | |
} | |
i = entry.Next; | |
} | |
} | |
value = default; | |
return false; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
private ref int GetBucket(uint keyHash) => ref _buckets[keyHash % (uint) _buckets.Length]; | |
private void Resize() | |
{ | |
var length = _length; | |
var newSize = GlossaryHelper.GetPrime(length == 0 ? 4 : length << 1); | |
var buckets = new int[newSize]; | |
var entries = new Entry[newSize]; | |
Array.Copy(_entries, entries, length); | |
for (var i = 0; i < length; i++) | |
{ | |
ref var entry = ref entries[i]; | |
if (entry.Next < -1) continue; | |
ref var bucket = ref buckets[(uint) entry.Key % (uint) buckets.Length]; | |
entry.Next = bucket - 1; | |
bucket = i + 1; | |
} | |
_buckets = buckets; | |
_entries = entries; | |
} | |
public struct Entry | |
{ | |
internal int Next; | |
public int Key; | |
public TValue Value; | |
} | |
public ref struct Enumerator | |
{ | |
private int _index; | |
private readonly int _length; | |
private readonly Entry[] _entries; | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public Enumerator(Entry[] entries, int length) | |
{ | |
_index = -1; | |
_length = length; | |
_entries = entries; | |
} | |
public bool MoveNext() | |
{ | |
var index = _index + 1; | |
var length = _length; | |
while ((uint) index < (uint) length) | |
{ | |
ref readonly var entry = ref _entries[index]; | |
if (entry.Next >= -1) | |
{ | |
_index = index; | |
return true; | |
} | |
index++; | |
} | |
_index = _length; | |
return false; | |
} | |
public readonly ref Entry Current | |
{ | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
get => ref _entries[_index]; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1413/22H2/2022Update/SunValley2) | |
AMD Ryzen 7 5800H with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores | |
.NET SDK=7.0.102 | |
[Host] : .NET 6.0.13 (6.0.1322.58009), X64 RyuJIT AVX2 | |
.NET 6.0 : .NET 6.0.13 (6.0.1322.58009), X64 RyuJIT AVX2 | |
.NET 7.0 : .NET 7.0.2 (7.0.222.60605), X64 RyuJIT AVX2 | |
.NET Core 3.1 : .NET Core 3.1.22 (CoreCLR 4.700.21.56803, CoreFX 4.700.21.57101), X64 RyuJIT AVX2 | |
.NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9139.0), X64 RyuJIT VectorSize=256 | |
| Method | Job | Runtime | Mean | Error | StdDev | Ratio | RatioSD | | |
|----------------------------- |------------------- |------------------- |---------:|----------:|----------:|------:|--------:| | |
| DictionaryTryGetValue | .NET 6.0 | .NET 6.0 | 4.085 us | 0.0797 us | 0.0885 us | 1.00 | 0.00 | | |
| GlossaryTryGetValue | .NET 6.0 | .NET 6.0 | 1.741 us | 0.0343 us | 0.0395 us | 0.43 | 0.01 | | |
| GlossaryNonSealedTryGetValue | .NET 6.0 | .NET 6.0 | 2.551 us | 0.0489 us | 0.0457 us | 0.63 | 0.02 | | |
| GlossarySealedTryGetValue | .NET 6.0 | .NET 6.0 | 2.499 us | 0.0495 us | 0.0853 us | 0.61 | 0.02 | | |
| | | | | | | | | | |
| DictionaryTryGetValue | .NET 7.0 | .NET 7.0 | 3.768 us | 0.0753 us | 0.0868 us | 1.00 | 0.00 | | |
| GlossaryTryGetValue | .NET 7.0 | .NET 7.0 | 1.510 us | 0.0284 us | 0.0265 us | 0.40 | 0.01 | | |
| GlossaryNonSealedTryGetValue | .NET 7.0 | .NET 7.0 | 2.638 us | 0.0492 us | 0.0527 us | 0.70 | 0.03 | | |
| GlossarySealedTryGetValue | .NET 7.0 | .NET 7.0 | 2.617 us | 0.0502 us | 0.0516 us | 0.70 | 0.02 | | |
| | | | | | | | | | |
| DictionaryTryGetValue | .NET Core 3.1 | .NET Core 3.1 | 4.965 us | 0.0882 us | 0.0825 us | 1.00 | 0.00 | | |
| GlossaryTryGetValue | .NET Core 3.1 | .NET Core 3.1 | 2.534 us | 0.0503 us | 0.0538 us | 0.51 | 0.01 | | |
| GlossaryNonSealedTryGetValue | .NET Core 3.1 | .NET Core 3.1 | 2.526 us | 0.0504 us | 0.0638 us | 0.51 | 0.01 | | |
| GlossarySealedTryGetValue | .NET Core 3.1 | .NET Core 3.1 | 2.528 us | 0.0404 us | 0.0378 us | 0.51 | 0.01 | | |
| | | | | | | | | | |
| DictionaryTryGetValue | .NET Framework 4.8 | .NET Framework 4.8 | 6.965 us | 0.1388 us | 0.1805 us | 1.00 | 0.00 | | |
| GlossaryTryGetValue | .NET Framework 4.8 | .NET Framework 4.8 | 2.575 us | 0.0430 us | 0.0403 us | 0.37 | 0.01 | | |
| GlossaryNonSealedTryGetValue | .NET Framework 4.8 | .NET Framework 4.8 | 2.546 us | 0.0118 us | 0.0104 us | 0.37 | 0.01 | | |
| GlossarySealedTryGetValue | .NET Framework 4.8 | .NET Framework 4.8 | 2.528 us | 0.0464 us | 0.0387 us | 0.37 | 0.01 | | |
// * Warnings * | |
MultimodalDistribution | |
BenchmarkTryGetValue.GlossarySealedTryGetValue: .NET 6.0 -> It seems that the distribution can have several modes (mValue = 3.07) | |
// * Hints * | |
Outliers | |
BenchmarkTryGetValue.DictionaryTryGetValue: .NET 6.0 -> 3 outliers were detected (3.87 us..3.98 us) | |
BenchmarkTryGetValue.GlossaryTryGetValue: .NET 6.0 -> 4 outliers were detected (1.66 us..1.68 us) | |
BenchmarkTryGetValue.GlossaryNonSealedTryGetValue: .NET 6.0 -> 2 outliers were detected (2.43 us, 2.46 us) | |
BenchmarkTryGetValue.DictionaryTryGetValue: .NET 7.0 -> 1 outlier was detected (3.58 us) | |
BenchmarkTryGetValue.GlossaryTryGetValue: .NET 7.0 -> 1 outlier was detected (1.46 us) | |
BenchmarkTryGetValue.GlossaryNonSealedTryGetValue: .NET 7.0 -> 3 outliers were detected (2.51 us..2.55 us) | |
BenchmarkTryGetValue.DictionaryTryGetValue: .NET Core 3.1 -> 1 outlier was detected (4.78 us) | |
BenchmarkTryGetValue.GlossaryNonSealedTryGetValue: .NET Core 3.1 -> 1 outlier was detected (2.36 us) | |
BenchmarkTryGetValue.GlossarySealedTryGetValue: .NET Core 3.1 -> 3 outliers were detected (2.44 us..2.50 us) | |
BenchmarkTryGetValue.DictionaryTryGetValue: .NET Framework 4.8 -> 1 outlier was detected (6.51 us) | |
BenchmarkTryGetValue.GlossaryTryGetValue: .NET Framework 4.8 -> 2 outliers were detected (2.49 us, 2.50 us) | |
BenchmarkTryGetValue.GlossaryNonSealedTryGetValue: .NET Framework 4.8 -> 1 outlier was removed (2.59 us) | |
BenchmarkTryGetValue.GlossarySealedTryGetValue: .NET Framework 4.8 -> 2 outliers were removed, 3 outliers were detected (2.40 us, 2.56 us, 2.64 us) | |
// * Legends * | |
Mean : Arithmetic mean of all measurements | |
Error : Half of 99.9% confidence interval | |
StdDev : Standard deviation of all measurements | |
Ratio : Mean of the ratio distribution ([Current]/[Baseline]) | |
RatioSD : Standard deviation of the ratio distribution ([Current]/[Baseline]) | |
1 us : 1 Microsecond (0.000001 sec) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment