// Copyright (c) All contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. #if !UNITY_2018_3_OR_NEWER using System; #pragma warning disable SA1649 // File name should match first type name namespace MessagePack.Internal { /* Safe for multiple-read, single-write. * Add and Get Key is asymmetric. */ internal interface IAsymmetricEqualityComparer { int GetHashCode(TKey1 key1); int GetHashCode(TKey2 key2); bool Equals(TKey1 x, TKey1 y); // when used rehash bool Equals(TKey1 x, TKey2 y); // when used get } internal class StringArraySegmentByteAscymmetricEqualityComparer : IAsymmetricEqualityComparer> { private static readonly bool Is32Bit = IntPtr.Size == 4; public bool Equals(byte[] x, byte[] y) { if (x.Length != y.Length) { return false; } for (int i = 0; i < x.Length; i++) { if (x[i] != y[i]) { return false; } } return true; } public bool Equals(byte[] x, ArraySegment y) { return x.AsSpan().SequenceEqual(y); } public int GetHashCode(byte[] key1) { return this.GetHashCode(new ArraySegment(key1, 0, key1.Length)); } public int GetHashCode(ArraySegment key2) { unchecked { if (Is32Bit) { return (int)FarmHash.Hash32(key2); } else { return (int)FarmHash.Hash64(key2); } } } } internal sealed class AsymmetricKeyHashTable { private Entry[] buckets; private int size; // only use in writer lock private readonly object writerLock = new object(); private readonly float loadFactor; private readonly IAsymmetricEqualityComparer comparer; public AsymmetricKeyHashTable(IAsymmetricEqualityComparer comparer) : this(4, 0.72f, comparer) { } public AsymmetricKeyHashTable(int capacity, float loadFactor, IAsymmetricEqualityComparer comparer) { var tableSize = CalculateCapacity(capacity, loadFactor); this.buckets = new Entry[tableSize]; this.loadFactor = loadFactor; this.comparer = comparer; } public TValue AddOrGet(TKey1 key1, Func valueFactory) { TValue v; this.TryAddInternal(key1, valueFactory, out v); return v; } public bool TryAdd(TKey1 key, TValue value) { return this.TryAdd(key, _ => value); // closure capture } public bool TryAdd(TKey1 key, Func valueFactory) { return this.TryAddInternal(key, valueFactory, out TValue _); } private bool TryAddInternal(TKey1 key, Func valueFactory, out TValue resultingValue) { lock (this.writerLock) { var nextCapacity = CalculateCapacity(this.size + 1, this.loadFactor); if (this.buckets.Length < nextCapacity) { // rehash var nextBucket = new Entry[nextCapacity]; for (int i = 0; i < this.buckets.Length; i++) { Entry e = this.buckets[i]; while (e != null) { var newEntry = new Entry { Key = e.Key, Value = e.Value, Hash = e.Hash }; this.AddToBuckets(nextBucket, key, newEntry, null, out resultingValue); e = e.Next; } } // add entry(if failed to add, only do resize) var successAdd = this.AddToBuckets(nextBucket, key, null, valueFactory, out resultingValue); // replace field(threadsafe for read) VolatileWrite(ref this.buckets, nextBucket); if (successAdd) { this.size++; } return successAdd; } else { // add entry(insert last is thread safe for read) var successAdd = this.AddToBuckets(this.buckets, key, null, valueFactory, out resultingValue); if (successAdd) { this.size++; } return successAdd; } } } private bool AddToBuckets(Entry[] buckets, TKey1 newKey, Entry newEntryOrNull, Func valueFactory, out TValue resultingValue) { var h = (newEntryOrNull != null) ? newEntryOrNull.Hash : this.comparer.GetHashCode(newKey); if (buckets[h & (buckets.Length - 1)] == null) { if (newEntryOrNull != null) { resultingValue = newEntryOrNull.Value; VolatileWrite(ref buckets[h & (buckets.Length - 1)], newEntryOrNull); } else { resultingValue = valueFactory(newKey); VolatileWrite(ref buckets[h & (buckets.Length - 1)], new Entry { Key = newKey, Value = resultingValue, Hash = h }); } } else { Entry searchLastEntry = buckets[h & (buckets.Length - 1)]; while (true) { if (this.comparer.Equals(searchLastEntry.Key, newKey)) { resultingValue = searchLastEntry.Value; return false; } if (searchLastEntry.Next == null) { if (newEntryOrNull != null) { resultingValue = newEntryOrNull.Value; VolatileWrite(ref searchLastEntry.Next, newEntryOrNull); } else { resultingValue = valueFactory(newKey); VolatileWrite(ref searchLastEntry.Next, new Entry { Key = newKey, Value = resultingValue, Hash = h }); } break; } searchLastEntry = searchLastEntry.Next; } } return true; } public bool TryGetValue(TKey2 key, out TValue value) { Entry[] table = this.buckets; var hash = this.comparer.GetHashCode(key); Entry entry = table[hash & table.Length - 1]; if (entry == null) { goto NOT_FOUND; } if (this.comparer.Equals(entry.Key, key)) { value = entry.Value; return true; } Entry next = entry.Next; while (next != null) { if (this.comparer.Equals(next.Key, key)) { value = next.Value; return true; } next = next.Next; } NOT_FOUND: value = default(TValue); return false; } private static int CalculateCapacity(int collectionSize, float loadFactor) { var initialCapacity = (int)(((float)collectionSize) / loadFactor); var capacity = 1; while (capacity < initialCapacity) { capacity <<= 1; } if (capacity < 8) { return 8; } return capacity; } private static void VolatileWrite(ref Entry location, Entry value) { System.Threading.Volatile.Write(ref location, value); } private static void VolatileWrite(ref Entry[] location, Entry[] value) { System.Threading.Volatile.Write(ref location, value); } private class Entry { #pragma warning disable SA1401 // Fields should be private internal TKey1 Key; internal TValue Value; internal int Hash; internal Entry Next; #pragma warning restore SA1401 // Fields should be private // from debugger only public override string ToString() { return "Count:" + this.Count; } internal int Count { get { var count = 1; Entry n = this; while (n.Next != null) { count++; n = n.Next; } return count; } } } } } #endif