diff --git a/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj b/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj index 3f4bdabb2e37b6..635a3217a35c0f 100644 --- a/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj +++ b/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum) @@ -156,6 +156,10 @@ The System.Collections.Immutable library is built-in as part of the shared frame + + + + diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs index 1772628aca0229..410f28e156efa1 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs @@ -122,6 +122,13 @@ private static FrozenDictionary CreateFromDictionary // the Equals/GetHashCode methods to be devirtualized and possibly inlined. if (typeof(TKey).IsValueType && ReferenceEquals(comparer, EqualityComparer.Default)) { +#if NET + if (DenseIntegralFrozenDictionary.TryCreate(source, out FrozenDictionary? result)) + { + return result; + } +#endif + if (source.Count <= Constants.MaxItemsInSmallValueTypeFrozenCollection) { // If the key is a something we know we can efficiently compare, use a specialized implementation diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Integer/DenseIntegralFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Integer/DenseIntegralFrozenDictionary.cs new file mode 100644 index 00000000000000..926225aadb1391 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Integer/DenseIntegralFrozenDictionary.cs @@ -0,0 +1,234 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace System.Collections.Frozen +{ + /// Provides a for densely-packed integral keys. + internal sealed class DenseIntegralFrozenDictionary + { + /// + /// Maximum allowed ratio of the number of key/value pairs to the range between the minimum and maximum keys. + /// + /// + /// + /// This is dialable. The closer the value gets to 0, the more likely this implementation will be used, + /// and the more memory will be consumed to store the values. The value of 0.1 means that up to 90% of the + /// slots in the values array may be unused. + /// + /// + /// As an example, DaysOfWeek's min is 0, its max is 6, and it has 7 values, such that 7 / (6 - 0 + 1) = 1.0; thus + /// with a threshold of 0.1, DaysOfWeek will use this implementation. But SocketError's min is -1, its max is 11004, and + /// it has 47 values, such that 47 / (11004 - (-1) + 1) = 0.004; thus, SocketError will not use this implementation. + /// + /// + private const double CountToLengthRatio = 0.1; + + public static bool TryCreate(Dictionary source, [NotNullWhen(true)] out FrozenDictionary? result) + where TKey : notnull + { + // Int32 and integer types that fit within Int32. This is to minimize difficulty later validating that + // inputs are in range of int: we can always cast everything to Int32 without loss of information. + + if (typeof(TKey) == typeof(byte) || (typeof(TKey).IsEnum && typeof(TKey).GetEnumUnderlyingType() == typeof(byte))) + return TryCreate(source, out result); + + if (typeof(TKey) == typeof(sbyte) || (typeof(TKey).IsEnum && typeof(TKey).GetEnumUnderlyingType() == typeof(sbyte))) + return TryCreate(source, out result); + + if (typeof(TKey) == typeof(ushort) || (typeof(TKey).IsEnum && typeof(TKey).GetEnumUnderlyingType() == typeof(ushort))) + return TryCreate(source, out result); + + if (typeof(TKey) == typeof(short) || (typeof(TKey).IsEnum && typeof(TKey).GetEnumUnderlyingType() == typeof(short))) + return TryCreate(source, out result); + + if (typeof(TKey) == typeof(int) || (typeof(TKey).IsEnum && typeof(TKey).GetEnumUnderlyingType() == typeof(int))) + return TryCreate(source, out result); + + result = null; + return false; + } + + private static bool TryCreate(Dictionary source, [NotNullWhen(true)] out FrozenDictionary? result) + where TKey : notnull + where TKeyUnderlying : unmanaged, IBinaryInteger + { + // Start enumerating the dictionary to ensure it has at least one element. + Dictionary.Enumerator e = source.GetEnumerator(); + if (e.MoveNext()) + { + // Get that element and treat it as the min and max. Then continue enumerating the remainder + // of the dictionary to count the number of elements and track the full min and max. + int count = 1; + int min = int.CreateTruncating((TKeyUnderlying)(object)e.Current.Key); + int max = min; + while (e.MoveNext()) + { + count++; + int key = int.CreateTruncating((TKeyUnderlying)(object)e.Current.Key); + if (key < min) + { + min = key; + } + else if (key > max) + { + max = key; + } + } + + // Based on the min and max, determine the spread. If the range fits within a non-negative Int32 + // and the ratio of the number of elements in the dictionary to the length is within the allowed + // threshold, create the new dictionary. + long length = (long)max - min + 1; + Debug.Assert(length > 0); + if (length <= int.MaxValue && + (double)count / length >= CountToLengthRatio) + { + // Create arrays of the keys and values, sorted ascending by key. + var keys = new TKey[count]; + var values = new TValue[keys.Length]; + int i = 0; + foreach (KeyValuePair entry in source) + { + keys[i] = entry.Key; + values[i] = entry.Value; + i++; + } + + if (i != keys.Length) + { + throw new InvalidOperationException(SR.CollectionModifiedDuringEnumeration); + } + + // Sort the values so that we can more easily check for contiguity but also so that + // the keys/values returned from various properties/enumeration are in a predictable order. + Array.Sort(keys, values); + + // Determine whether all of the keys are contiguous starting at 0. + bool isFull = true; + for (i = 0; i < keys.Length; i++) + { + if (int.CreateTruncating((TKeyUnderlying)(object)keys[i]) != i) + { + isFull = false; + break; + } + } + + if (isFull) + { + // All of the keys are contiguous starting at 0, so we can use an implementation that + // just stores all the values in an array indexed by key. This both provides faster access + // and allows the single values array to be used for lookups and for ValuesCore. + result = new WithFullValues(keys, values); + } + else + { + // Some of the keys in the length are missing, so create an array to hold optional values + // and populate the entries just for the elements we have. The 0th element of the optional + // values array corresponds to the element with the min key. + var optionalValues = new Optional[length]; + for (i = 0; i < keys.Length; i++) + { + optionalValues[int.CreateTruncating((TKeyUnderlying)(object)keys[i]) - min] = new(values[i], hasValue: true); + } + + result = new WithOptionalValues(keys, values, optionalValues, min); + } + + return true; + } + } + + result = null; + return false; + } + + /// Implementation used when all keys are contiguous starting at 0. + [DebuggerTypeProxy(typeof(DebuggerProxy<,,>))] + private sealed class WithFullValues(TKey[] keys, TValue[] values) : + FrozenDictionary(EqualityComparer.Default) + where TKey : notnull + where TKeyUnderlying : IBinaryInteger + { + private readonly TKey[] _keys = keys; + private readonly TValue[] _values = values; + + private protected override TKey[] KeysCore => _keys; + + private protected override TValue[] ValuesCore => _values; + + private protected override int CountCore => _keys.Length; + + private protected override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values); + + private protected override ref readonly TValue GetValueRefOrNullRefCore(TKey key) + { + int index = int.CreateTruncating((TKeyUnderlying)(object)key); + TValue[] values = _values; + if ((uint)index < (uint)values.Length) + { + return ref values[index]; + } + + return ref Unsafe.NullRef(); + } + } + + /// Implementation used when keys are not contiguous and/or do not start at 0. + [DebuggerTypeProxy(typeof(DebuggerProxy<,,>))] + private sealed class WithOptionalValues(TKey[] keys, TValue[] values, Optional[] optionalValues, int minInclusive) : + FrozenDictionary(EqualityComparer.Default) + where TKey : notnull + where TKeyUnderlying : IBinaryInteger + { + private readonly TKey[] _keys = keys; + private readonly TValue[] _values = values; + private readonly Optional[] _optionalValues = optionalValues; + private readonly int _minInclusive = minInclusive; + + private protected override TKey[] KeysCore => _keys; + + private protected override TValue[] ValuesCore => _values; + + private protected override int CountCore => _keys.Length; + + private protected override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values); + + private protected override ref readonly TValue GetValueRefOrNullRefCore(TKey key) + { + int index = int.CreateTruncating((TKeyUnderlying)(object)key) - _minInclusive; + Optional[] optionalValues = _optionalValues; + if ((uint)index < (uint)optionalValues.Length) + { + ref Optional value = ref optionalValues[index]; + if (value.HasValue) + { + return ref value.Value; + } + } + + return ref Unsafe.NullRef(); + } + } + + private readonly struct Optional(TValue value, bool hasValue) + { + public readonly TValue Value = value; + public readonly bool HasValue = hasValue; + } + + private sealed class DebuggerProxy(IReadOnlyDictionary dictionary) : + ImmutableDictionaryDebuggerProxy(dictionary) + where TKey : notnull; + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallValueTypeComparableFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallValueTypeComparableFrozenDictionary.cs index 884cb7aeca807c..e75c581a693caa 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallValueTypeComparableFrozenDictionary.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallValueTypeComparableFrozenDictionary.cs @@ -24,7 +24,6 @@ internal sealed class SmallValueTypeComparableFrozenDictionary : F internal SmallValueTypeComparableFrozenDictionary(Dictionary source) : base(EqualityComparer.Default) { - Debug.Assert(default(TKey) is IComparable); Debug.Assert(default(TKey) is not null); Debug.Assert(typeof(TKey).IsValueType); diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableEnumerableDebuggerProxy.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableEnumerableDebuggerProxy.cs index b3258e35eb921c..849cd2b4ba279b 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableEnumerableDebuggerProxy.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableEnumerableDebuggerProxy.cs @@ -16,7 +16,11 @@ namespace System.Collections.Immutable /// This class should only be used with immutable dictionaries, since it /// caches the dictionary into an array for display in the debugger. /// - internal sealed class ImmutableDictionaryDebuggerProxy where TKey : notnull + internal +#if !NET + sealed +#endif + class ImmutableDictionaryDebuggerProxy where TKey : notnull { /// /// The dictionary to show to the debugger. diff --git a/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTests.cs b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTests.cs index 86a6f3539278f8..84b251349365d3 100644 --- a/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTests.cs +++ b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Net; using System.Runtime.CompilerServices; using Xunit; @@ -523,6 +524,42 @@ public class FrozenDictionary_Generic_Tests_byte_byte : FrozenDictionary_Generic protected override byte Next(Random random) => (byte)random.Next(byte.MinValue, byte.MaxValue); } + public class FrozenDictionary_Generic_Tests_ContiguousFromZeroEnum_byte : FrozenDictionary_Generic_Tests_base_for_numbers + { + protected override bool AllowVeryLargeSizes => false; + + protected override ContiguousFromZeroEnum Next(Random random) => (ContiguousFromZeroEnum)random.Next(); + } + + public class FrozenDictionary_Generic_Tests_NonContiguousFromZeroEnum_byte : FrozenDictionary_Generic_Tests_base_for_numbers + { + protected override bool AllowVeryLargeSizes => false; + + protected override NonContiguousFromZeroEnum Next(Random random) => (NonContiguousFromZeroEnum)random.Next(); + } + + public enum ContiguousFromZeroEnum + { + A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, K1, L1, M1, N1, O1, P1, Q1, R1, S1, T1, U1, V1, W1, X1, Y1, Z1, + A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, K2, L2, M2, N2, O2, P2, Q2, R2, S2, T2, U2, V2, W2, X2, Y2, Z2, + A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, K3, L3, M3, N3, O3, P3, Q3, R3, S3, T3, U3, V3, W3, X3, Y3, Z3, + A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, K4, L4, M4, N4, O4, P4, Q4, R4, S4, T4, U4, V4, W4, X4, Y4, Z4, + } + + public enum NonContiguousFromZeroEnum + { + A1 = 1, B1, C1, D1, E1, F1, G1, H1, I1, J1, K1, L1, M1, N1, O1, P1, Q1, S1, T1, U1, V1, W1, X1, Y1, Z1, + A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, K2, L2, M2, N2, O2, P2, Q2, R2, S2, T2, U2, V2, W2, X2, Y2, Z2, + A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, K3, L3, M3, N3, O3, P3, Q3, R3, S3, T3, U3, V3, W3, X3, Y3, Z3, + A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, K4, L4, M4, N4, O4, P4, Q4, R4, S4, T4, U4, V4, W4, X4, Y4, Z4, + } + + public class FrozenDictionary_Generic_Tests_HttpStatusCode_byte : FrozenDictionary_Generic_Tests_base_for_numbers + { + protected override bool AllowVeryLargeSizes => false; + protected override HttpStatusCode Next(Random random) => (HttpStatusCode)random.Next(); + } + public class FrozenDictionary_Generic_Tests_sbyte_sbyte : FrozenDictionary_Generic_Tests_base_for_numbers { protected override bool AllowVeryLargeSizes => false;